Skip to content

Agent Memory runtime inject: lock-refresh inject channel + dispatch-time fold (Wave 3)#88

Merged
MarkKropf merged 1 commit into
mainfrom
wave3-memory-inject
Jun 2, 2026
Merged

Agent Memory runtime inject: lock-refresh inject channel + dispatch-time fold (Wave 3)#88
MarkKropf merged 1 commit into
mainfrom
wave3-memory-inject

Conversation

@MarkKropf

Copy link
Copy Markdown
Contributor

Summary

The donmai (Go) half of agent-memory runtime inject. Pairs with platform PR RenseiAI/platform#202. Spec: runs/2026-06-02-wave3-inject-spec.md.

Delivers platform-computed memory blocks into a running agent session via the existing handle.Inject (claude --resume, between-turn). The live handle is goroutine-local in the worker subprocess, so the transport rides the per-session lock-refresh heartbeat (which runs in that process) — not the daemon poll path.

What's in here

  • Pulser (runtime/heartbeat/pulser.go): refreshResponse.inject {deliveryId,text} + refreshRequest.ackedInject echo; nil-safe Config.OnInject; honored only when refreshed==true.
  • Runner (runner/loop.go, runner/runner.go, runner/steering.go): Options.MemoryInjectEnabled (env MEMORY_INJECT_ENABLED, default off); buffered injectCh + dedup; drain at the post-terminal seam → shared injectDirectivehandle.Inject (non-fatal on ErrUnsupported/SessionNotReady/InjectInFlight). Single-goroutine, no concurrency hazard.
  • v1 dispatch-fold: memoryBlock field through PollWorkItem → SessionDetail → QueuedWork → prompt/builder.go (all forwarders together; folds under a # Agent Memory heading). Works for all providers.

Wire contract (matches platform #202)

  • request: { workerId, issueId, ackedInject? }
  • response: { refreshed, inject?: { deliveryId, text } }

Verification

  • make lint → 0 issues; make test (go test -race ./...) → all 49 packages OK, no flake re-runs; coverage ≥70% on changed packages (heartbeat 86.7%, prompt 81.9%, runner 73.5%, daemon 79.6%, afcli 73.8%).

Rollout (Phase C — user-driven)

Merge (queue rejects --auto) → tag release → cask donmai.rb + donmai-worker ghcr image → brew upgrade donmai + cloud worker-image pickup → set MEMORY_INJECT_ENABLED=true (workers + platform) → run rensei-smokes real-mode inject smoke.

🤖 Generated with Claude Code

Implements the donmai (Go) half of the agent-memory inject feature
(runs/2026-06-02-wave3-inject-spec.md steps 1-3).

STEP 1 — dispatch-time fold (v1, all providers):
Thread a new optional `memoryBlock` field through every wire hop
(PollWorkItem + pollItemToSessionDetail, SessionDetail, prompt.QueuedWork
via the embedded struct + detailToQueuedWork) so Go's strict JSON decoder
never silently drops the platform's emit (SUP-1840 wire-gap precedent).
prompt/builder.go appends it under a "# Agent Memory" heading after the
resolved system prompt on every path (base template, override, raymond) —
additive; empty/whitespace is a no-op.

STEP 2 — heartbeat pulser transport (v2):
Extend the lock-refresh request with `ackedInject` and the response with
an optional `inject {deliveryId, text}` object. Add Config.OnInject
(nil-safe) + Pulser.lastAckedInject. doRefresh fires OnInject and records
the DeliveryID only on a successful refresh (refreshed=true); a refused
refresh ignores any piggybacked inject (ownership lost). The pulser runs
inside the worker process that owns the live agent.Handle, so it is the
only authenticated channel that can reach Handle.Inject.

STEP 3 — runner runtime inject:
Add Options.MemoryInjectEnabled (default off; env-gated via
MEMORY_INJECT_ENABLED in afcli/agent_run.go, mirroring ROUTING_*_ENABLED).
runLoop creates a buffered injectCh + per-Run dedup map and wires the
heartbeat OnInject closure ONLY when the feature is enabled AND the
provider advertises SupportsMessageInjection. The closure dedupes by
DeliveryID (heartbeat-goroutine-owned) and non-blocking-sends. At the
post-terminal seam the runner drains all buffered injects on the single
runner goroutine via a shared injectDirective() factored out of
attemptSteering (non-fatal on agent.ErrUnsupported /
claude.ErrSessionNotReady / claude.ErrInjectInFlight), then re-consumes
the resume turn's events.

Tests: pulser inject decode/ack + refreshed:false guard; three-hop
memoryBlock decode/forward tests; prompt fold tests; runner drain +
cap/empty/dedupe/channel-full table tests. make lint (0 issues) and
make test (go test -race ./..., 49 pkgs, no flake re-runs) both green.
Changed-package coverage all >=70%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MarkKropf
MarkKropf merged commit 1fd5f05 into main Jun 2, 2026
1 check passed
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