@termfleet/core 0.1.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/dist/agent-launch.d.ts +78 -0
- package/dist/agent-launch.js +247 -0
- package/dist/agent-session-id.d.ts +10 -0
- package/dist/agent-session-id.js +36 -0
- package/dist/agent-session-index-client.d.ts +7 -0
- package/dist/agent-session-index-client.js +86 -0
- package/dist/agent-session-index-worker.d.ts +1 -0
- package/dist/agent-session-index-worker.js +20 -0
- package/dist/agent-session-index.d.ts +34 -0
- package/dist/agent-session-index.js +527 -0
- package/dist/agent-session-tail.d.ts +33 -0
- package/dist/agent-session-tail.js +184 -0
- package/dist/agent-session-watcher.d.ts +36 -0
- package/dist/agent-session-watcher.js +194 -0
- package/dist/agent-session.d.ts +380 -0
- package/dist/agent-session.js +1688 -0
- package/dist/background-runner.d.ts +3 -0
- package/dist/background-runner.js +55 -0
- package/dist/boot-queue.d.ts +35 -0
- package/dist/boot-queue.js +66 -0
- package/dist/build-info.d.ts +5 -0
- package/dist/build-info.js +38 -0
- package/dist/collab/canvas-doc.d.ts +47 -0
- package/dist/collab/canvas-doc.js +83 -0
- package/dist/contracts/auth.d.ts +77 -0
- package/dist/contracts/auth.js +1 -0
- package/dist/contracts/canvas.d.ts +34 -0
- package/dist/contracts/canvas.js +76 -0
- package/dist/contracts/console-layout.d.ts +39 -0
- package/dist/contracts/console-layout.js +135 -0
- package/dist/contracts/files.d.ts +38 -0
- package/dist/contracts/files.js +37 -0
- package/dist/contracts/provider-url.d.ts +3 -0
- package/dist/contracts/provider-url.js +49 -0
- package/dist/contracts/registry.d.ts +58 -0
- package/dist/contracts/registry.js +285 -0
- package/dist/launch-trace.d.ts +6 -0
- package/dist/launch-trace.js +33 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -0
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +134 -0
- package/dist/local-providers.d.ts +32 -0
- package/dist/local-providers.js +184 -0
- package/dist/local-tunnel.d.ts +6 -0
- package/dist/local-tunnel.js +258 -0
- package/dist/provider-access-token.d.ts +11 -0
- package/dist/provider-access-token.js +77 -0
- package/dist/provider-client.d.ts +152 -0
- package/dist/provider-client.js +666 -0
- package/dist/provider-url-resolver.d.ts +16 -0
- package/dist/provider-url-resolver.js +37 -0
- package/dist/registry-client.d.ts +93 -0
- package/dist/registry-client.js +170 -0
- package/dist/registry.d.ts +56 -0
- package/dist/registry.js +406 -0
- package/dist/session-attention.d.ts +24 -0
- package/dist/session-attention.js +54 -0
- package/dist/session-lifecycle.d.ts +83 -0
- package/dist/session-lifecycle.js +658 -0
- package/dist/session-window.d.ts +3 -0
- package/dist/session-window.js +20 -0
- package/dist/terminal-client.d.ts +49 -0
- package/dist/terminal-client.js +89 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +21 -0
- package/package.json +26 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type AgentKind = "claude" | "codex" | "gemini";
|
|
2
|
+
export type AgentWindowCreateOptions = {
|
|
3
|
+
agent: AgentKind;
|
|
4
|
+
agentCwd?: string;
|
|
5
|
+
createTimeoutMs?: number;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
prompt?: string;
|
|
10
|
+
agentSessionId?: string;
|
|
11
|
+
resume?: boolean;
|
|
12
|
+
setupCommand?: string;
|
|
13
|
+
};
|
|
14
|
+
export type AgentWindowCreateResult = {
|
|
15
|
+
agent: AgentKind;
|
|
16
|
+
agentSessionId?: string;
|
|
17
|
+
command: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
result: unknown;
|
|
20
|
+
terminalId?: string;
|
|
21
|
+
};
|
|
22
|
+
export type AgentLaunchResponder = {
|
|
23
|
+
input: string;
|
|
24
|
+
matches(content: string): boolean;
|
|
25
|
+
};
|
|
26
|
+
export type AgentLaunchStage = "agent-start" | "preflight" | "session-capture" | "window-create";
|
|
27
|
+
export type AgentLaunchErrorDetail = {
|
|
28
|
+
capture?: string;
|
|
29
|
+
launchTraceFile?: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
session?: string;
|
|
32
|
+
windowId?: number;
|
|
33
|
+
};
|
|
34
|
+
export type AgentLaunchErrorPayload = AgentLaunchErrorDetail & {
|
|
35
|
+
code: string;
|
|
36
|
+
message: string;
|
|
37
|
+
stage: AgentLaunchStage;
|
|
38
|
+
};
|
|
39
|
+
export declare class AgentLaunchError extends Error {
|
|
40
|
+
readonly code: string;
|
|
41
|
+
readonly detail: AgentLaunchErrorDetail;
|
|
42
|
+
readonly stage: AgentLaunchStage;
|
|
43
|
+
constructor({ code, detail, message, stage }: {
|
|
44
|
+
code: string;
|
|
45
|
+
detail?: AgentLaunchErrorDetail;
|
|
46
|
+
message: string;
|
|
47
|
+
stage: AgentLaunchStage;
|
|
48
|
+
});
|
|
49
|
+
toJSON(): AgentLaunchErrorPayload;
|
|
50
|
+
}
|
|
51
|
+
export declare function parseAgentLaunchErrorPayload(value: unknown): AgentLaunchError | undefined;
|
|
52
|
+
export declare function isAgentLaunchStage(value: unknown): value is AgentLaunchStage;
|
|
53
|
+
export declare function buildAgentStartupCommand(options: AgentWindowCreateOptions & {
|
|
54
|
+
openedAtCwd?: boolean;
|
|
55
|
+
promptFile?: string;
|
|
56
|
+
}): {
|
|
57
|
+
agentSessionId?: string;
|
|
58
|
+
command: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function defaultAgentCommand(agent: AgentKind, sessionId?: string, options?: {
|
|
61
|
+
model?: string;
|
|
62
|
+
resume?: boolean;
|
|
63
|
+
trustedCwd?: string;
|
|
64
|
+
}): string;
|
|
65
|
+
export declare function buildStartupCommand(command: string, options: {
|
|
66
|
+
agentCwd?: string;
|
|
67
|
+
cwd?: string;
|
|
68
|
+
openedAtCwd?: boolean;
|
|
69
|
+
prompt?: string;
|
|
70
|
+
promptFile?: string;
|
|
71
|
+
setupCommand?: string;
|
|
72
|
+
}): string;
|
|
73
|
+
export declare function isShellPromptReady(content: string): boolean;
|
|
74
|
+
export declare function agentLaunchResponders(agent: AgentKind): AgentLaunchResponder[];
|
|
75
|
+
export declare function isAgentLaunchReady(content: string, agent: AgentKind): boolean;
|
|
76
|
+
export declare function detectAgentLaunchFailure(content: string, agent: AgentKind): string | undefined;
|
|
77
|
+
export declare function shellQuote(value: string): string;
|
|
78
|
+
export declare function isUuid(value: string): boolean;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Structured launch failure. Agent launches fail at distinct stages (preflight
|
|
2
|
+
// cwd checks, window creation, agent boot, terminal-session capture) and callers
|
|
3
|
+
// need to branch on a machine-readable stage/code instead of string-matching
|
|
4
|
+
// prose. Crosses the provider control socket as a payload via toJSON and is
|
|
5
|
+
// rebuilt by parseAgentLaunchErrorPayload on the client. The startup-hang case
|
|
6
|
+
// (no transcript after the first-write deadline) is the retryable agent-start
|
|
7
|
+
// failure — see isRetryableAgentLaunchError in the provider engine.
|
|
8
|
+
export class AgentLaunchError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
detail;
|
|
11
|
+
stage;
|
|
12
|
+
constructor({ code, detail = {}, message, stage }) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AgentLaunchError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.stage = stage;
|
|
18
|
+
}
|
|
19
|
+
toJSON() {
|
|
20
|
+
return {
|
|
21
|
+
code: this.code,
|
|
22
|
+
...this.detail,
|
|
23
|
+
message: this.message,
|
|
24
|
+
stage: this.stage
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function parseAgentLaunchErrorPayload(value) {
|
|
29
|
+
if (!value || typeof value !== "object") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const { capture, code, launchTraceFile, message, path, session, stage, windowId } = value;
|
|
33
|
+
if (typeof code !== "string" || typeof message !== "string" || !isAgentLaunchStage(stage)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return new AgentLaunchError({
|
|
37
|
+
code,
|
|
38
|
+
detail: {
|
|
39
|
+
...(typeof capture === "string" ? { capture } : {}),
|
|
40
|
+
...(typeof launchTraceFile === "string" ? { launchTraceFile } : {}),
|
|
41
|
+
...(typeof path === "string" ? { path } : {}),
|
|
42
|
+
...(typeof session === "string" ? { session } : {}),
|
|
43
|
+
...(typeof windowId === "number" && Number.isInteger(windowId) ? { windowId } : {})
|
|
44
|
+
},
|
|
45
|
+
message,
|
|
46
|
+
stage
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function isAgentLaunchStage(value) {
|
|
50
|
+
return value === "agent-start" || value === "preflight" || value === "session-capture" || value === "window-create";
|
|
51
|
+
}
|
|
52
|
+
export function buildAgentStartupCommand(options) {
|
|
53
|
+
if (options.agent === "codex" && options.agentSessionId) {
|
|
54
|
+
throw new Error("Codex launches do not support explicit session ids yet.");
|
|
55
|
+
}
|
|
56
|
+
if (options.resume && !options.agentSessionId) {
|
|
57
|
+
throw new Error("Resuming an agent session requires an explicit session id.");
|
|
58
|
+
}
|
|
59
|
+
// Claude and Gemini both take an explicit UUID session id (claude --session-id,
|
|
60
|
+
// gemini --session-id); codex does not.
|
|
61
|
+
const bareAgentSessionId = options.agent === "claude" || options.agent === "gemini"
|
|
62
|
+
? normalizeAgentSessionId(options.agent, options.agentSessionId ?? createUuid())
|
|
63
|
+
: undefined;
|
|
64
|
+
if (bareAgentSessionId && !isUuid(bareAgentSessionId)) {
|
|
65
|
+
throw new Error(`${options.agent} requires a UUID session id.`);
|
|
66
|
+
}
|
|
67
|
+
const command = buildStartupCommand(defaultAgentCommand(options.agent, bareAgentSessionId, {
|
|
68
|
+
model: options.model,
|
|
69
|
+
resume: Boolean(options.resume),
|
|
70
|
+
trustedCwd: options.agentCwd ?? options.cwd
|
|
71
|
+
}), {
|
|
72
|
+
agentCwd: options.agentCwd,
|
|
73
|
+
cwd: options.cwd,
|
|
74
|
+
openedAtCwd: options.openedAtCwd,
|
|
75
|
+
prompt: options.promptFile ? undefined : options.prompt,
|
|
76
|
+
promptFile: options.promptFile,
|
|
77
|
+
setupCommand: options.setupCommand
|
|
78
|
+
});
|
|
79
|
+
const agentSessionId = bareAgentSessionId ? `${options.agent}:${bareAgentSessionId}` : undefined;
|
|
80
|
+
return {
|
|
81
|
+
...(agentSessionId ? { agentSessionId } : {}),
|
|
82
|
+
command
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function normalizeAgentSessionId(agent, sessionId) {
|
|
86
|
+
return sessionId.startsWith(`${agent}:`) ? sessionId.slice(agent.length + 1) : sessionId;
|
|
87
|
+
}
|
|
88
|
+
export function defaultAgentCommand(agent, sessionId, options) {
|
|
89
|
+
const modelFlag = options?.model ? `--model ${shellQuote(options.model)}` : undefined;
|
|
90
|
+
if (agent === "claude") {
|
|
91
|
+
// --resume <id> reattaches to an existing conversation; --session-id <id>
|
|
92
|
+
// creates a new one with that id (and fails if it already exists).
|
|
93
|
+
const sessionFlag = sessionId
|
|
94
|
+
? options?.resume
|
|
95
|
+
? `--resume ${shellQuote(sessionId)}`
|
|
96
|
+
: `--session-id ${shellQuote(sessionId)}`
|
|
97
|
+
: undefined;
|
|
98
|
+
return [
|
|
99
|
+
"CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1",
|
|
100
|
+
"claude",
|
|
101
|
+
"--dangerously-skip-permissions",
|
|
102
|
+
modelFlag,
|
|
103
|
+
sessionFlag
|
|
104
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
105
|
+
}
|
|
106
|
+
if (agent === "gemini") {
|
|
107
|
+
// --skip-trust avoids the workspace-trust prompt; --session-id seeds a new
|
|
108
|
+
// session with our UUID (resume-by-uuid isn't supported — gemini resumes by
|
|
109
|
+
// index/latest, handled separately). The prompt is appended positionally by
|
|
110
|
+
// buildStartupCommand (gemini's `query` arg runs interactive by default).
|
|
111
|
+
const sessionFlag = sessionId && !options?.resume ? `--session-id ${shellQuote(sessionId)}` : undefined;
|
|
112
|
+
return [
|
|
113
|
+
"gemini",
|
|
114
|
+
"--skip-trust",
|
|
115
|
+
sessionFlag,
|
|
116
|
+
modelFlag
|
|
117
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
118
|
+
}
|
|
119
|
+
return [
|
|
120
|
+
"codex",
|
|
121
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
122
|
+
"--dangerously-bypass-hook-trust",
|
|
123
|
+
options?.trustedCwd ? `-c ${shellQuote(`projects.${JSON.stringify(options.trustedCwd)}.trust_level="trusted"`)} ` : undefined,
|
|
124
|
+
modelFlag
|
|
125
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
126
|
+
}
|
|
127
|
+
export function buildStartupCommand(command, options) {
|
|
128
|
+
const cwdPrefix = options.openedAtCwd || !options.cwd ? undefined : `cd ${shellQuote(options.cwd)}`;
|
|
129
|
+
// The prompt file is a short-lived launch artifact (written 0600). Read it into
|
|
130
|
+
// a shell var and delete it BEFORE the agent runs, so it never lingers in /tmp
|
|
131
|
+
// even for a long-running session. Argv exposure is identical to inlining
|
|
132
|
+
// "$(cat …)" — both expand into the launched process's arguments.
|
|
133
|
+
const quotedPromptFile = options.promptFile ? shellQuote(options.promptFile) : undefined;
|
|
134
|
+
const agentCommand = quotedPromptFile
|
|
135
|
+
? `__tf_prompt="$(cat ${quotedPromptFile})" && rm -f ${quotedPromptFile} && ${command} "$__tf_prompt"`
|
|
136
|
+
: options.prompt ? `${command} ${shellQuote(options.prompt)}` : command;
|
|
137
|
+
return [
|
|
138
|
+
cwdPrefix,
|
|
139
|
+
options.setupCommand,
|
|
140
|
+
options.agentCwd ? `cd ${shellQuote(options.agentCwd)}` : undefined,
|
|
141
|
+
agentCommand
|
|
142
|
+
].filter((part) => Boolean(part)).join(" && ");
|
|
143
|
+
}
|
|
144
|
+
// Conservative heuristic for "the pane's shell is at a prompt and reading
|
|
145
|
+
// input": the last visible line ends with a common prompt terminator. Used
|
|
146
|
+
// before typing a launch command into a freshly created pane, where typing
|
|
147
|
+
// during shell startup races the line editor.
|
|
148
|
+
export function isShellPromptReady(content) {
|
|
149
|
+
const lines = content.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
|
|
150
|
+
const lastLine = lines.at(-1);
|
|
151
|
+
return lastLine !== undefined && /[$%#>❯]$/.test(lastLine);
|
|
152
|
+
}
|
|
153
|
+
export function agentLaunchResponders(agent) {
|
|
154
|
+
if (agent === "claude")
|
|
155
|
+
return claudeLaunchResponders;
|
|
156
|
+
if (agent === "gemini")
|
|
157
|
+
return geminiLaunchResponders;
|
|
158
|
+
return codexLaunchResponders;
|
|
159
|
+
}
|
|
160
|
+
export function isAgentLaunchReady(content, agent) {
|
|
161
|
+
if (agent === "claude") {
|
|
162
|
+
return /Claude Code/i.test(content)
|
|
163
|
+
&& !content.includes("WARNING: Claude Code running in Bypass Permissions mode")
|
|
164
|
+
&& /[❯>]\s*$/.test(content.trimEnd());
|
|
165
|
+
}
|
|
166
|
+
if (agent === "gemini") {
|
|
167
|
+
return /gemini/i.test(content) && /[❯>]\s*$/.test(content.trimEnd());
|
|
168
|
+
}
|
|
169
|
+
return /codex/i.test(content) && /[❯>]\s*$/.test(content.trimEnd());
|
|
170
|
+
}
|
|
171
|
+
export function detectAgentLaunchFailure(content, agent) {
|
|
172
|
+
if (agent === "claude") {
|
|
173
|
+
if (/Select login method:/i.test(content)) {
|
|
174
|
+
return "Claude Code is not logged in for this Docker session.";
|
|
175
|
+
}
|
|
176
|
+
if (/Claude configuration file not found at:/i.test(content)) {
|
|
177
|
+
return "Claude Code configuration is missing for this Docker session.";
|
|
178
|
+
}
|
|
179
|
+
if (/A backup file exists at:/i.test(content) && /You can manually restore it by running:/i.test(content)) {
|
|
180
|
+
return "Claude Code configuration is missing for this Docker session, but a backup exists.";
|
|
181
|
+
}
|
|
182
|
+
if (/Please run\s+\/login/i.test(content) && /API Error:\s*401 Invalid authentication credentials/i.test(content)) {
|
|
183
|
+
return "Claude Code credentials are invalid for this Docker session. Run /login in the session or remount a valid Claude home.";
|
|
184
|
+
}
|
|
185
|
+
if (/EROFS:\s*read-only file system/i.test(content) && /\.claude/i.test(content)) {
|
|
186
|
+
return "Claude Code cannot write to its mounted .claude directory. Mount Claude credentials read-write for Docker workers.";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (agent === "gemini") {
|
|
190
|
+
// Verified against gemini-cli 0.47 run with no credentials: it prints exactly
|
|
191
|
+
// "Please set an Auth method in your …/.gemini/settings.json or specify one of
|
|
192
|
+
// the following environment variables …: GEMINI_API_KEY, …". High-precision and
|
|
193
|
+
// unambiguous — a healthy launch never prints it.
|
|
194
|
+
if (/Please set an Auth method/i.test(content)) {
|
|
195
|
+
return "Gemini is not authenticated. Set GEMINI_API_KEY (or configure an auth method in .gemini/settings.json) for this session.";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
export function shellQuote(value) {
|
|
201
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
202
|
+
}
|
|
203
|
+
export function isUuid(value) {
|
|
204
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
205
|
+
}
|
|
206
|
+
function createUuid() {
|
|
207
|
+
const randomUUID = globalThis.crypto?.randomUUID;
|
|
208
|
+
if (randomUUID) {
|
|
209
|
+
return randomUUID.call(globalThis.crypto);
|
|
210
|
+
}
|
|
211
|
+
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (character) => {
|
|
212
|
+
const value = Number(character);
|
|
213
|
+
return (value ^ Math.random() * 16 >> value / 4).toString(16);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const claudeLaunchResponders = [
|
|
217
|
+
{
|
|
218
|
+
input: "\n",
|
|
219
|
+
matches: (content) => /Is this a project you created or one you trust\?/i.test(content)
|
|
220
|
+
&& /Yes, I trust this folder/i.test(content)
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
input: "\n",
|
|
224
|
+
matches: (content) => /Choose the text style that looks best with your terminal/i.test(content)
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
input: "\x1b[B\n",
|
|
228
|
+
matches: (content) => /Try the new fullscreen renderer\?/i.test(content)
|
|
229
|
+
&& /Not now/i.test(content)
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
input: "2\n",
|
|
233
|
+
matches: (content) => content.includes("WARNING: Claude Code running in Bypass Permissions mode")
|
|
234
|
+
&& content.includes("Yes, I accept")
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
const codexLaunchResponders = [
|
|
238
|
+
{
|
|
239
|
+
input: "y\n",
|
|
240
|
+
matches: (content) => /trust|continue|confirm|approve|permission/i.test(content)
|
|
241
|
+
&& /codex/i.test(content)
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
// --skip-trust suppresses the workspace-trust prompt and Gemini is already
|
|
245
|
+
// authed, so no launch responders are needed in the common case. (bootstrapAgentLaunch
|
|
246
|
+
// gives up gracefully if the ready check never matches — the agent still launched.)
|
|
247
|
+
const geminiLaunchResponders = [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentProvider } from "./agent-session-index.js";
|
|
2
|
+
export declare const agentProviders: readonly ["claude", "codex", "gemini"];
|
|
3
|
+
export declare function isAgentProvider(value: string): value is AgentProvider;
|
|
4
|
+
export declare function formatAgentSessionId(agent: AgentProvider, bareId: string): string;
|
|
5
|
+
export declare function parseAgentSessionId(agentSessionId: string): {
|
|
6
|
+
agent: AgentProvider;
|
|
7
|
+
bareId: string;
|
|
8
|
+
} | undefined;
|
|
9
|
+
export declare function bareAgentSessionId(agentSessionId: string): string;
|
|
10
|
+
export declare function isLiveAgentSessionId(sessionId: string | null | undefined): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The agentSessionId is the identity of the primary abstraction: an agent prefix
|
|
2
|
+
// (claude:/codex:/gemini:) plus the agent's own bare session id. This is the ONE
|
|
3
|
+
// owner of that grammar — every surface (CLI, SDK, MCP, the provider socket gates,
|
|
4
|
+
// the session→window descent) parses/formats it here and shares one valid-prefix
|
|
5
|
+
// set, so adding a fourth agent is a single edit and the gates can't drift apart
|
|
6
|
+
// (they did: a live Gemini terminal used to be refused session-keyed input).
|
|
7
|
+
export const agentProviders = ["claude", "codex", "gemini"];
|
|
8
|
+
// A forked Claude sub-session runs under its own prefix; it hosts a live agent
|
|
9
|
+
// terminal (eligible for session-keyed input) but is not a top-level provider.
|
|
10
|
+
const liveAgentPrefixes = [...agentProviders.map((agent) => `${agent}:`), "claude-subagent:"];
|
|
11
|
+
export function isAgentProvider(value) {
|
|
12
|
+
return agentProviders.includes(value);
|
|
13
|
+
}
|
|
14
|
+
export function formatAgentSessionId(agent, bareId) {
|
|
15
|
+
return `${agent}:${bareId}`;
|
|
16
|
+
}
|
|
17
|
+
// Split a prefixed id into { agent, bareId }; undefined for an unprefixed or
|
|
18
|
+
// unknown-prefix id (the caller decides whether that is an error).
|
|
19
|
+
export function parseAgentSessionId(agentSessionId) {
|
|
20
|
+
const separator = agentSessionId.indexOf(":");
|
|
21
|
+
if (separator < 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const prefix = agentSessionId.slice(0, separator);
|
|
25
|
+
return isAgentProvider(prefix) ? { agent: prefix, bareId: agentSessionId.slice(separator + 1) } : undefined;
|
|
26
|
+
}
|
|
27
|
+
// The agent's bare service id: drops a known agent prefix, leaves an unprefixed
|
|
28
|
+
// id untouched.
|
|
29
|
+
export function bareAgentSessionId(agentSessionId) {
|
|
30
|
+
return parseAgentSessionId(agentSessionId)?.bareId ?? agentSessionId;
|
|
31
|
+
}
|
|
32
|
+
// True when this session id names a live agent terminal (claude/codex/gemini, or a
|
|
33
|
+
// claude subagent) — the eligibility gate for session-keyed / break-glass input.
|
|
34
|
+
export function isLiveAgentSessionId(sessionId) {
|
|
35
|
+
return !!sessionId && liveAgentPrefixes.some((prefix) => sessionId.startsWith(prefix));
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentSessionIndexPage } from "./agent-session-index.js";
|
|
2
|
+
export declare function listAgentSessionsInWorker(options?: {
|
|
3
|
+
cursor?: string | null;
|
|
4
|
+
limit?: number;
|
|
5
|
+
query?: string;
|
|
6
|
+
}): Promise<AgentSessionIndexPage>;
|
|
7
|
+
export declare function warmAgentSessionIndexInWorker(): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
let worker;
|
|
3
|
+
let nextId = 0;
|
|
4
|
+
const pending = new Map();
|
|
5
|
+
// A request is bounded work (paginated, 64KB head/tail reads, candidate cache).
|
|
6
|
+
// A worker wedged in a synchronous fs call emits neither "error" nor "exit", so
|
|
7
|
+
// without this the promise would hang forever and `pending` would grow per call.
|
|
8
|
+
const requestTimeoutMs = 30_000;
|
|
9
|
+
function ensureWorker() {
|
|
10
|
+
if (worker)
|
|
11
|
+
return worker;
|
|
12
|
+
// Dev runs from .ts via tsx; the built output is .js. In dev, a worker does
|
|
13
|
+
// not inherit tsx's loader, so spawn a plain-JS bootstrap that registers it
|
|
14
|
+
// and then imports the TS worker. In the built output, run the .js directly.
|
|
15
|
+
const isTypeScript = import.meta.url.endsWith(".ts");
|
|
16
|
+
const workerUrl = isTypeScript
|
|
17
|
+
? new URL("./agent-session-index-worker-boot.mjs", import.meta.url)
|
|
18
|
+
: new URL("./agent-session-index-worker.js", import.meta.url);
|
|
19
|
+
const next = new Worker(workerUrl);
|
|
20
|
+
next.on("message", (message) => {
|
|
21
|
+
const entry = pending.get(message.id);
|
|
22
|
+
if (!entry)
|
|
23
|
+
return;
|
|
24
|
+
pending.delete(message.id);
|
|
25
|
+
if (message.error)
|
|
26
|
+
entry.reject(new Error(message.error));
|
|
27
|
+
else if (message.page)
|
|
28
|
+
entry.resolve(message.page);
|
|
29
|
+
else
|
|
30
|
+
entry.reject(new Error("Agent session index worker returned no page."));
|
|
31
|
+
});
|
|
32
|
+
next.on("error", (error) => {
|
|
33
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
34
|
+
for (const entry of pending.values())
|
|
35
|
+
entry.reject(failure);
|
|
36
|
+
pending.clear();
|
|
37
|
+
// Terminate the dead worker so its thread/isolate and listeners are released;
|
|
38
|
+
// a bare `worker = undefined` would leak the isolate on repeated errors.
|
|
39
|
+
void next.terminate();
|
|
40
|
+
worker = undefined;
|
|
41
|
+
});
|
|
42
|
+
next.on("exit", (code) => {
|
|
43
|
+
// A worker can die WITHOUT first emitting "error" (OOM kill, terminate, a
|
|
44
|
+
// throw the runtime turns into a clean exit). Reject any in-flight requests
|
|
45
|
+
// so the chat list surfaces an error instead of hanging forever.
|
|
46
|
+
if (pending.size > 0) {
|
|
47
|
+
const failure = new Error(`Agent session index worker exited (code ${code}).`);
|
|
48
|
+
for (const entry of pending.values())
|
|
49
|
+
entry.reject(failure);
|
|
50
|
+
pending.clear();
|
|
51
|
+
}
|
|
52
|
+
worker = undefined;
|
|
53
|
+
});
|
|
54
|
+
// Don't keep the process alive on the worker alone (matters for the CLI/tests).
|
|
55
|
+
next.unref();
|
|
56
|
+
worker = next;
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
export function listAgentSessionsInWorker(options = {}) {
|
|
60
|
+
const target = ensureWorker();
|
|
61
|
+
const id = ++nextId;
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
if (!pending.has(id))
|
|
65
|
+
return;
|
|
66
|
+
pending.delete(id);
|
|
67
|
+
reject(new Error(`Agent session index timed out after ${requestTimeoutMs}ms.`));
|
|
68
|
+
// A wedged worker won't recover; terminate it so the next call respawns a
|
|
69
|
+
// fresh one (the "exit" handler rejects any other in-flight requests).
|
|
70
|
+
if (worker === target) {
|
|
71
|
+
void target.terminate();
|
|
72
|
+
worker = undefined;
|
|
73
|
+
}
|
|
74
|
+
}, requestTimeoutMs);
|
|
75
|
+
timer.unref();
|
|
76
|
+
pending.set(id, {
|
|
77
|
+
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
78
|
+
resolve: (page) => { clearTimeout(timer); resolve(page); }
|
|
79
|
+
});
|
|
80
|
+
target.postMessage({ id, ...options });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Prime the worker's caches off the request path (e.g. at provider startup).
|
|
84
|
+
export async function warmAgentSessionIndexInWorker() {
|
|
85
|
+
await listAgentSessionsInWorker({ limit: 1 });
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { buildAgentSessionIndex } from "./agent-session-index.js";
|
|
3
|
+
const port = parentPort;
|
|
4
|
+
if (!port) {
|
|
5
|
+
throw new Error("agent-session-index-worker must be run as a worker thread.");
|
|
6
|
+
}
|
|
7
|
+
port.on("message", async (request) => {
|
|
8
|
+
try {
|
|
9
|
+
const page = await buildAgentSessionIndex({
|
|
10
|
+
...(request.home ? { home: request.home } : {}),
|
|
11
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
12
|
+
...(request.cursor !== undefined ? { cursor: request.cursor } : {}),
|
|
13
|
+
...(request.query ? { query: request.query } : {})
|
|
14
|
+
});
|
|
15
|
+
port.postMessage({ id: request.id, page });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
port.postMessage({ error: error instanceof Error ? error.message : String(error), id: request.id });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type MessageKind = "command" | "context" | "message";
|
|
2
|
+
export type SubagentSummary = {
|
|
3
|
+
count: number;
|
|
4
|
+
};
|
|
5
|
+
export type AgentProvider = "claude" | "codex" | "gemini";
|
|
6
|
+
export type AgentSessionIndexRow = {
|
|
7
|
+
aiTitle?: string;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
preview: string;
|
|
10
|
+
previewKind: MessageKind;
|
|
11
|
+
previewRole: "assistant" | "user";
|
|
12
|
+
provider: AgentProvider;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
subagents?: SubagentSummary;
|
|
15
|
+
title: string;
|
|
16
|
+
titleKind: MessageKind;
|
|
17
|
+
transcriptPath: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
};
|
|
20
|
+
export type AgentSessionIndexPage = {
|
|
21
|
+
nextCursor: string | null;
|
|
22
|
+
rows: AgentSessionIndexRow[];
|
|
23
|
+
total: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function buildAgentSessionIndex(options?: {
|
|
26
|
+
cursor?: string | null;
|
|
27
|
+
home?: string;
|
|
28
|
+
limit?: number;
|
|
29
|
+
nowMs?: number;
|
|
30
|
+
query?: string;
|
|
31
|
+
}): Promise<AgentSessionIndexPage>;
|
|
32
|
+
export declare function warmAgentSessionIndex(options?: {
|
|
33
|
+
home?: string;
|
|
34
|
+
}): Promise<void>;
|