@gotgenes/pi-subagents 5.8.1 → 6.0.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 CHANGED
@@ -5,6 +5,41 @@ 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
+ ## [6.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.2...pi-subagents-v6.0.0) (2026-05-20)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * Subagent transcripts are now written in Pi's official JSONL session format via SessionManager.create() instead of the bespoke flat format. The output-file.ts module and its encodeCwd/createOutputFilePath/ writeInitialEntry/streamToOutputFile exports are removed. Transcript file paths change from /tmp/pi-subagents-<uid>/... to the Pi sessions directory.
14
+
15
+ ### Features
16
+
17
+ * add deriveSubagentSessionDir for session directory derivation ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([8442379](https://github.com/gotgenes/pi-packages/commit/8442379ae4433ba1428d703a24bef7b57c8624f2))
18
+ * remove bespoke output-file transcript format ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([1aab916](https://github.com/gotgenes/pi-packages/commit/1aab9166fc52e4f04e1d0a369788bf5c4a3da7c7))
19
+ * thread parent session info through spawn and run options ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([6f0d537](https://github.com/gotgenes/pi-packages/commit/6f0d537d745df63be43eb14de92caf42e65ab347))
20
+ * use persisted SessionManager for subagent sessions ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([ffafa69](https://github.com/gotgenes/pi-packages/commit/ffafa69d96068f881ec97e4f924245a308e542ba))
21
+ * wire session file path through agent-tool, remove output-file streaming ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([97acf0a](https://github.com/gotgenes/pi-packages/commit/97acf0a63cbe21e0954e79cd7db71e5632084454))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * use cwd in session-dir fallback path to namespace by project ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([0394420](https://github.com/gotgenes/pi-packages/commit/0394420237b9d23b35fe6c4e65b03fc267beaa0c))
27
+
28
+
29
+ ### Documentation
30
+
31
+ * plan session format transcript migration ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([68238c1](https://github.com/gotgenes/pi-packages/commit/68238c1c90c375c8469929399f18d0566e97a32c))
32
+ * **retro:** add retro notes for issue [#77](https://github.com/gotgenes/pi-packages/issues/77) ([004c99c](https://github.com/gotgenes/pi-packages/commit/004c99c4fba6b515360bb453eedbeb1218cebbc2))
33
+ * update architecture and package skill for session format migration ([#61](https://github.com/gotgenes/pi-packages/issues/61)) ([eef5e16](https://github.com/gotgenes/pi-packages/commit/eef5e16ad90118092badcbe2594c89c399919c15))
34
+
35
+ ## [5.8.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.1...pi-subagents-v5.8.2) (2026-05-20)
36
+
37
+
38
+ ### Documentation
39
+
40
+ * plan inject projectAgentsDir into AgentMenuDeps ([#77](https://github.com/gotgenes/pi-packages/issues/77)) ([d8cf039](https://github.com/gotgenes/pi-packages/commit/d8cf03944d0abc6230541d67655d89582665a615))
41
+ * **retro:** add retro notes for issue [#66](https://github.com/gotgenes/pi-packages/issues/66) ([ce0f04a](https://github.com/gotgenes/pi-packages/commit/ce0f04a3d84523a4c2e8b7bc998b5bec0f16970f))
42
+
8
43
  ## [5.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.8.0...pi-subagents-v5.8.1) (2026-05-20)
9
44
 
10
45
 
@@ -37,7 +37,7 @@ worktree.ts — git worktree isolation
37
37
  usage.ts — token usage tracking
38
38
  model-resolver.ts — fuzzy model name resolution
39
39
  invocation-config.ts — merge tool params with agent config
40
- output-file.ts — JSONL transcript streaming
40
+ session-dir.ts — subagent session directory derivation
41
41
  settings.ts — persistent operational settings
42
42
 
43
43
  cross-extension-rpc.ts — RPC over pi.events ← replacing
@@ -114,18 +114,18 @@ There is also a `Symbol.for("pi-subagents:manager")` export on `globalThis` that
114
114
  - **Group join** (`group-join.ts`) — 141 LOC removed.
115
115
  The grouped notification batching adds complexity for a marginal UX improvement.
116
116
  Individual completion notifications are sufficient.
117
- - **Output file** (`output-file.ts`) — 96 LOC removed.
118
- JSONL transcript streaming is a consumer concern; a separate extension can subscribe to lifecycle events and write transcripts.
117
+ - **Output file** (`output-file.ts`) — replaced by `session-dir.ts` + `SessionManager.create()` (#61).
118
+ Subagent transcripts are now written in Pi's official JSONL session format via the SDK's `SessionManager`, nested under the parent session directory.
119
119
 
120
120
  ### Estimated impact
121
121
 
122
- | Subsystem removed | LOC removed | LOC removed from index.ts |
123
- | ----------------- | ----------- | ------------------------- |
124
- | Scheduling | 612 | ~200 |
125
- | Ad-hoc RPC | 80 | ~50 |
126
- | Group join | 141 | ~100 |
127
- | Output file | 83 | ~50 |
128
- | **Total** | **~916** | **~400** |
122
+ | Subsystem removed | LOC removed | LOC removed from index.ts |
123
+ | ----------------- | ------------- | ------------------------- |
124
+ | Scheduling | 612 | ~200 |
125
+ | Ad-hoc RPC | 80 | ~50 |
126
+ | Group join | 141 | ~100 |
127
+ | Output file | 83 (replaced) | ~50 |
128
+ | **Total** | **~916** | **~400** |
129
129
 
130
130
  After removal and `index.ts` decomposition, the core shrinks from ~6,300 to ~5,400 LOC, and `index.ts` shrinks from ~1,894 to ~1,300 LOC.
131
131
 
@@ -314,9 +314,10 @@ Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
314
314
  Removed the `schedule` parameter from the `Agent` tool schema.
315
315
  Removed scheduler setup and lifecycle hooks from `index.ts`.
316
316
 
317
- ### Phase 3: Remove group-join, output-file, ad-hoc RPC
317
+ ### Phase 3: Remove group-join, ad-hoc RPC; replace output-file
318
318
 
319
- Delete `group-join.ts`, `output-file.ts`, `cross-extension-rpc.ts`.
319
+ Delete `group-join.ts`, `cross-extension-rpc.ts`.
320
+ Replace `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
320
321
  Simplify `index.ts` to use direct individual notifications.
321
322
  Emit lifecycle events on `pi.events` for external consumers.
322
323
 
@@ -395,8 +396,9 @@ Small cleanups that are safest after the structural changes settle.
395
396
 
396
397
  ### Phase 4: Features and cross-cutting concerns
397
398
 
398
- 1. **gotgenes/pi-packages#61** — Port transcript logging to Pi's official JSONL session format
399
- - Feature work that should happen after structural refactoring is complete so the output-file subsystem has a stable home.
399
+ 1. **gotgenes/pi-packages#61** — Port transcript logging to Pi's official JSONL session format
400
+ - Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts`.
401
+ - Subagent sessions are persisted under `<parent-session-dir>/<parent-session-basename>/tasks/` with `parentSession` header linking.
400
402
 
401
403
  2. **gotgenes/pi-packages#22** — Parent-session resolution for `nicobailon/pi-subagents` children
402
404
  - Cross-extension issue that spans `pi-permission-system` and `pi-subagents`.
@@ -417,7 +419,7 @@ Small cleanups that are safest after the structural changes settle.
417
419
  #66 (type casts) ◄─────(after structural changes settle)
418
420
  #77 (projectAgentsDir) ◄─(after #66 or parallel)
419
421
 
420
- #61 (transcript format) ◄(after structural refactor)
422
+ #61 (transcript format)
421
423
  #22 (parent session) ◄──(cross-extension, independent)
422
424
  ```
423
425
 
@@ -426,10 +428,11 @@ Small cleanups that are safest after the structural changes settle.
426
428
  The recommended sequence is:
427
429
 
428
430
  ```text
429
- #69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61
431
+ #69 ✓ → #71 ✓ → #80 ✓ → #76 ✓ → #84 ✓ → #72 ✓ → #87 ✓ → #70 ✓ → #66 → #77 → #61
430
432
  ```
431
433
 
432
434
  Phase 1 is complete; Phase 2 is complete.
435
+ Issue #61 (transcript format) is complete.
433
436
  The next issue is #66 (replace `as any` casts with proper SDK types).
434
437
  Issue #22 is a parallel cross-extension track and does not gate the structural work.
435
438
 
@@ -0,0 +1,284 @@
1
+ ---
2
+ issue: 61
3
+ issue_title: "feat: port subagent transcript logging to Pi's official JSONL session format"
4
+ ---
5
+
6
+ # Port subagent transcripts to Pi's official session format
7
+
8
+ ## Problem Statement
9
+
10
+ Subagent conversation transcripts are written as JSONL by `output-file.ts`, but use a bespoke flat format (`{ isSidechain, agentId, type, message, timestamp, cwd }`) that does not conform to Pi's official session file format.
11
+ Pi's `SessionManager` writes tree-structured JSONL with a session header, UUIDv7 entry IDs, `parentId` tree structure, and typed entry discriminants (`"message"`, `"compaction"`, etc.).
12
+ The bespoke format cannot be loaded by Pi's session tooling (session selector, export-html, resume) and requires manual `jq` inspection for debugging.
13
+
14
+ ## Goals
15
+
16
+ - Subagent transcripts written in Pi's official JSONL session format via `SessionManager`.
17
+ - Transcripts discoverable via the parent session path (nested under a parent-session-relative directory).
18
+ - Parent session linkage via the `parentSession` header field.
19
+ - Delete `output-file.ts` — the SDK's `SessionManager` handles all JSONL writing natively.
20
+ - Existing debugging use case preserved (full turn-by-turn history on disk).
21
+
22
+ ## Non-Goals
23
+
24
+ - Making subagent sessions resumable via Pi's `/resume` command (future work; requires session selection UX changes).
25
+ - Cross-extension parent-session resolution (issue #22 — separate track).
26
+ - Changing the `SessionManager` usage for the child `AgentSession` itself (the child session already uses `SessionManager.inMemory()`; we replace it with a persisted one).
27
+
28
+ ## Background
29
+
30
+ ### Current flow
31
+
32
+ 1. `agent-tool.ts` calls `createOutputFilePath(cwd, agentId, parentSessionId)` → creates `/tmp/pi-subagents-<uid>/<encoded-cwd>/<parentSessionId>/tasks/<agentId>.output`.
33
+ 2. `writeInitialEntry(path, agentId, prompt, cwd)` writes the first user message in the bespoke format.
34
+ 3. `streamToOutputFile(session, path, agentId, cwd)` subscribes to `turn_end` events and appends bespoke JSONL entries.
35
+ 4. `agent-manager.ts` calls the cleanup function on completion/error to do a final flush and unsubscribe.
36
+
37
+ ### Pi's SessionManager API
38
+
39
+ `SessionManager` from `@earendil-works/pi-coding-agent` provides:
40
+
41
+ - `SessionManager.create(cwd, sessionDir?)` — creates a persisted session file in the given directory.
42
+ - `newSession({ parentSession? })` — writes a `SessionHeader` with `parentSession` linking.
43
+ - `appendMessage(message)` — writes a `SessionMessageEntry` with auto-generated UUIDv7 `id` and `parentId` (tree structure).
44
+ - `appendCompaction(...)`, `appendCustomEntry(...)` — first-class support for all entry types.
45
+ - `getSessionFile()` — returns the path to the JSONL file on disk.
46
+
47
+ The child `AgentSession` (created by `createAgentSession`) accepts a `sessionManager` option.
48
+ Currently set to `SessionManager.inMemory(cwd)`, switching to `SessionManager.create(cwd, sessionDir)` makes the SDK write official-format JSONL automatically — no manual streaming code needed.
49
+
50
+ ### Reference implementations
51
+
52
+ nicobailon/pi-subagents places subagent sessions under `<parent-session-dir>/<parent-session-basename>/<runId>/`.
53
+ This keeps them discoverable via the parent session path without cluttering the main session list.
54
+ Our approach follows the same pattern: derive a `sessionDir` from the parent session file.
55
+
56
+ ### Constraints
57
+
58
+ - `agent-runner.ts` already imports `SessionManager` for the `inMemory()` call — switching to `create()` adds no new dependency.
59
+ - `ctx.sessionManager` is `ReadonlySessionManager` and provides `getSessionId()`, `getSessionFile()`, and `getSessionDir()`.
60
+ - The `code-style` skill requires keeping IO at the edges and not hiding dependencies.
61
+ The session directory derivation should be a pure function that receives the parent session file path.
62
+
63
+ ## Design Overview
64
+
65
+ ### Core change: persisted SessionManager in agent-runner
66
+
67
+ Replace `SessionManager.inMemory(cwd)` with `SessionManager.create(cwd, sessionDir)` inside `runAgent()`.
68
+ The `sessionDir` is derived from the parent's session file path, and the `parentSession` option links the child to the parent.
69
+
70
+ The key insight is that the SDK's `createAgentSession` already writes all messages through the `SessionManager` it receives.
71
+ By switching from in-memory to persisted, every message the child agent produces is automatically written in official JSONL format — no manual subscription or streaming code required.
72
+
73
+ ### Session directory derivation
74
+
75
+ ```typescript
76
+ /**
77
+ * Derive the session directory for a subagent from the parent session file.
78
+ * Layout: <parent-dir>/<parent-basename>/tasks/
79
+ *
80
+ * Example:
81
+ * parent: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_.jsonl
82
+ * result: ~/.pi/agent/sessions/--home-user-project--/2026-05-20T12-00-00Z_/tasks/
83
+ *
84
+ * Falls back to a temp directory when the parent session is not persisted.
85
+ */
86
+ function deriveSubagentSessionDir(
87
+ parentSessionFile: string | undefined,
88
+ cwd: string,
89
+ ): string;
90
+ ```
91
+
92
+ ### Data flow (after)
93
+
94
+ 1. `agent-tool.ts` passes `parentSessionFile` (from `ctx.sessionManager.getSessionFile()`) and `parentSessionId` to `agent-manager.ts` via `SpawnOptions`.
95
+ 2. `agent-manager.ts` threads them to `agent-runner.ts` via `RunOptions`.
96
+ 3. `agent-runner.ts` calls `deriveSubagentSessionDir(parentSessionFile, cwd)` and creates `SessionManager.create(cwd, sessionDir)` with `{ parentSession: parentSessionId }`.
97
+ 4. The `createAgentSession` SDK call receives this persisted `SessionManager`.
98
+ 5. All messages are written to disk automatically by the SDK.
99
+ 6. `agent-runner.ts` returns the session file path in `RunResult` (via `sessionManager.getSessionFile()`).
100
+ 7. `agent-tool.ts` stores the path on `AgentRecord.outputFile` for display in notifications/UI.
101
+
102
+ ### What gets deleted
103
+
104
+ - `output-file.ts` — entirely replaced by the persisted `SessionManager`.
105
+ - `output-file.test.ts` — the `encodeCwd` tests are no longer needed (Pi's `SessionManager` handles directory encoding internally).
106
+ - `streamToOutputFile`, `writeInitialEntry`, `createOutputFilePath` imports from `agent-tool.ts`.
107
+ - `outputCleanup` field from `AgentRecord` (no manual cleanup needed; the `SessionManager` is append-only).
108
+ - The `onSessionCreated` wrapper in `agent-tool.ts` that wires output-file streaming.
109
+
110
+ ### What changes
111
+
112
+ - `RunOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
113
+ - `RunResult` gains `sessionFile?: string` (path to the persisted session JSONL).
114
+ - `SpawnOptions` gains `parentSessionFile?: string` and `parentSessionId?: string`.
115
+ - `AgentRecord.outputCleanup` is removed (no manual flush/unsubscribe lifecycle).
116
+ - `agent-manager.ts` removes the `outputCleanup` calls in completion/error paths.
117
+ - `agent-tool.ts` sets `record.outputFile` from the `RunResult.sessionFile` instead of calling `createOutputFilePath`.
118
+ - `notification.ts`, `renderer.ts`, `types.ts` — `outputFile` field semantics unchanged (still a path string); only the source changes.
119
+
120
+ ### Edge cases
121
+
122
+ 1. **Parent session not persisted** (e.g., API/headless mode where the parent uses `SessionManager.inMemory()`): `ctx.sessionManager.getSessionFile()` returns `undefined`.
123
+ Fallback: use a temp directory under `/tmp/pi-subagents-<uid>/` (similar to current behavior) so transcripts are still written to disk.
124
+ 2. **Worktree isolation**: `effectiveCwd` differs from `ctx.cwd`.
125
+ The `SessionManager.create()` call uses `effectiveCwd` (same as current `inMemory` call), but the `sessionDir` is still derived from the parent session — the session header's `cwd` field correctly records the worktree path.
126
+
127
+ ## Module-Level Changes
128
+
129
+ ### New file: `src/session-dir.ts`
130
+
131
+ Pure function `deriveSubagentSessionDir(parentSessionFile, cwd)`.
132
+ Extracts the parent session basename, constructs `<parent-dir>/<parent-basename>/tasks/`.
133
+ Falls back to a temp directory when `parentSessionFile` is undefined.
134
+
135
+ ### Modified: `src/agent-runner.ts`
136
+
137
+ - Import `deriveSubagentSessionDir` from `session-dir.ts`.
138
+ - Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions`.
139
+ - Add `sessionFile?: string` to `RunResult`.
140
+ - Replace `SessionManager.inMemory(cfg.effectiveCwd)` with:
141
+
142
+ ```typescript
143
+ const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
144
+ const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
145
+ sessionManager.newSession({ parentSession: options.parentSessionId });
146
+ ```
147
+
148
+ - After `session.prompt()`, capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
149
+
150
+ ### Modified: `src/agent-manager.ts`
151
+
152
+ - Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions`.
153
+ - Thread them to `RunOptions` in the `runner.run()` call.
154
+ - Remove `outputCleanup` calls from the completion and error handlers.
155
+
156
+ ### Modified: `src/tools/agent-tool.ts`
157
+
158
+ - Remove import of `createOutputFilePath`, `streamToOutputFile`, `writeInitialEntry`.
159
+ - Remove the `onSessionCreated` wrapper that wired output-file streaming.
160
+ - Pass `parentSessionFile: ctx.sessionManager.getSessionFile()` and `parentSessionId: ctx.sessionManager.getSessionId()` in `SpawnOptions`.
161
+ - After spawn, set `record.outputFile` from the returned session file path (available via `onSessionCreated` callback which gives access to the session's `sessionManager`).
162
+
163
+ ### Modified: `src/types.ts`
164
+
165
+ - Remove `outputCleanup?: () => void` from `AgentRecord`.
166
+
167
+ ### Deleted: `src/output-file.ts`
168
+
169
+ Entire file removed.
170
+
171
+ ### Deleted: `test/output-file.test.ts`
172
+
173
+ Entire file removed.
174
+
175
+ ### Unchanged: `src/notification.ts`, `src/renderer.ts`
176
+
177
+ These read `record.outputFile` (a path string) — the field still exists, just populated differently.
178
+ The `NotificationDetails.outputFile` field is unchanged.
179
+
180
+ ## Test Impact Analysis
181
+
182
+ ### New tests enabled by extraction
183
+
184
+ 1. `test/session-dir.test.ts` — unit tests for `deriveSubagentSessionDir`:
185
+ - Parent session file present → derives correct nested path.
186
+ - Parent session file undefined → falls back to temp directory.
187
+ - Various path shapes (POSIX, Windows-like).
188
+
189
+ 2. `test/agent-runner.test.ts` — updated tests verifying:
190
+ - `SessionManager.create` is called (not `inMemory`) when `parentSessionFile` is provided.
191
+ - `newSession({ parentSession })` is called with the parent session ID.
192
+ - `RunResult.sessionFile` is populated from the session manager.
193
+
194
+ ### Tests that become redundant
195
+
196
+ - `test/output-file.test.ts` (`encodeCwd` tests) — the directory encoding is now handled by `deriveSubagentSessionDir` with a different layout.
197
+ The `encodeCwd` function is deleted.
198
+
199
+ ### Tests that stay as-is
200
+
201
+ - All `agent-manager.test.ts` tests that mock the runner — they don't depend on `output-file` internals.
202
+ - All `notification.test.ts` and `renderer.test.ts` tests — they read `record.outputFile` which remains a string.
203
+ - The `onSessionCreated` callback tests in `agent-tool.test.ts` need updating to remove the output-file wiring expectations.
204
+
205
+ ## TDD Order
206
+
207
+ ### Step 1: Add `deriveSubagentSessionDir` with tests
208
+
209
+ 1. Create `src/session-dir.ts` with the pure function.
210
+ 2. Create `test/session-dir.test.ts` with tests for parent-present and fallback cases.
211
+ 3. Commit: `feat: add deriveSubagentSessionDir for session directory derivation (#61)`
212
+
213
+ ### Step 2: Thread parent session info through SpawnOptions and RunOptions
214
+
215
+ 1. Add `parentSessionFile?: string` and `parentSessionId?: string` to `SpawnOptions` in `agent-manager.ts`.
216
+ 2. Add `parentSessionFile?: string` and `parentSessionId?: string` to `RunOptions` in `agent-runner.ts`.
217
+ 3. Add `sessionFile?: string` to `RunResult` in `agent-runner.ts`.
218
+ 4. Thread the new fields from `SpawnOptions` → `RunOptions` in `agent-manager.ts`.
219
+ 5. Update tests to verify the new fields are threaded.
220
+ 6. Commit: `feat: thread parent session info through spawn and run options (#61)`
221
+
222
+ ### Step 3: Switch agent-runner to persisted SessionManager
223
+
224
+ 1. In `runAgent()`, replace `SessionManager.inMemory(cfg.effectiveCwd)` with the persisted variant using `deriveSubagentSessionDir`.
225
+ 2. Call `sessionManager.newSession({ parentSession: options.parentSessionId })`.
226
+ 3. Capture `sessionManager.getSessionFile()` into `RunResult.sessionFile`.
227
+ 4. Update `agent-runner.test.ts` mocks to expect `SessionManager.create` instead of `SessionManager.inMemory`.
228
+ 5. Commit: `feat: use persisted SessionManager for subagent sessions (#61)`
229
+
230
+ ### Step 4: Remove output-file wiring from agent-tool and agent-manager
231
+
232
+ 1. Remove the `onSessionCreated` output-file wrapper in `agent-tool.ts`.
233
+ 2. Remove `createOutputFilePath`, `writeInitialEntry`, `streamToOutputFile` imports.
234
+ 3. Set `record.outputFile` from the session manager's file path (via `onSessionCreated` callback).
235
+ 4. Pass `parentSessionFile` and `parentSessionId` in spawn options.
236
+ 5. Remove `outputCleanup` calls from `agent-manager.ts` completion/error handlers.
237
+ 6. Remove `outputCleanup` from `AgentRecord` in `types.ts`.
238
+ 7. Update agent-tool and agent-manager tests.
239
+ 8. Commit: `feat: wire session file path through agent-tool, remove output-file streaming (#61)`
240
+
241
+ ### Step 5: Delete output-file.ts and its tests
242
+
243
+ 1. Delete `src/output-file.ts`.
244
+ 2. Delete `test/output-file.test.ts`.
245
+ 3. Run full test suite to verify no remaining references.
246
+ 4. Commit: `feat!: remove bespoke output-file transcript format (#61)`
247
+
248
+ ### Step 6: Documentation update
249
+
250
+ 1. Update `docs/architecture/architecture.md` — remove `output-file.ts` from the module listing, add `session-dir.ts`, mark #61 as complete.
251
+ 2. Commit: `docs: update architecture for session format migration (#61)`
252
+
253
+ ## Risks and Mitigations
254
+
255
+ ### Risk: SessionManager.create may fail in environments without a writable home directory
256
+
257
+ The temp-directory fallback in `deriveSubagentSessionDir` handles this case.
258
+ When the parent session file is unavailable (headless/API mode), we fall back to a temp directory just like the current implementation.
259
+
260
+ ### Risk: Persisted sessions accumulate disk space over time
261
+
262
+ Pi's session tooling already manages session storage.
263
+ Subagent sessions nested under the parent session directory are cleaned up when the parent session is deleted.
264
+ This is an improvement over the current `/tmp` location where files persist until system cleanup.
265
+
266
+ ### Risk: Tests that mock SessionManager.inMemory need updating
267
+
268
+ Step 3 explicitly plans for updating these mocks.
269
+ The change is localized to `agent-runner.ts` and its direct test file.
270
+
271
+ ### Risk: Breaking change for consumers that read outputFile paths
272
+
273
+ The `outputFile` field changes from `/tmp/pi-subagents-<uid>/.../tasks/<agentId>.output` to `~/.pi/agent/sessions/.../<timestamp>_<uuid>.jsonl`.
274
+ Consumers that parse the path (unlikely) would break, but the field is internal to the extension and only used for display.
275
+ The format of the file content changes from bespoke JSONL to Pi's official format — this is the intentional breaking change.
276
+
277
+ ## Open Questions
278
+
279
+ 1. Should the plan add a `sessionDir` field to `AgentRecord` alongside `outputFile`, or is the session file path sufficient for all use cases?
280
+ Deferred — start with `outputFile` pointing to the session file; add `sessionDir` if needed later.
281
+ 2. Should foreground agents also get persisted sessions, or only background agents?
282
+ The current `output-file` code only runs for background agents.
283
+ Persisted sessions are valuable for both — the plan applies the change in `agent-runner.ts` which serves both paths.
284
+ If this proves too noisy, a follow-up can gate it behind a setting.
@@ -0,0 +1,115 @@
1
+ ---
2
+ issue: 77
3
+ issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
4
+ ---
5
+
6
+ # Inject projectAgentsDir into AgentMenuDeps
7
+
8
+ ## Problem Statement
9
+
10
+ `createAgentsMenuHandler` computes the project agents directory by reading `process.cwd()` inline via a lambda on line 63 of `ui/agent-menu.ts`:
11
+
12
+ ```typescript
13
+ const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
14
+ ```
15
+
16
+ The `AgentMenuDeps` interface already carries `personalAgentsDir: string` as an explicit field, but the project-side equivalent bypasses the injection boundary entirely.
17
+ `projectAgentsDir()` is called in at least five places inside the handler (`findAgentFile`, `ejectAgent`, `disableAgent`, `showCreateWizard`, `showManualWizard`).
18
+ This violates the code-style rule: "Do not read `process.cwd()` inside library/utility functions — accept the value as a parameter."
19
+
20
+ ## Goals
21
+
22
+ - Add `projectAgentsDir: string` to `AgentMenuDeps`.
23
+ - Remove the inline `projectAgentsDir` lambda from `createAgentsMenuHandler`.
24
+ - Replace all five call sites with `deps.projectAgentsDir`.
25
+ - Wire the value at the call site in `index.ts` as `join(process.cwd(), ".pi", "agents")`.
26
+ - Update the test helper `makeDeps()` in `agent-menu.test.ts` to supply the new field.
27
+
28
+ ## Non-Goals
29
+
30
+ - Removing other `process.cwd()` calls in `index.ts` (e.g., `loadCustomAgents`, `GitWorktreeManager`).
31
+ Those are separate concerns tracked or already addressed elsewhere (e.g., #76 for `AgentManager`).
32
+ - Changing `personalAgentsDir` wiring or any other `AgentMenuDeps` fields.
33
+
34
+ ## Background
35
+
36
+ ### Relevant modules
37
+
38
+ | Module | Role |
39
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
40
+ | `src/ui/agent-menu.ts` | Contains `AgentMenuDeps` interface and `createAgentsMenuHandler` factory. Owns the inline `projectAgentsDir` lambda. |
41
+ | `src/index.ts` | Extension entry point. Constructs the `AgentMenuDeps` object at line 228. Already passes `personalAgentsDir`. |
42
+ | `test/ui/agent-menu.test.ts` | Tests for the menu handler. Has a `makeDeps()` helper that constructs `AgentMenuDeps`. |
43
+
44
+ ### Constraints
45
+
46
+ From code-style skill:
47
+
48
+ > Do not read `process.env`, `process.cwd()`, or `process.platform` inside library/utility functions — accept the value as a parameter.
49
+
50
+ This is the same pattern applied in #76 (inject `cwd` into `AgentManager`), now applied to the UI layer.
51
+
52
+ `AgentMenuDeps` is internal — the public API surface (`exports` in `package.json`) is `service.ts` only, so this is a non-breaking change for consumers.
53
+
54
+ ## Design Overview
55
+
56
+ The change is mechanical — add a field, remove a lambda, replace call sites:
57
+
58
+ 1. Add `projectAgentsDir: string` to `AgentMenuDeps` (mirrors the existing `personalAgentsDir` field).
59
+ 2. Delete the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda inside `createAgentsMenuHandler`.
60
+ 3. Replace all five `projectAgentsDir()` call sites with `deps.projectAgentsDir`:
61
+ - `findAgentFile` (line 68)
62
+ - `ejectAgent` (line 312)
63
+ - `disableAgent` (line 378)
64
+ - `showCreateWizard` (line 416)
65
+ - `showManualWizard` — uses the `targetDir` value from `showCreateWizard`, so not a direct call site
66
+ 4. At the call site in `index.ts`, add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the deps object.
67
+ 5. In `makeDeps()`, add `projectAgentsDir: "/test-project/.pi/agents"`.
68
+
69
+ No new types, no interface changes beyond the one added field.
70
+
71
+ ## Module-Level Changes
72
+
73
+ ### `src/ui/agent-menu.ts`
74
+
75
+ - Add `projectAgentsDir: string` to the `AgentMenuDeps` interface.
76
+ - Remove the `const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");` lambda.
77
+ - Replace `projectAgentsDir()` with `deps.projectAgentsDir` at all call sites inside the factory closure.
78
+ - The `join` import from `node:path` may become unused in this file if no other call uses it — verify and remove if so.
79
+
80
+ ### `src/index.ts`
81
+
82
+ - Add `projectAgentsDir: join(process.cwd(), ".pi", "agents")` to the `createAgentsMenuHandler({...})` call.
83
+ - Import `join` from `node:path` if not already imported.
84
+
85
+ ### `test/ui/agent-menu.test.ts`
86
+
87
+ - Add `projectAgentsDir: "/test-project/.pi/agents"` to `makeDeps()`.
88
+ - No other test logic changes.
89
+
90
+ ## Test Impact Analysis
91
+
92
+ 1. No new unit tests are strictly required — the refactoring is mechanical and preserves behavior.
93
+ However, a targeted test verifying that `findAgentFile` uses the injected `projectAgentsDir` (not `process.cwd()`) is valuable to prevent regression.
94
+ 2. No existing tests become redundant.
95
+ 3. All existing tests stay as-is; only the `makeDeps()` helper needs the new field.
96
+
97
+ ## TDD Order
98
+
99
+ 1. **Red: test that the injected projectAgentsDir is used** — add a test in `agent-menu.test.ts` that mocks `existsSync` and verifies `findAgentFile` (exercised via the menu handler) checks a path under the injected `projectAgentsDir`, not under `process.cwd()`.
100
+ Commit message: `test: verify projectAgentsDir injection in agent menu (#77)`
101
+
102
+ 2. **Green: add projectAgentsDir to AgentMenuDeps and wire it** — add the field to `AgentMenuDeps`, remove the inline lambda, replace all call sites with `deps.projectAgentsDir`, wire the value in `index.ts`, and update `makeDeps()` in the test helper.
103
+ Commit message: `refactor: inject projectAgentsDir into AgentMenuDeps (#77)`
104
+
105
+ ## Risks and Mitigations
106
+
107
+ | Risk | Mitigation |
108
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109
+ | Missing a `projectAgentsDir()` call site | `grep 'projectAgentsDir'` in `agent-menu.ts` confirms exactly five call sites (one definition + four usages). After the change, grep again to verify no `process.cwd()` calls remain. |
110
+ | `join` import becomes unused in `agent-menu.ts` | Check whether `join` is used elsewhere in the file (it is — `findAgentFile` uses `join` for the personal path). The import stays. |
111
+ | Behavioral change | Production call site passes `join(process.cwd(), ".pi", "agents")`, which produces the same value the lambda computed. No behavioral change. |
112
+
113
+ ## Open Questions
114
+
115
+ None — the issue's proposed change is unambiguous and mirrors the established `personalAgentsDir` pattern.
@@ -0,0 +1,44 @@
1
+ ---
2
+ issue: 66
3
+ issue_title: "refactor: replace `as any` casts in extracted tool/menu factories with proper SDK types"
4
+ ---
5
+
6
+ # Retro: #66 — replace `as any` casts with proper SDK types
7
+
8
+ ## Final Retrospective (2026-05-20T18:50:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Replaced all 14 `as any` casts in `src/index.ts` by typing 5 factory dep interfaces with proper SDK types (`ExtensionContext`, `AgentSession`, `ExtensionAPI`, `ModelRegistry`) and the newly-exported `SpawnOptions`.
13
+ Released as `pi-subagents-v5.8.1` with zero behavioral change across 520 tests.
14
+ The plan, implementation, and shipping were completed in a single session with 6 implementation commits.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - Thorough context-gathering during planning paid off: reading all 5 dep interfaces, the SDK `.d.ts` files, test mock helpers, and the `ExtensionUIContext` shape before writing the plan meant most steps landed on first `pnpm run check`.
21
+ - The plan's risk table anticipated the cascading `as any` issue on `createAgentTool` and prepared a mitigation ("add explicit `satisfies ToolDefinition<…>` if needed"), which was close to the actual fix needed.
22
+ - Steps 4 and 5 (`GetResultDeps`, `SteerToolDeps`) were trivially clean — single-type-import changes that compiled immediately.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `missing-context` — The plan assumed `NotificationDeps.sendMessage.display` could be optional (`display?: boolean`) but the SDK's `CustomMessage.display` is required `boolean`.
27
+ First `pnpm run check` after step 2 failed; required one extra edit-verify cycle.
28
+ Impact: added friction but no rework (fixed in the same commit).
29
+
30
+ - `missing-context` — The plan did not check the `execute` function's full signature in `agent-tool.ts`.
31
+ Removing the outer `as any` on `createAgentTool({...})` exposed three additional type mismatches: `onUpdate` parameter type (`unknown` vs `AgentToolResult<any>`), `signal` optionality (`AbortSignal` vs `AbortSignal | undefined`), and `params.description` (`unknown` vs `string`).
32
+ The plan flagged cascading risk but assumed only the return type would be affected.
33
+ Impact: step 3 required three extra edits and two extra `pnpm run check` cycles, noted as a deviation in the commit body.
34
+
35
+ - `missing-context` — Steps 6 and 7 had to be folded into one commit.
36
+ The plan listed them as independent steps but typing `AgentMenuManager.spawnAndWait(ctx: ExtensionContext)` immediately made `MenuContext` incompatible — `showGenerateWizard` passes `ctx: MenuContext` to the dep callback that now expects `ExtensionContext`.
37
+ The testing skill already warns: "when a TDD plan lists separate steps that share a type definition, changing that type in step N breaks steps N+1…N+k."
38
+ The planner failed to recognize that `MenuContext` and `AgentMenuManager.spawnAndWait` share a type dependency through `ctx`.
39
+ Impact: no rework — the steps were folded successfully — but the plan's step count was inaccurate.
40
+
41
+ #### What caused friction (user side)
42
+
43
+ - No user-side friction observed.
44
+ The user ran three sequential prompts (`/plan-issue 66`, `/tdd-plan`, `/ship-issue`) with no corrections or redirects needed.
@@ -0,0 +1,33 @@
1
+ ---
2
+ issue: 77
3
+ issue_title: "refactor: add projectAgentsDir to AgentMenuDeps instead of reading process.cwd() inline"
4
+ ---
5
+
6
+ # Retro: #77 — inject projectAgentsDir into AgentMenuDeps
7
+
8
+ ## Final Retrospective (2026-05-20T16:15:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned, implemented, and shipped a refactoring that adds `projectAgentsDir: string` to `AgentMenuDeps`, replacing the inline `process.cwd()` lambda in `createAgentsMenuHandler`.
13
+ The change mirrors the existing `personalAgentsDir` injection pattern.
14
+ Released as `pi-subagents-v5.8.2`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - Clean end-to-end execution: plan → TDD → ship with zero corrections or rework.
21
+ - The Red test was well-targeted: exercised `findAgentFile` through the menu navigation path and naturally failed because `process.cwd()` produced a different path than the injected `/test-project/.pi/agents`.
22
+ - Correctly identified at execution time that the plan's two-step TDD split was impractical (interface change is atomic) and combined into a single `refactor:` commit.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `instruction-violation` — The plan listed two TDD commits (step 1: `test:`, step 2: `refactor:`) but adding a required field to `AgentMenuDeps` is an atomic change — the test references the new field, so both must land together.
27
+ The testing skill's shared-type-definition rule ("changing that type in step N breaks steps N+1…N+k — fold them into one step") should have been applied during planning.
28
+ Impact: added friction but no rework — the deviation was self-identified at execution time and the steps were combined.
29
+ Self-identified.
30
+
31
+ #### What caused friction (user side)
32
+
33
+ - None observed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "5.8.1",
3
+ "version": "6.0.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -43,10 +43,13 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.4.14",
46
+ "@earendil-works/pi-ai": "0.75.4",
47
+ "@earendil-works/pi-coding-agent": "0.75.4",
48
+ "@earendil-works/pi-tui": "0.75.4",
46
49
  "@types/node": "^22.15.3",
50
+ "rumdl": "^0.1.93",
47
51
  "typescript": "^6.0.3",
48
- "vitest": "^4.1.5",
49
- "rumdl": "^0.1.93"
52
+ "vitest": "^4.1.5"
50
53
  },
51
54
  "pi": {
52
55
  "extensions": [
@@ -74,6 +74,10 @@ export interface SpawnOptions {
74
74
  onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
75
75
  /** Called when the session successfully compacts. */
76
76
  onCompaction?: (info: CompactionInfo) => void;
77
+ /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
78
+ parentSessionFile?: string;
79
+ /** Session ID of the parent agent (stored in the child session's parentSession header). */
80
+ parentSessionId?: string;
77
81
  }
78
82
 
79
83
  export class AgentManager {
@@ -205,6 +209,8 @@ export class AgentManager {
205
209
  inheritContext: options.inheritContext,
206
210
  thinkingLevel: options.thinkingLevel,
207
211
  cwd: worktreeCwd,
212
+ parentSessionFile: options.parentSessionFile,
213
+ parentSessionId: options.parentSessionId,
208
214
  signal: record.abortController!.signal,
209
215
  onToolActivity: (activity) => {
210
216
  if (activity.type === "end") record.toolUses++;
@@ -223,6 +229,10 @@ export class AgentManager {
223
229
  },
224
230
  onSessionCreated: (session) => {
225
231
  record.session = session;
232
+ // Capture the session file path early so it's available for display
233
+ // before the run completes (e.g. in background agent status messages).
234
+ const file = session.sessionManager?.getSessionFile?.();
235
+ if (file) record.outputFile = file;
226
236
  // Flush any steers that arrived before the session was ready
227
237
  if (record.pendingSteers?.length) {
228
238
  for (const msg of record.pendingSteers) {
@@ -233,7 +243,7 @@ export class AgentManager {
233
243
  options.onSessionCreated?.(session);
234
244
  },
235
245
  })
236
- .then(({ responseText, session, aborted, steered }) => {
246
+ .then(({ responseText, session, aborted, steered, sessionFile }) => {
237
247
  // Don't overwrite status if externally stopped via abort()
238
248
  if (record.status !== "stopped") {
239
249
  record.status = aborted ? "aborted" : steered ? "steered" : "completed";
@@ -241,15 +251,10 @@ export class AgentManager {
241
251
  record.result = responseText;
242
252
  record.session = session;
243
253
  record.completedAt ??= Date.now();
254
+ if (sessionFile) record.outputFile = sessionFile;
244
255
 
245
256
  detach();
246
257
 
247
- // Final flush of streaming output file
248
- if (record.outputCleanup) {
249
- try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup", err); }
250
- record.outputCleanup = undefined;
251
- }
252
-
253
258
  // Clean up worktree if used
254
259
  if (record.worktree) {
255
260
  const wtResult = this.worktrees.cleanup(record.worktree, options.description);
@@ -277,11 +282,7 @@ export class AgentManager {
277
282
 
278
283
  detach();
279
284
 
280
- // Final flush of streaming output file on error
281
- if (record.outputCleanup) {
282
- try { record.outputCleanup(); } catch (err) { debugLog("outputCleanup on error", err); }
283
- record.outputCleanup = undefined;
284
- }
285
+
285
286
 
286
287
  // Best-effort worktree cleanup on error
287
288
  if (record.worktree) {
@@ -17,6 +17,7 @@ import {
17
17
  import { buildParentContext, extractText } from "./context.js";
18
18
  import { detectEnv } from "./env.js";
19
19
  import { assembleSessionConfig } from "./session-config.js";
20
+ import { deriveSubagentSessionDir } from "./session-dir.js";
20
21
  import type { SubagentType, ThinkingLevel } from "./types.js";
21
22
 
22
23
  /** Names of tools registered by this extension that subagents must NOT inherit. */
@@ -82,6 +83,10 @@ export interface RunOptions {
82
83
  thinkingLevel?: ThinkingLevel;
83
84
  /** Override working directory (e.g. for worktree isolation). */
84
85
  cwd?: string;
86
+ /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
87
+ parentSessionFile?: string;
88
+ /** Session ID of the parent agent (stored in the child session's parentSession header). */
89
+ parentSessionId?: string;
85
90
  /** Called on tool start/end with activity info. */
86
91
  onToolActivity?: (activity: ToolActivity) => void;
87
92
  /** Called on streaming text deltas from the assistant response. */
@@ -127,6 +132,8 @@ export interface RunResult {
127
132
  aborted: boolean;
128
133
  /** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
129
134
  steered: boolean;
135
+ /** Path to the persisted session JSONL file, if the session was persisted. */
136
+ sessionFile?: string;
130
137
  }
131
138
 
132
139
  /** Options for resuming an existing agent session. */
@@ -240,10 +247,17 @@ export async function runAgent(
240
247
  });
241
248
  await loader.reload();
242
249
 
250
+ // Create a persisted SessionManager so transcripts are written in Pi's
251
+ // official JSONL format. Falls back to a temp directory when the parent
252
+ // session is not persisted (e.g. headless/API mode).
253
+ const sessionDir = deriveSubagentSessionDir(options.parentSessionFile, cfg.effectiveCwd);
254
+ const sessionManager = SessionManager.create(cfg.effectiveCwd, sessionDir);
255
+ sessionManager.newSession({ parentSession: options.parentSessionId });
256
+
243
257
  const sessionOpts: Parameters<typeof createAgentSession>[0] = {
244
258
  cwd: cfg.effectiveCwd,
245
259
  agentDir,
246
- sessionManager: SessionManager.inMemory(cfg.effectiveCwd),
260
+ sessionManager,
247
261
  settingsManager: SettingsManager.create(cfg.effectiveCwd, agentDir),
248
262
  modelRegistry: ctx.modelRegistry,
249
263
  model: cfg.model as Model<any> | undefined,
@@ -383,7 +397,13 @@ export async function runAgent(
383
397
 
384
398
  const responseText =
385
399
  collector.getText().trim() || getLastAssistantText(session);
386
- return { responseText, session, aborted, steered: softLimitReached };
400
+ return {
401
+ responseText,
402
+ session,
403
+ aborted,
404
+ steered: softLimitReached,
405
+ sessionFile: sessionManager.getSessionFile(),
406
+ };
387
407
  }
388
408
 
389
409
  /**
package/src/index.ts CHANGED
@@ -264,6 +264,7 @@ export default function (pi: ExtensionAPI) {
264
264
  ),
265
265
  emitEvent: (name, data) => pi.events.emit(name, data),
266
266
  personalAgentsDir: join(getAgentDir(), 'agents'),
267
+ projectAgentsDir: join(process.cwd(), '.pi', 'agents'),
267
268
  });
268
269
 
269
270
  pi.registerCommand('agents', {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * session-dir.ts — Pure function for deriving subagent session directories.
3
+ *
4
+ * Subagent sessions are nested under the parent session's basename so they are
5
+ * discoverable via the parent session path without cluttering the main session list.
6
+ */
7
+
8
+ import { tmpdir } from "node:os";
9
+ import { basename, dirname, join } from "node:path";
10
+
11
+ /**
12
+ * Derive the session directory for a subagent from the parent session file.
13
+ *
14
+ * Layout: `<parent-dir>/<parent-basename>/tasks/`
15
+ *
16
+ * Example:
17
+ * parent: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_.jsonl`
18
+ * result: `~/.pi/agent/sessions/--project--/2026-05-20T12-00-00Z_/tasks`
19
+ *
20
+ * Falls back to a temp directory when the parent session is not persisted
21
+ * (e.g. API/headless mode where the parent uses `SessionManager.inMemory()`).
22
+ */
23
+ export function deriveSubagentSessionDir(
24
+ parentSessionFile: string | undefined,
25
+ cwd: string,
26
+ ): string {
27
+ if (parentSessionFile) {
28
+ const dir = dirname(parentSessionFile);
29
+ const base = basename(parentSessionFile, ".jsonl");
30
+ return join(dir, base, "tasks");
31
+ }
32
+
33
+ // Fallback: use a temp directory keyed by uid and cwd so different
34
+ // projects don't collide when the parent session is not persisted.
35
+ const encoded = cwd.replace(/[/\\]/g, "-").replace(/^[A-Za-z]:-/, "").replace(/^-+/, "");
36
+ const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
37
+ return join(root, encoded, "tasks");
38
+ }
@@ -6,7 +6,7 @@ import { normalizeMaxTurns } from "../agent-runner.js";
6
6
  import { resolveAgentConfig, resolveType } from "../agent-types.js";
7
7
  import { resolveAgentInvocationConfig } from "../invocation-config.js";
8
8
  import { resolveInvocationModel } from "../model-resolver.js";
9
- import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "../output-file.js";
9
+
10
10
  import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
11
11
  import {
12
12
  type AgentActivity,
@@ -454,19 +454,12 @@ Guidelines:
454
454
  const { state: bgState, callbacks: bgCallbacks } =
455
455
  createActivityTracker(effectiveMaxTurns);
456
456
 
457
- // Wrap onSessionCreated to wire output file streaming.
458
457
  let id: string;
459
- const origBgOnSession = bgCallbacks.onSessionCreated;
460
- bgCallbacks.onSessionCreated = (session: any) => {
461
- origBgOnSession(session);
462
- const rec = deps.manager.getRecord(id);
463
- if (rec?.outputFile) {
464
- rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
465
- }
466
- };
467
458
 
468
459
  try {
469
460
  id = deps.manager.spawn(ctx, subagentType, params.prompt as string, {
461
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
462
+ parentSessionId: ctx.sessionManager.getSessionId(),
470
463
  description: params.description as string,
471
464
  model,
472
465
  maxTurns: effectiveMaxTurns,
@@ -482,16 +475,9 @@ Guidelines:
482
475
  return textResult(err instanceof Error ? err.message : String(err));
483
476
  }
484
477
 
485
- // Set output file synchronously after spawn
486
478
  const record = deps.manager.getRecord(id);
487
479
  if (record) {
488
480
  record.toolCallId = toolCallId;
489
- record.outputFile = createOutputFilePath(
490
- ctx.cwd,
491
- id,
492
- ctx.sessionManager.getSessionId(),
493
- );
494
- writeInitialEntry(record.outputFile, id, params.prompt as string, ctx.cwd);
495
481
  }
496
482
 
497
483
  deps.agentActivity.set(id, bgState);
@@ -596,6 +582,8 @@ Guidelines:
596
582
  isolation,
597
583
  invocation: agentInvocation,
598
584
  signal,
585
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
586
+ parentSessionId: ctx.sessionManager.getSessionId(),
599
587
  ...fgCallbacks,
600
588
  },
601
589
  );
package/src/types.ts CHANGED
@@ -78,10 +78,8 @@ export interface AgentRecord {
78
78
  worktreeResult?: { hasChanges: boolean; branch?: string };
79
79
  /** The tool_use_id from the original Agent tool call. */
80
80
  toolCallId?: string;
81
- /** Path to the streaming output transcript file. */
81
+ /** Path to the persisted session transcript file. */
82
82
  outputFile?: string;
83
- /** Cleanup function for the output file stream subscription. */
84
- outputCleanup?: () => void;
85
83
  /**
86
84
  * Lifetime usage breakdown, accumulated via `message_end` events. Survives
87
85
  * compaction. Total = input + output + cacheWrite (cacheRead deliberately
@@ -41,6 +41,7 @@ export interface AgentMenuDeps {
41
41
  ) => { message: string; level: string };
42
42
  emitEvent: (name: string, data: unknown) => void;
43
43
  personalAgentsDir: string;
44
+ projectAgentsDir: string;
44
45
  /** Returns the runtime default max turns (undefined = unlimited). */
45
46
  getDefaultMaxTurns: () => number | undefined;
46
47
  /** Returns the runtime grace turns value. */
@@ -60,12 +61,10 @@ export interface AgentMenuDeps {
60
61
  * Returns a function suitable for `pi.registerCommand("agents", { handler })`.
61
62
  */
62
63
  export function createAgentsMenuHandler(deps: AgentMenuDeps) {
63
- const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
64
-
65
64
  function findAgentFile(
66
65
  name: string,
67
66
  ): { path: string; location: "project" | "personal" } | undefined {
68
- const projectPath = join(projectAgentsDir(), `${name}.md`);
67
+ const projectPath = join(deps.projectAgentsDir, `${name}.md`);
69
68
  if (existsSync(projectPath)) return { path: projectPath, location: "project" };
70
69
  const personalPath = join(deps.personalAgentsDir, `${name}.md`);
71
70
  if (existsSync(personalPath)) return { path: personalPath, location: "personal" };
@@ -309,7 +308,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
309
308
  if (!location) return;
310
309
 
311
310
  const targetDir = location.startsWith("Project")
312
- ? projectAgentsDir()
311
+ ? deps.projectAgentsDir
313
312
  : deps.personalAgentsDir;
314
313
  mkdirSync(targetDir, { recursive: true });
315
314
 
@@ -375,7 +374,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
375
374
  if (!location) return;
376
375
 
377
376
  const targetDir = location.startsWith("Project")
378
- ? projectAgentsDir()
377
+ ? deps.projectAgentsDir
379
378
  : deps.personalAgentsDir;
380
379
  mkdirSync(targetDir, { recursive: true });
381
380
 
@@ -413,7 +412,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
413
412
  if (!location) return;
414
413
 
415
414
  const targetDir = location.startsWith("Project")
416
- ? projectAgentsDir()
415
+ ? deps.projectAgentsDir
417
416
  : deps.personalAgentsDir;
418
417
 
419
418
  const method = await ctx.ui.select("Creation method", [
@@ -1,99 +0,0 @@
1
- /**
2
- * output-file.ts — Streaming JSONL output file for agent transcripts.
3
- *
4
- * Creates a per-agent output file that streams conversation turns as JSONL,
5
- * matching Claude Code's task output file format.
6
- */
7
-
8
- import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
- import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
12
- import { debugLog } from "./debug.js";
13
-
14
- /**
15
- * Encode a cwd path as a filesystem-safe directory name. Handles:
16
- * - POSIX: "/home/user/project" → "home-user-project"
17
- * - Windows: "C:\Users\foo\project" → "Users-foo-project"
18
- * - UNC: "\\\\server\\share\\project" → "server-share-project"
19
- */
20
- export function encodeCwd(cwd: string): string {
21
- return cwd
22
- .replace(/[/\\]/g, "-") // both separators → dash
23
- .replace(/^[A-Za-z]:-/, "") // strip Windows drive prefix ("C:-")
24
- .replace(/^-+/, ""); // strip leading dashes (POSIX root, UNC)
25
- }
26
-
27
- /** Create the output file path, ensuring the directory exists.
28
- * Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
29
- export function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string {
30
- const encoded = encodeCwd(cwd);
31
- const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
32
- mkdirSync(root, { recursive: true, mode: 0o700 });
33
- // chmod is a no-op on Windows and throws on some Windows filesystems.
34
- // On Unix we still want to enforce 0o700 past umask, so only swallow on Windows.
35
- try {
36
- chmodSync(root, 0o700);
37
- } catch (err) {
38
- if (process.platform !== "win32") throw err;
39
- }
40
- const dir = join(root, encoded, sessionId, "tasks");
41
- mkdirSync(dir, { recursive: true });
42
- return join(dir, `${agentId}.output`);
43
- }
44
-
45
- /** Write the initial user prompt entry. */
46
- export function writeInitialEntry(path: string, agentId: string, prompt: string, cwd: string): void {
47
- const entry = {
48
- isSidechain: true,
49
- agentId,
50
- type: "user",
51
- message: { role: "user", content: prompt },
52
- timestamp: new Date().toISOString(),
53
- cwd,
54
- };
55
- writeFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
56
- }
57
-
58
- /**
59
- * Subscribe to session events and flush new messages to the output file on each turn_end.
60
- * Returns a cleanup function that does a final flush and unsubscribes.
61
- */
62
- export function streamToOutputFile(
63
- session: AgentSession,
64
- path: string,
65
- agentId: string,
66
- cwd: string,
67
- ): () => void {
68
- let writtenCount = 1; // initial user prompt already written
69
-
70
- const flush = () => {
71
- const messages = session.messages;
72
- while (writtenCount < messages.length) {
73
- const msg = messages[writtenCount];
74
- const entry = {
75
- isSidechain: true,
76
- agentId,
77
- type: msg.role === "assistant" ? "assistant" : msg.role === "user" ? "user" : "toolResult",
78
- message: msg,
79
- timestamp: new Date().toISOString(),
80
- cwd,
81
- };
82
- try {
83
- appendFileSync(path, JSON.stringify(entry) + "\n", "utf-8");
84
- } catch (err) {
85
- debugLog("write JSONL chunk", err);
86
- }
87
- writtenCount++;
88
- }
89
- };
90
-
91
- const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
92
- if (event.type === "turn_end") flush();
93
- });
94
-
95
- return () => {
96
- flush();
97
- unsubscribe();
98
- };
99
- }