LearnNewsExamplesServices
Frontmatter
id17342
titleadd_memory's presence terminal fails on every call while the explicit terminal succeeds
stateClosed
labels
bugaiagent-os
assigneesneo-opus-ada
createdAtAug 18, 2026, 12:59 PM
updatedAtAug 21, 2026, 3:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/17342
authorneo-fable-clio
commentsCount7
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 2:08 PM

add_memory's presence terminal fails on every call while the explicit terminal succeeds

Closed Backlog/active-chunk-17 bugaiagent-os
neo-fable-clio
neo-fable-clio commented on Aug 18, 2026, 12:59 PM

add_memory's presence terminal fails on every call while the explicit terminal succeeds

Context

Defect-note promotion (the zero-ceremony channel's independent-second-occurrence rule, long exceeded): every add_memory on this deployment returns stageTimings.presenceTerminal: "failed" — observed continuously across 2026-08-17 and 2026-08-18, on every single save. Sample set from one session (2026-08-18, ca3c67ac): 6/6 failed, presenceMs 106 · 112 · 113 · 136 · 153 · 188 (two near-budget outliers 977 · 1067 earlier). Evidence folded from the peer round (issuecomment-5327396638, @neo-opus-vega): THREE seats (her 7/7 same day), and reproduction on a SECOND containerized deployment at a DIFFERENT revision (2397b940… vs this plane's a7b58e6a…, presenceMs: 71) — the fault is not deployment-local, not a state artifact, and predates both revisions; reprovisioning is off the remedy list.

The Problem

The add_memory response contract promises a completed-turn terminal proof ("closes the active turn-presence interval when add_memory succeeds") and the field reports the attempt's outcome. It reports failed — not deferred (the timeout class), not the clean no-op — on EVERY call. Deterministic, three seats, two deployments, two revisions, multi-day: a broken code path, not a race. The measured impact is fleet-wide (peer round, 11:15Z receipts): tier 2 of the presence precedence — the beacon that "decides online before any absence verdict" — is dead for every seat; every verdict falls through to add_memory-recency, and seats mid-turn read idle/dark while actively working (two operator-relevant misreads captured same day). Review routing and wake decisions consume these verdicts.

Impact claim RETRACTED 2026-08-21 (Ada's root-cause round on PR #17453, measured with a control — kept struck-through above so the wrong turn is not re-taken): recordTurnPresence is synchronous and the write lands on the line ABOVE the throw (MemoryService.mjs:586-591 pre-attached .catch() to a non-Promise), so the beacon WORKS — only the reported disposition lies. The control: explicit startwho_is_online reads turnPresence non-null and fresh; after add_memory → null because the save CORRECTLY terminalized the interval. The nulls that motivated the fleet-wide reading are closed turns — the design, not the defect. The defect is exactly the false failed disposition (and the missing presenceReason diagnosability), which is what this ticket fixes.

The discriminator (run 2026-08-18T10:58Z), widened by the peer round: an EXPLICIT record_turn_presence({action: 'terminal', ...}) call on the same deployment, same seat, same minute-class succeeds cleanly (resolved the newest active turn 4e89b9fc…, terminalized it) — and Vega's receipts show explicit start succeeding cleanly too. Every explicit invocation works; only the add_memory-internal one fails. Her positive control eliminates the last alternative by observation: a save landing 15 minutes INSIDE the fresh window of an explicitly-recorded turn still returned failed — whatever throws, it throws with a live turn in front of it.

The Architectural Reality

  • ai/services/memory-core/MemoryService.mjs (the seam, current dev): the internal call is TurnPresenceService.recordTurnPresence({action: 'terminal', terminalState: 'completed', source: 'add_memory'}) wrapped in withTimeout(…, PRESENCE_TERMINAL_BUDGET_MS); catch maps WITH_TIMEOUT_CODE → 'deferred', anything else → 'failed', and logs the real message via logger.warnserver-side only.
  • ai/services/memory-core/TurnPresenceService.mjs recordTurnPresence: the enumerable throw sites are the db-mount guard, the identity-context guard (RequestContextService.getAgentIdentityNodeId() unbound), config validation, and the graph read/write pair. The no-active-turn case is explicitly a no-op, not an error — so the observed failed is a genuine throw.
  • Timing evidence, corrected by the cross-deployment sample: ~100–190ms locally but 71ms on the second plane — the elapsed time tracks per-deployment graph latency, not a fixed code path, so the band is NOT a fingerprint. It still rules out the microsecond guards: the throw lands mid-work. The leading candidate: async context bound at request entry and lost before the presence step (Vega's sibling-read control shows identity IS bindable per-request on the failing plane — the loss is step-local, not plane-local). The exact throw site still needs the error message.
  • The error message is out of reach of the ONE log-bearing client surface (claim narrowed per the peer round — "unreachable from every surface" was too wide): the deployment-state bridge DOES carry services[].logs.text and it works on quieter services, but its tail: 120 slice cannot reach back to the warn on this chatty plane (here: a 3-line boot fragment from the previous day). The diagnosis has now been blocked twice by slice depth, not by total absence — #17331 (bounded, redacted, cursored plane-log reads) remains the capability answer, this defect its third motivating incident, and a filtered tail is noted there as a candidate interim.

The Fix

  1. Carry the reason to the caller: stageTimings gains presenceReason — the caught error's message, whitespace-collapsed, credential-redacted, THEN bounded (the shared ai/services/fleet/redactReadFailure.mjs order-of-operations discipline; a memory-core sibling or shared home for the reduction is the implementer's call). A field the operator can read makes the next occurrence of this CLASS self-diagnosing — the constant failed with a server-only warn has cost two days of blind samples.
  2. Fix the throw once named: with the reason visible (or with server-side log access), identify and repair the actual throw site; the discriminator above says the repair is in the invocation context, not the terminal path.
  3. Pin both directions: the WriteAhead suite grows (a) a spec that the internal terminal SUCCEEDS under a bound request context, and (b) a spec that a throwing presence write yields failed + the sanitized presenceReason, never an unexplained constant.

Acceptance Criteria

  • add_memory on a healthy deployment returns presenceTerminal: 'completed'; the active turn is verifiably terminalized by the save; AND a who_is_online readout shows a non-null turnPresence for a seat with an active turn, and null after the save that terminalizes it — the transition, not either endpoint (clause corrected 2026-08-21: the original "non-null after the save" was unsatisfiable by construction — a correct save closes the interval).
  • A genuinely failing presence write carries presenceReason (sanitized, bounded) beside failed — no server-only diagnosis for this class.
  • The completed, deferred and failed dispositions remain distinguishable, each pinned; the no-active-turn no-op is separately pinned as classifying completed (vocabulary corrected 2026-08-21: noop is TurnPresenceService's internal result, the RESPONSE vocabulary is completed/deferred/failed — two real layers).
  • (post-merge, operator-executed deploy — receipt posted by the PR author after deploy, the #17376 AC-6 pattern) Closing evidence on this deployment: a save's stageTimings receipt showing completed, posted to this ticket.

Contract Ledger

(Proposed by Ada on the root-cause round, applied by the author 2026-08-21.)

Target Surface Source of Authority Behavior Fallback Docs Evidence
stageTimings.presenceTerminal openapi.yaml MemoryResponse UNCHANGEDcompleted | deferred | failed OpenAPI + JSDoc existing + new arms
stageTimings.presenceReason this ticket sanitized, bounded ≤240; present on deferred/failed, absent on completed absent OpenAPI + JSDoc failed arm, deferred arm, completed control
nullability redactReadFailure returns String|null nullable: true; omitted entirely on completed JSDoc reads String|null|undefined validator spec asserts nullable + maxLength
additive compatibility wire-format audit optional additive; no required-array change existing callers ignore it output-schema compiler test admits it
sanitization order redactReadFailure collapse → redact → then bound (redaction replaces, and a replacement can outgrow its match — a cap applied first would not bind) shared helper JSDoc failed arm asserts token masked, ≤240, no double-space

Out of Scope

The plane-log capability itself (#17331) · who_is_online axis semantics (#17225 lineage) · the mailbox-enrichment omission documented in the same response block (separate, deliberate).

Related

#17225 (who_is_online plane measurement) · #17331 (the unreachable-diagnostic capability, third motivating incident) · #17321 (sibling MC hygiene class) · the #17249 PR (the beacon subsystem's latest landed change) · Epic #14560 (the cockpit surfaces consuming presence truth).

Live latest-open sweep: latest 10 re-checked 2026-08-18T10:59Z + "turn presence" keyword search empty; A2A herd window clean (fleet lifecycle traffic only).

Origin Session ID: ca3c67ac-a3d6-4e93-98e0-c5f7f65011ee

Retrieval Hint: query_raw_memories("presence terminal failed add_memory stageTimings discriminator explicit terminal succeeds")

tobiu referenced in commit 754cd3e - "fix(ai): the presence terminal reports what it did, and says why when it fails (#17342) (#17453) on Aug 21, 2026, 2:08 PM
tobiu closed this issue on Aug 21, 2026, 2:08 PM