pi-squire 0.10.0 → 0.11.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 +18 -1
- package/README.md +69 -10
- package/package.json +1 -1
- package/src/index.ts +191 -35
- package/src/render.ts +2 -1
- package/src/result-message.ts +120 -0
- package/src/result-sessions.ts +35 -3
- package/src/results.ts +37 -116
- package/src/resume.ts +448 -0
- package/src/tracking-text.ts +41 -0
- package/src/tracking.ts +26 -49
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The text and details of the one message the parent receives for a settled delegation.
|
|
2
|
+
import { readMarks } from "./limits.ts";
|
|
3
|
+
import { cwdLine, modelLabel, snapshotLines, workspaceLine } from "./render.ts";
|
|
4
|
+
import { sessionIdOf } from "./result-sessions.ts";
|
|
5
|
+
import type { StartedRecorded } from "./result-types.ts";
|
|
6
|
+
import type { Inbound, MessageDetails, TaskQuote } from "./results.ts";
|
|
7
|
+
import type { WorkspaceSnapshot } from "./snapshot.ts";
|
|
8
|
+
|
|
9
|
+
/** A task quoted into the result header is cut at this many UTF-8 bytes. */
|
|
10
|
+
const TASK_CAP = 2 * 1024;
|
|
11
|
+
/** A result body is cut at this many UTF-8 bytes. */
|
|
12
|
+
const BODY_CAP = 32 * 1024;
|
|
13
|
+
|
|
14
|
+
type ResultDetails = Extract<MessageDetails, { kind: "result" }>;
|
|
15
|
+
|
|
16
|
+
/** Cut `text` to at most `max` UTF-8 bytes on a character boundary; undefined when it already fits. */
|
|
17
|
+
function cap(text: string, max: number): string | undefined {
|
|
18
|
+
const bytes = Buffer.from(text, "utf8");
|
|
19
|
+
if (bytes.length <= max) return undefined;
|
|
20
|
+
let end = max;
|
|
21
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--; // back off a split character
|
|
22
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The line naming a usage-limit mark this delegation recorded, if any; a `done` delegation finished on a fallback. */
|
|
26
|
+
function limitLine(sessionId: string, status: string): string | undefined {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const mark = readMarks(now).find((item) => item.delegations.includes(sessionId));
|
|
29
|
+
if (mark === undefined) return undefined;
|
|
30
|
+
const scope = mark.scope.includes("/") ? `model ${mark.scope}` : `provider ${mark.scope}`;
|
|
31
|
+
const until = new Date(mark.clearsAt).toISOString();
|
|
32
|
+
return `Usage limit: the delegate hit a usage limit on ${scope}; ${
|
|
33
|
+
mark.clearsAt > now ? `it is marked until ${until} and later delegations skip it.` : `it was marked until ${until}.`
|
|
34
|
+
}${status === "done" ? " It finished on a fallback model; its session names which." : ""}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The details that say a delegation reopened an earlier one's session. */
|
|
38
|
+
export function resumeDetails(record: StartedRecorded): { resumedFrom?: string; sessionId?: string } {
|
|
39
|
+
return {
|
|
40
|
+
...(record.resumedFrom === undefined ? {} : { resumedFrom: record.resumedFrom }),
|
|
41
|
+
...(record.sessionId === undefined ? {} : { sessionId: record.sessionId }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The details of a settled delegation's result message. */
|
|
46
|
+
export function resultDetails(
|
|
47
|
+
delegation: StartedRecorded,
|
|
48
|
+
name: string,
|
|
49
|
+
payload: Inbound,
|
|
50
|
+
session: string | undefined,
|
|
51
|
+
finished: string | undefined,
|
|
52
|
+
snapshot: WorkspaceSnapshot,
|
|
53
|
+
): ResultDetails {
|
|
54
|
+
const reply = payload.envelope;
|
|
55
|
+
const copy = payload.requests.find((request) => request.envelope?.id === delegation.requestId);
|
|
56
|
+
let task: TaskQuote;
|
|
57
|
+
if (copy === undefined) {
|
|
58
|
+
task = { note: `[delegate] The task ${delegation.requestId} has no copy in sent/; only its id is known.` };
|
|
59
|
+
} else {
|
|
60
|
+
const cut = cap(copy.envelope.body, TASK_CAP);
|
|
61
|
+
const text =
|
|
62
|
+
cut === undefined ? copy.envelope.body : `${cut}\n[delegate] Task cut at 2 KiB; the full copy is ${copy.path}`;
|
|
63
|
+
task = { text, path: copy.path };
|
|
64
|
+
}
|
|
65
|
+
const cut = cap(reply.body, BODY_CAP);
|
|
66
|
+
const body =
|
|
67
|
+
cut === undefined ? reply.body : `${cut}\n[delegate] Body cut at 32 KiB; the full envelope is ${payload.path}`;
|
|
68
|
+
const limit = reply.status === "stopped" ? undefined : limitLine(sessionIdOf(delegation), reply.status);
|
|
69
|
+
return {
|
|
70
|
+
kind: "result",
|
|
71
|
+
status: reply.status,
|
|
72
|
+
name,
|
|
73
|
+
agent: delegation.agent,
|
|
74
|
+
model: delegation.model,
|
|
75
|
+
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
76
|
+
id: delegation.id,
|
|
77
|
+
...resumeDetails(delegation),
|
|
78
|
+
requestId: delegation.requestId,
|
|
79
|
+
envelopePath: payload.path,
|
|
80
|
+
...(session === undefined ? {} : { sessionPath: session }),
|
|
81
|
+
...(delegation.cwd === undefined ? {} : { cwd: delegation.cwd }),
|
|
82
|
+
...(delegation.sessionDir === undefined ? {} : { sessionDir: delegation.sessionDir }),
|
|
83
|
+
...(delegation.workspace === undefined ? {} : { workspace: delegation.workspace }),
|
|
84
|
+
...(limit === undefined ? {} : { limit }),
|
|
85
|
+
task,
|
|
86
|
+
body,
|
|
87
|
+
workspaceSnapshot: snapshot,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The snapshot lines of the result text: a heading line, then its path lists indented. */
|
|
92
|
+
function checkoutText(snapshot: WorkspaceSnapshot): string[] {
|
|
93
|
+
const [first, ...rest] = snapshotLines(snapshot);
|
|
94
|
+
return [`Checkout when the result arrived: ${first}`, ...rest.map((line) => ` ${line}`)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The one message the parent model reads for a settled delegation. */
|
|
98
|
+
export function resultText(details: ResultDetails): string {
|
|
99
|
+
const { task } = details;
|
|
100
|
+
const quote =
|
|
101
|
+
"note" in task
|
|
102
|
+
? task.note
|
|
103
|
+
: `Task, quoted from ${task.path}:\n${task.text
|
|
104
|
+
.split("\n")
|
|
105
|
+
.map((line) => `> ${line}`)
|
|
106
|
+
.join("\n")}`;
|
|
107
|
+
return [
|
|
108
|
+
`[delegate] Result from ${details.agent} (${modelLabel(details.model, details.finishedModel)}), delegation ${details.id}${details.resumedFrom === undefined ? "" : ` (resumed from ${details.resumedFrom})`}, request ${details.requestId}.`,
|
|
109
|
+
`Status: ${details.status}`,
|
|
110
|
+
...(details.limit === undefined ? [] : [details.limit]),
|
|
111
|
+
quote,
|
|
112
|
+
`Working directory: ${cwdLine(details.cwd)}`,
|
|
113
|
+
...(details.workspace === undefined ? [] : [`Workspace: ${workspaceLine(details.workspace)}`]),
|
|
114
|
+
...(details.workspaceSnapshot === undefined ? [] : checkoutText(details.workspaceSnapshot)),
|
|
115
|
+
`Envelope: ${details.envelopePath}`,
|
|
116
|
+
`Delegate session: ${details.sessionPath ?? details.sessionId ?? details.id}`,
|
|
117
|
+
"",
|
|
118
|
+
details.body,
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
package/src/result-sessions.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, openSync, readdirSync, readFileSync, readSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import type { StartedRecorded } from "./result-types.ts";
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The Pi session id a delegation's mail, replies, usage-limit marks and file
|
|
7
|
+
* name use. A resumed delegation has its own id but reopens the first one's
|
|
8
|
+
* session; records from before resuming have no `sessionId`, and it is their `id`.
|
|
9
|
+
*/
|
|
10
|
+
export function sessionIdOf(record: { id: string; sessionId?: string }): string {
|
|
11
|
+
return record.sessionId ?? record.id;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The delegate session file in a directory, found by its `_<id>.jsonl` suffix, `id` being a session id. */
|
|
6
15
|
export function findSession(dir: string | undefined, id: string): string | undefined {
|
|
7
16
|
if (dir === undefined) return undefined;
|
|
8
17
|
let names: string[];
|
|
@@ -18,10 +27,33 @@ export function findSession(dir: string | undefined, id: string): string | undef
|
|
|
18
27
|
export function locateSession(record: StartedRecorded, fallbackDir: string | undefined): string | undefined {
|
|
19
28
|
const known = record.sessionPath ?? record.result?.sessionPath;
|
|
20
29
|
if (known !== undefined) return known;
|
|
21
|
-
const found = findSession(record.sessionDir ?? fallbackDir, record
|
|
30
|
+
const found = findSession(record.sessionDir ?? fallbackDir, sessionIdOf(record));
|
|
22
31
|
if (found !== undefined && isAbsolute(found)) record.sessionPath = found;
|
|
23
32
|
return found;
|
|
24
33
|
}
|
|
34
|
+
/** How much of a session file's start is read to find its header line. */
|
|
35
|
+
const HEADER_BYTES = 64 * 1024;
|
|
36
|
+
|
|
37
|
+
/** The working directory a session file's header stores, which Pi runs the session in; undefined when the file or the field is missing. */
|
|
38
|
+
export function sessionHeaderCwd(sessionPath: string): string | undefined {
|
|
39
|
+
let fd: number;
|
|
40
|
+
try {
|
|
41
|
+
fd = openSync(sessionPath, "r");
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const buffer = Buffer.alloc(HEADER_BYTES);
|
|
47
|
+
const text = buffer.subarray(0, readSync(fd, buffer, 0, HEADER_BYTES, 0)).toString("utf8");
|
|
48
|
+
const header = JSON.parse(text.split("\n", 1)[0]) as { type?: unknown; cwd?: unknown };
|
|
49
|
+
return header.type === "session" && typeof header.cwd === "string" && header.cwd !== "" ? header.cwd : undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
} finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
/** The provider/model of the last assistant message in the delegate session, if available. */
|
|
26
58
|
export function finishedModel(sessionPath: string | undefined): string | undefined {
|
|
27
59
|
if (sessionPath === undefined) return undefined;
|
package/src/results.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { finishedModel, locateSession } from "./result-sessions.ts";
|
|
10
|
+
import { cwdLine, modelLabel, renderMessage, workspaceLine } from "./render.ts";
|
|
11
|
+
import { resultDetails, resultText, resumeDetails } from "./result-message.ts";
|
|
12
|
+
import { finishedModel, locateSession, sessionIdOf } from "./result-sessions.ts";
|
|
13
13
|
import {
|
|
14
14
|
type AttemptState,
|
|
15
15
|
isStarted,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { inspectWorkspace, type WorkspaceSnapshot } from "./snapshot.ts";
|
|
21
21
|
import type { Workspace } from "./workspace.ts";
|
|
22
22
|
|
|
23
|
-
export { findSession, finishedModel } from "./result-sessions.ts";
|
|
23
|
+
export { findSession, finishedModel, sessionHeaderCwd, sessionIdOf } from "./result-sessions.ts";
|
|
24
24
|
export type { AttemptState, Recorded, ResourceFacts, StartedRecorded, WorkspaceAttempt } from "./result-types.ts";
|
|
25
25
|
export { isStarted, locateSession };
|
|
26
26
|
|
|
@@ -28,10 +28,6 @@ export { isStarted, locateSession };
|
|
|
28
28
|
export const CUSTOM_TYPE = "delegate";
|
|
29
29
|
/** The footer entry that counts delegations whose result has not arrived. */
|
|
30
30
|
export const STATUS_KEY = "delegate";
|
|
31
|
-
/** A task quoted into the result header is cut at this many UTF-8 bytes. */
|
|
32
|
-
const TASK_CAP = 2 * 1024;
|
|
33
|
-
/** A result body is cut at this many UTF-8 bytes. */
|
|
34
|
-
const BODY_CAP = 32 * 1024;
|
|
35
31
|
/** The `pi.events` channel a provider emits claimed envelopes on. */
|
|
36
32
|
const INBOUND = "message:inbound";
|
|
37
33
|
|
|
@@ -56,6 +52,10 @@ export type Delegation = {
|
|
|
56
52
|
sessionPath?: string;
|
|
57
53
|
/** How its directory was chosen and its Git checkout at launch; absent in older records. */
|
|
58
54
|
workspace?: Workspace;
|
|
55
|
+
/** The delegation this one resumed, when it reopened that delegation's session. */
|
|
56
|
+
resumedFrom?: string;
|
|
57
|
+
/** The Pi session id; absent means `id`. A resumed delegation holds the session id of the one it resumed. */
|
|
58
|
+
sessionId?: string;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -99,6 +99,10 @@ type CommonDetails = {
|
|
|
99
99
|
model: string;
|
|
100
100
|
finishedModel?: string;
|
|
101
101
|
id: string;
|
|
102
|
+
/** The delegation this one resumed. */
|
|
103
|
+
resumedFrom?: string;
|
|
104
|
+
/** The Pi session id when it is not `id`: a resumed delegation's. */
|
|
105
|
+
sessionId?: string;
|
|
102
106
|
requestId: string;
|
|
103
107
|
sessionPath?: string;
|
|
104
108
|
cwd?: string;
|
|
@@ -125,8 +129,6 @@ export type MessageDetails =
|
|
|
125
129
|
})
|
|
126
130
|
| (CommonDetails & { kind: "gone"; windowName: string });
|
|
127
131
|
|
|
128
|
-
type ResultDetails = Extract<MessageDetails, { kind: "result" }>;
|
|
129
|
-
|
|
130
132
|
/** The provider's payload on `message:inbound`. */
|
|
131
133
|
export type Inbound = {
|
|
132
134
|
envelope: Envelope;
|
|
@@ -198,102 +200,6 @@ export function deriveState(record: Recorded, window: WindowState): DelegationSt
|
|
|
198
200
|
return "unknown";
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
/** Cut `text` to at most `max` UTF-8 bytes on a character boundary; undefined when it already fits. */
|
|
202
|
-
function cap(text: string, max: number): string | undefined {
|
|
203
|
-
const bytes = Buffer.from(text, "utf8");
|
|
204
|
-
if (bytes.length <= max) return undefined;
|
|
205
|
-
let end = max;
|
|
206
|
-
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--; // back off a split character
|
|
207
|
-
return bytes.subarray(0, end).toString("utf8");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** The line naming a usage-limit mark this delegation recorded, if any; a `done` delegation finished on a fallback. */
|
|
211
|
-
function limitLine(id: string, status: string): string | undefined {
|
|
212
|
-
const now = Date.now();
|
|
213
|
-
const mark = readMarks(now).find((item) => item.delegations.includes(id));
|
|
214
|
-
if (mark === undefined) return undefined;
|
|
215
|
-
const scope = mark.scope.includes("/") ? `model ${mark.scope}` : `provider ${mark.scope}`;
|
|
216
|
-
const until = new Date(mark.clearsAt).toISOString();
|
|
217
|
-
return `Usage limit: the delegate hit a usage limit on ${scope}; ${
|
|
218
|
-
mark.clearsAt > now ? `it is marked until ${until} and later delegations skip it.` : `it was marked until ${until}.`
|
|
219
|
-
}${status === "done" ? " It finished on a fallback model; its session names which." : ""}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** The details of a settled delegation's result message. */
|
|
223
|
-
function resultDetails(
|
|
224
|
-
delegation: StartedRecorded,
|
|
225
|
-
payload: Inbound,
|
|
226
|
-
session: string | undefined,
|
|
227
|
-
finished: string | undefined,
|
|
228
|
-
snapshot: WorkspaceSnapshot,
|
|
229
|
-
): ResultDetails {
|
|
230
|
-
const reply = payload.envelope;
|
|
231
|
-
const copy = payload.requests.find((request) => request.envelope?.id === delegation.requestId);
|
|
232
|
-
let task: TaskQuote;
|
|
233
|
-
if (copy === undefined) {
|
|
234
|
-
task = { note: `[delegate] The task ${delegation.requestId} has no copy in sent/; only its id is known.` };
|
|
235
|
-
} else {
|
|
236
|
-
const cut = cap(copy.envelope.body, TASK_CAP);
|
|
237
|
-
const text =
|
|
238
|
-
cut === undefined ? copy.envelope.body : `${cut}\n[delegate] Task cut at 2 KiB; the full copy is ${copy.path}`;
|
|
239
|
-
task = { text, path: copy.path };
|
|
240
|
-
}
|
|
241
|
-
const cut = cap(reply.body, BODY_CAP);
|
|
242
|
-
const body =
|
|
243
|
-
cut === undefined ? reply.body : `${cut}\n[delegate] Body cut at 32 KiB; the full envelope is ${payload.path}`;
|
|
244
|
-
const limit = reply.status === "stopped" ? undefined : limitLine(delegation.id, reply.status);
|
|
245
|
-
return {
|
|
246
|
-
kind: "result",
|
|
247
|
-
status: reply.status,
|
|
248
|
-
name: recordedName(delegation),
|
|
249
|
-
agent: delegation.agent,
|
|
250
|
-
model: delegation.model,
|
|
251
|
-
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
252
|
-
id: delegation.id,
|
|
253
|
-
requestId: delegation.requestId,
|
|
254
|
-
envelopePath: payload.path,
|
|
255
|
-
...(session === undefined ? {} : { sessionPath: session }),
|
|
256
|
-
...(delegation.cwd === undefined ? {} : { cwd: delegation.cwd }),
|
|
257
|
-
...(delegation.sessionDir === undefined ? {} : { sessionDir: delegation.sessionDir }),
|
|
258
|
-
...(delegation.workspace === undefined ? {} : { workspace: delegation.workspace }),
|
|
259
|
-
...(limit === undefined ? {} : { limit }),
|
|
260
|
-
task,
|
|
261
|
-
body,
|
|
262
|
-
workspaceSnapshot: snapshot,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** The snapshot lines of the result text: a heading line, then its path lists indented. */
|
|
267
|
-
function checkoutText(snapshot: WorkspaceSnapshot): string[] {
|
|
268
|
-
const [first, ...rest] = snapshotLines(snapshot);
|
|
269
|
-
return [`Checkout when the result arrived: ${first}`, ...rest.map((line) => ` ${line}`)];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** The one message the parent model reads for a settled delegation. */
|
|
273
|
-
function resultText(details: ResultDetails): string {
|
|
274
|
-
const { task } = details;
|
|
275
|
-
const quote =
|
|
276
|
-
"note" in task
|
|
277
|
-
? task.note
|
|
278
|
-
: `Task, quoted from ${task.path}:\n${task.text
|
|
279
|
-
.split("\n")
|
|
280
|
-
.map((line) => `> ${line}`)
|
|
281
|
-
.join("\n")}`;
|
|
282
|
-
return [
|
|
283
|
-
`[delegate] Result from ${details.agent} (${modelLabel(details.model, details.finishedModel)}), delegation ${details.id}, request ${details.requestId}.`,
|
|
284
|
-
`Status: ${details.status}`,
|
|
285
|
-
...(details.limit === undefined ? [] : [details.limit]),
|
|
286
|
-
quote,
|
|
287
|
-
`Working directory: ${cwdLine(details.cwd)}`,
|
|
288
|
-
...(details.workspace === undefined ? [] : [`Workspace: ${workspaceLine(details.workspace)}`]),
|
|
289
|
-
...(details.workspaceSnapshot === undefined ? [] : checkoutText(details.workspaceSnapshot)),
|
|
290
|
-
`Envelope: ${details.envelopePath}`,
|
|
291
|
-
`Delegate session: ${details.sessionPath ?? details.id}`,
|
|
292
|
-
"",
|
|
293
|
-
details.body,
|
|
294
|
-
].join("\n");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
203
|
export function createResults(pi: ExtensionAPI): Results {
|
|
298
204
|
pi.registerMessageRenderer(CUSTOM_TYPE, renderMessage);
|
|
299
205
|
/** Every recorded delegation by its id, in the order recorded. */
|
|
@@ -308,12 +214,23 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
308
214
|
let footerShown = false;
|
|
309
215
|
const icon = Math.random() < 0.5 ? "👨" : "👩";
|
|
310
216
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
|
|
217
|
+
/**
|
|
218
|
+
* A delegation still going: no result, no recorded close, and not resumed.
|
|
219
|
+
* A resumed one's process is gone (resuming proved it), so the poll must not
|
|
220
|
+
* report it closed without a result; a late reply to it is still delivered.
|
|
221
|
+
*/
|
|
222
|
+
const running = (): StartedRecorded[] => {
|
|
223
|
+
const records = [...byId.values()];
|
|
224
|
+
const resumed = new Set(records.flatMap((record) => (isStarted(record) ? (record.resumedFrom ?? []) : [])));
|
|
225
|
+
return records.filter(
|
|
314
226
|
(record): record is StartedRecorded =>
|
|
315
|
-
isStarted(record) &&
|
|
227
|
+
isStarted(record) &&
|
|
228
|
+
record.result === undefined &&
|
|
229
|
+
record.closed !== true &&
|
|
230
|
+
record.gone !== true &&
|
|
231
|
+
!resumed.has(record.id),
|
|
316
232
|
);
|
|
233
|
+
};
|
|
317
234
|
|
|
318
235
|
/** The started record for `id`, if it is one. */
|
|
319
236
|
const startedById = (id: unknown): StartedRecorded | undefined => {
|
|
@@ -421,6 +338,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
421
338
|
model: record.model,
|
|
422
339
|
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
423
340
|
id: record.id,
|
|
341
|
+
...resumeDetails(record),
|
|
424
342
|
requestId: record.requestId,
|
|
425
343
|
...(session === undefined ? {} : { sessionPath: session }),
|
|
426
344
|
...(record.cwd === undefined ? {} : { cwd: record.cwd }),
|
|
@@ -433,11 +351,11 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
433
351
|
customType: CUSTOM_TYPE,
|
|
434
352
|
details,
|
|
435
353
|
content: [
|
|
436
|
-
`[delegate] Delegation ${record.id} (${record.agent}, ${modelLabel(record.model, finished)}), request ${record.requestId}, closed without a result: its window ${record.windowName} is gone and no reply arrived.`,
|
|
354
|
+
`[delegate] Delegation ${record.id}${record.resumedFrom === undefined ? "" : ` (resumed from ${record.resumedFrom})`} (${record.agent}, ${modelLabel(record.model, finished)}), request ${record.requestId}, closed without a result: its window ${record.windowName} is gone and no reply arrived.`,
|
|
437
355
|
`Working directory: ${cwdLine(record.cwd)}`,
|
|
438
356
|
...(record.workspace === undefined ? [] : [`Workspace: ${workspaceLine(record.workspace)}`]),
|
|
439
|
-
`Delegate session: ${session ?? record
|
|
440
|
-
"A reply that arrives later is still delivered.",
|
|
357
|
+
`Delegate session: ${session ?? sessionIdOf(record)}`,
|
|
358
|
+
"A reply that arrives later is still delivered. To continue the delegate's session in a new window, call delegation_resume with this delegation's id.",
|
|
441
359
|
].join("\n"),
|
|
442
360
|
display: true,
|
|
443
361
|
},
|
|
@@ -451,8 +369,11 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
451
369
|
if (payload?.handled === true) return;
|
|
452
370
|
const reply = payload?.envelope;
|
|
453
371
|
if (reply === undefined || !Array.isArray(reply.in_reply_to) || reply.in_reply_to.length === 0) return;
|
|
454
|
-
// Only the delegate's own reply counts: `from` must be the delegation's id.
|
|
455
|
-
const requestId = reply.in_reply_to.find((id) =>
|
|
372
|
+
// Only the delegate's own reply counts: `from` must be the delegation's session id.
|
|
373
|
+
const requestId = reply.in_reply_to.find((id) => {
|
|
374
|
+
const record = byRequest.get(id);
|
|
375
|
+
return record !== undefined && sessionIdOf(record) === reply.from;
|
|
376
|
+
});
|
|
456
377
|
if (requestId === undefined) return;
|
|
457
378
|
payload.handled = true;
|
|
458
379
|
if (shown.has(reply.id)) return;
|
|
@@ -462,7 +383,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
462
383
|
const finished = finishedModel(session);
|
|
463
384
|
// After the claim above, so a slow or failing inspection cannot let the provider or a duplicate deliver it too.
|
|
464
385
|
const snapshot = inspectWorkspace(delegation.workspace?.cwd, delegation.workspace?.git);
|
|
465
|
-
const details = resultDetails(delegation, payload, session, finished, snapshot);
|
|
386
|
+
const details = resultDetails(delegation, recordedName(delegation), payload, session, finished, snapshot);
|
|
466
387
|
// Steer, not follow-up: a busy parent gets the result before its next model call, not after it has answered.
|
|
467
388
|
pi.sendMessage(
|
|
468
389
|
{ customType: CUSTOM_TYPE, content: resultText(details), details, display: true },
|