pi-squire 0.9.5 → 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 +102 -1
- package/README.md +366 -18
- package/package.json +1 -1
- package/src/git.ts +177 -0
- package/src/index.ts +576 -70
- package/src/render.ts +108 -2
- package/src/result-message.ts +120 -0
- package/src/result-sessions.ts +79 -0
- package/src/result-types.ts +36 -0
- package/src/results.ts +138 -172
- package/src/resume.ts +448 -0
- package/src/snapshot.ts +207 -0
- package/src/tmux.ts +30 -3
- package/src/tracking-attempt.ts +65 -0
- package/src/tracking-text.ts +41 -0
- package/src/tracking.ts +90 -55
- package/src/workspace.ts +420 -0
- package/src/worktree-placement.ts +138 -0
- package/src/worktree.ts +474 -0
package/src/results.ts
CHANGED
|
@@ -4,22 +4,30 @@
|
|
|
4
4
|
// the message over, shows the parent one delegate result and records the
|
|
5
5
|
// result. The records are the one source of truth for a delegation's state,
|
|
6
6
|
// for the footer, `delegation_status` and `delegation_close`.
|
|
7
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
7
|
|
|
10
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
9
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
+
import {
|
|
14
|
+
type AttemptState,
|
|
15
|
+
isStarted,
|
|
16
|
+
type Recorded,
|
|
17
|
+
type StartedRecorded,
|
|
18
|
+
type WorkspaceAttempt,
|
|
19
|
+
} from "./result-types.ts";
|
|
20
|
+
import { inspectWorkspace, type WorkspaceSnapshot } from "./snapshot.ts";
|
|
21
|
+
import type { Workspace } from "./workspace.ts";
|
|
22
|
+
|
|
23
|
+
export { findSession, finishedModel, sessionHeaderCwd, sessionIdOf } from "./result-sessions.ts";
|
|
24
|
+
export type { AttemptState, Recorded, ResourceFacts, StartedRecorded, WorkspaceAttempt } from "./result-types.ts";
|
|
25
|
+
export { isStarted, locateSession };
|
|
14
26
|
|
|
15
27
|
/** The session entry type both delegation records and results use. */
|
|
16
28
|
export const CUSTOM_TYPE = "delegate";
|
|
17
29
|
/** The footer entry that counts delegations whose result has not arrived. */
|
|
18
30
|
export const STATUS_KEY = "delegate";
|
|
19
|
-
/** A task quoted into the result header is cut at this many UTF-8 bytes. */
|
|
20
|
-
const TASK_CAP = 2 * 1024;
|
|
21
|
-
/** A result body is cut at this many UTF-8 bytes. */
|
|
22
|
-
const BODY_CAP = 32 * 1024;
|
|
23
31
|
/** The `pi.events` channel a provider emits claimed envelopes on. */
|
|
24
32
|
const INBOUND = "message:inbound";
|
|
25
33
|
|
|
@@ -36,6 +44,18 @@ export type Delegation = {
|
|
|
36
44
|
requestId: string;
|
|
37
45
|
/** Whether the delegate closes its own window after a normal completion; absent in records from before auto-exit. */
|
|
38
46
|
autoExit?: boolean;
|
|
47
|
+
/** The directory its window opened in; absent in records from before it was recorded, where it is unknown. */
|
|
48
|
+
cwd?: string;
|
|
49
|
+
/** The session-storage directory passed as `--session-dir`; absent in older records. */
|
|
50
|
+
sessionDir?: string;
|
|
51
|
+
/** The delegate's session file once found; never set at launch, when the file may not exist yet. */
|
|
52
|
+
sessionPath?: string;
|
|
53
|
+
/** How its directory was chosen and its Git checkout at launch; absent in older records. */
|
|
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;
|
|
39
59
|
};
|
|
40
60
|
|
|
41
61
|
/**
|
|
@@ -67,6 +87,8 @@ export type Result = {
|
|
|
67
87
|
sessionPath?: string;
|
|
68
88
|
/** The `provider/model` of the delegate's last assistant message, when its session file holds one. */
|
|
69
89
|
finishedModel?: string;
|
|
90
|
+
/** The checkout as observed when the result arrived; absent in results from before it was kept. */
|
|
91
|
+
workspaceSnapshot?: WorkspaceSnapshot;
|
|
70
92
|
};
|
|
71
93
|
|
|
72
94
|
/** What both delegate messages carry for their view, beside the text the model reads. */
|
|
@@ -77,8 +99,15 @@ type CommonDetails = {
|
|
|
77
99
|
model: string;
|
|
78
100
|
finishedModel?: string;
|
|
79
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;
|
|
80
106
|
requestId: string;
|
|
81
107
|
sessionPath?: string;
|
|
108
|
+
cwd?: string;
|
|
109
|
+
sessionDir?: string;
|
|
110
|
+
workspace?: Workspace;
|
|
82
111
|
};
|
|
83
112
|
|
|
84
113
|
/** The task as the result quotes it: its text and copy path, or why there is none. */
|
|
@@ -95,21 +124,11 @@ export type MessageDetails =
|
|
|
95
124
|
task: TaskQuote;
|
|
96
125
|
/** The reply body, capped. */
|
|
97
126
|
body: string;
|
|
127
|
+
/** The checkout when the result arrived, the same value as the recorded result's; absent in older messages. */
|
|
128
|
+
workspaceSnapshot?: WorkspaceSnapshot;
|
|
98
129
|
})
|
|
99
130
|
| (CommonDetails & { kind: "gone"; windowName: string });
|
|
100
131
|
|
|
101
|
-
type ResultDetails = Extract<MessageDetails, { kind: "result" }>;
|
|
102
|
-
|
|
103
|
-
/** One delegation as it stands: what was recorded at its start, plus what came after. */
|
|
104
|
-
export type Recorded = Delegation & {
|
|
105
|
-
/** Set when a result arrived. */
|
|
106
|
-
result?: Result;
|
|
107
|
-
/** Set when `delegation_close` recorded a close. */
|
|
108
|
-
closed?: boolean;
|
|
109
|
-
/** Set when the poll found the window gone with no result; a result outranks it. */
|
|
110
|
-
gone?: boolean;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
132
|
/** The provider's payload on `message:inbound`. */
|
|
114
133
|
export type Inbound = {
|
|
115
134
|
envelope: Envelope;
|
|
@@ -121,12 +140,14 @@ export type Inbound = {
|
|
|
121
140
|
export type Results = {
|
|
122
141
|
/** Rebuild the records from the session's custom entries, as on a resume. */
|
|
123
142
|
restore(ctx: ExtensionContext): void;
|
|
124
|
-
/** Record a delegation and show it as running. */
|
|
143
|
+
/** Record a delegation and show it as running; it supersedes an attempt with the same id. */
|
|
125
144
|
recordStart(delegation: Delegation, ctx: ExtensionContext): void;
|
|
145
|
+
/** Record a workspace attempt, or its next state; never counted as running. */
|
|
146
|
+
recordAttempt(attempt: WorkspaceAttempt, ctx: ExtensionContext): void;
|
|
126
147
|
/** Record a close; does nothing when there is no such delegation or it is already closed. */
|
|
127
148
|
recordClose(id: string): void;
|
|
128
149
|
/** Delegations with no result, no recorded close and no gone-window record. */
|
|
129
|
-
running():
|
|
150
|
+
running(): StartedRecorded[];
|
|
130
151
|
/** Record that the window is gone with no result, and tell the parent once. False when it no longer applies. */
|
|
131
152
|
recordGone(id: string): boolean;
|
|
132
153
|
/** Every delegation, in the order it was recorded. */
|
|
@@ -143,7 +164,14 @@ export type WindowState = "open" | "gone" | "unknown";
|
|
|
143
164
|
* closed without a result (the poll found the window missing); `window-closed`
|
|
144
165
|
* is a window tmux no longer lists, seen when asked directly.
|
|
145
166
|
*/
|
|
146
|
-
export type DelegationState =
|
|
167
|
+
export type DelegationState =
|
|
168
|
+
| "closed"
|
|
169
|
+
| "done"
|
|
170
|
+
| "gone"
|
|
171
|
+
| "running"
|
|
172
|
+
| "window-closed"
|
|
173
|
+
| "unknown"
|
|
174
|
+
| AttemptState["phase"];
|
|
147
175
|
|
|
148
176
|
/** How much of a delegation id a delegate's session name carries. */
|
|
149
177
|
const SHORT_ID = 8;
|
|
@@ -163,6 +191,7 @@ export function recordedName(record: Recorded): string {
|
|
|
163
191
|
|
|
164
192
|
/** A delegation's state: a recorded close, then a result, then a gone record, then the window. */
|
|
165
193
|
export function deriveState(record: Recorded, window: WindowState): DelegationState {
|
|
194
|
+
if (!isStarted(record)) return record.attempt.phase;
|
|
166
195
|
if (record.closed === true) return "closed";
|
|
167
196
|
if (record.result !== undefined) return "done";
|
|
168
197
|
if (record.gone === true) return "gone";
|
|
@@ -171,135 +200,12 @@ export function deriveState(record: Recorded, window: WindowState): DelegationSt
|
|
|
171
200
|
return "unknown";
|
|
172
201
|
}
|
|
173
202
|
|
|
174
|
-
/** Cut `text` to at most `max` UTF-8 bytes on a character boundary; undefined when it already fits. */
|
|
175
|
-
function cap(text: string, max: number): string | undefined {
|
|
176
|
-
const bytes = Buffer.from(text, "utf8");
|
|
177
|
-
if (bytes.length <= max) return undefined;
|
|
178
|
-
let end = max;
|
|
179
|
-
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--; // back off a split character
|
|
180
|
-
return bytes.subarray(0, end).toString("utf8");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** The delegate's session file in `dir`, found by its `_<id>.jsonl` suffix. */
|
|
184
|
-
export function findSession(dir: string | undefined, id: string): string | undefined {
|
|
185
|
-
if (dir === undefined) return undefined;
|
|
186
|
-
let names: string[];
|
|
187
|
-
try {
|
|
188
|
-
names = readdirSync(dir);
|
|
189
|
-
} catch {
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
const name = names.find((candidate) => candidate.endsWith(`_${id}.jsonl`));
|
|
193
|
-
return name === undefined ? undefined : join(dir, name);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* The `provider/model` of the last assistant message in a delegate's session
|
|
198
|
-
* file: the model that actually answered, after any fallback or `/model`.
|
|
199
|
-
* Undefined when the file is missing or holds no assistant message.
|
|
200
|
-
*/
|
|
201
|
-
export function finishedModel(sessionPath: string | undefined): string | undefined {
|
|
202
|
-
if (sessionPath === undefined) return undefined;
|
|
203
|
-
let lines: string[];
|
|
204
|
-
try {
|
|
205
|
-
lines = readFileSync(sessionPath, "utf8").split("\n");
|
|
206
|
-
} catch {
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
210
|
-
let entry: { type?: unknown; message?: { role?: unknown; provider?: unknown; model?: unknown } };
|
|
211
|
-
try {
|
|
212
|
-
entry = JSON.parse(lines[i]);
|
|
213
|
-
} catch {
|
|
214
|
-
continue; // a line still being written, or a blank one
|
|
215
|
-
}
|
|
216
|
-
const message = entry?.message;
|
|
217
|
-
if (entry?.type !== "message" || message?.role !== "assistant") continue;
|
|
218
|
-
if (typeof message.provider === "string" && typeof message.model === "string")
|
|
219
|
-
return `${message.provider}/${message.model}`;
|
|
220
|
-
}
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** The line naming a usage-limit mark this delegation recorded, if any; a `done` delegation finished on a fallback. */
|
|
225
|
-
function limitLine(id: string, status: string): string | undefined {
|
|
226
|
-
const now = Date.now();
|
|
227
|
-
const mark = readMarks(now).find((item) => item.delegations.includes(id));
|
|
228
|
-
if (mark === undefined) return undefined;
|
|
229
|
-
const scope = mark.scope.includes("/") ? `model ${mark.scope}` : `provider ${mark.scope}`;
|
|
230
|
-
const until = new Date(mark.clearsAt).toISOString();
|
|
231
|
-
return `Usage limit: the delegate hit a usage limit on ${scope}; ${
|
|
232
|
-
mark.clearsAt > now ? `it is marked until ${until} and later delegations skip it.` : `it was marked until ${until}.`
|
|
233
|
-
}${status === "done" ? " It finished on a fallback model; its session names which." : ""}`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** The details of a settled delegation's result message. */
|
|
237
|
-
function resultDetails(
|
|
238
|
-
delegation: Delegation,
|
|
239
|
-
payload: Inbound,
|
|
240
|
-
session: string | undefined,
|
|
241
|
-
finished: string | undefined,
|
|
242
|
-
): ResultDetails {
|
|
243
|
-
const reply = payload.envelope;
|
|
244
|
-
const copy = payload.requests.find((request) => request.envelope?.id === delegation.requestId);
|
|
245
|
-
let task: TaskQuote;
|
|
246
|
-
if (copy === undefined) {
|
|
247
|
-
task = { note: `[delegate] The task ${delegation.requestId} has no copy in sent/; only its id is known.` };
|
|
248
|
-
} else {
|
|
249
|
-
const cut = cap(copy.envelope.body, TASK_CAP);
|
|
250
|
-
const text =
|
|
251
|
-
cut === undefined ? copy.envelope.body : `${cut}\n[delegate] Task cut at 2 KiB; the full copy is ${copy.path}`;
|
|
252
|
-
task = { text, path: copy.path };
|
|
253
|
-
}
|
|
254
|
-
const cut = cap(reply.body, BODY_CAP);
|
|
255
|
-
const body =
|
|
256
|
-
cut === undefined ? reply.body : `${cut}\n[delegate] Body cut at 32 KiB; the full envelope is ${payload.path}`;
|
|
257
|
-
const limit = reply.status === "stopped" ? undefined : limitLine(delegation.id, reply.status);
|
|
258
|
-
return {
|
|
259
|
-
kind: "result",
|
|
260
|
-
status: reply.status,
|
|
261
|
-
name: recordedName(delegation),
|
|
262
|
-
agent: delegation.agent,
|
|
263
|
-
model: delegation.model,
|
|
264
|
-
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
265
|
-
id: delegation.id,
|
|
266
|
-
requestId: delegation.requestId,
|
|
267
|
-
envelopePath: payload.path,
|
|
268
|
-
...(session === undefined ? {} : { sessionPath: session }),
|
|
269
|
-
...(limit === undefined ? {} : { limit }),
|
|
270
|
-
task,
|
|
271
|
-
body,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** The one message the parent model reads for a settled delegation. */
|
|
276
|
-
function resultText(details: ResultDetails): string {
|
|
277
|
-
const { task } = details;
|
|
278
|
-
const quote =
|
|
279
|
-
"note" in task
|
|
280
|
-
? task.note
|
|
281
|
-
: `Task, quoted from ${task.path}:\n${task.text
|
|
282
|
-
.split("\n")
|
|
283
|
-
.map((line) => `> ${line}`)
|
|
284
|
-
.join("\n")}`;
|
|
285
|
-
return [
|
|
286
|
-
`[delegate] Result from ${details.agent} (${modelLabel(details.model, details.finishedModel)}), delegation ${details.id}, request ${details.requestId}.`,
|
|
287
|
-
`Status: ${details.status}`,
|
|
288
|
-
...(details.limit === undefined ? [] : [details.limit]),
|
|
289
|
-
quote,
|
|
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. */
|
|
300
206
|
const byId = new Map<string, Recorded>();
|
|
301
|
-
/**
|
|
302
|
-
const byRequest = new Map<string,
|
|
207
|
+
/** Started records by their request's envelope id, for matching replies; attempts never match a reply. */
|
|
208
|
+
const byRequest = new Map<string, StartedRecorded>();
|
|
303
209
|
/** Reply envelope ids a result message has already been sent for. */
|
|
304
210
|
const shown = new Set<string>();
|
|
305
211
|
/** The latest session context, for the footer and the session directory. */
|
|
@@ -308,11 +214,29 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
308
214
|
let footerShown = false;
|
|
309
215
|
const icon = Math.random() < 0.5 ? "👨" : "👩";
|
|
310
216
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
226
|
+
(record): record is StartedRecorded =>
|
|
227
|
+
isStarted(record) &&
|
|
228
|
+
record.result === undefined &&
|
|
229
|
+
record.closed !== true &&
|
|
230
|
+
record.gone !== true &&
|
|
231
|
+
!resumed.has(record.id),
|
|
315
232
|
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** The started record for `id`, if it is one. */
|
|
236
|
+
const startedById = (id: unknown): StartedRecorded | undefined => {
|
|
237
|
+
const record = typeof id === "string" ? byId.get(id) : undefined;
|
|
238
|
+
return record !== undefined && isStarted(record) ? record : undefined;
|
|
239
|
+
};
|
|
316
240
|
|
|
317
241
|
/** The footer counts the running delegations, and hides itself at zero. */
|
|
318
242
|
function updateFooter(): void {
|
|
@@ -332,22 +256,45 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
332
256
|
shown.clear();
|
|
333
257
|
for (const entry of context.sessionManager.getEntries()) {
|
|
334
258
|
if (entry.type !== "custom" || entry.customType !== CUSTOM_TYPE) continue;
|
|
335
|
-
const data = entry.data as
|
|
259
|
+
const data = entry.data as
|
|
260
|
+
| Partial<Delegation & { kind: string; result: Result; closed: boolean; gone: boolean }>
|
|
261
|
+
| undefined;
|
|
336
262
|
if (data === undefined || typeof data.id !== "string") continue;
|
|
337
|
-
|
|
338
|
-
|
|
263
|
+
// An attempt may carry a real request id; it is checked before the legacy start rule below.
|
|
264
|
+
if (data.kind === "workspace-attempt") {
|
|
265
|
+
const existing = byId.get(data.id);
|
|
266
|
+
if (existing === undefined || !isStarted(existing)) {
|
|
267
|
+
byId.set(data.id, { ...(data as unknown as WorkspaceAttempt) });
|
|
268
|
+
}
|
|
269
|
+
} else if (typeof data.requestId === "string") {
|
|
270
|
+
const record: StartedRecorded = { ...(data as Delegation) };
|
|
339
271
|
byId.set(record.id, record);
|
|
340
272
|
byRequest.set(record.requestId, record);
|
|
341
273
|
} else if (typeof data.result?.replyId === "string") {
|
|
342
274
|
shown.add(data.result.replyId);
|
|
343
|
-
const record =
|
|
275
|
+
const record = startedById(data.id);
|
|
344
276
|
if (record !== undefined) record.result = data.result;
|
|
345
277
|
} else if (data.closed === true) {
|
|
346
|
-
const record =
|
|
278
|
+
const record = startedById(data.id);
|
|
347
279
|
if (record !== undefined) record.closed = true;
|
|
348
280
|
} else if (data.gone === true) {
|
|
349
|
-
const record =
|
|
350
|
-
if (record
|
|
281
|
+
const record = startedById(data.id);
|
|
282
|
+
if (record === undefined) continue;
|
|
283
|
+
record.gone = true;
|
|
284
|
+
if (typeof data.sessionPath === "string") record.sessionPath = data.sessionPath;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// A parent that stopped mid-launch cannot say how far it got; nothing is cleaned up or relaunched.
|
|
288
|
+
for (const record of byId.values()) {
|
|
289
|
+
if (isStarted(record)) continue;
|
|
290
|
+
const { phase, resources } = record.attempt;
|
|
291
|
+
if (phase === "planned" || phase === "created") {
|
|
292
|
+
record.attempt = {
|
|
293
|
+
phase: "interrupted",
|
|
294
|
+
resources,
|
|
295
|
+
lastPhase: phase,
|
|
296
|
+
reason: `the parent session ended while the launch was ${phase}; whether anything was created is unknown`,
|
|
297
|
+
};
|
|
351
298
|
}
|
|
352
299
|
}
|
|
353
300
|
updateFooter();
|
|
@@ -356,14 +303,20 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
356
303
|
function recordStart(delegation: Delegation, context: ExtensionContext): void {
|
|
357
304
|
ctx = context;
|
|
358
305
|
pi.appendEntry(CUSTOM_TYPE, delegation);
|
|
359
|
-
const record:
|
|
306
|
+
const record: StartedRecorded = { ...delegation };
|
|
360
307
|
byId.set(record.id, record);
|
|
361
308
|
byRequest.set(record.requestId, record);
|
|
362
309
|
updateFooter();
|
|
363
310
|
}
|
|
364
311
|
|
|
312
|
+
function recordAttempt(attempt: WorkspaceAttempt, context: ExtensionContext): void {
|
|
313
|
+
ctx = context;
|
|
314
|
+
pi.appendEntry(CUSTOM_TYPE, attempt);
|
|
315
|
+
byId.set(attempt.id, { ...attempt });
|
|
316
|
+
}
|
|
317
|
+
|
|
365
318
|
function recordClose(id: string): void {
|
|
366
|
-
const record =
|
|
319
|
+
const record = startedById(id);
|
|
367
320
|
if (record === undefined || record.closed === true) return;
|
|
368
321
|
pi.appendEntry(CUSTOM_TYPE, { id, closed: true });
|
|
369
322
|
record.closed = true;
|
|
@@ -371,12 +324,12 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
371
324
|
}
|
|
372
325
|
|
|
373
326
|
function recordGone(id: string): boolean {
|
|
374
|
-
const record =
|
|
327
|
+
const record = startedById(id);
|
|
375
328
|
if (record === undefined || record.result !== undefined || record.closed === true || record.gone === true)
|
|
376
329
|
return false;
|
|
377
|
-
const session =
|
|
330
|
+
const session = locateSession(record, ctx?.sessionManager.getSessionDir());
|
|
378
331
|
const finished = finishedModel(session);
|
|
379
|
-
pi.appendEntry(CUSTOM_TYPE, { id, gone: true });
|
|
332
|
+
pi.appendEntry(CUSTOM_TYPE, { id, gone: true, ...(session === undefined ? {} : { sessionPath: session }) });
|
|
380
333
|
record.gone = true;
|
|
381
334
|
const details: MessageDetails = {
|
|
382
335
|
kind: "gone",
|
|
@@ -385,8 +338,12 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
385
338
|
model: record.model,
|
|
386
339
|
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
387
340
|
id: record.id,
|
|
341
|
+
...resumeDetails(record),
|
|
388
342
|
requestId: record.requestId,
|
|
389
343
|
...(session === undefined ? {} : { sessionPath: session }),
|
|
344
|
+
...(record.cwd === undefined ? {} : { cwd: record.cwd }),
|
|
345
|
+
...(record.sessionDir === undefined ? {} : { sessionDir: record.sessionDir }),
|
|
346
|
+
...(record.workspace === undefined ? {} : { workspace: record.workspace }),
|
|
390
347
|
windowName: record.windowName,
|
|
391
348
|
};
|
|
392
349
|
pi.sendMessage(
|
|
@@ -394,9 +351,11 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
394
351
|
customType: CUSTOM_TYPE,
|
|
395
352
|
details,
|
|
396
353
|
content: [
|
|
397
|
-
`[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.`,
|
|
398
|
-
`
|
|
399
|
-
|
|
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.`,
|
|
355
|
+
`Working directory: ${cwdLine(record.cwd)}`,
|
|
356
|
+
...(record.workspace === undefined ? [] : [`Workspace: ${workspaceLine(record.workspace)}`]),
|
|
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.",
|
|
400
359
|
].join("\n"),
|
|
401
360
|
display: true,
|
|
402
361
|
},
|
|
@@ -410,16 +369,21 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
410
369
|
if (payload?.handled === true) return;
|
|
411
370
|
const reply = payload?.envelope;
|
|
412
371
|
if (reply === undefined || !Array.isArray(reply.in_reply_to) || reply.in_reply_to.length === 0) return;
|
|
413
|
-
// Only the delegate's own reply counts: `from` must be the delegation's id.
|
|
414
|
-
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
|
+
});
|
|
415
377
|
if (requestId === undefined) return;
|
|
416
378
|
payload.handled = true;
|
|
417
379
|
if (shown.has(reply.id)) return;
|
|
418
|
-
const delegation = byRequest.get(requestId) as
|
|
380
|
+
const delegation = byRequest.get(requestId) as StartedRecorded;
|
|
419
381
|
shown.add(reply.id);
|
|
420
|
-
const session =
|
|
382
|
+
const session = locateSession(delegation, ctx?.sessionManager.getSessionDir());
|
|
421
383
|
const finished = finishedModel(session);
|
|
422
|
-
|
|
384
|
+
// After the claim above, so a slow or failing inspection cannot let the provider or a duplicate deliver it too.
|
|
385
|
+
const snapshot = inspectWorkspace(delegation.workspace?.cwd, delegation.workspace?.git);
|
|
386
|
+
const details = resultDetails(delegation, recordedName(delegation), payload, session, finished, snapshot);
|
|
423
387
|
// Steer, not follow-up: a busy parent gets the result before its next model call, not after it has answered.
|
|
424
388
|
pi.sendMessage(
|
|
425
389
|
{ customType: CUSTOM_TYPE, content: resultText(details), details, display: true },
|
|
@@ -431,6 +395,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
431
395
|
envelopePath: payload.path,
|
|
432
396
|
...(session === undefined ? {} : { sessionPath: session }),
|
|
433
397
|
...(finished === undefined ? {} : { finishedModel: finished }),
|
|
398
|
+
workspaceSnapshot: snapshot,
|
|
434
399
|
};
|
|
435
400
|
pi.appendEntry(CUSTOM_TYPE, { id: delegation.id, result });
|
|
436
401
|
delegation.result = result;
|
|
@@ -442,6 +407,7 @@ export function createResults(pi: ExtensionAPI): Results {
|
|
|
442
407
|
return {
|
|
443
408
|
restore,
|
|
444
409
|
recordStart,
|
|
410
|
+
recordAttempt,
|
|
445
411
|
recordClose,
|
|
446
412
|
running,
|
|
447
413
|
recordGone,
|