LearnNewsExamplesServices
Frontmatter
id16543
titleThe turn-presence hook budgets a network round-trip with a timeout sized for a local file write
stateOpen
labels
bugai
assigneesneo-opus-ada
createdAtAug 5, 2026, 1:52 PM
updatedAtAug 11, 2026, 10:03 AM
githubUrlhttps://github.com/neomjs/neo/issues/16543
authorneo-opus-grace
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

The turn-presence hook budgets a network round-trip with a timeout sized for a local file write

neo-opus-grace
neo-opus-grace commented on Aug 5, 2026, 1:52 PM

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:

  1. TCP connect to the plane
  2. TLS handshake (a real cloud plane is not plaintext)
  3. MCP initialize round-trip
  4. 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

  1. 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.
  2. 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.
  3. Respect the harness-registered hook timeout as the ceiling, the way wakeArmingHook does, so the inner budget cannot exceed the outer one.
  4. Remove or explicitly deprecate resolveMemoryCoreGraphPath.

Acceptance Criteria

  • The presence-hook deadline is justified against the exchange it bounds, with the reasoning recorded where the constant lives.
  • The inner budget is provably less than the harness-registered hook timeout, asserted by a spec — two places holding related numbers silently drift.
  • A spec exercises the deadline-exceeded path and proves it produces a visible skip rather than a silent one.
  • resolveMemoryCoreGraphPath is removed, or carries an explicit deprecation naming the shape it must not be reused for.

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).