Skip to content

Host: count and journal a traced unmount's abort-path outcome - #4

Open
mengmengmengqiang wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
tokenaissance:feat/unmount-outcome-count
Open

mengmengmengqiang wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
tokenaissance:feat/unmount-outcome-count

Conversation

@mengmengmengqiang

@mengmengmengqiang mengmengmengqiang commented Oct 28, 2026 •

Copy link
Copy Markdown

What and why

A normal unmount that has to end a stalled, already-detached connection takes LayerFS's abort path and now reports it beside the ordinary reply, as the unmount telemetry section of the traced call (Ephemeral-AI-Lab/layerfs#501, tokenaissance/layerfs#2). The SDK trace line carries that telemetry under daemon; success still answers Unmounted, a stopped unmount keeps its TeardownCustody, and a plain unmount carries no section at all.

The Host is the durable half: the daemon is ephemeral and the trace files are temporary, so this change reads the section from the trace line, records each outcome in the Host's state folder, and counts it by stage and abort disposition. It adds no second consumer — no metrics server, OTel or event bus: the journal is the one sink and unmountsOf its one reader. A plain unmount records nothing.

Four-layer map

Layer Where Owns
Entities packages/model/src/unmount.js the value: sectionOf / outcomeOf, the stable key set, STAGES and ABORTS. Plain data, no imports, no I/O.
Use Cases packages/model/src/unmount-count.js UnmountOutcomes.record (journals through the port) and countOf / summaryOf (counts by stage and abort disposition, with waitedMs total and max).
Interface Adapters packages/host/src/unmount-journal.js UnmountJournal: the append-only <stateDir>/unmounts.jsonl, read back per project, a torn last line skipped.
Frameworks & Drivers packages/host/src/collector.js, index.js trace-line collection (recordUnmount on the raw line, before stripped), the composition root, and the count surface layerfsTraces.unmountsOf(project).

Dependency rule: unmount.js imports nothing; unmount-count.js imports only the Entities; unmount-journal.js imports only node:fs and node:path; the Host imports the model. Nothing under packages/model imports the Host, and there is no in-memory counter to drift from the journal — the aggregate is counted from what the adapter stored.

Telemetry keys

The section's stable keys, as the daemon writes them (daemon: [{ timing, unmount: { … } }]):

Key Meaning
stage Join (the stalled drain was ended by the abort), Abort (the write was short or failed), Revoke or Close (a later step stopped with the abort recorded)
abort the connection's single abort write: Written, Short or Failed
bytes a Short write's accepted count (only then)
errno a Failed write's original errno (only then)
detached kernel detach was already known when the abort was made
loops_exited, loops_joined receive loops exited and joined at the observation
active, retained request owner counts at the observation
waited_ms milliseconds from the stalled drain to the end of the abort path

The journal record adds at, project, agent, id and reply (Unmounted on success, or the refusal's code — Failed for a retained stop). The aggregate is { total, byStage, byAbort, waitedMs: { total, max }, records }; a stage or disposition outside the stable sets is counted under its own key instead of being dropped.

Visual evidence

Where the unmount outcome belongs

The Host side is the use case plus the journal; the daemon only renders the telemetry section. The count surface is layerfsTraces.unmountsOf(project), backed by <stateDir>/unmounts.jsonl; no metrics server, no alerting in this PR.

Tests

Run in a clean worktree of this branch, LAYERFS_SOURCE=../layerfs-main (the checkout available here is at 9a03429adc, not the pinned fabefdf87a), each under a 120 s alarm:

$ pnpm test
host: ok
agent wrappers: ok
bindings-shared: ok
mock: ok (10 scenarios)
PASS cli: 43 methods routed, 31 lines
playback: ok (16 steps)
initlimit: ok
historycache: ok
PASS model trace: 36 lines, 3 Branches, 1 Sandboxes, 7 attributed nodes
PASS model unmount: the abort-path value, its counts and its journal port
checks: ok

pnpm test passed twice; node packages/model/tests/unmount.mjs and node packages/host/tests/host.mjs passed three runs each.

  • packages/model/tests/unmount.mjs — the section in the daemon map, success (Written) and retained (Failed), Short + bytes, Failed + errno, a plain unmount and malformed sections, the key set, and the use case's counting and journal port.
  • packages/host/tests/host.mjs — driven through the real trace-file collector: a success, a retained stop, a short write and a failed write are counted and journaled under website; a plain unmount and a line with no section add nothing; the journal is read back by a restarted Host; another project sees zeros; daemon telemetry still never reaches the Session records.

Beyond the suite, the real SDK trace writer of the #501/#2 branch (api/agent/src/trace.js) was driven for a success, a retained stop, a Short and a Failed write and a plain unmount: every line parses exactly, including the section's key order and the bytes/errno placement.

Not run here

  • pnpm run build: the configured client build needs a deepseek-harness checkout with tsdown (DSH_SOURCE); none is present in this environment. The client bundle is unchanged by this PR.
  • pnpm run check:layerfs: refused by the environment, layerfs-source: .layerfs -> …/layerfs-main, at 9a03429adc, not the pinned fabefdf87a.

Limitations

  • Only the structured trace section is read; the daemon's one LAYERFS_DAEMON_UNMOUNT_ABORT stderr line is not.
  • Only lines the Host reads live are journaled; a line already in an agent's trace file when the Host starts is history.
  • Records are keyed by project name, like the trace-line journal, so a removed project re-initialized under the same name counts the older records.
  • The file is append-only and never pruned, and gets one record per abort path, not one per unmount.
  • The section is emitted by a Linux daemon only (the abort path is Linux-only).

References

A traced normal unmount that ends a stalled, already-detached connection
carries LayerFS's `unmount` telemetry section (layerfs #501,
tokenaissance/layerfs#2): `stage`, `abort`, `bytes` or `errno`, `detached`,
`loops_exited`, `loops_joined`, `active`, `retained` and `waited_ms`. The
Host now reads it from the trace line's `daemon` telemetry before that
telemetry is stripped, counts it by stage and abort disposition, and
journals each outcome to `<stateDir>/unmounts.jsonl`.

Layers: Entities `packages/model/src/unmount.js` (the value; no imports, no
I/O); Use Case `packages/model/src/unmount-count.js` (records through the
journal port, counts read from it); Interface Adapter
`packages/host/src/unmount-journal.js` (the JSONL file); the Host's trace
collection in `collector.js` feeds them, `index.js` composes them, and the
count surfaces as the `layerfsTraces` service's `unmountsOf(project)`. No
metrics server, OTel or event bus: the journal is the one sink and
`unmountsOf` its one reader. A plain unmount carries no section and records
nothing.

Verification, LAYERFS_SOURCE at ../layerfs-main (9a03429adc, not the pinned
fabefdf87a: that is the LayerFS checkout available here):
- pnpm test: host, agent wrappers, bindings-shared, mock (10 scenarios), cli,
  playback, initlimit, historycache, model trace, model unmount, checks:
  PASS (120 s alarm), twice.
- node packages/model/tests/unmount.mjs and packages/host/tests/host.mjs:
  PASS, three runs each.
- The real SDK trace writer of the #501/Ephemeral-AI-Lab#2 branch (api/agent/src/trace.js)
  was driven for a success, a retained stop, a short write, a failed write
  and a plain unmount; every line parses exactly, including the section's
  key order and bytes/errno placement.
- pnpm run build: the configured build needs a deepseek-harness checkout
  with tsdown (DSH_SOURCE); none is here. The client bundle is unchanged.
- pnpm run check:layerfs: refused, the available LayerFS checkout is at
  9a03429adc, not the pinned fabefdf87a.

Tests deleted or skipped: none.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant