fix(scope): harden final ownership boundaries
This commit is contained in:
@@ -1010,7 +1010,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
* quiescence"): for each session settle any pending prompt `cancelled`, then
|
||||
* run that session's {@link AgentHandle} `dispose()` — which stops the loop
|
||||
* (sets `disposed`, aborts the in-flight step), AWAITS the loop's exit (the
|
||||
* final `turn/end` + `session/flush` are captured while `onAppend` is still
|
||||
* final `turn/end` + `session/flush` are captured while the store-owned append observer is still
|
||||
* attached), unregisters the agent, and removes its session from the store.
|
||||
* The per-session disposes run in parallel. Idempotent — clears the `sessions`
|
||||
* map first and memoizes, so a second call (close racing dispose) is a no-op.
|
||||
|
||||
@@ -159,8 +159,8 @@ describe('acp bridge — disposal & HMR safety', () => {
|
||||
it('the final turn closing events are persisted across an AgentHandle dispose (durability)', async () => {
|
||||
// The teardown-ORDER guarantee: a per-agent dispose must stop the loop,
|
||||
// AWAIT its exit (so the loop's final `turn/end` + `session/flush` fire
|
||||
// through the still-attached `session.onAppend` → `session/event`), and only
|
||||
// THEN detach onAppend + remove the session. If the order were inverted
|
||||
// through the still-attached store observer → `session/event`), and only
|
||||
// THEN detach that observer + remove the session. If the order were inverted
|
||||
// (detach first), the closing events would never reach persistence. Drive a
|
||||
// CLEAN turn to completion, dispose JUST the bridge, then re-load the
|
||||
// persisted log from disk and assert the closing turn/end is on disk — the
|
||||
@@ -190,7 +190,7 @@ describe('acp bridge — disposal & HMR safety', () => {
|
||||
// produced BY the dispose itself. Here the model stream HANGS, so the turn is
|
||||
// still open when teardown runs: the composite agent effect stops the loop,
|
||||
// the loop unwinds and appends `turn/end {disposed}` + runs its final
|
||||
// `session/flush` — all while `onAppend` is still attached (the session
|
||||
// `session/flush` — all while the store-owned append observer is still attached (the session
|
||||
// detach is the LAST disposer in the same effect's LIFO chain) — and only
|
||||
// THEN is the session detached. If the order were inverted (or the session
|
||||
// were a racing SIBLING effect), the abort-produced `turn/end` would never
|
||||
@@ -255,7 +255,7 @@ describe('acp bridge — disposal & HMR safety', () => {
|
||||
// into ONE composite effect whose disposers run as a `.then()` chain. The
|
||||
// register disposer emits `agent/disposed`; if a listener throws and the
|
||||
// emit is UNCONTAINED, the rejected chain skips the LATER session-detach
|
||||
// disposer — stranding the session in the store with `onAppend` attached (a
|
||||
// disposer — stranding the session in the store with its append observer attached (a
|
||||
// leak AND a durability hole, since the new design relies on detach
|
||||
// running). The emit must be contained. Register a throwing listener, drive
|
||||
// a clean turn, dispose, and assert the session was STILL removed.
|
||||
|
||||
Reference in New Issue
Block a user