Automatic coding-time tracking for codetime.dev, ported
to the DeepSeek Harness
(dsh). This is the native dsh telemetry backend — the dsh-side sibling of
codetime-cli's agent adapters for Claude Code / Codex / OpenCode / Pi.
It reports session, turn, tool, file, and model activity to the same agent
ingest endpoint the CLI uses (POST /v3/agent/ingest), so dsh activity lands
on the same dashboard as your other AI-agent tools.
Built for dsh 0.2.x (verified against 0.2.0-rc.2).
dsh already ships a telemetry seam (@deepseek-ai/dsh-session-telemetry) that
captures session events live (or on demand), projects them, runs the
session-telemetry/record redaction waterfall, and hands
SessionTelemetryRecords to any backend that implements emit / flush /
shutdown. This package is such a backend:
SessionTelemetryCoordinatordrivesemit(record)for every projected session event.- Each record is translated into a codetime canonical event
(
session.started,turn.started,prompt.submitted,tool.started,tool.completed,file.changed,command.completed,model.usage, …). - Events accumulate per session and are rolled up (15-minute buckets, per-model
/ per-tool / per-file / per-turn aggregates) with the exact wire format
codetime-cliuses. - A flush POSTs
{ rollups, replace: true }to/v3/agent/ingest, upserting each session's rollup by its stable key.emitonly queues in memory (no I/O), so it never blocks the session firehose; batching, the periodic timer (flushIntervalMs), thesession/flushhint, and the bounded shutdown drain own the network.
dsh session/event type |
codetime canonical event |
|---|---|
turn/start / turn/end |
turn.started / turn.completed | turn.failed |
user/message (direct prompt) |
prompt.submitted |
assistant/message (with usage) |
model.usage |
tool/call |
tool.started |
tool/result |
tool.completed | tool.failed + file.read/changed/searched |
tool/result (bash/pwsh/…) |
command.completed | command.failed |
compaction/end |
context.compacted |
session created / disposed |
session.started / session.ended |
File activities are derived from the tool name and its parsed arguments:
| tool | derived activity |
|---|---|
read, read_image |
read |
write |
write, linesAdded from content |
edit |
edit, linesAdded/linesRemoved from new_string/old_string |
str_replace_editor |
read for view, write for create, otherwise edit with old_str/new_str |
grep, glob |
search against path |
A tool whose tool/result reports message.isError is a failure: it becomes
tool.failed (or command.failed), counts in failureCount, and contributes
no file activity, because the arguments describe a change that never
happened. session.cwd becomes the codetime project and workspaceId.
The package is a host-plane plugin (a process-global sessionTelemetry
Service) shipped as a dsh bundle, so one command is the whole install:
dsh plugin --profile web add dsh-codetimepackage.json declares the bundle:
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }dsh reconciles the profile's dsh.profile.bundles against its installed
packages and appends dsh-codetime to the bundle stack; the profile boot then
merges cordis.patch.yml exactly like a manual mount
line. Nothing has to be edited by hand.
To run a checkout before the next npm release, add the directory instead of the
package name (pnpm installs it as a local link):
dsh plugin --profile web add /path/to/dsh-codetime # or E:\path\to\dsh-codetimeManual mount (for a non-bundle setup)
Merge the rows from cordis.patch.yml into your profile's
cordis.patch.yml (or $DSH_HOME/cordis.patch.yml) after the dsh-base and
dsh-web-app layers.
⚠️ sessionTelemetryis a singleton — one backend per process. The base bundle always mountssession-telemetry-otel(even in its defaultFEEDBACK_ONLYmode it registers the Service), so disable it before mounting this backend — the shippedcordis.patch.ymlalready does both.
dsh 0.2.x applies two gates before a package becomes a plugin. npm test
asserts both against the running runtime:
| Gate | Requirement |
|---|---|
| Shape | The package must declare dsh.bundle.patch naming its patch file (or an ordered list). Without it the install is refused with not-a-bundle / <name> declares no dsh.bundle, or the package lands as a plain dependency that never becomes a profile layer. |
| Peers | Every @deepseek-ai/dsh* peerDependencies range must satisfy the running dsh version (evaluatePluginCompatibility). These packages are supplied by the host profile, not by this package, so the peers are declared as * — the convention the other out-of-repo dsh plugins use. Pinning a prerelease range (^0.1.0-rc.6) makes the next dsh prerelease reject the plugin outright. |
| Key | Default | Meaning |
|---|---|---|
mode |
DISABLED (the shipped patch sets FULL) |
FULL (capture every session live), FEEDBACK_ONLY (upload a session's log only after the human records /feedback), or DISABLED. An unknown value fails the boot instead of silently degrading. |
apiUrl |
https://codetime.dev |
API base URL. |
flushIntervalMs |
60000 |
Rollup flush cadence. |
shutdownTimeoutMs |
5000 |
Upper bound on the final drain at teardown. |
token |
— | Bearer token; overrides the environment and the shared config file. |
Token resolution (first match wins): token config → CODETIME_TOKEN env →
token field of ~/.codetime/config.json. If you already signed in with the
codetime CLI or another editor extension, the shared
~/.codetime/config.json token is picked up automatically. The machine-id in
~/.codetime/machine-id identifies the machine on the dashboard (created on
first use, shared with the CLI).
In FEEDBACK_ONLY nothing leaves the process until a feedback/record event is
appended to that session's own canonical log — ordinary activity is never
batched out. The capture is then on demand with includeHistory, so the
upload covers the whole canonical log, not just the events after the
feedback. Feedback inherited from a fork parent authorizes nothing for the
child, and feedback committed through the message-feedback Remote
(feedback/committed, which never publishes a live session) is rebuilt from its
committed inspection before capture. Mount your own session-telemetry/record
rules to redact the export.
- Event buffers are held per session in memory and re-sent whole each flush (idempotent upsert); very long-lived sessions grow their in-memory buffer.
- Historical sessions are not backfilled — this reports only activity the
live process observes. Pair it with a
codetime-clidshbackfill adapter to import~/.dsh/sessions/**/session.jsonl.zstdhistory. - No redaction rules are shipped: records leave the process exactly as captured
by the seam (after any
session-telemetry/recordwaterfall a deployment mounts).
npm install # or: pnpm install
npm testThe suite boots the backend on a real cordis app with the genuine sessions and
timer services, drives real Session appends through the seam, and asserts the
captured ingest requests — plus both dsh install gates (dsh.bundle shape and
peer compatibility) and the parsed bundle patch itself.
Publishing runs through GitHub Actions using an npm Trusted Publisher
(OIDC): the workflow requests a short-lived token with id-token: write, so no
npm token is ever stored in the repository.
-
If
dsh-codetimedoes not exist on npm yet, publish the first version once from the command line to create it:npm publish --access public
-
On npmjs.com, open the package → Settings → Publishing access → Add trusted publisher (GitHub Actions):
Field Value Owner codetime-devRepository dsh-codetimeWorkflow .github/workflows/publish.ymlLeave Environment empty.
npm version patch # or: minor / major — bumps package.json, commits, tags vX.Y.Z
git push --follow-tagsPushing the v* tag triggers publish.yml,
which runs npm publish --provenance --access public. You can also trigger it
manually from the repository's Actions tab.