@heyhuynhgiabuu/pi-task 0.1.3 → 0.1.5
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 +23 -0
- package/README.md +28 -1
- package/dist/conversation.d.ts +39 -0
- package/dist/conversation.js +123 -0
- package/dist/helpers.d.ts +3 -1
- package/dist/helpers.js +23 -2
- package/dist/index.d.ts +17 -0
- package/dist/index.js +182 -8
- package/dist/subagent/tmux.d.ts +4 -0
- package/dist/subagent/tmux.js +21 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.4] — 2026-06-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Detect the current tmux pane size before launching a task pane and choose
|
|
12
|
+
the split direction based on available space: side-by-side for wide panes,
|
|
13
|
+
stacked for narrow panes.
|
|
14
|
+
- Target the exact pane that was measured when running `tmux split-window`,
|
|
15
|
+
avoiding focus races where a different pane could be split.
|
|
16
|
+
- Apply the same pane-size-aware split logic to the subagent tmux helper.
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
|
|
20
|
+
- `npm test` passes
|
|
21
|
+
- `npm run typecheck` passes
|
|
22
|
+
- `npm run build` passes
|
|
23
|
+
- `npm run smoke` passes
|
|
24
|
+
- `npm pack --dry-run` succeeds
|
|
25
|
+
- Real tmux integration check passed for narrow `120x40` and wide `200x40`
|
|
26
|
+
sessions.
|
|
27
|
+
|
|
28
|
+
[0.1.4]: https://github.com/heyhuynhgiabuu/pi-task/releases/tag/v0.1.4
|
|
29
|
+
|
|
7
30
|
## [0.1.3] — 2026-06-21
|
|
8
31
|
|
|
9
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Foreground task:
|
|
|
45
45
|
|
|
46
46
|
Background task:
|
|
47
47
|
|
|
48
|
-
```
|
|
48
|
+
```
|
|
49
49
|
{
|
|
50
50
|
"agent_type": "scout",
|
|
51
51
|
"description": "Research SDK docs",
|
|
@@ -54,6 +54,33 @@ Background task:
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
Durable specialist conversation:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
{
|
|
61
|
+
"agent_type": "scout",
|
|
62
|
+
"conversation_id": "research-ai",
|
|
63
|
+
"description": "Ask research assistant",
|
|
64
|
+
"background": false,
|
|
65
|
+
"prompt": "Continue our prior research thread. What did we conclude about retrieval evaluation?"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
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.
|
|
70
|
+
|
|
71
|
+
Stored files:
|
|
72
|
+
|
|
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
|
+
```
|
|
81
|
+
|
|
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.
|
|
83
|
+
|
|
57
84
|
## Agent precedence
|
|
58
85
|
|
|
59
86
|
When two agents have the same name, later sources override earlier ones:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversational subagent helpers.
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConversationMetadata {
|
|
9
|
+
conversation_id: string;
|
|
10
|
+
task_id: string;
|
|
11
|
+
artifact: string;
|
|
12
|
+
agent_type: string;
|
|
13
|
+
session_dir: string;
|
|
14
|
+
session_name: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
last_used_at: string;
|
|
17
|
+
last_prompt?: string;
|
|
18
|
+
}
|
|
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;
|
|
25
|
+
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;
|
|
32
|
+
taskId: string;
|
|
33
|
+
conversationId: string;
|
|
34
|
+
agentType: string;
|
|
35
|
+
sessionDir: string;
|
|
36
|
+
sessionName: string;
|
|
37
|
+
prompt: string;
|
|
38
|
+
}): ConversationMetadata;
|
|
39
|
+
export declare function renderConversationSessions(piDir: string): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
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");
|
|
6
|
+
}
|
|
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;
|
|
17
|
+
}
|
|
18
|
+
export function normalizeConversationId(value) {
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
return undefined;
|
|
21
|
+
const conversationId = value.trim();
|
|
22
|
+
if (!conversationId)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (!/^[A-Za-z0-9._-]{1,80}$/.test(conversationId)) {
|
|
25
|
+
throw new Error("conversation_id must be 1-80 chars and contain only letters, numbers, '.', '_' or '-'");
|
|
26
|
+
}
|
|
27
|
+
return conversationId;
|
|
28
|
+
}
|
|
29
|
+
export function readConversationRegistry(piDir) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(readFileSync(getConversationRegistryPath(piDir), "utf-8"));
|
|
32
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
const registry = {};
|
|
36
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
37
|
+
if (typeof value === "string")
|
|
38
|
+
registry[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return registry;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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");
|
|
50
|
+
}
|
|
51
|
+
export function readConversationMetadata(metadataPath) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
54
|
+
if (!parsed.conversation_id || !parsed.task_id)
|
|
55
|
+
return undefined;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
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",
|
|
72
|
+
"",
|
|
73
|
+
"```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),
|
|
79
|
+
"```",
|
|
80
|
+
"",
|
|
81
|
+
"## Last prompt",
|
|
82
|
+
"",
|
|
83
|
+
metadata.last_prompt ?? "",
|
|
84
|
+
"",
|
|
85
|
+
].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,
|
|
101
|
+
};
|
|
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");
|
|
105
|
+
return metadata;
|
|
106
|
+
}
|
|
107
|
+
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) {
|
|
111
|
+
return 'No durable task conversations found. Start one with task({ conversation_id: "research-ai", ... }).';
|
|
112
|
+
}
|
|
113
|
+
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}`);
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -50,7 +50,9 @@ export declare function parseResultXml(raw: string): ParsedResult;
|
|
|
50
50
|
export declare function formatMs(ms: number): string;
|
|
51
51
|
export declare function parseIdTimestamp(id: string): number;
|
|
52
52
|
export declare function shellQuote(value: string): string;
|
|
53
|
-
export
|
|
53
|
+
export type TmuxSplitDirection = "-h" | "-v";
|
|
54
|
+
export declare function chooseTmuxSplitDirection(paneWidth: number, paneHeight: number): TmuxSplitDirection;
|
|
55
|
+
export declare function buildTmuxSplitWindowArgs(cwd: string, command: string, direction?: TmuxSplitDirection, targetPane?: string | null): string[];
|
|
54
56
|
export interface BackgroundReceiptInput {
|
|
55
57
|
taskId: string;
|
|
56
58
|
agentType: string;
|
package/dist/helpers.js
CHANGED
|
@@ -128,8 +128,29 @@ export function parseIdTimestamp(id) {
|
|
|
128
128
|
export function shellQuote(value) {
|
|
129
129
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
130
130
|
}
|
|
131
|
-
export function
|
|
132
|
-
|
|
131
|
+
export function chooseTmuxSplitDirection(paneWidth, paneHeight) {
|
|
132
|
+
const minSideBySideWidth = 160;
|
|
133
|
+
const minStackedHeight = 24;
|
|
134
|
+
if (Number.isFinite(paneWidth) && paneWidth >= minSideBySideWidth) {
|
|
135
|
+
return "-h";
|
|
136
|
+
}
|
|
137
|
+
if (Number.isFinite(paneHeight) && paneHeight >= minStackedHeight) {
|
|
138
|
+
return "-v";
|
|
139
|
+
}
|
|
140
|
+
return "-h";
|
|
141
|
+
}
|
|
142
|
+
export function buildTmuxSplitWindowArgs(cwd, command, direction = "-h", targetPane) {
|
|
143
|
+
const args = [
|
|
144
|
+
"split-window",
|
|
145
|
+
direction,
|
|
146
|
+
"-P",
|
|
147
|
+
"-F",
|
|
148
|
+
"#{pane_id}",
|
|
149
|
+
];
|
|
150
|
+
if (targetPane)
|
|
151
|
+
args.push("-t", targetPane);
|
|
152
|
+
args.push("-c", cwd, command);
|
|
153
|
+
return args;
|
|
133
154
|
}
|
|
134
155
|
export function formatBackgroundReceipt(input) {
|
|
135
156
|
return [
|
package/dist/index.d.ts
CHANGED
|
@@ -15,4 +15,21 @@
|
|
|
15
15
|
* detection, 30-minute timeout.
|
|
16
16
|
*/
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
export /** Details attached to tool result for rendering. */ interface TaskDetails {
|
|
19
|
+
task_id: string;
|
|
20
|
+
agent_type: string;
|
|
21
|
+
description: string;
|
|
22
|
+
conversation_id?: string;
|
|
23
|
+
phase: "done" | "timeout" | "aborted" | "failed";
|
|
24
|
+
status?: string;
|
|
25
|
+
summary?: string;
|
|
26
|
+
findings?: string;
|
|
27
|
+
evidence?: string;
|
|
28
|
+
confidence?: string;
|
|
29
|
+
duration_ms?: number;
|
|
30
|
+
turn_count?: number;
|
|
31
|
+
tool_uses?: number;
|
|
32
|
+
background?: boolean;
|
|
33
|
+
tmux_session?: string;
|
|
34
|
+
}
|
|
18
35
|
export default function (pi: ExtensionAPI): void;
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,9 @@ import { randomUUID } from "node:crypto";
|
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { Type } from "@sinclair/typebox";
|
|
24
|
+
import { getArtifactsDir, normalizeConversationId, readConversationMetadata, readConversationRegistry, renderConversationSessions, taskArtifactName, taskIdFromArtifactName, writeConversationArtifacts, writeConversationRegistry, } from "./conversation.js";
|
|
24
25
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
|
-
import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
|
|
26
|
+
import { TASK_BACKGROUND_DEFAULT, TASK_RESULT_XML_INSTRUCTIONS, TASK_TOOL_DESCRIPTION, buildTmuxSplitWindowArgs, chooseTmuxSplitDirection, formatBackgroundReceipt, buildPiArgs, parseResultXml, formatMs, shellQuote, discoverAgents, formatAgentList, countToolUses, readRecentToolCalls, } from "./helpers.js";
|
|
26
27
|
import { runSdkSubagent } from "./subagent/runSdk.js";
|
|
27
28
|
import { checkTaskCompletion, waitForTaskCompletion as waitForSessionTaskCompletion, } from "./subagent/waitCompletion.js";
|
|
28
29
|
import { buildAgentToolSelection } from "./agent-tools.js";
|
|
@@ -31,7 +32,7 @@ const BUNDLED_AGENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "a
|
|
|
31
32
|
const BACKGROUND_CHECK_MS = 10_000; // poll every 10 sec
|
|
32
33
|
const COUNT_POLL_MS = 3_000; // update toolcall counts every 3 sec
|
|
33
34
|
const TASK_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes
|
|
34
|
-
//
|
|
35
|
+
// Conversation helpers live in ./conversation.js.
|
|
35
36
|
function readRegistry(piDir) {
|
|
36
37
|
const path = join(piDir, "task-registry.json");
|
|
37
38
|
try {
|
|
@@ -79,9 +80,28 @@ function getCurrentPaneId() {
|
|
|
79
80
|
return null;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
function getCurrentPaneSize(targetPane) {
|
|
84
|
+
try {
|
|
85
|
+
const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
|
|
86
|
+
if (targetPane)
|
|
87
|
+
args.splice(1, 0, "-t", targetPane);
|
|
88
|
+
const raw = tmuxCmd(args);
|
|
89
|
+
const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
|
|
90
|
+
const width = Number(widthRaw);
|
|
91
|
+
const height = Number(heightRaw);
|
|
92
|
+
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
93
|
+
return null;
|
|
94
|
+
return { width, height };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
82
100
|
function splitWindowPane(cwd, command) {
|
|
83
101
|
const originalPane = getCurrentPaneId();
|
|
84
|
-
const
|
|
102
|
+
const paneSize = getCurrentPaneSize(originalPane);
|
|
103
|
+
const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
|
|
104
|
+
const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
|
|
85
105
|
return { paneId, originalPane };
|
|
86
106
|
}
|
|
87
107
|
function killAgentPane(paneId, originalPane) {
|
|
@@ -171,6 +191,7 @@ export default function (pi) {
|
|
|
171
191
|
startedAt: entry.startedAt,
|
|
172
192
|
toolUses: 0,
|
|
173
193
|
turns: 0,
|
|
194
|
+
conversationId: entry.conversationId,
|
|
174
195
|
recentCalls: [],
|
|
175
196
|
};
|
|
176
197
|
backgroundTasks.set(entry.id, bgtask);
|
|
@@ -459,7 +480,10 @@ export default function (pi) {
|
|
|
459
480
|
description: "A short (3-5 word) summary of the task",
|
|
460
481
|
}),
|
|
461
482
|
task_id: Type.Optional(Type.String({
|
|
462
|
-
description: "Resume
|
|
483
|
+
description: "Resume an existing background task by id instead of starting a new task.",
|
|
484
|
+
})),
|
|
485
|
+
conversation_id: Type.Optional(Type.String({
|
|
486
|
+
description: "Durable specialist conversation id. Reuses .pi/artifacts/task-<id>/sessions when called again.",
|
|
463
487
|
})),
|
|
464
488
|
background: Type.Optional(Type.Boolean({
|
|
465
489
|
description: "Run in background (async). You will be notified when it completes. DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background.",
|
|
@@ -489,13 +513,115 @@ export default function (pi) {
|
|
|
489
513
|
isError: true,
|
|
490
514
|
};
|
|
491
515
|
}
|
|
492
|
-
// ── Resolve task identity: new or resume
|
|
516
|
+
// ── Resolve task identity: new, task resume, or conversation resume ──
|
|
517
|
+
const conversationId = normalizeConversationId(params.conversation_id);
|
|
518
|
+
const conversationRegistry = conversationId
|
|
519
|
+
? readConversationRegistry(piDir)
|
|
520
|
+
: {};
|
|
521
|
+
const registeredArtifact = conversationId
|
|
522
|
+
? conversationRegistry[conversationId]
|
|
523
|
+
: undefined;
|
|
524
|
+
const registeredTaskId = registeredArtifact
|
|
525
|
+
? taskIdFromArtifactName(registeredArtifact)
|
|
526
|
+
: undefined;
|
|
527
|
+
if (params.task_id &&
|
|
528
|
+
registeredTaskId &&
|
|
529
|
+
params.task_id !== registeredTaskId) {
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: `conversation_id "${conversationId}" maps to ${taskArtifactName(registeredTaskId)}, not ${taskArtifactName(params.task_id)}. Omit task_id or use the mapped task id.`,
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
details: {
|
|
538
|
+
phase: "failed",
|
|
539
|
+
error: "conversation_id/task_id mismatch",
|
|
540
|
+
},
|
|
541
|
+
isError: true,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
493
544
|
let id;
|
|
494
545
|
let sessionName;
|
|
495
546
|
let artifactDir;
|
|
496
547
|
let resultPath;
|
|
497
548
|
let resume = false;
|
|
498
|
-
if (
|
|
549
|
+
if (registeredTaskId) {
|
|
550
|
+
id = registeredTaskId;
|
|
551
|
+
sessionName = taskArtifactName(id);
|
|
552
|
+
artifactDir = join(getArtifactsDir(piDir), sessionName);
|
|
553
|
+
resultPath = join(artifactDir, "RESULT.md");
|
|
554
|
+
if (!existsSync(artifactDir)) {
|
|
555
|
+
return {
|
|
556
|
+
content: [
|
|
557
|
+
{
|
|
558
|
+
type: "text",
|
|
559
|
+
text: `conversation_id "${conversationId}" points to missing artifact directory: ${artifactDir}`,
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
details: {
|
|
563
|
+
phase: "failed",
|
|
564
|
+
error: "Conversation artifact dir missing",
|
|
565
|
+
conversation_id: conversationId,
|
|
566
|
+
},
|
|
567
|
+
isError: true,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const metadata = readConversationMetadata(join(artifactDir, "metadata.json"));
|
|
571
|
+
if (metadata?.agent_type && metadata.agent_type !== agent.name) {
|
|
572
|
+
return {
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: "text",
|
|
576
|
+
text: `conversation_id "${conversationId}" belongs to agent "${metadata.agent_type}", not "${agent.name}". Use the original agent_type or start a different conversation_id.`,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
details: {
|
|
580
|
+
phase: "failed",
|
|
581
|
+
error: "conversation_id agent_type mismatch",
|
|
582
|
+
conversation_id: conversationId,
|
|
583
|
+
},
|
|
584
|
+
isError: true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
resume = true;
|
|
588
|
+
const entry = readRegistry(piDir).find((candidate) => candidate.id === id);
|
|
589
|
+
if (params.background !== false &&
|
|
590
|
+
entry?.paneId &&
|
|
591
|
+
paneExists(entry.paneId)) {
|
|
592
|
+
const bgtask = {
|
|
593
|
+
dir: artifactDir,
|
|
594
|
+
agentType: entry.agentType,
|
|
595
|
+
sessionName,
|
|
596
|
+
paneId: entry.paneId,
|
|
597
|
+
originalPane: null,
|
|
598
|
+
description: params.description || entry.description,
|
|
599
|
+
startedAt: entry.startedAt,
|
|
600
|
+
toolUses: 0,
|
|
601
|
+
turns: 0,
|
|
602
|
+
conversationId,
|
|
603
|
+
recentCalls: [],
|
|
604
|
+
};
|
|
605
|
+
backgroundTasks.set(id, bgtask);
|
|
606
|
+
return {
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `Resumed conversation "${conversationId}" via ${taskArtifactName(id)}. The subagent is running in background and will notify on completion.`,
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
details: {
|
|
614
|
+
task_id: id,
|
|
615
|
+
agent_type: agent.name,
|
|
616
|
+
description: params.description,
|
|
617
|
+
conversation_id: conversationId,
|
|
618
|
+
tmux_session: sessionName,
|
|
619
|
+
background: true,
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (params.task_id) {
|
|
499
625
|
// Look up the task in the persistent registry
|
|
500
626
|
const entries = readRegistry(piDir);
|
|
501
627
|
const entry = entries.find((e) => e.id === params.task_id);
|
|
@@ -549,6 +675,7 @@ export default function (pi) {
|
|
|
549
675
|
startedAt: entry.startedAt,
|
|
550
676
|
toolUses: 0,
|
|
551
677
|
turns: 0,
|
|
678
|
+
conversationId: entry.conversationId,
|
|
552
679
|
recentCalls: [],
|
|
553
680
|
};
|
|
554
681
|
backgroundTasks.set(id, bgtask);
|
|
@@ -563,6 +690,7 @@ export default function (pi) {
|
|
|
563
690
|
task_id: id,
|
|
564
691
|
agent_type: agent.name,
|
|
565
692
|
description: params.description,
|
|
693
|
+
conversation_id: entry.conversationId ?? conversationId,
|
|
566
694
|
tmux_session: sessionName,
|
|
567
695
|
background: true,
|
|
568
696
|
},
|
|
@@ -571,11 +699,41 @@ export default function (pi) {
|
|
|
571
699
|
}
|
|
572
700
|
else {
|
|
573
701
|
id = `${Date.now().toString(36)}-${randomUUID().slice(0, 4)}`;
|
|
574
|
-
sessionName =
|
|
575
|
-
artifactDir = join(piDir,
|
|
702
|
+
sessionName = taskArtifactName(id);
|
|
703
|
+
artifactDir = join(getArtifactsDir(piDir), sessionName);
|
|
576
704
|
await mkdir(artifactDir, { recursive: true });
|
|
577
705
|
resultPath = join(artifactDir, "RESULT.md");
|
|
578
706
|
}
|
|
707
|
+
if (conversationId && !hasTmux()) {
|
|
708
|
+
return {
|
|
709
|
+
content: [
|
|
710
|
+
{
|
|
711
|
+
type: "text",
|
|
712
|
+
text: "Durable conversations require the tmux/CLI backend so Pi can save and reopen the subagent session. Install/start tmux or omit conversation_id for a one-shot SDK task.",
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
details: {
|
|
716
|
+
phase: "failed",
|
|
717
|
+
error: "tmux required for durable conversation",
|
|
718
|
+
conversation_id: conversationId,
|
|
719
|
+
},
|
|
720
|
+
isError: true,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (conversationId) {
|
|
724
|
+
await mkdir(artifactDir, { recursive: true });
|
|
725
|
+
conversationRegistry[conversationId] = taskArtifactName(id);
|
|
726
|
+
writeConversationRegistry(piDir, conversationRegistry);
|
|
727
|
+
writeConversationArtifacts({
|
|
728
|
+
taskDir: artifactDir,
|
|
729
|
+
taskId: id,
|
|
730
|
+
conversationId,
|
|
731
|
+
agentType: agent.name,
|
|
732
|
+
sessionDir: join(artifactDir, "sessions"),
|
|
733
|
+
sessionName,
|
|
734
|
+
prompt: params.prompt,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
579
737
|
const descText = params.description || "";
|
|
580
738
|
const isBackground = params.background ?? TASK_BACKGROUND_DEFAULT;
|
|
581
739
|
// default true
|
|
@@ -642,6 +800,7 @@ export default function (pi) {
|
|
|
642
800
|
startedAt: Date.now(),
|
|
643
801
|
toolUses: 0,
|
|
644
802
|
turns: 0,
|
|
803
|
+
conversationId,
|
|
645
804
|
recentCalls: [],
|
|
646
805
|
};
|
|
647
806
|
if (foregroundTask) {
|
|
@@ -660,6 +819,7 @@ export default function (pi) {
|
|
|
660
819
|
startedAt: Date.now(),
|
|
661
820
|
toolUses: 0,
|
|
662
821
|
turns: 0,
|
|
822
|
+
conversationId,
|
|
663
823
|
recentCalls: [],
|
|
664
824
|
};
|
|
665
825
|
backgroundTasks.set(id, bgtask);
|
|
@@ -671,6 +831,7 @@ export default function (pi) {
|
|
|
671
831
|
startedAt: bgtask.startedAt,
|
|
672
832
|
piDir,
|
|
673
833
|
dir: artifactDir,
|
|
834
|
+
conversationId,
|
|
674
835
|
};
|
|
675
836
|
const entries = readRegistry(piDir);
|
|
676
837
|
entries.push(entry);
|
|
@@ -703,6 +864,7 @@ export default function (pi) {
|
|
|
703
864
|
background: true,
|
|
704
865
|
backend: "sdk",
|
|
705
866
|
result_path: resultPath,
|
|
867
|
+
conversation_id: conversationId,
|
|
706
868
|
},
|
|
707
869
|
};
|
|
708
870
|
}
|
|
@@ -717,6 +879,7 @@ export default function (pi) {
|
|
|
717
879
|
backend: "sdk",
|
|
718
880
|
session_path: sessionPath,
|
|
719
881
|
result_path: resultPath,
|
|
882
|
+
conversation_id: conversationId,
|
|
720
883
|
},
|
|
721
884
|
};
|
|
722
885
|
}
|
|
@@ -815,6 +978,7 @@ export default function (pi) {
|
|
|
815
978
|
tool_uses: toolUses,
|
|
816
979
|
turn_count: turns,
|
|
817
980
|
background: false,
|
|
981
|
+
conversation_id: conversationId,
|
|
818
982
|
},
|
|
819
983
|
};
|
|
820
984
|
}
|
|
@@ -829,6 +993,7 @@ export default function (pi) {
|
|
|
829
993
|
startedAt: Date.now(),
|
|
830
994
|
toolUses: 0,
|
|
831
995
|
turns: 0,
|
|
996
|
+
conversationId,
|
|
832
997
|
recentCalls: [],
|
|
833
998
|
};
|
|
834
999
|
backgroundTasks.set(id, bgtask);
|
|
@@ -842,6 +1007,7 @@ export default function (pi) {
|
|
|
842
1007
|
paneId,
|
|
843
1008
|
piDir,
|
|
844
1009
|
dir: artifactDir,
|
|
1010
|
+
conversationId,
|
|
845
1011
|
};
|
|
846
1012
|
// Write to JSON registry for on-load restore
|
|
847
1013
|
const entries = readRegistry(piDir);
|
|
@@ -955,4 +1121,12 @@ export default function (pi) {
|
|
|
955
1121
|
return new Text(line, 0, 0);
|
|
956
1122
|
},
|
|
957
1123
|
});
|
|
1124
|
+
pi.registerCommand("task-sessions", {
|
|
1125
|
+
description: "List durable pi-task conversations",
|
|
1126
|
+
handler: async (_args, ctx) => {
|
|
1127
|
+
const cwd = ctx.sessionManager?.getCwd?.() ?? process.cwd();
|
|
1128
|
+
const { piDir } = discoverAgents(cwd);
|
|
1129
|
+
ctx.ui.notify(renderConversationSessions(piDir), "info");
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
958
1132
|
}
|
package/dist/subagent/tmux.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export declare function tmuxCmd(args: string[]): string;
|
|
|
5
5
|
export declare function hasTmux(): boolean;
|
|
6
6
|
export declare function paneExists(paneId: string): boolean;
|
|
7
7
|
export declare function getCurrentPaneId(): string | null;
|
|
8
|
+
export declare function getCurrentPaneSize(targetPane?: string | null): {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
} | null;
|
|
8
12
|
export declare function splitWindowPane(cwd: string, command: string): {
|
|
9
13
|
paneId: string;
|
|
10
14
|
originalPane: string | null;
|
package/dist/subagent/tmux.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tmux helpers for subagent panes (shared by task extension).
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { buildTmuxSplitWindowArgs, chooseTmuxSplitDirection } from "../helpers.js";
|
|
5
6
|
export function tmuxCmd(args) {
|
|
6
7
|
return execFileSync("tmux", args, {
|
|
7
8
|
encoding: "utf-8",
|
|
@@ -34,18 +35,28 @@ export function getCurrentPaneId() {
|
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
export function getCurrentPaneSize(targetPane) {
|
|
39
|
+
try {
|
|
40
|
+
const args = ["display-message", "-p", "#{pane_width} #{pane_height}"];
|
|
41
|
+
if (targetPane)
|
|
42
|
+
args.splice(1, 0, "-t", targetPane);
|
|
43
|
+
const raw = tmuxCmd(args);
|
|
44
|
+
const [widthRaw, heightRaw] = raw.trim().split(/\s+/, 2);
|
|
45
|
+
const width = Number(widthRaw);
|
|
46
|
+
const height = Number(heightRaw);
|
|
47
|
+
if (!Number.isFinite(width) || !Number.isFinite(height))
|
|
48
|
+
return null;
|
|
49
|
+
return { width, height };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
37
55
|
export function splitWindowPane(cwd, command) {
|
|
38
56
|
const originalPane = getCurrentPaneId();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"-P",
|
|
43
|
-
"-F",
|
|
44
|
-
"#{pane_id}",
|
|
45
|
-
"-c",
|
|
46
|
-
cwd,
|
|
47
|
-
command,
|
|
48
|
-
]);
|
|
57
|
+
const paneSize = getCurrentPaneSize(originalPane);
|
|
58
|
+
const direction = chooseTmuxSplitDirection(paneSize?.width ?? 0, paneSize?.height ?? 0);
|
|
59
|
+
const paneId = tmuxCmd(buildTmuxSplitWindowArgs(cwd, command, direction, originalPane));
|
|
49
60
|
return { paneId, originalPane };
|
|
50
61
|
}
|
|
51
62
|
export function killAgentPane(paneId, originalPane) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-task",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Delegating task/subagent extension for Pi: foreground/background subagents, widgets, tmux observability, SDK fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|