@esso0428/pi-subagents 0.15.5 → 0.15.7

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +10 -19
  2. package/README.md +5 -5
  3. package/dist/agent-history-list.d.ts +19 -0
  4. package/dist/agent-history-list.d.ts.map +1 -0
  5. package/dist/agent-history-list.js +69 -0
  6. package/dist/agent-history-list.js.map +1 -0
  7. package/dist/agent-history.d.ts +2 -1
  8. package/dist/agent-history.d.ts.map +1 -0
  9. package/dist/agent-history.js +32 -34
  10. package/dist/agent-history.js.map +1 -0
  11. package/dist/agent-manager.d.ts +24 -8
  12. package/dist/agent-manager.d.ts.map +1 -0
  13. package/dist/agent-manager.js +171 -93
  14. package/dist/agent-manager.js.map +1 -0
  15. package/dist/agent-runner.d.ts +1 -0
  16. package/dist/agent-runner.d.ts.map +1 -0
  17. package/dist/agent-runner.js +75 -86
  18. package/dist/agent-runner.js.map +1 -0
  19. package/dist/agent-types.d.ts +1 -0
  20. package/dist/agent-types.d.ts.map +1 -0
  21. package/dist/agent-types.js +28 -45
  22. package/dist/agent-types.js.map +1 -0
  23. package/dist/context.d.ts +1 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/context.js +3 -6
  26. package/dist/context.js.map +1 -0
  27. package/dist/cross-extension-rpc.d.ts +1 -0
  28. package/dist/cross-extension-rpc.d.ts.map +1 -0
  29. package/dist/cross-extension-rpc.js +6 -9
  30. package/dist/cross-extension-rpc.js.map +1 -0
  31. package/dist/custom-agents.d.ts +1 -0
  32. package/dist/custom-agents.d.ts.map +1 -0
  33. package/dist/custom-agents.js +16 -18
  34. package/dist/custom-agents.js.map +1 -0
  35. package/dist/default-agents.d.ts +1 -0
  36. package/dist/default-agents.d.ts.map +1 -0
  37. package/dist/default-agents.js +2 -4
  38. package/dist/default-agents.js.map +1 -0
  39. package/dist/enabled-models.d.ts +1 -0
  40. package/dist/enabled-models.d.ts.map +1 -0
  41. package/dist/enabled-models.js +12 -16
  42. package/dist/enabled-models.js.map +1 -0
  43. package/dist/env.d.ts +1 -0
  44. package/dist/env.d.ts.map +1 -0
  45. package/dist/env.js +2 -4
  46. package/dist/env.js.map +1 -0
  47. package/dist/group-join.d.ts +1 -0
  48. package/dist/group-join.d.ts.map +1 -0
  49. package/dist/group-join.js +2 -5
  50. package/dist/group-join.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +306 -292
  54. package/dist/index.js.map +1 -0
  55. package/dist/invocation-config.d.ts +1 -0
  56. package/dist/invocation-config.d.ts.map +1 -0
  57. package/dist/invocation-config.js +3 -6
  58. package/dist/invocation-config.js.map +1 -0
  59. package/dist/memory.d.ts +1 -0
  60. package/dist/memory.d.ts.map +1 -0
  61. package/dist/memory.js +25 -34
  62. package/dist/memory.js.map +1 -0
  63. package/dist/model-resolver.d.ts +1 -0
  64. package/dist/model-resolver.d.ts.map +1 -0
  65. package/dist/model-resolver.js +2 -4
  66. package/dist/model-resolver.js.map +1 -0
  67. package/dist/nico-overrides.d.ts +1 -0
  68. package/dist/nico-overrides.d.ts.map +1 -0
  69. package/dist/nico-overrides.js +15 -20
  70. package/dist/nico-overrides.js.map +1 -0
  71. package/dist/output-file.d.ts +1 -0
  72. package/dist/output-file.d.ts.map +1 -0
  73. package/dist/output-file.js +17 -22
  74. package/dist/output-file.js.map +1 -0
  75. package/dist/prompts.d.ts +1 -0
  76. package/dist/prompts.d.ts.map +1 -0
  77. package/dist/prompts.js +2 -4
  78. package/dist/prompts.js.map +1 -0
  79. package/dist/schedule-store.d.ts +1 -0
  80. package/dist/schedule-store.d.ts.map +1 -0
  81. package/dist/schedule-store.js +17 -21
  82. package/dist/schedule-store.js.map +1 -0
  83. package/dist/schedule.d.ts +1 -0
  84. package/dist/schedule.d.ts.map +1 -0
  85. package/dist/schedule.js +9 -12
  86. package/dist/schedule.js.map +1 -0
  87. package/dist/settings.d.ts +1 -0
  88. package/dist/settings.d.ts.map +1 -0
  89. package/dist/settings.js +16 -23
  90. package/dist/settings.js.map +1 -0
  91. package/dist/skill-loader.d.ts +1 -0
  92. package/dist/skill-loader.d.ts.map +1 -0
  93. package/dist/skill-loader.js +21 -23
  94. package/dist/skill-loader.js.map +1 -0
  95. package/dist/status-note.d.ts +1 -0
  96. package/dist/status-note.d.ts.map +1 -0
  97. package/dist/status-note.js +2 -4
  98. package/dist/status-note.js.map +1 -0
  99. package/dist/types.d.ts +5 -1
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -4
  102. package/dist/types.js.map +1 -0
  103. package/dist/ui/agent-widget.d.ts +2 -5
  104. package/dist/ui/agent-widget.d.ts.map +1 -0
  105. package/dist/ui/agent-widget.js +29 -51
  106. package/dist/ui/agent-widget.js.map +1 -0
  107. package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
  108. package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
  109. package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
  110. package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
  111. package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
  112. package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
  113. package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
  114. package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
  115. package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
  116. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
  117. package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
  118. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
  119. package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
  120. package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
  121. package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
  122. package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
  123. package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
  124. package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
  125. package/dist/ui/ccstyle/diff/render-utils.js +25 -0
  126. package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
  127. package/dist/ui/ccstyle/diff/types.d.ts +22 -0
  128. package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
  129. package/dist/ui/ccstyle/diff/types.js +10 -0
  130. package/dist/ui/ccstyle/diff/types.js.map +1 -0
  131. package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
  132. package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
  133. package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
  134. package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
  135. package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
  136. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
  137. package/dist/ui/ccstyle/tool-renderer.js +58 -0
  138. package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
  139. package/dist/ui/ccstyle/tool-result.d.ts +24 -0
  140. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
  141. package/dist/ui/ccstyle/tool-result.js +193 -0
  142. package/dist/ui/ccstyle/tool-result.js.map +1 -0
  143. package/dist/ui/conversation-blocks.d.ts +46 -0
  144. package/dist/ui/conversation-blocks.d.ts.map +1 -0
  145. package/dist/ui/conversation-blocks.js +306 -0
  146. package/dist/ui/conversation-blocks.js.map +1 -0
  147. package/dist/ui/conversation-nvim.d.ts +8 -0
  148. package/dist/ui/conversation-nvim.d.ts.map +1 -0
  149. package/dist/ui/conversation-nvim.js +117 -0
  150. package/dist/ui/conversation-nvim.js.map +1 -0
  151. package/dist/ui/conversation-role.d.ts +13 -0
  152. package/dist/ui/conversation-role.d.ts.map +1 -0
  153. package/dist/ui/conversation-role.js +39 -0
  154. package/dist/ui/conversation-role.js.map +1 -0
  155. package/dist/ui/conversation-search.d.ts +39 -0
  156. package/dist/ui/conversation-search.d.ts.map +1 -0
  157. package/dist/ui/conversation-search.js +124 -0
  158. package/dist/ui/conversation-search.js.map +1 -0
  159. package/dist/ui/conversation-timeline.d.ts +62 -0
  160. package/dist/ui/conversation-timeline.d.ts.map +1 -0
  161. package/dist/ui/conversation-timeline.js +263 -0
  162. package/dist/ui/conversation-timeline.js.map +1 -0
  163. package/dist/ui/conversation-viewer.d.ts +62 -20
  164. package/dist/ui/conversation-viewer.d.ts.map +1 -0
  165. package/dist/ui/conversation-viewer.js +449 -224
  166. package/dist/ui/conversation-viewer.js.map +1 -0
  167. package/dist/ui/fleet-list.d.ts +24 -11
  168. package/dist/ui/fleet-list.d.ts.map +1 -0
  169. package/dist/ui/fleet-list.js +86 -78
  170. package/dist/ui/fleet-list.js.map +1 -0
  171. package/dist/ui/schedule-menu.d.ts +1 -0
  172. package/dist/ui/schedule-menu.d.ts.map +1 -0
  173. package/dist/ui/schedule-menu.js +2 -4
  174. package/dist/ui/schedule-menu.js.map +1 -0
  175. package/dist/ui/viewer-keys.d.ts +1 -0
  176. package/dist/ui/viewer-keys.d.ts.map +1 -0
  177. package/dist/ui/viewer-keys.js +8 -10
  178. package/dist/ui/viewer-keys.js.map +1 -0
  179. package/dist/usage.d.ts +1 -0
  180. package/dist/usage.d.ts.map +1 -0
  181. package/dist/usage.js +5 -10
  182. package/dist/usage.js.map +1 -0
  183. package/dist/worktree.d.ts +1 -0
  184. package/dist/worktree.d.ts.map +1 -0
  185. package/dist/worktree.js +27 -31
  186. package/dist/worktree.js.map +1 -0
  187. package/dist/write-execution.d.ts +42 -0
  188. package/dist/write-execution.d.ts.map +1 -0
  189. package/dist/write-execution.js +138 -0
  190. package/dist/write-execution.js.map +1 -0
  191. package/package.json +1 -1
  192. package/src/agent-history-list.ts +91 -0
  193. package/src/agent-history.ts +12 -17
  194. package/src/agent-manager.ts +169 -81
  195. package/src/agent-runner.ts +12 -13
  196. package/src/index.ts +117 -98
  197. package/src/output-file.ts +1 -1
  198. package/src/types.ts +4 -1
  199. package/src/ui/agent-widget.ts +3 -9
  200. package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
  201. package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
  202. package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
  203. package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
  204. package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
  205. package/src/ui/ccstyle/diff/render-utils.ts +32 -0
  206. package/src/ui/ccstyle/diff/types.ts +33 -0
  207. package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
  208. package/src/ui/ccstyle/tool-renderer.ts +71 -0
  209. package/src/ui/ccstyle/tool-result.ts +238 -0
  210. package/src/ui/conversation-blocks.ts +404 -0
  211. package/src/ui/conversation-nvim.ts +127 -0
  212. package/src/ui/conversation-role.ts +45 -0
  213. package/src/ui/conversation-search.ts +161 -0
  214. package/src/ui/conversation-timeline.ts +315 -0
  215. package/src/ui/conversation-viewer.ts +418 -210
  216. package/src/ui/fleet-list.ts +73 -52
  217. package/src/write-execution.ts +180 -0
  218. package/test/agent-history-list.test.ts +121 -0
  219. package/test/agent-manager-history.test.ts +59 -58
  220. package/test/agent-menu-history-navigation.test.ts +86 -0
  221. package/test/output-file-history.test.ts +50 -0
  222. package/test/ui/conversation-blocks.test.ts +103 -0
  223. package/test/ui/conversation-diff.test.ts +91 -0
  224. package/test/ui/conversation-search.test.ts +55 -0
  225. package/test/ui/conversation-viewer.test.ts +182 -0
  226. package/test/ui/fleet-list.test.ts +131 -0
  227. package/test/write-execution.test.ts +83 -0
  228. package/tsconfig.json +16 -0
  229. package/vitest.temp.config.mts +6 -0
  230. package/dist/ui/markdown-result.d.ts +0 -3
  231. package/dist/ui/markdown-result.js +0 -53
  232. package/src/ui/markdown-result.ts +0 -56
  233. package/test/agent-history.test.ts +0 -63
  234. package/test/ui/markdown-result.test.ts +0 -45
@@ -23,13 +23,101 @@ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; toke
23
23
 
24
24
  /** Default max concurrent background agents. */
25
25
  const DEFAULT_MAX_CONCURRENT = 4;
26
- const TERMINAL_STATUSES = new Set<AgentRecord["status"]>([
27
- "completed",
28
- "steered",
29
- "aborted",
30
- "stopped",
31
- "error",
32
- ]);
26
+ const RESTORABLE_STATUSES = new Set(["completed", "steered", "stopped", "aborted", "error"] as const);
27
+ const THINKING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "off"]);
28
+
29
+ type RestorableAgentStatus = "completed" | "steered" | "stopped" | "aborted" | "error";
30
+
31
+ /** Narrow persisted strings before putting them into runtime/UI state. */
32
+ function isSafePersistedString(value: unknown, maxLength: number): value is string {
33
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength && !/[\0\r\n]/.test(value);
34
+ }
35
+
36
+ function isFiniteTimestamp(value: unknown): value is number {
37
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
38
+ }
39
+
40
+ function isValidUsage(value: unknown): value is { input: number; output: number; cacheWrite: number } {
41
+ if (!value || typeof value !== "object") return false;
42
+ const usage = value as Record<string, unknown>;
43
+ return ["input", "output", "cacheWrite"].every((key) => {
44
+ const n = usage[key];
45
+ return typeof n === "number" && Number.isFinite(n) && n >= 0;
46
+ });
47
+ }
48
+
49
+ function isSafeTranscriptLocator(value: unknown): value is string {
50
+ return typeof value === "string"
51
+ && /^\.pi-subagents\/agent-transcripts\/[^/]+\.jsonl$/.test(value)
52
+ && !value.includes("..")
53
+ && !value.includes("\\")
54
+ && !value.includes("\0");
55
+ }
56
+
57
+ function isSafeInvocation(value: unknown): value is AgentInvocation {
58
+ if (!value || typeof value !== "object") return false;
59
+ const invocation = value as Record<string, unknown>;
60
+ for (const key of ["modelName", "effectiveModelName"]) {
61
+ if (invocation[key] !== undefined && !isSafePersistedString(invocation[key], 512)) return false;
62
+ }
63
+ for (const key of ["thinking", "effectiveThinking"]) {
64
+ if (invocation[key] !== undefined && (typeof invocation[key] !== "string" || !THINKING_LEVELS.has(invocation[key]))) return false;
65
+ }
66
+ if (invocation.maxTurns !== undefined && (!Number.isInteger(invocation.maxTurns) || (invocation.maxTurns as number) < 0)) return false;
67
+ for (const key of ["isolated", "inheritContext", "runInBackground"]) {
68
+ if (invocation[key] !== undefined && typeof invocation[key] !== "boolean") return false;
69
+ }
70
+ if (invocation.isolation !== undefined && invocation.isolation !== "worktree") return false;
71
+ return true;
72
+ }
73
+
74
+ function cloneInvocation(value: AgentInvocation | undefined): AgentInvocation | undefined {
75
+ return value ? {
76
+ modelName: value.modelName,
77
+ effectiveModelName: value.effectiveModelName,
78
+ thinking: value.thinking,
79
+ effectiveThinking: value.effectiveThinking,
80
+ maxTurns: value.maxTurns,
81
+ isolated: value.isolated,
82
+ inheritContext: value.inheritContext,
83
+ runInBackground: value.runInBackground,
84
+ isolation: value.isolation,
85
+ } : undefined;
86
+ }
87
+
88
+ /** Validate one persisted terminal record without constructing runtime handles. */
89
+ export function isRestorableAgentRecord(value: unknown): value is {
90
+ id: string;
91
+ type: string;
92
+ description: string;
93
+ status: RestorableAgentStatus;
94
+ startedAt: number;
95
+ completedAt: number;
96
+ result?: string;
97
+ error?: string;
98
+ toolUses?: number;
99
+ lifetimeUsage?: { input: number; output: number; cacheWrite: number };
100
+ transcriptPath?: string;
101
+ invocation?: AgentInvocation;
102
+ } {
103
+ if (!value || typeof value !== "object") return false;
104
+ const record = value as Record<string, unknown>;
105
+ if (!isSafePersistedString(record.id, 256)
106
+ || !isSafePersistedString(record.type, 256)
107
+ || !isSafePersistedString(record.description, 4096)
108
+ || typeof record.status !== "string"
109
+ || !RESTORABLE_STATUSES.has(record.status as RestorableAgentStatus)
110
+ || !isFiniteTimestamp(record.startedAt)
111
+ || !isFiniteTimestamp(record.completedAt)
112
+ || record.completedAt < record.startedAt) return false;
113
+ if (record.result !== undefined && !isSafePersistedString(record.result, 2_000_000)) return false;
114
+ if (record.error !== undefined && !isSafePersistedString(record.error, 64_000)) return false;
115
+ if (record.toolUses !== undefined && (!Number.isInteger(record.toolUses) || (record.toolUses as number) < 0)) return false;
116
+ if (record.lifetimeUsage !== undefined && !isValidUsage(record.lifetimeUsage)) return false;
117
+ if (record.transcriptPath !== undefined && !isSafeTranscriptLocator(record.transcriptPath)) return false;
118
+ if (record.invocation !== undefined && !isSafeInvocation(record.invocation)) return false;
119
+ return true;
120
+ }
33
121
 
34
122
  /**
35
123
  * Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
@@ -104,45 +192,6 @@ interface SpawnOptions {
104
192
  onCompaction?: (info: CompactionInfo) => void;
105
193
  }
106
194
 
107
- const RESTORABLE_STATUSES = new Set<AgentRecord["status"]>([
108
- "completed",
109
- "steered",
110
- "aborted",
111
- "stopped",
112
- "error",
113
- ]);
114
-
115
- type PersistedAgentRecord = Pick<
116
- AgentRecord,
117
- "id" | "type" | "description" | "status" | "result" | "error" | "startedAt" | "completedAt" | "transcriptPath"
118
- >;
119
-
120
- function isRestorableRecord(value: unknown): value is PersistedAgentRecord {
121
- if (!value || typeof value !== "object") return false;
122
- const record = value as Record<string, unknown>;
123
- const transcriptPath = record.transcriptPath;
124
- const validTranscriptPath =
125
- transcriptPath === undefined ||
126
- (typeof transcriptPath === "string" &&
127
- transcriptPath.startsWith(".pi-subagents/agent-transcripts/") &&
128
- !transcriptPath.includes("..") &&
129
- !transcriptPath.includes("\\"));
130
- return (
131
- typeof record.id === "string" &&
132
- record.id.length > 0 &&
133
- typeof record.type === "string" &&
134
- typeof record.description === "string" &&
135
- RESTORABLE_STATUSES.has(record.status as AgentRecord["status"]) &&
136
- typeof record.startedAt === "number" &&
137
- Number.isFinite(record.startedAt) &&
138
- typeof record.completedAt === "number" &&
139
- Number.isFinite(record.completedAt) &&
140
- (record.result === undefined || typeof record.result === "string") &&
141
- (record.error === undefined || typeof record.error === "string") &&
142
- validTranscriptPath
143
- );
144
- }
145
-
146
195
  export class AgentManager {
147
196
  private agents = new Map<string, AgentRecord>();
148
197
  private cleanupInterval: ReturnType<typeof setInterval>;
@@ -323,6 +372,12 @@ export class AgentManager {
323
372
  },
324
373
  onSessionCreated: (session) => {
325
374
  record.session = session;
375
+ const model = session.model;
376
+ record.invocation = {
377
+ ...(record.invocation ?? {}),
378
+ ...(model && { effectiveModelName: model.name ?? model.id }),
379
+ effectiveThinking: session.thinkingLevel,
380
+ };
326
381
  // Flush any steers that arrived before the session was ready
327
382
  if (record.pendingSteers?.length) {
328
383
  for (const msg of record.pendingSteers) {
@@ -501,6 +556,12 @@ export class AgentManager {
501
556
  record.completedAt = undefined;
502
557
  record.result = undefined;
503
558
  record.error = undefined;
559
+ const resumedModel = record.session.model;
560
+ record.invocation = {
561
+ ...(record.invocation ?? {}),
562
+ ...(resumedModel && { effectiveModelName: resumedModel.name ?? resumedModel.id }),
563
+ effectiveThinking: record.session.thinkingLevel,
564
+ };
504
565
 
505
566
  try {
506
567
  const { text, failure } = await resumeAgent(record.session, prompt, {
@@ -556,47 +617,61 @@ export class AgentManager {
556
617
  return this.agents.get(id);
557
618
  }
558
619
 
559
- /**
560
- * Restore terminal records persisted in a parent session.
561
- *
562
- * Restored records deliberately have no live session, promise, or abort
563
- * controller. Invalid data is ignored because session entries are persisted
564
- * extension data and may have been written by an older version.
565
- */
566
- restoreCompleted(records: readonly unknown[]): void {
567
- const restoredIds = new Set<string>();
568
- // getBranch() is chronological; newest persisted state wins on duplicate IDs.
569
- for (const value of [...records].reverse()) {
570
- if (!isRestorableRecord(value)) continue;
571
- if (restoredIds.has(value.id) || this.agents.has(value.id)) continue;
572
- restoredIds.add(value.id);
573
-
574
- this.agents.set(value.id, {
575
- id: value.id,
576
- type: value.type,
577
- description: value.description,
578
- status: value.status,
579
- result: value.result,
580
- error: value.error,
581
- transcriptPath: value.transcriptPath,
582
- toolUses: 0,
583
- startedAt: value.startedAt,
584
- completedAt: value.completedAt,
585
- lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
586
- compactionCount: 0,
587
- // Historical records have no inline tool surface and should remain
588
- // visible in the background widget.
589
- isBackground: true,
590
- });
591
- }
592
- }
593
-
594
620
  listAgents(): AgentRecord[] {
595
621
  return [...this.agents.values()].sort(
596
622
  (a, b) => b.startedAt - a.startedAt,
597
623
  );
598
624
  }
599
625
 
626
+ /** Restore terminal records persisted by a parent branch without runtime handles. */
627
+ restoreCompleted(records: readonly unknown[]): void {
628
+ const latest = new Map<string, ReturnType<typeof this.createRestoredRecord>>();
629
+ for (const value of records) {
630
+ if (isRestorableAgentRecord(value)) {
631
+ latest.set(value.id, this.createRestoredRecord(value));
632
+ }
633
+ }
634
+
635
+ for (const [id, restored] of latest) {
636
+ const existing = this.agents.get(id);
637
+ if (existing?.status === "running" || existing?.status === "queued") continue;
638
+ this.agents.set(id, restored);
639
+ }
640
+ }
641
+
642
+ private createRestoredRecord(record: {
643
+ id: string;
644
+ type: string;
645
+ description: string;
646
+ status: RestorableAgentStatus;
647
+ startedAt: number;
648
+ completedAt: number;
649
+ result?: string;
650
+ error?: string;
651
+ toolUses?: number;
652
+ lifetimeUsage?: { input: number; output: number; cacheWrite: number };
653
+ transcriptPath?: string;
654
+ invocation?: AgentInvocation;
655
+ }): AgentRecord {
656
+ return {
657
+ id: record.id,
658
+ type: record.type,
659
+ description: record.description,
660
+ status: record.status,
661
+ result: record.result,
662
+ error: record.error,
663
+ toolUses: record.toolUses ?? 0,
664
+ startedAt: record.startedAt,
665
+ completedAt: record.completedAt,
666
+ transcriptPath: record.transcriptPath,
667
+ invocation: cloneInvocation(record.invocation),
668
+ lifetimeUsage: record.lifetimeUsage
669
+ ? { ...record.lifetimeUsage }
670
+ : { input: 0, output: 0, cacheWrite: 0 },
671
+ compactionCount: 0,
672
+ };
673
+ }
674
+
600
675
  abort(id: string): boolean {
601
676
  const record = this.agents.get(id);
602
677
  if (!record) return false;
@@ -628,6 +703,19 @@ export class AgentManager {
628
703
  for (const [id, record] of this.agents) {
629
704
  if (record.status === "running" || record.status === "queued") continue;
630
705
  if ((record.completedAt ?? 0) >= cutoff) continue;
706
+ // A durable transcript is the source of truth for history. Release the
707
+ // live session after the TTL, but retain a lightweight record so opening
708
+ // history again in this session does not silently lose its identity or
709
+ // locator. Records without durable storage remain eligible for eviction.
710
+ if (record.transcriptPath) {
711
+ try { record.session?.dispose?.(); } catch { /* ignore cleanup failures */ }
712
+ record.session = undefined;
713
+ try { record.outputCleanup?.(); } catch { /* ignore cleanup failures */ }
714
+ record.outputCleanup = undefined;
715
+ record.outputFile = undefined;
716
+ record.historyFile = undefined;
717
+ continue;
718
+ }
631
719
  this.removeRecord(id, record);
632
720
  }
633
721
  }
@@ -6,7 +6,7 @@ import { readFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
- import type { ExtensionContext, LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
9
+ import type { ExtensionContext, LoadExtensionsResult, ModelRuntime } from "@earendil-works/pi-coding-agent";
10
10
  import {
11
11
  type AgentSession,
12
12
  type AgentSessionEvent,
@@ -25,6 +25,7 @@ import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
25
25
  import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
26
26
  import { preloadSkills } from "./skill-loader.js";
27
27
  import type { SubagentType, ThinkingLevel } from "./types.js";
28
+ import { createTrackedWriteTool } from "./write-execution.js";
28
29
 
29
30
  /**
30
31
  * Tool names registered by THIS extension. Single source of truth so the
@@ -781,32 +782,30 @@ export async function runAgent(
781
782
  const sessionManager = agentConfig?.persistSession
782
783
  ? SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir)
783
784
  : SessionManager.inMemory(effectiveCwd);
785
+ // Keep write tracking session-local. The custom definition shadows only this
786
+ // child session's built-in write entry; the parent registry is untouched.
787
+ const trackedWriteTool = toolNames.includes("write")
788
+ ? createTrackedWriteTool(effectiveCwd)
789
+ : undefined;
784
790
 
785
791
  // Pi 0.80.8 replaced createAgentSession's modelRegistry option with
786
792
  // modelRuntime, but ExtensionContext still exposes only the registry facade.
787
793
  // Pass both so the full supported Pi range retains the parent's providers.
788
- const parentModelRuntime = (ctx.modelRegistry as unknown as { runtime?: unknown }).runtime;
789
- type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
790
- // `modelRuntime` was added in Pi 0.80.8. Infer it when available, while
791
- // falling back to unknown for older Pi declarations.
792
- type CompatibleModelRuntime = CreateAgentSessionOptions extends {
793
- modelRuntime?: infer Runtime;
794
- } ? Runtime : unknown;
795
- const sessionOpts: CreateAgentSessionOptions & {
794
+ const parentModelRuntime = (ctx.modelRegistry as unknown as { runtime?: ModelRuntime | null }).runtime ?? undefined;
795
+ const sessionOpts: Parameters<typeof createAgentSession>[0] & {
796
796
  modelRegistry: ExtensionContext["modelRegistry"];
797
- modelRuntime?: CompatibleModelRuntime;
797
+ modelRuntime?: ModelRuntime;
798
798
  } = {
799
799
  cwd: effectiveCwd,
800
800
  agentDir,
801
801
  sessionManager,
802
802
  settingsManager,
803
803
  modelRegistry: ctx.modelRegistry,
804
- ...(parentModelRuntime != null && {
805
- modelRuntime: parentModelRuntime as CompatibleModelRuntime,
806
- }),
804
+ ...(parentModelRuntime !== undefined && { modelRuntime: parentModelRuntime }),
807
805
  model,
808
806
  tools: sessionTools,
809
807
  resourceLoader: loader,
808
+ ...(trackedWriteTool && { customTools: [trackedWriteTool as any] }),
810
809
  };
811
810
  if (sessionExcludeTools) {
812
811
  sessionOpts.excludeTools = sessionExcludeTools;