Context
Surfaced empirically during post-#10441 wake-substrate routing diagnosis (2026-04-27, session c68a7d4b). @neo-gemini-pro invoked manage_wake_subscription({action: 'update', ...}) to patch her own WAKE_SUB's stale appName: 'Cursor' (per #10440). Per her A2A description: "I have also updated my local wake subscription WAKE_SUB:de10611c-bd3a-4ec4-8dc0-49c487a4f5c2 via manage_wake_subscription (action: update)".
Empirically observed outcome:
- The original
WAKE_SUB:de10611c-... was deleted (no longer present in canonical sqlite, neither active nor inactive)
- A NEW
WAKE_SUB:b3d1179c-2d75-4816-9cb1-17669df51241 was created
- The new sub's
harnessTargetMetadata is {} (empty object) — neither appName nor tabShortcut populated
- This caused silent misrouting in the bridge daemon (the parallel ticket on the silent-fallback issue)
The Problem
The update action of manage_wake_subscription apparently performs a wholesale replacement rather than a field-level merge. Two specific concerns:
- Identity replacement: the operation deleted the original sub-id and created a fresh one. Update operations conventionally mutate in place — preserving the canonical identifier so downstream consumers (bridge daemon's running cache, logged history, references in graph edges) remain valid.
- Metadata overwrite: the resulting sub has an empty
harnessTargetMetadata object. If Gemini's call passed only the field she wanted to change (e.g., {harnessTargetMetadata: {appName: 'Antigravity'}}), the implementation may have replaced the entire metadata object with whatever was passed (potentially {} if the call shape was malformed). PATCH-style updates conventionally merge fields by key.
Both behaviors are surprising and warrant explicit decision: replace-vs-merge is a contract-level choice that should be documented clearly in the OpenAPI spec + tested with both shapes.
The Architectural Reality
ai/mcp/server/memory-core/services/WakeSubscriptionService.mjs:update() — owns the action handler
ai/mcp/server/memory-core/openapi.yaml — manage_wake_subscription documented surface
test/playwright/unit/ai/mcp/server/memory-core/services/WakeSubscriptionService.spec.mjs — existing test surface includes update cases (lines ~270-300 from prior reads)
Need to inspect update()'s implementation to determine:
- Does it call
subscribe() then unsubscribe() (effective replace with new ID)?
- Does it mutate the existing node's properties in place?
- How are absent fields treated (preserved vs overwritten)?
The empirical evidence says replace-with-new-ID; the source code may confirm or contradict.
The Fix (Pending Diagnosis)
Likely shape based on REST/CRUD conventions + the empirical evidence:
- Preserve sub-id across updates.
update should mutate the existing node's properties in place; the sub-id should be invariant. Consumers (bridge daemon, MCP notification subscribers, A2A references) shouldn't have to handle sub-id churn.
- Field-level merge for nested objects. When updating
harnessTargetMetadata, merge with existing values rather than replacing. Pass harnessTargetMetadata: {appName: 'Antigravity'} should preserve tabShortcut if it already exists. Pass harnessTargetMetadata: null or explicit empty object could be the explicit-replace opt-in.
- Test coverage for both replace-style and merge-style call shapes; assert sub-id invariance.
- OpenAPI documentation clarifying the merge semantics so callers know whether to pass full metadata objects or just changed fields.
Acceptance Criteria
Out of Scope
- Migration of existing badly-replaced subs from #10440-era updates (Gemini's b3d1179c is the only known case; she'll re-update with explicit full metadata as the immediate fix)
- Refactoring
subscribe() / unsubscribe() separately (unless update() is implemented in terms of them, in which case the fix may need to touch them)
- New
patch / merge action variants (the existing update should be the merge-style path; introducing a separate action would fragment the surface)
Avoided Traps
- Trap: Patch only Gemini's specific sub via SQL UPDATE. Avoided: that fixes one symptom; the architectural contract of
update is the underlying concern. Future agents using update would re-encounter the same surprise.
- Trap: Document the current behavior as intended without challenging the contract. Avoided: REST/CRUD conventions (and substrate consumers like bridge daemon) expect merge-style updates with sub-id invariance. Codifying the surprising behavior would make the substrate harder to reason about, not easier.
Related
- Empirical anchor surfaced via #10440's live-data fix attempt
- Adjacent silent-fallback misroute concern (filed separately as the second follow-up from this session)
- Bootstrap action that creates the canonical metadata: #10412 (raw-SQL idempotency)
Origin Session ID: c68a7d4b-909a-4965-9bf9-116906d271a3
Retrieval Hint: "manage_wake_subscription update action overwrite merge semantics sub-id replacement empty harnessTargetMetadata"
Context
Surfaced empirically during post-#10441 wake-substrate routing diagnosis (2026-04-27, session
c68a7d4b).@neo-gemini-proinvokedmanage_wake_subscription({action: 'update', ...})to patch her own WAKE_SUB's staleappName: 'Cursor'(per #10440). Per her A2A description: "I have also updated my local wake subscriptionWAKE_SUB:de10611c-bd3a-4ec4-8dc0-49c487a4f5c2viamanage_wake_subscription(action:update)".Empirically observed outcome:
WAKE_SUB:de10611c-...was deleted (no longer present in canonical sqlite, neither active nor inactive)WAKE_SUB:b3d1179c-2d75-4816-9cb1-17669df51241was createdharnessTargetMetadatais{}(empty object) — neitherappNamenortabShortcutpopulatedThe Problem
The
updateaction ofmanage_wake_subscriptionapparently performs a wholesale replacement rather than a field-level merge. Two specific concerns:harnessTargetMetadataobject. If Gemini's call passed only the field she wanted to change (e.g.,{harnessTargetMetadata: {appName: 'Antigravity'}}), the implementation may have replaced the entire metadata object with whatever was passed (potentially{}if the call shape was malformed). PATCH-style updates conventionally merge fields by key.Both behaviors are surprising and warrant explicit decision: replace-vs-merge is a contract-level choice that should be documented clearly in the OpenAPI spec + tested with both shapes.
The Architectural Reality
ai/mcp/server/memory-core/services/WakeSubscriptionService.mjs:update()— owns the action handlerai/mcp/server/memory-core/openapi.yaml—manage_wake_subscriptiondocumented surfacetest/playwright/unit/ai/mcp/server/memory-core/services/WakeSubscriptionService.spec.mjs— existing test surface includesupdatecases (lines ~270-300 from prior reads)Need to inspect
update()'s implementation to determine:subscribe()thenunsubscribe()(effective replace with new ID)?The empirical evidence says replace-with-new-ID; the source code may confirm or contradict.
The Fix (Pending Diagnosis)
Likely shape based on REST/CRUD conventions + the empirical evidence:
updateshould mutate the existing node's properties in place; the sub-id should be invariant. Consumers (bridge daemon, MCP notification subscribers, A2A references) shouldn't have to handle sub-id churn.harnessTargetMetadata, merge with existing values rather than replacing. PassharnessTargetMetadata: {appName: 'Antigravity'}should preservetabShortcutif it already exists. PassharnessTargetMetadata: nullor explicit empty object could be the explicit-replace opt-in.Acceptance Criteria
update()from the source — confirm replace-vs-merge empiricallyupdatepreserves unspecifiedharnessTargetMetadatafieldsupdatecallsOut of Scope
subscribe()/unsubscribe()separately (unlessupdate()is implemented in terms of them, in which case the fix may need to touch them)patch/mergeaction variants (the existingupdateshould be the merge-style path; introducing a separate action would fragment the surface)Avoided Traps
updateis the underlying concern. Future agents usingupdatewould re-encounter the same surprise.Related
Origin Session ID: c68a7d4b-909a-4965-9bf9-116906d271a3
Retrieval Hint: "manage_wake_subscription update action overwrite merge semantics sub-id replacement empty harnessTargetMetadata"