@heyhuynhgiabuu/pi-task 0.1.5 → 0.1.6

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
@@ -4,6 +4,51 @@ All notable changes to `@heyhuynhgiabuu/pi-task` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.1.6] — 2026-06-25
8
+
9
+ ### Changed
10
+
11
+ - Per-task data is now in flat files at the top of `.pi/artifacts/`.
12
+ No per-task subdirs, no `<task-id>` paths. The pikit canonical
13
+ files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md) are flat at the
14
+ same level; pi-task files now sit alongside them.
15
+ - Refined the task TUI widget and background completion rendering:
16
+ foreground/background task stats now use consistent colors, background
17
+ completion summaries use a padded themed result block, completed
18
+ background widgets no longer duplicate the main-pane completion, and
19
+ final tool-call counts now match the live widget count.
20
+
21
+ ### Layout
22
+
23
+ - `.pi/artifacts/TASKS.md` — one `### <task-id>` block per task, with
24
+ H4 subsections for `#### Metadata` (JSON) and `#### Result`.
25
+ - `.pi/artifacts/task-sessions.json` — registry mapping
26
+ `conversation_id` to `{ task_id, session_file }`. Renamed from
27
+ the v0.1.5 `task-conversations.json`.
28
+ - The subagent's session is auto-saved by pi at
29
+ `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task reads
30
+ the last assistant message from there to populate `#### Result`
31
+ in `TASKS.md`. The subagent's final assistant message IS the
32
+ result; no separate result file is required.
33
+
34
+ ### Removed
35
+
36
+ - `.pi/artifacts/task-<id>/` per-task subdirs (and the
37
+ `metadata.json` + `SESSION.md` + `sessions/` files inside them).
38
+ All per-task data lives in `TASKS.md` blocks now.
39
+ - `.pi/artifacts/task-conversations.json` — replaced by
40
+ `task-sessions.json`.
41
+ - The `taskArtifactName(taskId)` / `taskIdFromArtifactName(name)`
42
+ helpers and the `getArtifactsDir(piDir)` / `getTaskDir(piDir)` /
43
+ `getTaskRunsDir(piDir)` helpers.
44
+
45
+ ### Verified
46
+
47
+ - `npm test` passes
48
+ - `npm run typecheck` passes
49
+ - `npm run build` passes
50
+ - `npm run smoke` passes
51
+
7
52
  ## [0.1.4] — 2026-06-21
8
53
 
9
54
  ### Fixed
@@ -76,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
76
121
  - `npm view @heyhuynhgiabuu/pi-task@0.1.2 pi` returns
77
122
  `{ extensions: [ './dist/index.js' ] }`
78
123
 
79
- [0.1.2]: https://github.com/buddingnewinsights/pi-task/releases/tag/v0.1.2
124
+ [0.1.2]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.2
80
125
 
81
126
  ## [0.1.1] — 2025
82
127
 
@@ -116,6 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
116
161
 
117
162
  See the git history: `git log --oneline -- CHANGELOG.md`.
118
163
 
119
- [0.1.1]: https://github.com/buddingnewinsights/pi-task/releases/tag/v0.1.1
120
- [Keep a Changelog]: https://keepachangelog.com/
121
- [Semantic Versioning]: https://semver.org/
164
+ [0.1.1]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.1
165
+ [0.1.4]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.4
166
+ [0.1.5]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.5
167
+ [0.1.6]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.6
168
+ [Keep a Changelog]: https://keepachangelog.com/
169
+ [Semantic Versioning]: https://semver.org/
package/README.md CHANGED
@@ -66,20 +66,25 @@ Durable specialist conversation:
66
66
  }
67
67
  ```
68
68
 
69
- `conversation_id` maps to one existing `task-<id>` artifact under `.pi/artifacts/` and reuses its `sessions/` directory on later calls. This is for scoped specialist memory, e.g. a reusable research assistant. Use `/task-sessions` to list known durable conversations.
69
+ `conversation_id` maps to a durable subagent run. Reused across calls
70
+ to keep specialist memory, e.g. a reusable research assistant.
71
+ Use `/task-sessions` to list known durable conversations.
70
72
 
71
- Stored files:
73
+ Stored files (all flat at the top of `.pi/artifacts/`, no
74
+ per-task subdirs):
72
75
 
73
- ```
74
- .pi/artifacts/task-registry.json
75
- .pi/artifacts/task-<id>/CONTEXT.md
76
- .pi/artifacts/task-<id>/RESULT.md
77
- .pi/artifacts/task-<id>/SESSION.md
78
- .pi/artifacts/task-<id>/metadata.json
79
- .pi/artifacts/task-<id>/sessions/
80
- ```
76
+ ```
77
+ .pi/artifacts/TASKS.md # one ### <task-id> block per task
78
+ .pi/artifacts/task-sessions.json # conversation_id -> { task_id, session_file }
79
+ ```
80
+
81
+ The subagent's session is auto-saved by pi at
82
+ `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task reads
83
+ the last assistant message from there to populate
84
+ `#### Result` in `TASKS.md`. The subagent's final message IS
85
+ the result; no separate result file is required.
81
86
 
82
- Note: true conversation resume requires the tmux/CLI backend so Pi can reopen the saved subagent session. SDK fallback can run one-shot tasks, but it cannot resume a prior Pi session.
87
+ Note: true conversation resume requires the tmux/CLI backend so Pi can reopen the saved subagent session. SDK fallback can run one-shot tasks, but it cannot resume a prior Pi session.
83
88
 
84
89
  ## Agent precedence
85
90
 
@@ -1,39 +1,86 @@
1
1
  /**
2
2
  * Conversational subagent helpers.
3
3
  *
4
- * Durable subagent conversations reuse the existing
5
- * `.pi/artifacts/task-<id>/` artifact convention and add a small
6
- * `conversation_id` -> `task-<id>` registry under the same artifacts dir.
4
+ * Per-task data lives in `.pi/artifacts/TASKS.md` as `### <task-id>` blocks.
5
+ * A small `task-sessions.json` registry in the same directory maps
6
+ * `conversation_id` to the auto-saved session file path so the
7
+ * subagent can be resumed later.
8
+ *
9
+ * The subagent's session is auto-saved by pi at
10
+ * `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task does not
11
+ * maintain its own session storage.
12
+ *
13
+ * All artifacts live flat at the top of `.pi/artifacts/`, alongside the
14
+ * pikit canonical files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md).
15
+ * No subdirs. No per-task paths.
7
16
  */
17
+ export declare const TASKS_FILE = "TASKS.md";
18
+ export declare const TASK_SESSIONS_REGISTRY_FILE = "task-sessions.json";
8
19
  export interface ConversationMetadata {
9
20
  conversation_id: string;
10
21
  task_id: string;
11
- artifact: string;
12
22
  agent_type: string;
13
- session_dir: string;
14
- session_name: string;
23
+ session_file: string;
15
24
  created_at: string;
16
25
  last_used_at: string;
17
26
  last_prompt?: string;
18
27
  }
19
- export type ConversationRegistry = Record<string, string>;
20
- export declare const CONVERSATION_REGISTRY_FILE = "task-conversations.json";
21
- export declare function getArtifactsDir(piDir: string): string;
22
- export declare function getConversationRegistryPath(piDir: string): string;
23
- export declare function taskArtifactName(taskId: string): string;
24
- export declare function taskIdFromArtifactName(artifactName: string): string;
28
+ export type TaskSessionsRegistry = Record<string, {
29
+ task_id: string;
30
+ session_file: string;
31
+ }>;
32
+ export declare function getTasksFilePath(piDir: string): string;
33
+ export declare function getTaskSessionsRegistryPath(piDir: string): string;
25
34
  export declare function normalizeConversationId(value: unknown): string | undefined;
26
- export declare function readConversationRegistry(piDir: string): ConversationRegistry;
27
- export declare function writeConversationRegistry(piDir: string, registry: ConversationRegistry): void;
28
- export declare function readConversationMetadata(metadataPath: string): ConversationMetadata | undefined;
29
- export declare function buildSessionCard(metadata: ConversationMetadata): string;
30
- export declare function writeConversationArtifacts(options: {
31
- taskDir: string;
35
+ export declare function readTaskSessionsRegistry(piDir: string): TaskSessionsRegistry;
36
+ export declare function writeTaskSessionsRegistry(piDir: string, registry: TaskSessionsRegistry): void;
37
+ /**
38
+ * Find a `### <task-id>` block in TASKS.md. Returns the block content
39
+ * (everything between the heading and the next H3 or EOF) plus the
40
+ * status line if present. Returns undefined if no block exists.
41
+ */
42
+ export declare function readTaskBlock(piDir: string, taskId: string): {
43
+ status: string | null;
44
+ body: string;
45
+ } | undefined;
46
+ export declare function listTaskBlocks(piDir: string): Map<string, {
47
+ status: string | null;
48
+ body: string;
49
+ }>;
50
+ /**
51
+ * Append or update a `### <task-id>` block in TASKS.md. If the block
52
+ * already exists, its body is replaced. Otherwise, the block is
53
+ * appended at the end of the file.
54
+ */
55
+ export declare function writeTaskBlock(options: {
56
+ piDir: string;
57
+ taskId: string;
58
+ status: "active" | "done" | "abandoned";
59
+ updated: string;
60
+ body: string;
61
+ }): void;
62
+ export declare function parseMetadataFromBody(body: string | undefined): {
63
+ created_at?: string;
64
+ last_used_at?: string;
65
+ agent_type?: string;
66
+ session_file?: string;
67
+ conversation_id?: string;
68
+ last_prompt?: string;
69
+ } | undefined;
70
+ export interface WriteTaskBlockInput {
71
+ piDir: string;
32
72
  taskId: string;
33
73
  conversationId: string;
34
74
  agentType: string;
35
- sessionDir: string;
36
- sessionName: string;
75
+ sessionFile: string;
37
76
  prompt: string;
38
- }): ConversationMetadata;
77
+ result: string;
78
+ resultLabel?: string;
79
+ }
80
+ /**
81
+ * Persist a completed task: write (or update) the `### <task-id>` block
82
+ * in TASKS.md with metadata and result as H4 subsections. Also updates
83
+ * the task-sessions registry.
84
+ */
85
+ export declare function writeConversationArtifacts(input: WriteTaskBlockInput): ConversationMetadata;
39
86
  export declare function renderConversationSessions(piDir: string): string;
@@ -1,19 +1,28 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- export const CONVERSATION_REGISTRY_FILE = "task-conversations.json";
4
- export function getArtifactsDir(piDir) {
5
- return join(piDir, "artifacts");
3
+ /**
4
+ * Conversational subagent helpers.
5
+ *
6
+ * Per-task data lives in `.pi/artifacts/TASKS.md` as `### <task-id>` blocks.
7
+ * A small `task-sessions.json` registry in the same directory maps
8
+ * `conversation_id` to the auto-saved session file path so the
9
+ * subagent can be resumed later.
10
+ *
11
+ * The subagent's session is auto-saved by pi at
12
+ * `~/.pi/agent/sessions/<cwd>/<session-id>.jsonl`. pi-task does not
13
+ * maintain its own session storage.
14
+ *
15
+ * All artifacts live flat at the top of `.pi/artifacts/`, alongside the
16
+ * pikit canonical files (TODO.md, PLAN.md, PROGRESS.md, DECISIONS.md).
17
+ * No subdirs. No per-task paths.
18
+ */
19
+ export const TASKS_FILE = "TASKS.md";
20
+ export const TASK_SESSIONS_REGISTRY_FILE = "task-sessions.json";
21
+ export function getTasksFilePath(piDir) {
22
+ return join(piDir, "artifacts", TASKS_FILE);
6
23
  }
7
- export function getConversationRegistryPath(piDir) {
8
- return join(getArtifactsDir(piDir), CONVERSATION_REGISTRY_FILE);
9
- }
10
- export function taskArtifactName(taskId) {
11
- return taskId.startsWith("task-") ? taskId : `task-${taskId}`;
12
- }
13
- export function taskIdFromArtifactName(artifactName) {
14
- return artifactName.startsWith("task-")
15
- ? artifactName.slice("task-".length)
16
- : artifactName;
24
+ export function getTaskSessionsRegistryPath(piDir) {
25
+ return join(piDir, "artifacts", TASK_SESSIONS_REGISTRY_FILE);
17
26
  }
18
27
  export function normalizeConversationId(value) {
19
28
  if (typeof value !== "string")
@@ -26,16 +35,21 @@ export function normalizeConversationId(value) {
26
35
  }
27
36
  return conversationId;
28
37
  }
29
- export function readConversationRegistry(piDir) {
38
+ export function readTaskSessionsRegistry(piDir) {
30
39
  try {
31
- const parsed = JSON.parse(readFileSync(getConversationRegistryPath(piDir), "utf-8"));
40
+ const parsed = JSON.parse(readFileSync(getTaskSessionsRegistryPath(piDir), "utf-8"));
32
41
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
33
42
  return {};
34
43
  }
35
44
  const registry = {};
36
45
  for (const [key, value] of Object.entries(parsed)) {
37
- if (typeof value === "string")
38
- registry[key] = value;
46
+ if (value &&
47
+ typeof value === "object" &&
48
+ typeof value.task_id === "string" &&
49
+ typeof value.session_file === "string") {
50
+ const v = value;
51
+ registry[key] = { task_id: v.task_id, session_file: v.session_file };
52
+ }
39
53
  }
40
54
  return registry;
41
55
  }
@@ -43,81 +57,182 @@ export function readConversationRegistry(piDir) {
43
57
  return {};
44
58
  }
45
59
  }
46
- export function writeConversationRegistry(piDir, registry) {
47
- const artifactsDir = getArtifactsDir(piDir);
48
- mkdirSync(artifactsDir, { recursive: true });
49
- writeFileSync(getConversationRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
60
+ export function writeTaskSessionsRegistry(piDir, registry) {
61
+ mkdirSync(join(piDir, "artifacts"), { recursive: true });
62
+ writeFileSync(getTaskSessionsRegistryPath(piDir), `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
50
63
  }
51
- export function readConversationMetadata(metadataPath) {
64
+ /**
65
+ * Find a `### <task-id>` block in TASKS.md. Returns the block content
66
+ * (everything between the heading and the next H3 or EOF) plus the
67
+ * status line if present. Returns undefined if no block exists.
68
+ */
69
+ export function readTaskBlock(piDir, taskId) {
70
+ let content;
52
71
  try {
53
- const parsed = JSON.parse(readFileSync(metadataPath, "utf-8"));
54
- if (!parsed.conversation_id || !parsed.task_id)
55
- return undefined;
56
- return parsed;
72
+ content = readFileSync(getTasksFilePath(piDir), "utf-8");
57
73
  }
58
74
  catch {
59
75
  return undefined;
60
76
  }
77
+ return parseTaskBlocks(content).get(taskId);
61
78
  }
62
- export function buildSessionCard(metadata) {
63
- return [
64
- `# ${metadata.conversation_id}`,
65
- "",
66
- `Agent: ${metadata.agent_type}`,
67
- `Task: ${taskArtifactName(metadata.task_id)}`,
68
- `Last used: ${metadata.last_used_at}`,
69
- `Session dir: ${metadata.session_dir}`,
70
- "",
71
- "## Resume",
79
+ export function listTaskBlocks(piDir) {
80
+ let content;
81
+ try {
82
+ content = readFileSync(getTasksFilePath(piDir), "utf-8");
83
+ }
84
+ catch {
85
+ return new Map();
86
+ }
87
+ return parseTaskBlocks(content);
88
+ }
89
+ function parseTaskBlocks(content) {
90
+ const blocks = new Map();
91
+ const lines = content.split("\n");
92
+ let currentTaskId = null;
93
+ let currentStatus = null;
94
+ let currentBody = [];
95
+ const flush = () => {
96
+ if (currentTaskId !== null) {
97
+ blocks.set(currentTaskId, {
98
+ status: currentStatus,
99
+ body: currentBody.join("\n"),
100
+ });
101
+ }
102
+ currentTaskId = null;
103
+ currentStatus = null;
104
+ currentBody = [];
105
+ };
106
+ for (const line of lines) {
107
+ const heading = line.match(/^###\s+(\S+)\s*$/);
108
+ if (heading) {
109
+ flush();
110
+ currentTaskId = heading[1];
111
+ continue;
112
+ }
113
+ if (currentTaskId === null)
114
+ continue;
115
+ const statusMatch = line.match(/^status:\s*(\S+)/);
116
+ if (statusMatch) {
117
+ currentStatus = statusMatch[1].toLowerCase();
118
+ continue;
119
+ }
120
+ currentBody.push(line);
121
+ }
122
+ flush();
123
+ return blocks;
124
+ }
125
+ /**
126
+ * Append or update a `### <task-id>` block in TASKS.md. If the block
127
+ * already exists, its body is replaced. Otherwise, the block is
128
+ * appended at the end of the file.
129
+ */
130
+ export function writeTaskBlock(options) {
131
+ const path = getTasksFilePath(options.piDir);
132
+ let content = "";
133
+ try {
134
+ content = readFileSync(path, "utf-8");
135
+ if (!content.endsWith("\n"))
136
+ content += "\n";
137
+ }
138
+ catch {
139
+ content = "";
140
+ }
141
+ const heading = `### ${options.taskId}`;
142
+ const statusLine = `status: ${options.status} | updated: ${options.updated}`;
143
+ const block = `${heading}\n${statusLine}\n\n${options.body}\n`;
144
+ const headingRe = new RegExp(`^### ${escapeRegExp(options.taskId)}\\s*$`, "m");
145
+ const match = content.match(headingRe);
146
+ if (match && match.index !== undefined) {
147
+ const start = match.index;
148
+ const after = content.slice(start);
149
+ const nextHeading = after.search(/^###\s+\S+/m);
150
+ const end = nextHeading > 0 ? start + nextHeading : content.length;
151
+ content = content.slice(0, start) + block + content.slice(end);
152
+ }
153
+ else {
154
+ if (content.length > 0 && !content.endsWith("\n\n")) {
155
+ content += "\n";
156
+ }
157
+ content += block;
158
+ }
159
+ mkdirSync(join(options.piDir, "artifacts"), { recursive: true });
160
+ writeFileSync(path, content, "utf-8");
161
+ }
162
+ function escapeRegExp(value) {
163
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
164
+ }
165
+ export function parseMetadataFromBody(body) {
166
+ if (!body)
167
+ return undefined;
168
+ const match = body.match(/```json\n([\s\S]*?)\n```/);
169
+ if (!match)
170
+ return undefined;
171
+ try {
172
+ return JSON.parse(match[1]);
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ /**
179
+ * Persist a completed task: write (or update) the `### <task-id>` block
180
+ * in TASKS.md with metadata and result as H4 subsections. Also updates
181
+ * the task-sessions registry.
182
+ */
183
+ export function writeConversationArtifacts(input) {
184
+ const now = new Date().toISOString();
185
+ const existing = readTaskBlock(input.piDir, input.taskId);
186
+ const previous = parseMetadataFromBody(existing?.body);
187
+ const metadata = {
188
+ conversation_id: input.conversationId,
189
+ task_id: input.taskId,
190
+ agent_type: input.agentType,
191
+ session_file: input.sessionFile,
192
+ created_at: previous?.created_at ?? now,
193
+ last_used_at: now,
194
+ last_prompt: input.prompt,
195
+ };
196
+ const body = [
197
+ "#### Metadata",
72
198
  "",
73
199
  "```json",
74
- JSON.stringify({
75
- agent_type: metadata.agent_type,
76
- conversation_id: metadata.conversation_id,
77
- prompt: "Continue from the prior specialist conversation.",
78
- }, null, 2),
200
+ JSON.stringify(metadata, null, 2),
79
201
  "```",
80
202
  "",
81
- "## Last prompt",
203
+ "#### Result",
82
204
  "",
83
- metadata.last_prompt ?? "",
205
+ input.result.trim(),
84
206
  "",
85
207
  ].join("\n");
86
- }
87
- export function writeConversationArtifacts(options) {
88
- const now = new Date().toISOString();
89
- const metadataPath = join(options.taskDir, "metadata.json");
90
- const existing = readConversationMetadata(metadataPath);
91
- const metadata = {
92
- conversation_id: options.conversationId,
93
- task_id: options.taskId,
94
- artifact: taskArtifactName(options.taskId),
95
- agent_type: options.agentType,
96
- session_dir: options.sessionDir,
97
- session_name: options.sessionName,
98
- created_at: existing?.created_at ?? now,
99
- last_used_at: now,
100
- last_prompt: options.prompt,
208
+ writeTaskBlock({
209
+ piDir: input.piDir,
210
+ taskId: input.taskId,
211
+ status: "done",
212
+ updated: now,
213
+ body,
214
+ });
215
+ const registry = readTaskSessionsRegistry(input.piDir);
216
+ registry[input.conversationId] = {
217
+ task_id: input.taskId,
218
+ session_file: input.sessionFile,
101
219
  };
102
- mkdirSync(options.taskDir, { recursive: true });
103
- writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf-8");
104
- writeFileSync(join(options.taskDir, "SESSION.md"), buildSessionCard(metadata), "utf-8");
220
+ writeTaskSessionsRegistry(input.piDir, registry);
105
221
  return metadata;
106
222
  }
107
223
  export function renderConversationSessions(piDir) {
108
- const registry = readConversationRegistry(piDir);
109
- const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
110
- if (entries.length === 0) {
224
+ const blocks = listTaskBlocks(piDir);
225
+ if (blocks.size === 0) {
111
226
  return 'No durable task conversations found. Start one with task({ conversation_id: "research-ai", ... }).';
112
227
  }
228
+ const entries = Array.from(blocks.entries()).sort(([a], [b]) => a.localeCompare(b));
113
229
  const lines = ["Durable task conversations:"];
114
- for (const [conversationId, artifactName] of entries) {
115
- const taskId = taskIdFromArtifactName(artifactName);
116
- const metadata = readConversationMetadata(join(getArtifactsDir(piDir), taskArtifactName(taskId), "metadata.json"));
117
- const suffix = metadata
118
- ? ` ${metadata.agent_type}, last used ${metadata.last_used_at}`
119
- : "";
120
- lines.push(`${conversationId} -> ${taskArtifactName(taskId)}${suffix}`);
230
+ for (const [taskId, block] of entries) {
231
+ const metadata = parseMetadataFromBody(block.body);
232
+ const agent = metadata?.agent_type ?? "unknown";
233
+ const last = metadata?.last_used_at ?? "unknown";
234
+ const conv = metadata?.conversation_id ?? "(no conversation_id)";
235
+ lines.push(`${conv} -> ${taskId} — ${agent}, last used ${last}`);
121
236
  }
122
237
  return lines.join("\n");
123
238
  }
package/dist/helpers.d.ts CHANGED
@@ -72,12 +72,12 @@ export declare function formatAgentList(agents: AgentConfig[]): string;
72
72
  * Build pi CLI arguments for spawning or resuming a sub-agent session.
73
73
  *
74
74
  * - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
75
- * - Resume: pass `resume=true` `--session <name>` is included so pi
76
- * continues the existing session file in --session-dir.
77
- */
78
- export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[]): string[];
75
+ * - Resume: pass `resume=true` and optionally `resumeSessionRef`
76
+ * `--session <ref>` is included so pi continues an existing session.
77
+ */
78
+ export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[], resumeSessionRef?: string): string[];
79
79
  /** Count tool uses and turns from pi JSONL session files. */
80
- export declare function countToolUses(sessionDir: string): {
80
+ export declare function countToolUses(sessionDir: string, sessionName?: string): {
81
81
  toolUses: number;
82
82
  turns: number;
83
83
  };
@@ -94,7 +94,7 @@ export declare function summarizeArgs(toolName: string, args: unknown): string;
94
94
  * Returns total counts plus the last `limit` records in chronological order.
95
95
  * Safe against malformed lines and missing fields.
96
96
  */
97
- export declare function readRecentToolCalls(sessionDir: string, limit?: number): {
97
+ export declare function readRecentToolCalls(sessionDir: string, limit?: number, sessionName?: string): {
98
98
  toolUses: number;
99
99
  turns: number;
100
100
  recent: ToolCallRecord[];
package/dist/helpers.js CHANGED
@@ -256,22 +256,42 @@ export function formatAgentList(agents) {
256
256
  * Build pi CLI arguments for spawning or resuming a sub-agent session.
257
257
  *
258
258
  * - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
259
- * - Resume: pass `resume=true` `--session <name>` is included so pi
260
- * continues the existing session file in --session-dir.
261
- */
262
- export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames) {
259
+ * - Resume: pass `resume=true` and optionally `resumeSessionRef`
260
+ * `--session <ref>` is included so pi continues an existing session.
261
+ */
262
+ export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames, resumeSessionRef) {
263
263
  return buildPiArgv({
264
264
  agent,
265
265
  sessionName,
266
266
  sessionDir,
267
267
  promptContent,
268
268
  resume,
269
+ resumeSessionRef,
269
270
  parentToolNames,
270
271
  });
271
272
  }
272
273
  // ─── JSONL Session Helpers ───────────────────────────────────────────────────
274
+ function matchesJsonlSessionName(content, sessionName) {
275
+ if (!sessionName)
276
+ return true;
277
+ for (const rawLine of content.split("\n")) {
278
+ const line = rawLine.trim();
279
+ if (!line)
280
+ continue;
281
+ try {
282
+ const entry = JSON.parse(line);
283
+ if (entry.type === "session_info") {
284
+ return (entry.name ?? entry.session_info?.name) === sessionName;
285
+ }
286
+ }
287
+ catch {
288
+ // Skip malformed lines
289
+ }
290
+ }
291
+ return false;
292
+ }
273
293
  /** Count tool uses and turns from pi JSONL session files. */
274
- export function countToolUses(sessionDir) {
294
+ export function countToolUses(sessionDir, sessionName) {
275
295
  let toolUses = 0;
276
296
  let turns = 0;
277
297
  try {
@@ -280,6 +300,8 @@ export function countToolUses(sessionDir) {
280
300
  const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
281
301
  for (const file of files) {
282
302
  const content = readFileSync(join(sessionDir, file), "utf-8");
303
+ if (!matchesJsonlSessionName(content, sessionName))
304
+ continue;
283
305
  for (const rawLine of content.split("\n")) {
284
306
  const line = rawLine.trim();
285
307
  if (!line)
@@ -368,7 +390,7 @@ export function summarizeArgs(toolName, args) {
368
390
  * Returns total counts plus the last `limit` records in chronological order.
369
391
  * Safe against malformed lines and missing fields.
370
392
  */
371
- export function readRecentToolCalls(sessionDir, limit = 12) {
393
+ export function readRecentToolCalls(sessionDir, limit = 12, sessionName) {
372
394
  let toolUses = 0;
373
395
  let turns = 0;
374
396
  const calls = [];
@@ -379,6 +401,8 @@ export function readRecentToolCalls(sessionDir, limit = 12) {
379
401
  const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
380
402
  for (const file of files) {
381
403
  const content = readFileSync(join(sessionDir, file), "utf-8");
404
+ if (!matchesJsonlSessionName(content, sessionName))
405
+ continue;
382
406
  for (const rawLine of content.split("\n")) {
383
407
  const line = rawLine.trim();
384
408
  if (!line)