Context
Follow-up to #16513 / PR #16527, merged 2026-08-05. The operator merged rather than expand that PR's scope — correct call — and raised the question this ticket answers:
"our local dockerized agent os => files exist on this machine. versus a real cloud deployment => files exist on a different machine. i am not sure that the PR includes this scenario."
Measured answer to the question as asked: the write path is machine-agnostic. TurnPresenceHookWriter on dev has no filesystem surface left — no fs, no path, no rootDir, no import.meta.url, no better-sqlite3 — and resolveTurnPresenceRuntimeConfig takes only env. A different machine is satisfied vacuously, because nothing remains that could resolve a local path.
But the instinct was right, and the mechanism is one layer over.
The Problem
TurnPresenceConfig.mjs:12-17:
export const TURN_PRESENCE_DEFAULTS = Object.freeze({
freshMs : 30 * 60 * 1000,
ttlMs : 60 * 60 * 1000,
noteMaxChars : 512,
hookWriteTimeoutMs: 1500
});1500 was sized for what the hook used to do: import('better-sqlite3'), open a local file, run one INSERT. On local disk that is generous.
PR #16527 changed what that number covers without changing the number. It is now passed as deadlineMs to recordTurnPresenceOverMcp, whose own contract states it is the budget for all stages combined — so 1500ms must now cover:
- TCP connect to the plane
- TLS handshake (a real cloud plane is not plaintext)
- MCP
initialize round-trip
- the
tools/call round-trip itself
Against a co-located container over loopback, that fits. Against a remote plane on a different machine — the operator's exact scenario — a cold TLS connection alone can approach or exceed it.
The failure is quiet by design, which is what makes it bad here. An exceeded deadline aborts and the hook reports a named skip on stderr. No crash, no red test — the seat simply stops emitting presence, intermittently, on precisely the deployment where nobody is watching a terminal. That is the same "unmeasured state that looks measured" failure #16513 existed to remove, reintroduced through a constant nobody re-derived when the transport changed.
The Architectural Reality
- The number was correct for its original operation and is still named for it (
hookWriteTimeoutMs — a write timeout). The name now under-describes a four-stage network exchange, which is part of why the change did not draw attention.
- The sibling precedent already diverges:
readSubscriptionsOverMcp declares DEFAULT_TIMEOUT_MS = 8000 for the same class of exchange over the same transport, and wakeArmingHook derives its budget explicitly (HOOK_TIMEOUT_MS - PUBLISH_MARGIN_MS) rather than inheriting one. Turn presence inherited instead.
- Harness hooks are also wall-clock bounded by their own registration (
.claude/settings.json carries a timeout per hook), so any new value has a ceiling that must be respected rather than guessed — wakeArmingHook documents that coupling and asserts it in a spec.
Second, smaller finding
resolveMemoryCoreGraphPath is now orphaned. Verified against merged dev by walking every .mjs blob in the tree: the only file containing it is TurnPresenceConfig.mjs itself, where it is defined. Its sole caller was the writer PR #16527 replaced.
It is the last carrier of the checkout-relative path pattern this whole ticket family removed, and a live export invites exactly the reuse #16513 was filed against. Remove it, or mark it explicitly as the deprecated shape with the reason.
The Fix
- Re-derive the presence-hook budget from what it now measures — a bounded network exchange — rather than inheriting a file-write constant.
readSubscriptionsOverMcp's 8000 and wakeArmingHook's derived form are the in-tree precedents.
- Rename it so the name describes the operation (it is no longer a write timeout), or keep the name and document explicitly that it bounds a full MCP exchange.
- Respect the harness-registered hook timeout as the ceiling, the way
wakeArmingHook does, so the inner budget cannot exceed the outer one.
- Remove or explicitly deprecate
resolveMemoryCoreGraphPath.
Acceptance Criteria
Out of Scope
- Retrying a failed presence write. Presence is an enhancement, not a precondition, and a retry storm against an unreachable plane is worse than a skipped beacon.
- The wake-daemon half of
#16513, which is #16526.
Decision Record impact
none — a constant and its rationale.
Related
#16513 / PR #16527 (the transport change that repurposed the constant) · #16526 (the daemon half) · ai/daemons/wake/readSubscriptionsOverMcp.mjs and .claude/hooks/wakeArmingHook.mjs (the two in-tree budget precedents).
Context
Follow-up to
#16513/ PR #16527, merged 2026-08-05. The operator merged rather than expand that PR's scope — correct call — and raised the question this ticket answers:Measured answer to the question as asked: the write path is machine-agnostic.
TurnPresenceHookWriterondevhas no filesystem surface left — nofs, nopath, norootDir, noimport.meta.url, nobetter-sqlite3— andresolveTurnPresenceRuntimeConfigtakes onlyenv. A different machine is satisfied vacuously, because nothing remains that could resolve a local path.But the instinct was right, and the mechanism is one layer over.
The Problem
TurnPresenceConfig.mjs:12-17:export const TURN_PRESENCE_DEFAULTS = Object.freeze({ freshMs : 30 * 60 * 1000, ttlMs : 60 * 60 * 1000, noteMaxChars : 512, hookWriteTimeoutMs: 1500 });1500was sized for what the hook used to do:import('better-sqlite3'), open a local file, run oneINSERT. On local disk that is generous.PR #16527 changed what that number covers without changing the number. It is now passed as
deadlineMstorecordTurnPresenceOverMcp, whose own contract states it is the budget for all stages combined — so 1500ms must now cover:initializeround-triptools/callround-trip itselfAgainst a co-located container over loopback, that fits. Against a remote plane on a different machine — the operator's exact scenario — a cold TLS connection alone can approach or exceed it.
The failure is quiet by design, which is what makes it bad here. An exceeded deadline aborts and the hook reports a named skip on stderr. No crash, no red test — the seat simply stops emitting presence, intermittently, on precisely the deployment where nobody is watching a terminal. That is the same "unmeasured state that looks measured" failure
#16513existed to remove, reintroduced through a constant nobody re-derived when the transport changed.The Architectural Reality
hookWriteTimeoutMs— a write timeout). The name now under-describes a four-stage network exchange, which is part of why the change did not draw attention.readSubscriptionsOverMcpdeclaresDEFAULT_TIMEOUT_MS = 8000for the same class of exchange over the same transport, andwakeArmingHookderives its budget explicitly (HOOK_TIMEOUT_MS - PUBLISH_MARGIN_MS) rather than inheriting one. Turn presence inherited instead..claude/settings.jsoncarries atimeoutper hook), so any new value has a ceiling that must be respected rather than guessed —wakeArmingHookdocuments that coupling and asserts it in a spec.Second, smaller finding
resolveMemoryCoreGraphPathis now orphaned. Verified against mergeddevby walking every.mjsblob in the tree: the only file containing it isTurnPresenceConfig.mjsitself, where it is defined. Its sole caller was the writer PR #16527 replaced.It is the last carrier of the checkout-relative path pattern this whole ticket family removed, and a live export invites exactly the reuse
#16513was filed against. Remove it, or mark it explicitly as the deprecated shape with the reason.The Fix
readSubscriptionsOverMcp's8000andwakeArmingHook's derived form are the in-tree precedents.wakeArmingHookdoes, so the inner budget cannot exceed the outer one.resolveMemoryCoreGraphPath.Acceptance Criteria
resolveMemoryCoreGraphPathis removed, or carries an explicit deprecation naming the shape it must not be reused for.Out of Scope
#16513, which is#16526.Decision Record impact
none— a constant and its rationale.Related
#16513/ PR #16527 (the transport change that repurposed the constant) ·#16526(the daemon half) ·ai/daemons/wake/readSubscriptionsOverMcp.mjsand.claude/hooks/wakeArmingHook.mjs(the two in-tree budget precedents).