@gotgenes/pi-subagents 11.1.0 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/docs/architecture/architecture.md +13 -11
- package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +180 -0
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +45 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +5 -23
- package/src/lifecycle/agent.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [11.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.1.0...pi-subagents-v11.2.0) (2026-05-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add Agent.resume() with internal observer lifecycle ([6cffb47](https://github.com/gotgenes/pi-packages/commit/6cffb47079e385b0ccd12e358c12357291be2ef0))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* release abort-signal listener when worktree setup fails ([ce2cac6](https://github.com/gotgenes/pi-packages/commit/ce2cac6788ffc90316f759e40e4df29576a70128))
|
|
19
|
+
|
|
8
20
|
## [11.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.0.1...pi-subagents-v11.1.0) (2026-05-28)
|
|
9
21
|
|
|
10
22
|
|
|
@@ -121,6 +121,8 @@ classDiagram
|
|
|
121
121
|
+markError()
|
|
122
122
|
+markStopped()
|
|
123
123
|
+resetForResume()
|
|
124
|
+
+run()
|
|
125
|
+
+resume(prompt, signal)
|
|
124
126
|
+abort(): boolean
|
|
125
127
|
+queueSteer(message)
|
|
126
128
|
+flushPendingSteers(session)
|
|
@@ -136,7 +138,7 @@ classDiagram
|
|
|
136
138
|
class AgentManager {
|
|
137
139
|
+spawn(snapshot, type, prompt, config)
|
|
138
140
|
+spawnAndWait(snapshot, type, prompt, config)
|
|
139
|
-
+resume(id,
|
|
141
|
+
+resume(id, prompt, signal)
|
|
140
142
|
+getRecord(id): Agent
|
|
141
143
|
+listAgents(): Agent[]
|
|
142
144
|
+abort(id)
|
|
@@ -738,15 +740,15 @@ The scheduling concern (queue, concurrency counter, drain) is tangled into `Agen
|
|
|
738
740
|
|
|
739
741
|
### Findings summary
|
|
740
742
|
|
|
741
|
-
| Finding
|
|
742
|
-
|
|
|
743
|
-
| ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~
|
|
744
|
-
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | 5 | 3 |
|
|
745
|
-
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 |
|
|
746
|
-
| ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~
|
|
747
|
-
| ~~`onSessionCreated` callback flows through 3 layers~~
|
|
748
|
-
|
|
|
749
|
-
|
|
|
743
|
+
| Finding | Category | Impact | Risk | Priority |
|
|
744
|
+
| ---------------------------------------------------------------------- | ------------ | ------ | ---- | -------- |
|
|
745
|
+
| ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | ✅ |
|
|
746
|
+
| ~~Agent cannot run itself — manager orchestrates 10 external touches~~ | C: Coupling | 5 | 3 | ✅ |
|
|
747
|
+
| ~~Scheduling tangled into `AgentManager` (3 fields, 3 methods)~~ | A: Coupling | 4 | 2 | ✅ |
|
|
748
|
+
| ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | ✅ |
|
|
749
|
+
| ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
|
|
750
|
+
| ~~`resume()` duplicates observer subscribe/unsubscribe pattern~~ | A: Redundant | 2 | 1 | ✅ |
|
|
751
|
+
| ~~`exec`/`registry` relay-only deps on `AgentManager`~~ | C: Coupling | 2 | 1 | ✅ |
|
|
750
752
|
|
|
751
753
|
### Step 1: Evolve AgentRecord into Agent with behavior — [#227] ✅ Complete
|
|
752
754
|
|
|
@@ -812,7 +814,7 @@ Drain calls `agent.run()` directly — no worktree setup, no args threading.
|
|
|
812
814
|
- Smell: A (tangled concerns) + C (cross-concern leak via `notifyConcurrencyChanged`)
|
|
813
815
|
- Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable; queue interface is trivial (agent has everything)
|
|
814
816
|
|
|
815
|
-
### Step 6: Agent.resume() with internal observer lifecycle — [#232]
|
|
817
|
+
### Step 6: Agent.resume() with internal observer lifecycle — [#232] ✅
|
|
816
818
|
|
|
817
819
|
Agent has the runner from construction.
|
|
818
820
|
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 232
|
|
3
|
+
issue_title: "Agent.resume() with internal observer lifecycle (Phase 15, Step 6)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent.resume() with internal observer lifecycle
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
After #229 (`Agent.run()` absorbs `startAgent`), the agent owns its entire run lifecycle but `AgentManager.resume()` still duplicates the observer subscribe/use/release pattern that `run()` handles internally.
|
|
11
|
+
The manager manually calls `subscribeAgentObserver`, wraps `runner.resume()` in a try/catch/finally, marks completion/error, and unsubscribes — the same acquire → use → release resource shape `Agent.run()` already encapsulates.
|
|
12
|
+
This is the last "manager reaches into Agent" duplication in the Phase 15 roadmap (priority 8, smell A: redundant pattern).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Add `Agent.resume(prompt, signal?)` that owns its observer subscription lifecycle, mirroring `run()`'s internal wiring.
|
|
17
|
+
- Reduce `AgentManager.resume()` to a guard-plus-delegation method (no `subscribeAgentObserver`, no try/finally).
|
|
18
|
+
- Preserve the existing public contract of `AgentManager.resume()` exactly: same signature, same `Agent | undefined` return, same behavior when the record or session is missing.
|
|
19
|
+
- Keep the change non-breaking (`feat:`, not `feat!:`).
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- No change to `runner.resume()` / `resumeAgent()` in `agent-runner.ts`.
|
|
24
|
+
- No change to the abort semantics of resume — the parent `signal` continues to flow straight through to `runner.resume({ signal })` (resume does not route through the agent's `abortController`, matching today's behavior).
|
|
25
|
+
- No queue interaction on resume — resume is not subject to the concurrency queue, so `onStarted`/`onRunFinished` are not fired (unchanged from today).
|
|
26
|
+
- No full rewrite of the stale `AgentManager`/`Agent` class diagram in `architecture.md` — that diagram already diverged in #229 (missing `run()`, stale `setupWorktree`/`completeRun`/`setOnRunFinished` signatures); a comprehensive diagram refresh is out of scope here.
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
Relevant modules (all under `packages/pi-subagents/src/`):
|
|
31
|
+
|
|
32
|
+
- `lifecycle/agent.ts` — the `Agent` class.
|
|
33
|
+
Already owns the per-run listener state (`_unsub`, `_detachFn`), the `attachObserver(unsub)` / `releaseListeners()` pair, `resetForResume(startedAt)` (which calls `releaseListeners()`), and `markCompleted` / `markError`.
|
|
34
|
+
Holds `_runner` and `observer` (an `AgentLifecycleObserver`) from construction (#229).
|
|
35
|
+
`Agent.run()` is the template to follow: it wires the observer via `attachObserver(subscribeAgentObserver(session, this, { onCompact: (r, info) => this.observer?.onCompacted?.(r, info) }))`.
|
|
36
|
+
- `lifecycle/agent-manager.ts` — `AgentManager.resume()` currently does the manual subscribe/try-finally dance and imports `subscribeAgentObserver` solely for that.
|
|
37
|
+
- `observation/record-observer.ts` — `subscribeAgentObserver(session, record, options)` returns an unsubscribe function; observes `tool_execution_end`, `message_end`, `compaction_end`.
|
|
38
|
+
- `lifecycle/agent-runner.ts` — `AgentRunner.resume(session, prompt, options?)` returns `Promise<string>` (the response text).
|
|
39
|
+
|
|
40
|
+
Constraint from AGENTS.md / `package-pi-subagents` skill: pi-subagents is a narrow core; this is a pure internal refactor (Tell-Don't-Ask, "state owns its mutations") with no policy or API-surface change.
|
|
41
|
+
|
|
42
|
+
### Observer routing equivalence
|
|
43
|
+
|
|
44
|
+
The manager's old resume wired compaction to the `AgentManagerObserver`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
subscribeAgentObserver(session, record, {
|
|
48
|
+
onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`Agent.resume()` instead routes through the per-agent `AgentLifecycleObserver` (`this.observer?.onCompacted?.`), exactly as `run()` does.
|
|
53
|
+
That lifecycle observer is built by `AgentManager.buildObserver()`, whose `onCompacted` forwards to `this.observer?.onAgentCompacted(agent, info)`.
|
|
54
|
+
Net routing is identical — compaction events still reach the manager-level `AgentManagerObserver.onAgentCompacted`.
|
|
55
|
+
|
|
56
|
+
## Design Overview
|
|
57
|
+
|
|
58
|
+
### `Agent.resume()`
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
async resume(prompt: string, signal?: AbortSignal): Promise<void> {
|
|
62
|
+
if (!this._runner) {
|
|
63
|
+
throw new Error("Agent not configured for execution — missing runner");
|
|
64
|
+
}
|
|
65
|
+
const session = this.session;
|
|
66
|
+
if (!session) {
|
|
67
|
+
throw new Error("Agent not configured for resume — missing session");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.resetForResume(Date.now()); // sets running, clears result/error, releases stale listeners
|
|
71
|
+
this.attachObserver(subscribeAgentObserver(session, this, {
|
|
72
|
+
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const responseText = await this._runner.resume(session, prompt, { signal });
|
|
77
|
+
this.markCompleted(responseText);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.markError(err);
|
|
80
|
+
} finally {
|
|
81
|
+
this.releaseListeners();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Decision model:
|
|
87
|
+
|
|
88
|
+
- `resetForResume()` already calls `releaseListeners()`, so any leftover handle from a prior run/resume is cleared before the new subscription is attached.
|
|
89
|
+
- The new subscription handle is stored via `attachObserver()` (reusing the `_unsub` slot shared with `run()`), and released in `finally` via `releaseListeners()`.
|
|
90
|
+
- Errors are captured (`markError`) rather than rethrown — `resume()` resolves like `run()`.
|
|
91
|
+
- The two guards (missing runner, missing session) mirror `run()`'s guard style.
|
|
92
|
+
They are defensive: the manager guards `agent?.session` before delegating, so the session guard is unreachable in normal flow but protects the invariant for direct `Agent.resume()` callers/tests.
|
|
93
|
+
|
|
94
|
+
### `AgentManager.resume()` (delegation)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
async resume(id: string, prompt: string, signal?: AbortSignal): Promise<Agent | undefined> {
|
|
98
|
+
const agent = this.agents.get(id);
|
|
99
|
+
if (!agent?.session) return undefined;
|
|
100
|
+
await agent.resume(prompt, signal);
|
|
101
|
+
return agent;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Edge cases preserved:
|
|
106
|
+
|
|
107
|
+
- Missing record → `undefined` (no throw).
|
|
108
|
+
- Record present but no session → `undefined` (no throw).
|
|
109
|
+
- Session present → delegate, return the agent.
|
|
110
|
+
|
|
111
|
+
After this change `agent-manager.ts` no longer references `subscribeAgentObserver` — that import must be removed.
|
|
112
|
+
`this.runner` is still used by `spawn()` (passed to the `Agent` constructor), so the `runner` field stays.
|
|
113
|
+
|
|
114
|
+
## Module-Level Changes
|
|
115
|
+
|
|
116
|
+
- `src/lifecycle/agent.ts`
|
|
117
|
+
- Add the public async method `resume(prompt: string, signal?: AbortSignal): Promise<void>` (placed near `run()` per the stepdown rule).
|
|
118
|
+
- No new imports — `subscribeAgentObserver` is already imported for `run()`.
|
|
119
|
+
- `src/lifecycle/agent-manager.ts`
|
|
120
|
+
- Replace the body of `resume()` with the guard-plus-delegation form above.
|
|
121
|
+
- Remove the now-unused `import { subscribeAgentObserver } from "#src/observation/record-observer";`.
|
|
122
|
+
- No other methods change.
|
|
123
|
+
- `src/lifecycle/agent-runner.ts` — unchanged.
|
|
124
|
+
- `src/observation/record-observer.ts` — unchanged.
|
|
125
|
+
- `docs/architecture/architecture.md` — light doc touch:
|
|
126
|
+
- In the class diagram, update `AgentManager.resume(id, snapshot, exec)` → `resume(id, prompt, signal)` and add `Agent.resume(prompt, signal)` (and, while there, `Agent.run()`, which #229 omitted).
|
|
127
|
+
- Mark Step 6 in the Phase 15 roadmap table/section as complete (`✅`).
|
|
128
|
+
- Note: the class diagram has pre-existing staleness from #229; this touch only corrects the resume-related entries, not the whole diagram.
|
|
129
|
+
|
|
130
|
+
Symbol-removal check: the only removed symbol is the `subscribeAgentObserver` import in `agent-manager.ts`.
|
|
131
|
+
`grep` confirms `subscribeAgentObserver` is still imported and used in `agent.ts` and defined in `record-observer.ts`, so the export stays live.
|
|
132
|
+
|
|
133
|
+
No file in Module-Level Changes is claimed unchanged in Non-Goals (the Non-Goals list `agent-runner.ts` and `record-observer.ts`, which are genuinely untouched).
|
|
134
|
+
|
|
135
|
+
## Test Impact Analysis
|
|
136
|
+
|
|
137
|
+
This is an extraction/relocation of behavior from the manager into the agent.
|
|
138
|
+
|
|
139
|
+
1. New unit tests enabled — `Agent.resume()` can now be tested directly on `Agent` (file `test/lifecycle/agent.test.ts`), which was previously impossible because resume logic lived only in the manager.
|
|
140
|
+
New direct coverage:
|
|
141
|
+
- `resume()` transitions to `completed` and sets `result` from the runner's response text.
|
|
142
|
+
- `resume()` transitions to `error` (and does not throw) when `runner.resume()` rejects.
|
|
143
|
+
- `resume()` subscribes the record-observer to the session (usage/compaction events accumulate on the agent) and releases the subscription in `finally` (handle cleared after completion and after error).
|
|
144
|
+
- `resume()` throws on missing runner / missing session (guard symmetry with `run()`).
|
|
145
|
+
- Compaction during resume forwards through `this.observer?.onCompacted?.`.
|
|
146
|
+
|
|
147
|
+
2. Existing tests that become redundant — none should be deleted.
|
|
148
|
+
The two manager-level resume tests in `test/lifecycle/agent-manager.test.ts` (`resume() also accumulates usage and increments compactions on the same record` and `calls injected runner.resume when resuming an agent`) now exercise the delegation + observer-forwarding integration rather than the inlined logic.
|
|
149
|
+
They stay as integration coverage of `AgentManager.resume()` → `Agent.resume()` and the `onCompacted` → `onAgentCompacted` routing.
|
|
150
|
+
`test/helpers/make-deps.test.ts` (calls `manager.resume(...)`) stays.
|
|
151
|
+
|
|
152
|
+
3. Existing tests that must stay as-is — the manager-level resume tests above genuinely exercise the manager's guard + delegation seam and the observer routing through `buildObserver`, which the agent-level tests do not cover.
|
|
153
|
+
|
|
154
|
+
## TDD Order
|
|
155
|
+
|
|
156
|
+
1. `test/lifecycle/agent.test.ts` — add `Agent.resume()` happy-path + error + guard tests, then implement `Agent.resume()` in `agent.ts`.
|
|
157
|
+
Covers: completed/result on success, error (no throw) on rejection, observer subscribe + `releaseListeners()` in `finally`, compaction forwarding via `onCompacted`, and the missing-runner / missing-session guards.
|
|
158
|
+
At this point both the new `Agent.resume()` and the old `AgentManager.resume()` body coexist (lift-and-shift: introduce the new method alongside the old logic).
|
|
159
|
+
Commit: `feat: add Agent.resume() with internal observer lifecycle`
|
|
160
|
+
2. `test/lifecycle/agent-manager.test.ts` — keep the existing resume tests green, then collapse `AgentManager.resume()` to the guard-plus-delegation form and remove the unused `subscribeAgentObserver` import in the same commit.
|
|
161
|
+
Removing the import and rewriting the body must land together — the type checker flags the unused import immediately, and the existing manager-level resume tests verify the delegation still satisfies the same contract.
|
|
162
|
+
Commit: `refactor: delegate AgentManager.resume() to Agent.resume()`
|
|
163
|
+
3. `docs/architecture/architecture.md` — update the class diagram resume entries (and add `Agent.run()`/`Agent.resume()`), mark Step 6 complete.
|
|
164
|
+
Commit: `docs: mark Phase 15 Step 6 (Agent.resume) complete`
|
|
165
|
+
|
|
166
|
+
## Risks and Mitigations
|
|
167
|
+
|
|
168
|
+
- Risk: observer routing diverges (compaction events stop reaching `onAgentCompacted`).
|
|
169
|
+
Mitigation: the existing manager-level test `resume() also accumulates usage and increments compactions on the same record` asserts `compactionCount` after resume; it stays green only if routing is preserved.
|
|
170
|
+
- Risk: listener leak if `releaseListeners()` is missed on the error path.
|
|
171
|
+
Mitigation: `releaseListeners()` is in `finally`; a dedicated agent-level test asserts the unsub handle is released after both success and error.
|
|
172
|
+
- Risk: behavior change in abort handling if resume is rerouted through `abortController`.
|
|
173
|
+
Mitigation: explicitly keep `signal` flowing straight to `runner.resume({ signal })` (Non-Goal), identical to today.
|
|
174
|
+
- Risk: removing the `subscribeAgentObserver` import while another caller still needs it.
|
|
175
|
+
Mitigation: `grep` confirms `agent.ts` is the only other importer and `record-observer.ts` still exports it.
|
|
176
|
+
|
|
177
|
+
## Open Questions
|
|
178
|
+
|
|
179
|
+
- Whether to later refresh the full `AgentManager`/`Agent` class diagram in `architecture.md` (stale since #229).
|
|
180
|
+
Deferred — out of scope for this issue; a focused follow-up can resync the whole diagram.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 232
|
|
3
|
+
issue_title: "Agent.resume() with internal observer lifecycle (Phase 15, Step 6)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #232 — Agent.resume() with internal observer lifecycle (Phase 15, Step 6)
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-28T18:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 3-step plan to move the observer subscribe/use/release pattern out of `AgentManager.resume()` into a new `Agent.resume(prompt, signal?)`, mirroring the `run()` wiring added in #229.
|
|
13
|
+
This is the last "manager reaches into Agent" duplication in the Phase 15 roadmap (Step 6, priority 8).
|
|
14
|
+
Confirmed the prerequisite #229 is closed and `Agent` already holds `_runner`, `observer`, `attachObserver`/`releaseListeners`, and `resetForResume`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- Non-breaking (`feat:`) — `AgentManager.resume()` keeps its signature and `Agent | undefined` contract; `Agent.resume()` is additive.
|
|
19
|
+
No `ask_user` needed; the issue's proposed change is concrete and unambiguous.
|
|
20
|
+
- Observer routing equivalence verified: old code wired `onCompact` → `AgentManagerObserver.onAgentCompacted`; new code routes through the per-agent `AgentLifecycleObserver.onCompacted`, which `buildObserver()` forwards to `onAgentCompacted`.
|
|
21
|
+
Net routing identical.
|
|
22
|
+
- Abort semantics intentionally preserved — `signal` flows straight to `runner.resume({ signal })`, not through the agent's `abortController` (resume differs from `run()` here; flagged as a Non-Goal to avoid accidental behavior change).
|
|
23
|
+
- Removing the `subscribeAgentObserver` import from `agent-manager.ts` must land in the same commit as the body rewrite (type checker flags the unused import). `grep` confirmed `agent.ts` remains the importer and `record-observer.ts` keeps the export live.
|
|
24
|
+
- Discovered the `architecture.md` class diagram is stale from #229 (missing `Agent.run()`, stale `setupWorktree`/`completeRun`/`setOnRunFinished` signatures, old `resume(id, snapshot, exec)`).
|
|
25
|
+
Scoped only a light touch (resume-related entries + Step 6 ✅); full diagram refresh deferred as a follow-up.
|
|
26
|
+
- Lift-and-shift TDD order: step 1 introduces `Agent.resume()` alongside the old manager logic; step 2 collapses the manager method and removes the import together.
|
|
27
|
+
Existing manager-level resume tests act as the integration safety net and stay.
|
|
28
|
+
|
|
29
|
+
## Stage: Implementation — TDD (2026-05-28T19:00:00Z)
|
|
30
|
+
|
|
31
|
+
### Session summary
|
|
32
|
+
|
|
33
|
+
Completed all 3 TDD steps in 3 commits plus a bonus `fix:` commit, totalling 4 new commits.
|
|
34
|
+
`Agent.resume()` added with full observer lifecycle, `AgentManager.resume()` collapsed to guard-plus-delegation, `subscribeAgentObserver` import removed from `agent-manager.ts`, and `architecture.md` updated.
|
|
35
|
+
Test count: 1042 → 1053 (+11).
|
|
36
|
+
|
|
37
|
+
### Observations
|
|
38
|
+
|
|
39
|
+
- **Bonus fix found mid-session:** A user question revealed a listener leak introduced in #229 — `Agent.run()` called `wireSignal()` before `setupWorktree()`, but the worktree-failure catch block returned without `releaseListeners()`, leaving the parent `AbortSignal` holding a reference to the errored agent.
|
|
40
|
+
Fixed TDD-style: failing test first (`"releases the parent-signal listener when worktree setup fails"` in `agent.test.ts`), then one-line fix adding `this.releaseListeners()` to the catch block in `run()`.
|
|
41
|
+
Committed as a separate `fix:` commit with a body attributing the regression to #229.
|
|
42
|
+
- **Pre-completion reviewer: WARN** — one non-blocking finding: the Phase 15 findings-summary table in `architecture.md` didn't mark the resolved rows (consistent pre-existing pattern from #229–#231).
|
|
43
|
+
Fixed by adding strikethrough + ✅ to all four resolved finding rows (#229 "Agent cannot run itself", #230 "Scheduling", #231 "exec/registry", #232 "resume()") in an additional `docs:` commit.
|
|
44
|
+
All other reviewer checks passed (Mermaid diagrams validated with `mmdc`, fallow clean, code design clean).
|
|
45
|
+
- **Reviewer warning resolved:** The findings table gap was pre-existing across four issues; closing it in this commit makes the table accurate going into Phase 16.
|
package/package.json
CHANGED
|
@@ -15,7 +15,6 @@ import type { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
|
15
15
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
16
16
|
import type { WorktreeManager } from "#src/lifecycle/worktree";
|
|
17
17
|
|
|
18
|
-
import { subscribeAgentObserver } from "#src/observation/record-observer";
|
|
19
18
|
import type { RunConfig } from "#src/runtime";
|
|
20
19
|
import type { AgentInvocation, CompactionInfo, IsolationMode, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
|
|
21
20
|
|
|
@@ -173,34 +172,17 @@ export class AgentManager {
|
|
|
173
172
|
|
|
174
173
|
/**
|
|
175
174
|
* Resume an existing agent session with a new prompt.
|
|
175
|
+
* Delegates to Agent.resume(), which owns the observer subscription lifecycle.
|
|
176
176
|
*/
|
|
177
177
|
async resume(
|
|
178
178
|
id: string,
|
|
179
179
|
prompt: string,
|
|
180
180
|
signal?: AbortSignal,
|
|
181
181
|
): Promise<Agent | undefined> {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
record.resetForResume(Date.now());
|
|
187
|
-
|
|
188
|
-
const unsubResume = subscribeAgentObserver(session, record, {
|
|
189
|
-
onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const responseText = await this.runner.resume(session, prompt, {
|
|
194
|
-
signal,
|
|
195
|
-
});
|
|
196
|
-
record.markCompleted(responseText);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
record.markError(err);
|
|
199
|
-
} finally {
|
|
200
|
-
unsubResume();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return record;
|
|
182
|
+
const agent = this.agents.get(id);
|
|
183
|
+
if (!agent?.session) return undefined;
|
|
184
|
+
await agent.resume(prompt, signal);
|
|
185
|
+
return agent;
|
|
204
186
|
}
|
|
205
187
|
|
|
206
188
|
getRecord(id: string): Agent | undefined {
|
package/src/lifecycle/agent.ts
CHANGED
|
@@ -250,6 +250,7 @@ export class Agent {
|
|
|
250
250
|
this.setupWorktree();
|
|
251
251
|
} catch (err) {
|
|
252
252
|
this.markError(err);
|
|
253
|
+
this.releaseListeners();
|
|
253
254
|
this.observer?.onRunFinished?.(this);
|
|
254
255
|
return;
|
|
255
256
|
}
|
|
@@ -285,6 +286,39 @@ export class Agent {
|
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Resume an existing session with a new prompt, managing the observer
|
|
291
|
+
* subscription lifecycle internally (same wiring as run()).
|
|
292
|
+
*
|
|
293
|
+
* Requires runner and an existing session (set when the original run created it).
|
|
294
|
+
* The returned promise always resolves (errors are captured internally).
|
|
295
|
+
* The parent signal flows straight through to runner.resume — resume does not
|
|
296
|
+
* route through this.abortController.
|
|
297
|
+
*/
|
|
298
|
+
async resume(prompt: string, signal?: AbortSignal): Promise<void> {
|
|
299
|
+
if (!this._runner) {
|
|
300
|
+
throw new Error("Agent not configured for execution — missing runner");
|
|
301
|
+
}
|
|
302
|
+
const session = this.session;
|
|
303
|
+
if (!session) {
|
|
304
|
+
throw new Error("Agent not configured for resume — missing session");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.resetForResume(Date.now());
|
|
308
|
+
this.attachObserver(subscribeAgentObserver(session, this, {
|
|
309
|
+
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const responseText = await this._runner.resume(session, prompt, { signal });
|
|
314
|
+
this.markCompleted(responseText);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
this.markError(err);
|
|
317
|
+
} finally {
|
|
318
|
+
this.releaseListeners();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
288
322
|
/** Increment tool use count. Called by record-observer on tool_execution_end. */
|
|
289
323
|
incrementToolUses(): void {
|
|
290
324
|
this._toolUses++;
|