agent-relay-orchestrator 0.62.2 → 0.63.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/package.json +2 -2
- package/src/spawn/command.ts +105 -0
- package/src/spawn/constants.ts +13 -0
- package/src/spawn/guests.ts +241 -0
- package/src/spawn/index.ts +10 -0
- package/src/spawn/log-utils.ts +22 -0
- package/src/spawn/runtime.ts +151 -0
- package/src/spawn/sessions.ts +103 -0
- package/src/spawn/spawn-agent.ts +94 -0
- package/src/spawn/supervisor.ts +347 -0
- package/src/spawn/systemd.ts +17 -0
- package/src/spawn/terminal.ts +312 -0
- package/src/spawn/types.ts +132 -0
- package/src/spawn.ts +1 -1477
- package/src/workspace-probe/cleanup.ts +79 -0
- package/src/workspace-probe/deps.ts +307 -0
- package/src/workspace-probe/git-state.ts +202 -0
- package/src/workspace-probe/index.ts +8 -0
- package/src/workspace-probe/merge.ts +494 -0
- package/src/workspace-probe/names.ts +77 -0
- package/src/workspace-probe/parse.ts +30 -0
- package/src/workspace-probe/probe.ts +101 -0
- package/src/workspace-probe/types.ts +50 -0
- package/src/workspace-probe.ts +1 -1308
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import type { OrchestratorConfig } from "../config";
|
|
4
|
+
import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
|
|
5
|
+
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
6
|
+
import { LOG_DIR } from "./constants";
|
|
7
|
+
import { logLines } from "./log-utils";
|
|
8
|
+
import { findSessionRecord, isSessionRecordAlive, loadState, logFilePath, readRunnerInfo } from "./runtime";
|
|
9
|
+
import { isSessionAlive } from "./sessions";
|
|
10
|
+
import type { TerminalInputResult, TerminalInputToken, TerminalSnapshot } from "./types";
|
|
11
|
+
|
|
12
|
+
export function captureSession(
|
|
13
|
+
name: string,
|
|
14
|
+
config: OrchestratorConfig,
|
|
15
|
+
lines = 100,
|
|
16
|
+
options: { raw?: boolean } = {},
|
|
17
|
+
): { session: string; lines: string[]; running: boolean } {
|
|
18
|
+
if (!name.startsWith(`${config.tmuxPrefix}-`)) throw new Error("session is not managed by this orchestrator");
|
|
19
|
+
|
|
20
|
+
const records = loadState();
|
|
21
|
+
const record = records.find((r) => r.name === name);
|
|
22
|
+
const logFile = record?.logFile ?? logFilePath(name);
|
|
23
|
+
const running = record ? isSessionRecordAlive(record) : false;
|
|
24
|
+
|
|
25
|
+
let content: string;
|
|
26
|
+
try {
|
|
27
|
+
content = readFileSync(logFile, "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
return { session: name, lines: [], running };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const allLines = logLines(content, !options.raw);
|
|
33
|
+
const safeLines = Math.min(Math.max(lines, 1), 1000);
|
|
34
|
+
return {
|
|
35
|
+
session: name,
|
|
36
|
+
lines: allLines.slice(-safeLines),
|
|
37
|
+
running,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Shared with the runner's logger via the SDK helper, so reader + writer
|
|
42
|
+
// resolve the same session-mirror filename for a given agent id.
|
|
43
|
+
function safeMirrorLogName(value: string): string {
|
|
44
|
+
return sanitizeFsName(value, { replacement: "_", maxLen: 180 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read the clean, ANSI-free session-mirror diagnostics log for a managed agent.
|
|
48
|
+
// Accepts either the tmux session name or the agent id; the mirror log is keyed by
|
|
49
|
+
// agent id. Returns the same shape as captureSession so the proxy is uniform.
|
|
50
|
+
export function captureSessionMirror(
|
|
51
|
+
name: string,
|
|
52
|
+
_config: OrchestratorConfig,
|
|
53
|
+
lines = 200,
|
|
54
|
+
): { session: string; lines: string[]; running: boolean; mirror: true } {
|
|
55
|
+
const records = loadState();
|
|
56
|
+
const record = records.find((r) => r.name === name) ?? records.find((r) => r.agentId === name);
|
|
57
|
+
const agentId = record?.agentId ?? name;
|
|
58
|
+
const running = record ? isSessionRecordAlive(record) : false;
|
|
59
|
+
// The mirror log lives in the same directory as the provider log (both written
|
|
60
|
+
// by the same user on this host). Derive from the record's logFile when known so
|
|
61
|
+
// it tracks any per-session log relocation.
|
|
62
|
+
const logDir = record?.logFile ? dirname(record.logFile) : process.env.AGENT_RELAY_LOG_DIR || LOG_DIR;
|
|
63
|
+
const mirrorPath = join(logDir, `session-mirror-${safeMirrorLogName(agentId)}.log`);
|
|
64
|
+
let content: string;
|
|
65
|
+
try {
|
|
66
|
+
content = readFileSync(mirrorPath, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return { session: name, lines: [], running, mirror: true };
|
|
69
|
+
}
|
|
70
|
+
const allLines = content.split(/\r?\n/).filter(Boolean);
|
|
71
|
+
const safeLines = Math.min(Math.max(lines, 1), 2000);
|
|
72
|
+
return { session: name, lines: allLines.slice(-safeLines), running, mirror: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function captureTerminal(name: string, config: OrchestratorConfig): TerminalSnapshot {
|
|
76
|
+
if (!name.startsWith(`${config.tmuxPrefix}-`)) throw new Error("session is not managed by this orchestrator");
|
|
77
|
+
|
|
78
|
+
const agentAlive = isSessionAlive(name);
|
|
79
|
+
const socketName = tmuxSocketForSession(name);
|
|
80
|
+
const running = tmuxHasSession(name, socketName);
|
|
81
|
+
if (!running) {
|
|
82
|
+
return { session: name, content: "", running: false, agentAlive, capturedAt: Date.now() };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const size = tmuxPaneSize(name, socketName);
|
|
86
|
+
const { content, cursor } = captureConsistent(
|
|
87
|
+
() => captureContent(name, socketName),
|
|
88
|
+
() => tmuxCursorPos(name, socketName),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
session: name,
|
|
93
|
+
content,
|
|
94
|
+
running: true,
|
|
95
|
+
agentAlive,
|
|
96
|
+
...size,
|
|
97
|
+
...cursor,
|
|
98
|
+
capturedAt: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Capture cursor and content *consistently*. They come from separate tmux invocations,
|
|
103
|
+
// so if the pane scrolls between them (e.g. tool output streaming while the agent
|
|
104
|
+
// "thinks"), cursorY ends up off-by-one against the captured grid — the parked cursor
|
|
105
|
+
// then lands a row off and the TUI's next relative redraw stacks a stale statusline row
|
|
106
|
+
// (bottom-box ghost). Read content, then cursor, then content again; accept only when the
|
|
107
|
+
// two content reads bracket the cursor read unchanged, which proves the cursor reflects
|
|
108
|
+
// that exact grid. Fall through with the latest capture if the pane never holds still.
|
|
109
|
+
export function captureConsistent(
|
|
110
|
+
readContent: () => string,
|
|
111
|
+
readCursor: () => { cursorX?: number; cursorY?: number },
|
|
112
|
+
maxAttempts = 4,
|
|
113
|
+
): { content: string; cursor: { cursorX?: number; cursorY?: number } } {
|
|
114
|
+
let content = readContent();
|
|
115
|
+
let cursor = readCursor();
|
|
116
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
117
|
+
const recheck = readContent();
|
|
118
|
+
if (recheck === content) break;
|
|
119
|
+
content = recheck;
|
|
120
|
+
cursor = readCursor();
|
|
121
|
+
}
|
|
122
|
+
return { content, cursor };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function captureContent(name: string, socketName?: string): string {
|
|
126
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "capture-pane", "-p", "-e", "-S", "-1000", "-t", name), {
|
|
127
|
+
stdin: "ignore",
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "pipe",
|
|
130
|
+
});
|
|
131
|
+
if (result.exitCode !== 0) {
|
|
132
|
+
const stderr = result.stderr.toString().trim();
|
|
133
|
+
throw new Error(stderr || `tmux capture-pane failed with exit code ${result.exitCode}`);
|
|
134
|
+
}
|
|
135
|
+
return result.stdout.toString();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function terminalInputTokens(data: string): TerminalInputToken[] {
|
|
139
|
+
const tokens: TerminalInputToken[] = [];
|
|
140
|
+
let literal = "";
|
|
141
|
+
const flushLiteral = () => {
|
|
142
|
+
if (!literal) return;
|
|
143
|
+
tokens.push({ type: "literal", value: literal });
|
|
144
|
+
literal = "";
|
|
145
|
+
};
|
|
146
|
+
const escapeSequences: Array<[string, string]> = [
|
|
147
|
+
["\x1b[A", "Up"],
|
|
148
|
+
["\x1b[B", "Down"],
|
|
149
|
+
["\x1b[C", "Right"],
|
|
150
|
+
["\x1b[D", "Left"],
|
|
151
|
+
["\x1b[H", "Home"],
|
|
152
|
+
["\x1b[F", "End"],
|
|
153
|
+
["\x1b[3~", "Delete"],
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (let index = 0; index < data.length;) {
|
|
157
|
+
const match = escapeSequences.find(([sequence]) => data.startsWith(sequence, index));
|
|
158
|
+
if (match) {
|
|
159
|
+
flushLiteral();
|
|
160
|
+
tokens.push({ type: "key", value: match[1] });
|
|
161
|
+
index += match[0].length;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ch = data[index]!;
|
|
166
|
+
if (ch === "\r" || ch === "\n") {
|
|
167
|
+
flushLiteral();
|
|
168
|
+
tokens.push({ type: "key", value: "Enter" });
|
|
169
|
+
} else if (ch === "\t") {
|
|
170
|
+
flushLiteral();
|
|
171
|
+
tokens.push({ type: "key", value: "Tab" });
|
|
172
|
+
} else if (ch === "\u0003") {
|
|
173
|
+
flushLiteral();
|
|
174
|
+
tokens.push({ type: "key", value: "C-c" });
|
|
175
|
+
} else if (ch === "\u007f" || ch === "\b") {
|
|
176
|
+
flushLiteral();
|
|
177
|
+
tokens.push({ type: "key", value: "BSpace" });
|
|
178
|
+
} else if (ch === "\x1b") {
|
|
179
|
+
flushLiteral();
|
|
180
|
+
tokens.push({ type: "key", value: "Escape" });
|
|
181
|
+
} else if (ch >= " " || ch > "\x7f") {
|
|
182
|
+
literal += ch;
|
|
183
|
+
}
|
|
184
|
+
index += 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
flushLiteral();
|
|
188
|
+
return tokens;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validation contract shared by the HTTP terminal routes and the websocket terminal
|
|
192
|
+
// frames (orchestrator/src/api.ts). Both transports MUST enforce the same envelope —
|
|
193
|
+
// keep these the single source of truth (see #143). Pure: no tmux, safe to unit-test.
|
|
194
|
+
const TERMINAL_INPUT_MAX = 4096;
|
|
195
|
+
|
|
196
|
+
export function validateTerminalInputData(input: unknown): string {
|
|
197
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("terminal input body must be an object");
|
|
198
|
+
const data = (input as { data?: unknown }).data;
|
|
199
|
+
if (typeof data !== "string") throw new Error("terminal input data must be a string");
|
|
200
|
+
if (data.length > TERMINAL_INPUT_MAX) throw new Error(`terminal input exceeds ${TERMINAL_INPUT_MAX} characters`);
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function validateTerminalResize(input: unknown): { cols: number; rows: number } {
|
|
205
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("resize body must be an object");
|
|
206
|
+
const cols = (input as { cols?: unknown }).cols;
|
|
207
|
+
const rows = (input as { rows?: unknown }).rows;
|
|
208
|
+
// typeof narrows for the bounds comparison; Number.isFinite additionally rejects
|
|
209
|
+
// NaN/Infinity — without it NaN slips past the bounds check below (every NaN
|
|
210
|
+
// comparison is false), the exact malformed-resize frame the websocket path used to
|
|
211
|
+
// forward via Number(frame.cols).
|
|
212
|
+
if (typeof cols !== "number" || typeof rows !== "number" || !Number.isFinite(cols) || !Number.isFinite(rows)) {
|
|
213
|
+
throw new Error("cols and rows must be numbers");
|
|
214
|
+
}
|
|
215
|
+
if (cols < 10 || cols > 500 || rows < 5 || rows > 200) throw new Error("cols must be 10-500, rows must be 5-200");
|
|
216
|
+
return { cols: Math.round(cols), rows: Math.round(rows) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function sendTerminalInput(name: string, config: OrchestratorConfig, input: unknown): TerminalInputResult {
|
|
220
|
+
if (!name.startsWith(`${config.tmuxPrefix}-`)) throw new Error("session is not managed by this orchestrator");
|
|
221
|
+
const socketName = tmuxSocketForSession(name);
|
|
222
|
+
if (!tmuxHasSession(name, socketName)) throw new Error("terminal session is not running");
|
|
223
|
+
const data = validateTerminalInputData(input);
|
|
224
|
+
|
|
225
|
+
const tokens = terminalInputTokens(data);
|
|
226
|
+
for (const token of tokens) {
|
|
227
|
+
const args = token.type === "literal"
|
|
228
|
+
? tmuxCommand(socketName, "send-keys", "-t", name, "-l", token.value)
|
|
229
|
+
: tmuxCommand(socketName, "send-keys", "-t", name, token.value);
|
|
230
|
+
const result = Bun.spawnSync(args, {
|
|
231
|
+
stdin: "ignore",
|
|
232
|
+
stdout: "pipe",
|
|
233
|
+
stderr: "pipe",
|
|
234
|
+
});
|
|
235
|
+
if (result.exitCode !== 0) {
|
|
236
|
+
const stderr = result.stderr.toString().trim();
|
|
237
|
+
throw new Error(stderr || `tmux send-keys failed with exit code ${result.exitCode}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
session: name,
|
|
243
|
+
running: true,
|
|
244
|
+
sent: tokens.length,
|
|
245
|
+
capturedAt: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resizeTerminal(name: string, config: OrchestratorConfig, input: unknown): { session: string; cols: number; rows: number } {
|
|
250
|
+
if (!name.startsWith(`${config.tmuxPrefix}-`)) throw new Error("session is not managed by this orchestrator");
|
|
251
|
+
const socketName = tmuxSocketForSession(name);
|
|
252
|
+
if (!tmuxHasSession(name, socketName)) throw new Error("terminal session is not running");
|
|
253
|
+
const clamped = validateTerminalResize(input);
|
|
254
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "resize-window", "-t", name, "-x", String(clamped.cols), "-y", String(clamped.rows)), {
|
|
255
|
+
stdin: "ignore",
|
|
256
|
+
stdout: "pipe",
|
|
257
|
+
stderr: "pipe",
|
|
258
|
+
});
|
|
259
|
+
if (result.exitCode !== 0) {
|
|
260
|
+
const stderr = result.stderr.toString().trim();
|
|
261
|
+
throw new Error(stderr || `tmux resize-window failed with exit code ${result.exitCode}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { session: name, ...clamped };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function tmuxSocketForSession(name: string): string | undefined {
|
|
268
|
+
const record = loadState().find((item) => item.name === name);
|
|
269
|
+
return record ? readRunnerInfo(record)?.tmuxSocket : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Shared tmux helpers; tmuxCommand re-exported for ./terminal-stream.
|
|
273
|
+
export { tmuxCommand };
|
|
274
|
+
|
|
275
|
+
// Lightweight liveness for the live terminal stream's backfill metadata — avoids a full
|
|
276
|
+
// capture-pane just to learn whether the pane/agent are still up.
|
|
277
|
+
export function sessionLiveness(name: string): { running: boolean; agentAlive: boolean } {
|
|
278
|
+
const socketName = tmuxSocketForSession(name);
|
|
279
|
+
return { running: tmuxHasSession(name, socketName), agentAlive: isSessionAlive(name) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function tmuxPaneSize(name: string, socketName?: string): { cols?: number; rows?: number } {
|
|
283
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "display-message", "-p", "-t", name, "#{pane_width} #{pane_height}"), {
|
|
284
|
+
stdin: "ignore",
|
|
285
|
+
stdout: "pipe",
|
|
286
|
+
stderr: "ignore",
|
|
287
|
+
});
|
|
288
|
+
if (result.exitCode !== 0) return {};
|
|
289
|
+
const [colsRaw, rowsRaw] = result.stdout.toString().trim().split(/\s+/, 2);
|
|
290
|
+
const cols = Number(colsRaw);
|
|
291
|
+
const rows = Number(rowsRaw);
|
|
292
|
+
return {
|
|
293
|
+
...(Number.isFinite(cols) && cols > 0 ? { cols } : {}),
|
|
294
|
+
...(Number.isFinite(rows) && rows > 0 ? { rows } : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function tmuxCursorPos(name: string, socketName?: string): { cursorX?: number; cursorY?: number } {
|
|
299
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "display-message", "-p", "-t", name, "#{cursor_x} #{cursor_y}"), {
|
|
300
|
+
stdin: "ignore",
|
|
301
|
+
stdout: "pipe",
|
|
302
|
+
stderr: "ignore",
|
|
303
|
+
});
|
|
304
|
+
if (result.exitCode !== 0) return {};
|
|
305
|
+
const [xRaw, yRaw] = result.stdout.toString().trim().split(/\s+/, 2);
|
|
306
|
+
const cursorX = Number(xRaw);
|
|
307
|
+
const cursorY = Number(yRaw);
|
|
308
|
+
return {
|
|
309
|
+
...(Number.isFinite(cursorX) ? { cursorX } : {}),
|
|
310
|
+
...(Number.isFinite(cursorY) ? { cursorY } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { OrchestratorConfig } from "../config";
|
|
2
|
+
import type { AgentLifecycle, WorkspaceMetadata, WorkspaceMode } from "agent-relay-sdk";
|
|
3
|
+
|
|
4
|
+
export interface SpawnOptions {
|
|
5
|
+
provider: "claude" | "codex";
|
|
6
|
+
cwd: string;
|
|
7
|
+
rig?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
effort?: string;
|
|
10
|
+
profile?: string; workspaceMode?: WorkspaceMode; lifecycle?: AgentLifecycle;
|
|
11
|
+
workspace?: WorkspaceMetadata;
|
|
12
|
+
/** Untracked files/dirs to symlink from main into an isolated worktree (relay's global workspace config). */
|
|
13
|
+
workspaceSymlinks?: string[];
|
|
14
|
+
agentProfile?: Record<string, unknown>;
|
|
15
|
+
label?: string;
|
|
16
|
+
agentId?: string;
|
|
17
|
+
approvalMode: string;
|
|
18
|
+
prompt?: string;
|
|
19
|
+
systemPromptAppend?: string;
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
capabilities?: string[];
|
|
23
|
+
providerArgs?: string[];
|
|
24
|
+
policyName?: string;
|
|
25
|
+
spawnRequestId?: string;
|
|
26
|
+
automationId?: string;
|
|
27
|
+
automationRunId?: string;
|
|
28
|
+
/** How the spawn was requested (`mcp` = an agent via the MCP surface, else dashboard/CLI). Drives
|
|
29
|
+
* the origin tag so an MCP-spawned worker isn't mislabeled `dashboard-spawned` (#330). */
|
|
30
|
+
requestedVia?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SessionInfo {
|
|
34
|
+
name: string;
|
|
35
|
+
sessionName: string;
|
|
36
|
+
pid: number;
|
|
37
|
+
alive: boolean;
|
|
38
|
+
supervisor: SessionSupervisor["type"];
|
|
39
|
+
systemdUnit?: string;
|
|
40
|
+
terminalSession?: string;
|
|
41
|
+
terminalAvailable: boolean;
|
|
42
|
+
logFile: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TerminalGuestSession {
|
|
46
|
+
session: string;
|
|
47
|
+
mode: "guest";
|
|
48
|
+
provider: string;
|
|
49
|
+
running: boolean;
|
|
50
|
+
interactive: boolean;
|
|
51
|
+
expiresAt: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TerminalSnapshot {
|
|
55
|
+
session: string;
|
|
56
|
+
content: string;
|
|
57
|
+
running: boolean;
|
|
58
|
+
// `running` only means the tmux pane still exists — tmux keeps a pane after the
|
|
59
|
+
// process inside it exits. `agentAlive` is the real liveness of the agent process
|
|
60
|
+
// (pid / systemd unit) so the dashboard can flag an orphaned, stale terminal.
|
|
61
|
+
agentAlive: boolean;
|
|
62
|
+
cols?: number;
|
|
63
|
+
rows?: number;
|
|
64
|
+
cursorX?: number;
|
|
65
|
+
cursorY?: number;
|
|
66
|
+
capturedAt: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type TerminalInputToken =
|
|
70
|
+
| { type: "literal"; value: string }
|
|
71
|
+
| { type: "key"; value: string };
|
|
72
|
+
|
|
73
|
+
export interface TerminalInputResult {
|
|
74
|
+
session: string;
|
|
75
|
+
running: boolean;
|
|
76
|
+
sent: number;
|
|
77
|
+
capturedAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SessionRecord {
|
|
81
|
+
name: string;
|
|
82
|
+
pid: number;
|
|
83
|
+
supervisor?: SessionSupervisor;
|
|
84
|
+
provider: string;
|
|
85
|
+
model?: string;
|
|
86
|
+
effort?: string;
|
|
87
|
+
profile?: string; workspaceMode?: WorkspaceMode; lifecycle?: AgentLifecycle;
|
|
88
|
+
workspace?: WorkspaceMetadata;
|
|
89
|
+
label?: string;
|
|
90
|
+
cwd: string;
|
|
91
|
+
logFile: string;
|
|
92
|
+
runnerInfoFile?: string;
|
|
93
|
+
agentId: string;
|
|
94
|
+
approvalMode: string;
|
|
95
|
+
policyName?: string;
|
|
96
|
+
spawnRequestId?: string;
|
|
97
|
+
automationId?: string;
|
|
98
|
+
automationRunId?: string;
|
|
99
|
+
startedAt: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SessionSupervisor {
|
|
103
|
+
type: "process" | "systemd";
|
|
104
|
+
unit?: string;
|
|
105
|
+
launchScript?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SpawnedRunner {
|
|
109
|
+
pid: number;
|
|
110
|
+
supervisor: SessionSupervisor;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RunnerInfo {
|
|
114
|
+
agentId: string;
|
|
115
|
+
runnerId: string;
|
|
116
|
+
provider: string;
|
|
117
|
+
controlUrl: string;
|
|
118
|
+
tmuxSession?: string;
|
|
119
|
+
tmuxSocket?: string;
|
|
120
|
+
pid?: number;
|
|
121
|
+
startedAt?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface TerminalAttachSpec {
|
|
125
|
+
mode: "guest";
|
|
126
|
+
provider: string;
|
|
127
|
+
cwd: string;
|
|
128
|
+
command: string[];
|
|
129
|
+
env?: Record<string, string>;
|
|
130
|
+
title?: string;
|
|
131
|
+
ttlMs?: number;
|
|
132
|
+
}
|