fix(acp): address Codex review — strict ctx.get, correct fiber-ownership doc + test

- AgentLoop.resume uses `this.ctx.get('sessionPersistence')` (strict) instead
  of the `, false` overload: still topology-independent, but an inactive/
  absent backend reads as undefined (rejected by the existing guard) rather
  than being handed back mid-teardown.
- Correct the bridge teardown comment: an ACP-created agent's registry entry
  binds to the BRIDGE fiber (the factory is reached through the bridge's
  traceable proxy, so AgentLoop.start's `this.ctx.effect` registration uses the
  caller context), not the AgentLoop fiber — so an ACP-only HMR dispose
  reclaims it. Add a regression test pinning that ownership.
- Sync the ctx.get guidance in the post-mortem, packages/AGENTS.md, and the
  dsh-code-review skill to the strict form.
This commit is contained in:
Tianyi Cui
2026-06-18 03:48:33 +08:00
parent 86ec067bff
commit 7c168fca34
6 files changed
+53 -27

No files matched your search

+16 -14
View File
@@ -152,20 +152,22 @@ export class AgentLoop extends Service implements AgentFactory {
* by the time this runs the service exists.
*/
async resume(options: ResumeAgentOptions): Promise<Agent> {
// Read the service through `ctx.get(name, false)` — a direct global-store
// lookup keyed by the isolate symbol — NOT `this.ctx.sessionPersistence`.
// AgentLoop deliberately does NOT inject `sessionPersistence` (injecting it
// would pend non-persistent demos forever). The property proxy resolves a
// service by walking the current fiber's parent chain; from AgentLoop's own
// fiber (which lacks the inject) that walk never reaches the sibling backend
// fiber and throws "cannot get property … without inject". Worse, when the
// call arrives via a traceable shadow (e.g. the ACP bridge child fiber →
// `ctx.agents.resume()` → `this.factory.resume()`), the walk starts at the
// SHADOW's root fiber and fails the same way. `ctx.get(…, false)` sidesteps
// the fiber walk entirely (the same bypass the proxy itself takes when
// `!ctx.fiber.runtime`), so resume works from any caller fiber. `false`
// skips the ACTIVE-state check, since the backend lives on another fiber.
const persistence = this.ctx.get('sessionPersistence', false)
// Read the service through `ctx.get('sessionPersistence')` — a direct
// global-store lookup keyed by the isolate symbol — NOT
// `this.ctx.sessionPersistence`. AgentLoop deliberately does NOT inject
// `sessionPersistence` (injecting it would pend non-persistent demos
// forever). The `ctx.<name>` property proxy resolves a service by an
// ancestor-only walk of the current fiber's parent chain; from AgentLoop's
// own fiber (which lacks the inject) that walk never reaches the sibling
// backend fiber and throws "cannot get property … without inject". Worse,
// when the call arrives via a traceable shadow (e.g. the ACP bridge child
// fiber → `ctx.agents.resume()` → `this.factory.resume()`), the walk starts
// at the shadow's origin fiber and fails the same way. `ctx.get(name)`
// sidesteps the fiber walk entirely (a store lookup by the global isolate
// key), so resume works from any caller fiber. It is strict by default: a
// backend that is not ACTIVE (absent, or mid-teardown) reads as undefined
// and we reject below, rather than handing back an unusable handle.
const persistence = this.ctx.get('sessionPersistence')
if (persistence === undefined) {
throw new Error('cannot resume: session persistence is not configured (load a dsh-session-persistence backend)')
}