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/tmux.ts
CHANGED
|
@@ -13,6 +13,20 @@ export type WindowSpec = {
|
|
|
13
13
|
/** One window as tmux lists it: its id, its name, and whether the program in its pane has exited. */
|
|
14
14
|
export type TmuxWindow = { id: string; name: string; exited: boolean };
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Why a window could not be opened, and whether one may exist anyway:
|
|
18
|
+
* `not-opened` only when nothing was dispatched to tmux that could create it.
|
|
19
|
+
* Anything else, a timeout or a malformed answer included, is `unknown`.
|
|
20
|
+
*/
|
|
21
|
+
export class WindowOpenError extends Error {
|
|
22
|
+
readonly certainty: "not-opened" | "unknown";
|
|
23
|
+
constructor(message: string, certainty: "not-opened" | "unknown") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "WindowOpenError";
|
|
26
|
+
this.certainty = certainty;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export interface TmuxClient {
|
|
17
31
|
/** Whether this process runs in a tmux pane. */
|
|
18
32
|
insideTmux(): boolean;
|
|
@@ -65,11 +79,24 @@ export function createTmuxClient(options: { timeoutMs?: number } = {}): TmuxClie
|
|
|
65
79
|
async openWindow(spec) {
|
|
66
80
|
const args = ["new-window", "-d", "-P", "-F", "#{window_id}", "-n", spec.name, "-c", spec.cwd];
|
|
67
81
|
for (const [key, value] of Object.entries(spec.env)) args.push("-e", `${key}=${value}`);
|
|
68
|
-
|
|
82
|
+
let session: string | undefined;
|
|
83
|
+
try {
|
|
84
|
+
session = await targetSession();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// The lookup only reads; no window can exist yet.
|
|
87
|
+
throw new WindowOpenError((err as Error).message, "not-opened");
|
|
88
|
+
}
|
|
69
89
|
if (session !== undefined) args.push("-t", session);
|
|
70
90
|
args.push("--", ...spec.argv);
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
let out: string;
|
|
92
|
+
try {
|
|
93
|
+
out = await run(args);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// After dispatch even an error or timeout may have left a window.
|
|
96
|
+
throw new WindowOpenError((err as Error).message, "unknown");
|
|
97
|
+
}
|
|
98
|
+
const windowId = out.trim();
|
|
99
|
+
if (!/^@[0-9]+$/.test(windowId)) throw new WindowOpenError("tmux reported an invalid window id", "unknown");
|
|
73
100
|
return windowId;
|
|
74
101
|
},
|
|
75
102
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { WorktreePlanSchema } from "./workspace.ts";
|
|
3
|
+
|
|
4
|
+
const ATTEMPT_PHASES = ["planned", "created", "rolled-back", "retained", "interrupted"] as const;
|
|
5
|
+
|
|
6
|
+
const ResourceSchema = Type.Union(
|
|
7
|
+
[
|
|
8
|
+
Type.Object({ kind: Type.Literal("none") }),
|
|
9
|
+
Type.Object({ kind: Type.Literal("branch"), branch: Type.String(), head: Type.String() }),
|
|
10
|
+
Type.Object({ kind: Type.Literal("worktree"), cwd: Type.String(), branch: Type.String(), head: Type.String() }),
|
|
11
|
+
Type.Object({
|
|
12
|
+
kind: Type.Literal("unknown"),
|
|
13
|
+
reason: Type.String(),
|
|
14
|
+
observedPaths: Type.Array(Type.String()),
|
|
15
|
+
observedRefs: Type.Array(Type.String()),
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
{ description: "What is known to exist for a launch that has no window, as facts." },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const AttemptStateSchema = Type.Union([
|
|
22
|
+
Type.Object({ phase: Type.Literal("planned"), resources: ResourceSchema }),
|
|
23
|
+
Type.Object({ phase: Type.Literal("created"), resources: ResourceSchema }),
|
|
24
|
+
Type.Object({
|
|
25
|
+
phase: Type.Literal("rolled-back"),
|
|
26
|
+
resources: Type.Object({ kind: Type.Literal("none") }),
|
|
27
|
+
cause: Type.String(),
|
|
28
|
+
}),
|
|
29
|
+
Type.Object({
|
|
30
|
+
phase: Type.Literal("retained"),
|
|
31
|
+
resources: ResourceSchema,
|
|
32
|
+
cause: Type.String(),
|
|
33
|
+
recovery: Type.Array(Type.String(), {
|
|
34
|
+
description: "Non-forced commands that remove exactly the resources known to remain.",
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
Type.Object({
|
|
38
|
+
phase: Type.Literal("interrupted"),
|
|
39
|
+
resources: ResourceSchema,
|
|
40
|
+
lastPhase: Type.Union([Type.Literal("planned"), Type.Literal("created")]),
|
|
41
|
+
reason: Type.String(),
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** A managed launch that has no window: never running, never closable, with no window id. */
|
|
46
|
+
export const AttemptSchema = Type.Object({
|
|
47
|
+
kind: Type.Literal("workspace-attempt"),
|
|
48
|
+
id: Type.String(),
|
|
49
|
+
parentId: Type.String(),
|
|
50
|
+
agent: Type.String(),
|
|
51
|
+
model: Type.String(),
|
|
52
|
+
thinking: Type.String(),
|
|
53
|
+
name: Type.Optional(Type.String()),
|
|
54
|
+
sessionDir: Type.String(),
|
|
55
|
+
plan: WorktreePlanSchema,
|
|
56
|
+
attempt: AttemptStateSchema,
|
|
57
|
+
requestId: Type.Optional(Type.String({ description: "The task's request id, only when mail really returned one." })),
|
|
58
|
+
state: Type.Union(
|
|
59
|
+
ATTEMPT_PHASES.map((phase) => Type.Literal(phase)),
|
|
60
|
+
{
|
|
61
|
+
description:
|
|
62
|
+
"planned, created: a launch in progress. rolled-back: failed before a window and everything was removed. retained: failed before a window and something remains (see attempt.recovery). interrupted: the parent ended mid-launch; what exists is unknown. No window was opened.",
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// How `delegation_status` words a delegation's state, auto-exit and age.
|
|
2
|
+
import type { DelegationState, StartedRecorded } from "./results.ts";
|
|
3
|
+
|
|
4
|
+
/** Records from before auto-exit existed kept their windows open, so they read off. */
|
|
5
|
+
export function autoExitLine(record: StartedRecorded): string {
|
|
6
|
+
return record.autoExit === true
|
|
7
|
+
? "on — the delegate closes its window after a normal completion unless the user took over there"
|
|
8
|
+
: "off — the window stays open after the result";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The state, worded for the `state:` line. */
|
|
12
|
+
export function stateLine(state: DelegationState, record: StartedRecorded): string {
|
|
13
|
+
switch (state) {
|
|
14
|
+
case "running":
|
|
15
|
+
return "running — the window is open and no result has arrived";
|
|
16
|
+
case "gone":
|
|
17
|
+
return "closed without a result — the window is gone and no reply arrived; a late reply would make it done";
|
|
18
|
+
case "done":
|
|
19
|
+
return `done — result status: ${record.result?.status ?? "unknown"}; the result reaches this session as a [delegate] message on its own`;
|
|
20
|
+
case "closed":
|
|
21
|
+
return "closed — delegation_close recorded it";
|
|
22
|
+
case "window-closed":
|
|
23
|
+
return "closed — the window is gone";
|
|
24
|
+
case "unknown":
|
|
25
|
+
return "unknown — tmux could not be asked whether the window is open";
|
|
26
|
+
case "planned":
|
|
27
|
+
case "created":
|
|
28
|
+
case "rolled-back":
|
|
29
|
+
case "retained":
|
|
30
|
+
case "interrupted":
|
|
31
|
+
return `${state} — no window id/start was recorded; a window may still exist`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** How long ago, worded for the `state:` line: `just now`, `N min ago` or `N h M min ago`. */
|
|
36
|
+
export function ago(ms: number): string {
|
|
37
|
+
const minutes = Math.floor(ms / 60_000);
|
|
38
|
+
if (minutes < 1) return "just now";
|
|
39
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
40
|
+
return `${Math.floor(minutes / 60)} h ${minutes % 60} min ago`;
|
|
41
|
+
}
|
package/src/tracking.ts
CHANGED
|
@@ -8,18 +8,26 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { Type } from "typebox";
|
|
9
9
|
|
|
10
10
|
import { type Mark, readMarks } from "./limits.ts";
|
|
11
|
-
import { modelLabel } from "./render.ts";
|
|
11
|
+
import { attemptLines, cwdLine, modelLabel, snapshotLines, workspaceLine } from "./render.ts";
|
|
12
12
|
import {
|
|
13
13
|
type DelegationState,
|
|
14
14
|
deriveState,
|
|
15
|
-
|
|
15
|
+
isStarted,
|
|
16
|
+
locateSession,
|
|
16
17
|
type Recorded,
|
|
17
18
|
type Results,
|
|
18
19
|
recordedName,
|
|
20
|
+
type StartedRecorded,
|
|
21
|
+
sessionIdOf,
|
|
19
22
|
type WindowState,
|
|
23
|
+
type WorkspaceAttempt,
|
|
20
24
|
} from "./results.ts";
|
|
25
|
+
import { inspectWorkspace, type WorkspaceSnapshot, WorkspaceSnapshotSchema } from "./snapshot.ts";
|
|
21
26
|
import type { TmuxClient, TmuxWindow } from "./tmux.ts";
|
|
27
|
+
import { AttemptSchema } from "./tracking-attempt.ts";
|
|
28
|
+
import { ago, autoExitLine, stateLine } from "./tracking-text.ts";
|
|
22
29
|
import type { ReadingError } from "./usage.ts";
|
|
30
|
+
import { WorkspaceSchema } from "./workspace.ts";
|
|
23
31
|
|
|
24
32
|
/** The hook a tick emits so the provider claims waiting replies before windows are checked. */
|
|
25
33
|
const SCAN = "message:scan";
|
|
@@ -49,10 +57,11 @@ export function toolResult(
|
|
|
49
57
|
/** Every tool here touches only local state: tmux, the session's entries and files under the agent directory. */
|
|
50
58
|
const CLOSED_WORLD = { openWorldHint: false } as const;
|
|
51
59
|
|
|
52
|
-
const
|
|
60
|
+
const STARTED_STATES = ["closed", "done", "gone", "running", "window-closed", "unknown"] as const;
|
|
53
61
|
|
|
54
|
-
/** One delegation in `delegation_status`'s structured result; `viewDetails` builds it. */
|
|
55
|
-
const
|
|
62
|
+
/** One started delegation in `delegation_status`'s structured result; `viewDetails` builds it. */
|
|
63
|
+
const StartedSchema = Type.Object({
|
|
64
|
+
kind: Type.Literal("started"),
|
|
56
65
|
id: Type.String(),
|
|
57
66
|
name: Type.String(),
|
|
58
67
|
agent: Type.String(),
|
|
@@ -62,13 +71,20 @@ const DelegationSchema = Type.Object({
|
|
|
62
71
|
),
|
|
63
72
|
thinking: Type.String(),
|
|
64
73
|
autoExit: Type.Boolean(),
|
|
74
|
+
resumedFrom: Type.Optional(Type.String({ description: "The delegation whose session this one reopened." })),
|
|
65
75
|
windowId: Type.String(),
|
|
66
76
|
windowName: Type.String(),
|
|
77
|
+
cwd: Type.Optional(
|
|
78
|
+
Type.String({ description: "The directory its window opened in; absent for records from before it was kept." }),
|
|
79
|
+
),
|
|
80
|
+
sessionDir: Type.Optional(Type.String({ description: "The session-storage directory it was told to use." })),
|
|
81
|
+
workspace: Type.Optional(WorkspaceSchema),
|
|
82
|
+
workspaceSnapshot: WorkspaceSnapshotSchema,
|
|
67
83
|
state: Type.Union(
|
|
68
|
-
|
|
84
|
+
STARTED_STATES.map((state) => Type.Literal(state)),
|
|
69
85
|
{
|
|
70
86
|
description:
|
|
71
|
-
"running: window open, no result. done: a result arrived (see status, envelopePath). gone: the window went with no result; a late reply makes it done. closed: delegation_close recorded it. window-closed: the window is gone. unknown: tmux could not be asked. done and closed are final; gone and window-closed turn done if a late reply arrives.",
|
|
87
|
+
"running: window open, no result. done: a result arrived (see status, envelopePath). gone: the window went with no result; a late reply makes it done. closed: delegation_close recorded it. window-closed: the window is gone. unknown: tmux could not be asked. done and closed are final, though a later delegation_resume continues one as a new delegation; gone and window-closed turn done if a late reply arrives.",
|
|
72
88
|
},
|
|
73
89
|
),
|
|
74
90
|
closed: Type.Boolean(),
|
|
@@ -81,6 +97,8 @@ const DelegationSchema = Type.Object({
|
|
|
81
97
|
),
|
|
82
98
|
});
|
|
83
99
|
|
|
100
|
+
const DelegationSchema = Type.Union([StartedSchema, AttemptSchema]);
|
|
101
|
+
|
|
84
102
|
export const StatusSchema = Type.Object({
|
|
85
103
|
delegations: Type.Array(DelegationSchema),
|
|
86
104
|
marks: Type.Array(
|
|
@@ -104,7 +122,7 @@ export const CloseSchema = Type.Object({
|
|
|
104
122
|
});
|
|
105
123
|
|
|
106
124
|
/** A tool as registered; `execute` takes the tool's own parameters, which the registration call does not check. */
|
|
107
|
-
type ToolDef = {
|
|
125
|
+
export type ToolDef = {
|
|
108
126
|
name: string;
|
|
109
127
|
label: string;
|
|
110
128
|
description: string;
|
|
@@ -115,7 +133,7 @@ type ToolDef = {
|
|
|
115
133
|
};
|
|
116
134
|
|
|
117
135
|
/** Every window on the server, asked once; a failed ask is `undefined`, never an empty list. */
|
|
118
|
-
async function listWindows(tmux: TmuxClient): Promise<TmuxWindow[] | undefined> {
|
|
136
|
+
export async function listWindows(tmux: TmuxClient): Promise<TmuxWindow[] | undefined> {
|
|
119
137
|
try {
|
|
120
138
|
return await tmux.listWindows();
|
|
121
139
|
} catch {
|
|
@@ -124,64 +142,40 @@ async function listWindows(tmux: TmuxClient): Promise<TmuxWindow[] | undefined>
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
/** The window a delegation owns: one with both its recorded id and its recorded name, since ids restart with every tmux server. */
|
|
127
|
-
function ownWindow(windows: TmuxWindow[], record:
|
|
145
|
+
export function ownWindow(windows: TmuxWindow[], record: StartedRecorded): TmuxWindow | undefined {
|
|
128
146
|
return windows.find((window) => window.id === record.windowId && window.name === record.windowName);
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
/** What the listing says about a delegation's window; a matching window whose program exited counts as gone. */
|
|
132
|
-
function windowState(windows: TmuxWindow[] | undefined, record:
|
|
150
|
+
export function windowState(windows: TmuxWindow[] | undefined, record: StartedRecorded): WindowState {
|
|
133
151
|
if (windows === undefined) return "unknown";
|
|
134
152
|
const own = ownWindow(windows, record);
|
|
135
153
|
return own !== undefined && !own.exited ? "open" : "gone";
|
|
136
154
|
}
|
|
137
155
|
|
|
138
|
-
/** Records from before auto-exit existed kept their windows open, so they read off. */
|
|
139
|
-
function autoExitLine(record: Recorded): string {
|
|
140
|
-
return record.autoExit === true
|
|
141
|
-
? "on — the delegate closes its window after a normal completion unless the user took over there"
|
|
142
|
-
: "off — the window stays open after the result";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** The state, worded for the `state:` line. */
|
|
146
|
-
function stateLine(state: DelegationState, record: Recorded): string {
|
|
147
|
-
switch (state) {
|
|
148
|
-
case "running":
|
|
149
|
-
return "running — the window is open and no result has arrived";
|
|
150
|
-
case "gone":
|
|
151
|
-
return "closed without a result — the window is gone and no reply arrived; a late reply would make it done";
|
|
152
|
-
case "done":
|
|
153
|
-
return `done — result status: ${record.result?.status ?? "unknown"}; the result reaches this session as a [delegate] message on its own`;
|
|
154
|
-
case "closed":
|
|
155
|
-
return "closed — delegation_close recorded it";
|
|
156
|
-
case "window-closed":
|
|
157
|
-
return "closed — the window is gone";
|
|
158
|
-
case "unknown":
|
|
159
|
-
return "unknown — tmux could not be asked whether the window is open";
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
156
|
/** One delegation as `delegation_status` reports it; its text and details both read from this. */
|
|
164
157
|
type View = {
|
|
165
|
-
record:
|
|
158
|
+
record: StartedRecorded;
|
|
166
159
|
state: DelegationState;
|
|
167
160
|
name: string;
|
|
168
161
|
sessionPath: string | undefined;
|
|
169
162
|
/** When the session file was last written; set only for a running delegation with a file. */
|
|
170
163
|
lastActive: Date | undefined;
|
|
164
|
+
/** The checkout observed for this status call; the recorded result keeps its own. */
|
|
165
|
+
snapshot: WorkspaceSnapshot;
|
|
166
|
+
/** The delegation that resumed this one, if any. */
|
|
167
|
+
resumedAs: string | undefined;
|
|
171
168
|
};
|
|
172
169
|
|
|
173
|
-
/** How long ago, worded for the `state:` line: `just now`, `N min ago` or `N h M min ago`. */
|
|
174
|
-
function ago(ms: number): string {
|
|
175
|
-
const minutes = Math.floor(ms / 60_000);
|
|
176
|
-
if (minutes < 1) return "just now";
|
|
177
|
-
if (minutes < 60) return `${minutes} min ago`;
|
|
178
|
-
return `${Math.floor(minutes / 60)} h ${minutes % 60} min ago`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
170
|
/** The view of a delegation: its state and the fields the text and details share. */
|
|
182
|
-
function view(
|
|
171
|
+
function view(
|
|
172
|
+
record: StartedRecorded,
|
|
173
|
+
windows: TmuxWindow[] | undefined,
|
|
174
|
+
ctx: ExtensionContext,
|
|
175
|
+
resumedAs: string | undefined,
|
|
176
|
+
): View {
|
|
183
177
|
const state = deriveState(record, windowState(windows, record));
|
|
184
|
-
const sessionPath = record
|
|
178
|
+
const sessionPath = locateSession(record, ctx.sessionManager.getSessionDir());
|
|
185
179
|
let lastActive: Date | undefined;
|
|
186
180
|
if (state === "running" && sessionPath !== undefined) {
|
|
187
181
|
try {
|
|
@@ -190,27 +184,36 @@ function view(record: Recorded, windows: TmuxWindow[] | undefined, ctx: Extensio
|
|
|
190
184
|
// a failed stat counts as no file
|
|
191
185
|
}
|
|
192
186
|
}
|
|
193
|
-
|
|
187
|
+
const snapshot = inspectWorkspace(record.workspace?.cwd, record.workspace?.git);
|
|
188
|
+
return { record, state, name: recordedName(record), sessionPath, lastActive, snapshot, resumedAs };
|
|
194
189
|
}
|
|
195
190
|
|
|
196
191
|
/** The text block for one delegation. */
|
|
197
|
-
function viewText({ record, state, name, sessionPath, lastActive }: View): string {
|
|
192
|
+
function viewText({ record, state, name, sessionPath, lastActive, snapshot, resumedAs }: View): string {
|
|
193
|
+
const [checkout, ...categories] = snapshotLines(snapshot);
|
|
198
194
|
const seen = lastActive === undefined ? "" : ` (last active ${ago(Date.now() - lastActive.getTime())})`;
|
|
199
195
|
const lines = [
|
|
200
196
|
`${record.id} ${record.agent} (${modelLabel(record.model, record.result?.finishedModel)}, thinking ${record.thinking})`,
|
|
201
197
|
` name: ${name}`,
|
|
198
|
+
...(record.resumedFrom === undefined ? [] : [` resumed from ${record.resumedFrom}`]),
|
|
199
|
+
...(resumedAs === undefined ? [] : [` resumed as ${resumedAs}`]),
|
|
202
200
|
` state: ${stateLine(state, record)}${seen}`,
|
|
203
201
|
` window: ${record.windowName} (${record.windowId})`,
|
|
202
|
+
` cwd: ${cwdLine(record.cwd)}`,
|
|
203
|
+
...(record.workspace === undefined ? [] : [` workspace: ${workspaceLine(record.workspace)}`]),
|
|
204
|
+
` checkout now: ${checkout}`,
|
|
205
|
+
...categories.map((line) => ` ${line}`),
|
|
204
206
|
` auto-exit: ${autoExitLine(record)}`,
|
|
205
|
-
` session: ${sessionPath ?? `${record
|
|
207
|
+
` session: ${sessionPath ?? `${sessionIdOf(record)} (no session file yet)`}`,
|
|
206
208
|
];
|
|
207
209
|
if (record.result !== undefined) lines.push(` envelope: ${record.result.envelopePath}`);
|
|
208
210
|
return lines.join("\n");
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
/** The structured details for one delegation. */
|
|
212
|
-
function viewDetails({ record, state, name, sessionPath, lastActive }: View): Record<string, unknown> {
|
|
214
|
+
function viewDetails({ record, state, name, sessionPath, lastActive, snapshot }: View): Record<string, unknown> {
|
|
213
215
|
return {
|
|
216
|
+
kind: "started",
|
|
214
217
|
id: record.id,
|
|
215
218
|
name,
|
|
216
219
|
agent: record.agent,
|
|
@@ -218,8 +221,13 @@ function viewDetails({ record, state, name, sessionPath, lastActive }: View): Re
|
|
|
218
221
|
...(record.result?.finishedModel === undefined ? {} : { finishedModel: record.result.finishedModel }),
|
|
219
222
|
thinking: record.thinking,
|
|
220
223
|
autoExit: record.autoExit === true,
|
|
224
|
+
...(record.resumedFrom === undefined ? {} : { resumedFrom: record.resumedFrom }),
|
|
221
225
|
windowId: record.windowId,
|
|
222
226
|
windowName: record.windowName,
|
|
227
|
+
...(record.cwd === undefined ? {} : { cwd: record.cwd }),
|
|
228
|
+
...(record.sessionDir === undefined ? {} : { sessionDir: record.sessionDir }),
|
|
229
|
+
...(record.workspace === undefined ? {} : { workspace: record.workspace }),
|
|
230
|
+
workspaceSnapshot: snapshot,
|
|
223
231
|
state,
|
|
224
232
|
closed: record.closed === true,
|
|
225
233
|
closedWithoutResult: record.gone === true && record.result === undefined && record.closed !== true,
|
|
@@ -230,6 +238,16 @@ function viewDetails({ record, state, name, sessionPath, lastActive }: View): Re
|
|
|
230
238
|
};
|
|
231
239
|
}
|
|
232
240
|
|
|
241
|
+
/** One unstarted attempt as the status text words it. */
|
|
242
|
+
function attemptText(record: WorkspaceAttempt): string {
|
|
243
|
+
return [
|
|
244
|
+
`${record.id} ${record.agent} (${modelLabel(record.model, undefined)}, thinking ${record.thinking})`,
|
|
245
|
+
` name: ${recordedName(record)}`,
|
|
246
|
+
` state: ${record.attempt.phase} — no window id/start was recorded; a window may still exist (close requires a verified window id)`,
|
|
247
|
+
...attemptLines(record).map((line) => ` ${line}`),
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
233
251
|
/** The usage-limit marks in force now, as `delegation_status` reports them. */
|
|
234
252
|
function activeMarks(): { scope: string; source: Mark["source"]; reason: string; clearsAt: string }[] {
|
|
235
253
|
const now = Date.now();
|
|
@@ -325,7 +343,7 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
325
343
|
name: "delegation_status",
|
|
326
344
|
label: "Delegation status",
|
|
327
345
|
description:
|
|
328
|
-
"The delegations this session started, each reported as running (the window is open and no result has arrived yet), done (with the result envelope's status) or closed (closed with delegation_close, or its window is gone). A delegation whose window went with no reply is reported closed without a result until a late reply makes it done. Call it to check on a delegation that seems stuck or to find its files; a result arrives as a message on its own, so there is no need to call it to wait for one. Names the delegation id, agent, session name, model, thinking, window, delegate session path and result envelope path. A running delegation also shows when its session file was last written; a long gap may mean it is stuck but does not prove it. A delegate's window being open means it is connected, not that its task is unfinished or finished; only a result means done.",
|
|
346
|
+
"The delegations this session started, each reported as running (the window is open and no result has arrived yet), done (with the result envelope's status) or closed (closed with delegation_close, or its window is gone). A delegation whose window went with no reply is reported closed without a result until a late reply makes it done. Call it to check on a delegation that seems stuck or to find its files; a result arrives as a message on its own, so there is no need to call it to wait for one. Names the delegation id, agent, session name, model, thinking, window, working directory, delegate session path and result envelope path. A running delegation also shows when its session file was last written; a long gap may mean it is stuck but does not prove it. A delegate's window being open means it is connected, not that its task is unfinished or finished; only a result means done.",
|
|
329
347
|
parameters: Type.Object({
|
|
330
348
|
id: Type.Optional(Type.String({ description: "One delegation id; omit to report every delegation." })),
|
|
331
349
|
}),
|
|
@@ -340,10 +358,19 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
340
358
|
): Promise<ReturnType<typeof toolResult>> {
|
|
341
359
|
const records = selectRecords(results, params.id);
|
|
342
360
|
const windows = await listWindows(tmux);
|
|
343
|
-
const
|
|
361
|
+
const resumedAs = (id: string) => results.list().find((item) => isStarted(item) && item.resumedFrom === id)?.id;
|
|
362
|
+
const views = records.map((record) =>
|
|
363
|
+
isStarted(record) ? view(record, windows, ctx, resumedAs(record.id)) : record,
|
|
364
|
+
);
|
|
344
365
|
const marks = activeMarks();
|
|
345
366
|
const errors = readingErrors();
|
|
346
|
-
const details = {
|
|
367
|
+
const details = {
|
|
368
|
+
delegations: views.map((item) =>
|
|
369
|
+
"record" in item ? viewDetails(item) : { ...item, state: item.attempt.phase },
|
|
370
|
+
),
|
|
371
|
+
marks,
|
|
372
|
+
readingErrors: errors,
|
|
373
|
+
};
|
|
347
374
|
const block =
|
|
348
375
|
marks.length === 0 && errors.length === 0
|
|
349
376
|
? []
|
|
@@ -358,7 +385,10 @@ function statusTool(results: Results, tmux: TmuxClient, readingErrors: () => Rea
|
|
|
358
385
|
const parts =
|
|
359
386
|
views.length === 0
|
|
360
387
|
? ["No delegations are recorded in this session.", ...block]
|
|
361
|
-
: [
|
|
388
|
+
: [
|
|
389
|
+
`${heading}\n\n${views.map((item) => ("record" in item ? viewText(item) : attemptText(item))).join("\n\n")}`,
|
|
390
|
+
...block,
|
|
391
|
+
];
|
|
362
392
|
return toolResult(parts.join("\n\n"), details);
|
|
363
393
|
},
|
|
364
394
|
};
|
|
@@ -385,6 +415,11 @@ function closeTool(results: Results, tmux: TmuxClient): ToolDef {
|
|
|
385
415
|
_ctx: ExtensionContext,
|
|
386
416
|
): Promise<ReturnType<typeof toolResult>> {
|
|
387
417
|
const [record] = selectRecords(results, params.id);
|
|
418
|
+
if (!isStarted(record)) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`delegation ${record.id} is a workspace attempt (${record.attempt.phase}): no verified window id was recorded; a window may still exist, so close is unavailable`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
388
423
|
const closed = (alreadyClosed: boolean, killed: boolean) => ({
|
|
389
424
|
id: record.id,
|
|
390
425
|
windowId: record.windowId,
|