@zgeoff/atc 0.1.11 → 1.0.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/README.md +54 -15
- package/package.json +7 -2
- package/src/{agent-adapter.ts → agents/agent-adapter.ts} +31 -14
- package/src/{build-cli-command.ts → agents/build-cli-command.ts} +1 -1
- package/src/agents/build-hook-settings.ts +59 -0
- package/src/{claude-adapter.ts → agents/claude-adapter.ts} +41 -80
- package/src/{codex-adapter.ts → agents/codex-adapter.ts} +41 -30
- package/src/agents/gateway-adapter.ts +106 -0
- package/src/{grok-adapter.ts → agents/grok-adapter.ts} +83 -46
- package/src/agents/resolve-agent-home.ts +13 -0
- package/src/agents/truncate-detail.ts +7 -0
- package/src/agents/write-hook-settings.ts +37 -0
- package/src/cli.ts +25 -14
- package/src/{boot-daemon.ts → client/boot-daemon.ts} +8 -7
- package/src/client/build-client-machine.ts +119 -0
- package/src/client/collect-agent-picks.ts +31 -0
- package/src/{daemon-client.ts → client/daemon-client.ts} +4 -4
- package/src/{dirs.ts → client/dirs.ts} +8 -0
- package/src/client/format-overlay-agent-mark.ts +15 -0
- package/src/{index.ts → client/index.ts} +181 -550
- package/src/client/keys.ts +111 -0
- package/src/client/parse-daemon-event.ts +114 -0
- package/src/{pick-tab-target.ts → client/pick-tab-target.ts} +2 -2
- package/src/client/spawn-picker.ts +305 -0
- package/src/client/to-mirror-session.ts +65 -0
- package/src/{ui.ts → client/ui.ts} +13 -8
- package/src/{attach-registry.ts → daemon/attach-registry.ts} +12 -10
- package/src/daemon/build-session-event.ts +43 -0
- package/src/{daemon-connection.ts → daemon/daemon-connection.ts} +194 -108
- package/src/{daemon.ts → daemon/daemon.ts} +142 -301
- package/src/{hooks.ts → daemon/hooks.ts} +6 -4
- package/src/daemon/mint-session-id.ts +12 -0
- package/src/{permission-registry.ts → daemon/permission-registry.ts} +5 -3
- package/src/daemon/restore-fleet.ts +141 -0
- package/src/daemon/run-eject-handoff.ts +33 -0
- package/src/{screen-model.ts → daemon/screen-model.ts} +1 -1
- package/src/daemon/session-runtime.ts +64 -0
- package/src/{sessions.ts → daemon/sessions.ts} +98 -144
- package/src/{start-headless-run.ts → daemon/start-headless-run.ts} +9 -3
- package/src/daemon/start-headless-turn.ts +57 -0
- package/src/hook-report.ts +2 -2
- package/src/mcp-server.ts +33 -44
- package/src/protocol/parse-request-params.ts +31 -0
- package/src/{protocol.ts → protocol/protocol.ts} +1 -1
- package/src/protocol/request-param-schemas.ts +109 -0
- package/src/shared/agent-session-id.ts +8 -0
- package/src/shared/build-optional-boolean.ts +10 -0
- package/src/shared/build-optional-string-array.ts +14 -0
- package/src/shared/build-optional-string.ts +10 -0
- package/src/shared/collect-gateways.ts +113 -0
- package/src/{config.ts → shared/config.ts} +47 -31
- package/src/{get-build.ts → shared/get-build.ts} +21 -6
- package/src/shared/get-record.ts +20 -0
- package/src/shared/session-id.ts +8 -0
- package/src/shared/to-agent-session-id.ts +12 -0
- package/src/shared/to-session-id.ts +11 -0
- package/src/shared/to-shell-arg.ts +7 -0
- package/src/statusline.ts +2 -2
- package/src/store/bun-sqlite-driver.ts +96 -0
- package/src/store/fleet-entry.ts +72 -0
- package/src/store/run-migrations.ts +242 -0
- package/src/store/state-store.ts +226 -0
- package/src/format-overlay-agent-mark.ts +0 -17
- package/src/state-store.ts +0 -227
- /package/src/{normalize-hook-event.ts → agents/normalize-hook-event.ts} +0 -0
- /package/src/{print-codex-hook-file.ts → agents/print-codex-hook-file.ts} +0 -0
- /package/src/{print-grok-hook-file.ts → agents/print-grok-hook-file.ts} +0 -0
- /package/src/{build-leader-chords.ts → client/build-leader-chords.ts} +0 -0
- /package/src/{daemon-error.ts → protocol/daemon-error.ts} +0 -0
- /package/src/{outbound-queue.ts → protocol/outbound-queue.ts} +0 -0
- /package/src/{collect-clean-env.ts → shared/collect-clean-env.ts} +0 -0
- /package/src/{report.ts → shared/report.ts} +0 -0
- /package/src/{reset-input-modes.ts → shared/reset-input-modes.ts} +0 -0
- /package/src/{resolve-repo-root.ts → shared/resolve-repo-root.ts} +0 -0
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import type { AgentAdapter
|
|
2
|
+
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
3
|
+
import { MAX_CHUNK, PROTOCOL_V } from '../protocol/protocol';
|
|
4
|
+
import type { EventMsg } from '../protocol/protocol';
|
|
5
|
+
import type { SessionID } from '../shared/session-id';
|
|
6
|
+
import { StateStore } from '../store/state-store';
|
|
3
7
|
import { AttachRegistry } from './attach-registry';
|
|
4
|
-
import
|
|
8
|
+
import { buildSessionEvent } from './build-session-event';
|
|
5
9
|
import { DaemonConnection } from './daemon-connection';
|
|
6
10
|
import type { DaemonContext, OutputClient } from './daemon-connection';
|
|
7
11
|
import { startHookServer } from './hooks';
|
|
8
12
|
import { PermissionRegistry } from './permission-registry';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
13
|
+
import { restoreFleet } from './restore-fleet';
|
|
14
|
+
import { runEjectHandoff } from './run-eject-handoff';
|
|
11
15
|
import { ScreenModel } from './screen-model';
|
|
16
|
+
import { SessionRuntime } from './session-runtime';
|
|
12
17
|
import { SessionManager } from './sessions';
|
|
13
|
-
import type {
|
|
14
|
-
import {
|
|
18
|
+
import type { SessionDescriptor, SessionState } from './sessions';
|
|
19
|
+
import { startHeadlessTurn } from './start-headless-turn';
|
|
15
20
|
|
|
16
21
|
export interface DaemonOptions {
|
|
17
22
|
readonly socketPath: string;
|
|
@@ -21,9 +26,10 @@ export interface DaemonOptions {
|
|
|
21
26
|
readonly build: string;
|
|
22
27
|
readonly adapter: AgentAdapter;
|
|
23
28
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
29
|
+
// Adapters registered over and above the default one, each keyed by the id
|
|
30
|
+
// it declares. Lookup never falls back across ids: a grok session with no
|
|
31
|
+
// grok adapter is unsupported, not a Claude spawn.
|
|
32
|
+
readonly adapters?: readonly AgentAdapter[];
|
|
27
33
|
|
|
28
34
|
// SQLite path for daemon state; a fleet.json at legacyFleetPath seeds the
|
|
29
35
|
// fleet table once so upgrading keeps the restorable fleet.
|
|
@@ -56,7 +62,7 @@ export interface DaemonOptions {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export interface DaemonHandle {
|
|
59
|
-
readonly stop: () => void
|
|
65
|
+
readonly stop: () => Promise<void>;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/**
|
|
@@ -68,15 +74,16 @@ export interface DaemonHandle {
|
|
|
68
74
|
* because transports guarantee byte integrity and such a line means a buggy
|
|
69
75
|
* or hostile peer.
|
|
70
76
|
*/
|
|
71
|
-
export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
72
|
-
let stopDaemon: (() => void) | null = null;
|
|
77
|
+
export async function startDaemon(opts: DaemonOptions): Promise<DaemonHandle> {
|
|
78
|
+
let stopDaemon: (() => Promise<void>) | null = null;
|
|
73
79
|
|
|
74
80
|
if (opts.pidPath !== undefined) {
|
|
75
81
|
writeFileSync(opts.pidPath, String(process.pid));
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
const store =
|
|
79
|
-
|
|
84
|
+
const store = await StateStore.open(opts.dbPath, opts.legacyFleetPath);
|
|
85
|
+
|
|
86
|
+
const mgr = new SessionManager(opts.adapter, store, opts.statusPath, opts.adapters ?? []);
|
|
80
87
|
const clients = new Set<DaemonConnection>();
|
|
81
88
|
|
|
82
89
|
const emitEvent = (event: EventMsg) => {
|
|
@@ -102,130 +109,52 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
102
109
|
emitEvent({ v: PROTOCOL_V, ev: 'permission.resolved', request: id, decision });
|
|
103
110
|
};
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
// A staggered fleet restore parks a resolver here while it waits for the
|
|
113
|
-
// session it just revived to report it has booted; the reporter fires it on
|
|
114
|
-
// SessionStart, and a dying revive fires it too so a failed resume does not
|
|
115
|
-
// hold up the rest. Resolving a settled promise again is a no-op.
|
|
116
|
-
const bootWaiters = new Map<string, () => void>();
|
|
117
|
-
|
|
118
|
-
// Blocks until the session reports SessionStart, it dies, or the cap
|
|
119
|
-
// elapses (a positive cap only; zero waits on the signal alone).
|
|
120
|
-
const waitForBoot = (sessionID: string, capMs: number): Promise<void> => {
|
|
121
|
-
const settled = Promise.withResolvers<void>();
|
|
122
|
-
const timer = capMs > 0 ? setTimeout(settled.resolve, capMs) : undefined;
|
|
123
|
-
|
|
124
|
-
bootWaiters.set(sessionID, settled.resolve);
|
|
125
|
-
|
|
126
|
-
if (timer !== undefined) {
|
|
127
|
-
restoreTimers.add(timer);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return (async () => {
|
|
131
|
-
await settled.promise;
|
|
132
|
-
|
|
133
|
-
bootWaiters.delete(sessionID);
|
|
134
|
-
|
|
135
|
-
if (timer !== undefined) {
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
|
|
138
|
-
restoreTimers.delete(timer);
|
|
139
|
-
}
|
|
140
|
-
})();
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const startHeadlessTurn = (sessionID: string, prompt: string): boolean => {
|
|
144
|
-
const s = mgr.sessions.find((x) => x.id === sessionID);
|
|
145
|
-
const runner = s === undefined ? null : (mgr.findAdapter(s.agent)?.headlessRunner ?? null);
|
|
146
|
-
|
|
147
|
-
if (s === undefined || runner === null || headlessRuns.has(sessionID)) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
112
|
+
// One runtime per live session: its screen model, output sequence
|
|
113
|
+
// counter, PTY dims, its resize/detect/boot timers, its live headless
|
|
114
|
+
// run, and its pending eject and boot waiters. Created when the session
|
|
115
|
+
// manager announces the session and disposed the moment it is removed, so
|
|
116
|
+
// removal is always a single lookup plus dispose.
|
|
117
|
+
const runtimes = new Map<SessionID, SessionRuntime>();
|
|
150
118
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const handle = runner(
|
|
154
|
-
{
|
|
155
|
-
cwd: s.cwd,
|
|
156
|
-
prompt,
|
|
157
|
-
...(s.agentSessionID === undefined ? {} : { resume: s.agentSessionID }),
|
|
158
|
-
permissionMode: 'auto',
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
onOutput: (text) => {
|
|
162
|
-
mgr.onOutput(s, text);
|
|
163
|
-
},
|
|
164
|
-
onDone: (summary) => {
|
|
165
|
-
headlessRuns.delete(sessionID);
|
|
166
|
-
|
|
167
|
-
const doneMsg = summary === '' ? 'headless turn done' : summary;
|
|
168
|
-
|
|
169
|
-
mgr.updateSurfaceState(sessionID, 'done', doneMsg);
|
|
170
|
-
},
|
|
171
|
-
onNeedsYou: (msg) => {
|
|
172
|
-
headlessRuns.delete(sessionID);
|
|
173
|
-
mgr.updateSurfaceState(sessionID, 'needs_you', msg);
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
headlessRuns.set(sessionID, handle);
|
|
179
|
-
|
|
180
|
-
return true;
|
|
181
|
-
};
|
|
119
|
+
const findRuntime = (sessionID: SessionID) => runtimes.get(sessionID);
|
|
182
120
|
|
|
183
121
|
const attachments = new AttachRegistry<OutputClient>();
|
|
184
|
-
const seqs = new Map<string, number>();
|
|
185
|
-
const ptyDims = new Map<string, Dims>();
|
|
186
|
-
const screens = new Map<string, ScreenModel>();
|
|
187
|
-
const resizeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
188
|
-
const detectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
189
|
-
const restoreTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
190
|
-
const pendingLastUsed = new Set<string>();
|
|
191
122
|
|
|
192
123
|
// The screen tier of the detector stack: once a session's output has
|
|
193
124
|
// quiesced, judge the serialized screen and flip running/needs_you.
|
|
194
|
-
const scheduleDetect = (sessionID:
|
|
125
|
+
const scheduleDetect = (sessionID: SessionID) => {
|
|
126
|
+
if (!mgr.hasScreenDetector) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
195
130
|
const s = mgr.sessions.find((x) => x.id === sessionID);
|
|
196
131
|
const detector = s === undefined ? null : (mgr.findAdapter(s.agent)?.screenDetector ?? null);
|
|
132
|
+
const runtime = runtimes.get(sessionID);
|
|
197
133
|
|
|
198
|
-
if (detector === null) {
|
|
134
|
+
if (detector === null || runtime === undefined) {
|
|
199
135
|
return;
|
|
200
136
|
}
|
|
201
137
|
|
|
202
|
-
|
|
138
|
+
clearTimeout(runtime.detectTimer);
|
|
203
139
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
detectTimers.set(
|
|
209
|
-
sessionID,
|
|
210
|
-
setTimeout(() => {
|
|
211
|
-
detectTimers.delete(sessionID);
|
|
212
|
-
void applyScreenJudgment(sessionID);
|
|
213
|
-
}, 300),
|
|
214
|
-
);
|
|
140
|
+
runtime.detectTimer = setTimeout(() => {
|
|
141
|
+
runtime.detectTimer = undefined;
|
|
142
|
+
void applyScreenJudgment(sessionID);
|
|
143
|
+
}, 300);
|
|
215
144
|
};
|
|
216
145
|
|
|
217
146
|
// Replay is the serialized screen sent as ordinary output events: the
|
|
218
147
|
// client cannot tell replay from live and does not need to. A clear leads
|
|
219
148
|
// so a stale or desynced client screen resets first.
|
|
220
|
-
const sendReplay = async (sessionID:
|
|
221
|
-
const
|
|
149
|
+
const sendReplay = async (sessionID: SessionID, client: OutputClient) => {
|
|
150
|
+
const runtime = runtimes.get(sessionID);
|
|
222
151
|
|
|
223
|
-
if (
|
|
152
|
+
if (runtime === undefined || runtime.screen === null) {
|
|
224
153
|
return;
|
|
225
154
|
}
|
|
226
155
|
|
|
227
|
-
const replay = `\u001B[2J\u001B[H${await
|
|
228
|
-
const seq =
|
|
156
|
+
const replay = `\u001B[2J\u001B[H${await runtime.screen.renderReplay()}`;
|
|
157
|
+
const seq = runtime.seq;
|
|
229
158
|
|
|
230
159
|
for (let i = 0; i < replay.length; i += MAX_CHUNK) {
|
|
231
160
|
const chunk = replay.slice(i, i + MAX_CHUNK);
|
|
@@ -238,24 +167,28 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
238
167
|
}
|
|
239
168
|
};
|
|
240
169
|
|
|
241
|
-
const applyEffectiveDims = (sessionID:
|
|
170
|
+
const applyEffectiveDims = (sessionID: SessionID) => {
|
|
242
171
|
const dims = attachments.findEffectiveDims(sessionID);
|
|
243
172
|
|
|
244
173
|
if (dims === null) {
|
|
245
174
|
return;
|
|
246
175
|
}
|
|
247
176
|
|
|
248
|
-
const
|
|
177
|
+
const runtime = runtimes.get(sessionID);
|
|
178
|
+
const prev = runtime?.dims ?? null;
|
|
249
179
|
|
|
250
|
-
if (prev !==
|
|
180
|
+
if (prev !== null && prev.cols === dims.cols && prev.rows === dims.rows) {
|
|
251
181
|
return;
|
|
252
182
|
}
|
|
253
183
|
|
|
254
184
|
const s = mgr.sessions.find((x) => x.id === sessionID);
|
|
255
185
|
|
|
256
186
|
s?.pty?.resize(dims.cols, dims.rows);
|
|
257
|
-
|
|
258
|
-
|
|
187
|
+
runtime?.screen?.updateDims(dims.cols, dims.rows);
|
|
188
|
+
|
|
189
|
+
if (runtime !== undefined) {
|
|
190
|
+
runtime.dims = dims;
|
|
191
|
+
}
|
|
259
192
|
|
|
260
193
|
emitEvent({
|
|
261
194
|
v: PROTOCOL_V,
|
|
@@ -268,27 +201,26 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
268
201
|
|
|
269
202
|
// Debounced so two clients resizing in opposite directions cannot produce
|
|
270
203
|
// a SIGWINCH storm; a no-op effective size never reaches the PTY.
|
|
271
|
-
const scheduleResize = (sessionID:
|
|
272
|
-
|
|
204
|
+
const scheduleResize = (sessionID: SessionID) => {
|
|
205
|
+
const runtime = runtimes.get(sessionID);
|
|
206
|
+
|
|
207
|
+
if (runtime === undefined || runtime.resizeTimer !== undefined) {
|
|
273
208
|
return;
|
|
274
209
|
}
|
|
275
210
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
setTimeout(() => {
|
|
279
|
-
resizeTimers.delete(sessionID);
|
|
211
|
+
runtime.resizeTimer = setTimeout(() => {
|
|
212
|
+
runtime.resizeTimer = undefined;
|
|
280
213
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
);
|
|
214
|
+
applyEffectiveDims(sessionID);
|
|
215
|
+
}, 50);
|
|
284
216
|
};
|
|
285
217
|
|
|
286
|
-
const applyScreenJudgment = async (sessionID:
|
|
218
|
+
const applyScreenJudgment = async (sessionID: SessionID) => {
|
|
287
219
|
const s = mgr.sessions.find((x) => x.id === sessionID);
|
|
288
220
|
const detector = s === undefined ? null : (mgr.findAdapter(s.agent)?.screenDetector ?? null);
|
|
289
|
-
const model =
|
|
221
|
+
const model = runtimes.get(sessionID)?.screen ?? null;
|
|
290
222
|
|
|
291
|
-
if (detector === null || model ===
|
|
223
|
+
if (detector === null || model === null) {
|
|
292
224
|
return;
|
|
293
225
|
}
|
|
294
226
|
|
|
@@ -306,9 +238,11 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
306
238
|
};
|
|
307
239
|
|
|
308
240
|
mgr.onOutput = (s, data) => {
|
|
241
|
+
const runtime = runtimes.get(s.id);
|
|
242
|
+
|
|
309
243
|
// The screen model consumes every byte continuously — background output
|
|
310
244
|
// is consumed, not discarded.
|
|
311
|
-
|
|
245
|
+
runtime?.screen?.record(data);
|
|
312
246
|
scheduleDetect(s.id);
|
|
313
247
|
|
|
314
248
|
const conns = attachments.collectClients(s.id);
|
|
@@ -317,7 +251,7 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
317
251
|
return;
|
|
318
252
|
}
|
|
319
253
|
|
|
320
|
-
let seq =
|
|
254
|
+
let seq = runtime?.seq ?? 0;
|
|
321
255
|
|
|
322
256
|
for (let i = 0; i < data.length; i += MAX_CHUNK) {
|
|
323
257
|
const chunk = data.slice(i, i + MAX_CHUNK);
|
|
@@ -331,13 +265,16 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
331
265
|
}
|
|
332
266
|
}
|
|
333
267
|
|
|
334
|
-
|
|
268
|
+
if (runtime !== undefined) {
|
|
269
|
+
runtime.seq = seq;
|
|
270
|
+
}
|
|
335
271
|
};
|
|
336
272
|
|
|
337
273
|
// Permission requests are synthesized from attention transitions: entering
|
|
338
274
|
// needs_you opens one, and leaving it (answered directly in the terminal,
|
|
339
|
-
// or the session dying) dismisses whatever is pending.
|
|
340
|
-
|
|
275
|
+
// or the session dying) dismisses whatever is pending. Each session's
|
|
276
|
+
// previous state is tracked purely to detect that transition.
|
|
277
|
+
const lastStates = new Map<SessionID, SessionState>();
|
|
341
278
|
|
|
342
279
|
const recordAttention: SessionManager['onEvent'] = (kind, s) => {
|
|
343
280
|
if (kind === 'removed') {
|
|
@@ -363,76 +300,50 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
363
300
|
mgr.onEvent = (kind, s) => {
|
|
364
301
|
recordAttention(kind, s);
|
|
365
302
|
|
|
303
|
+
if (kind === 'added') {
|
|
304
|
+
runtimes.set(s.id, new SessionRuntime());
|
|
305
|
+
}
|
|
306
|
+
|
|
366
307
|
// A revive that dies before it ever announces itself must still release
|
|
367
308
|
// the staggered restore, or a failed resume would hold up the fleet.
|
|
368
|
-
if (kind === '
|
|
369
|
-
|
|
309
|
+
if (kind === 'state' && s.pty === null) {
|
|
310
|
+
runtimes.get(s.id)?.bootWaiter?.();
|
|
370
311
|
}
|
|
371
312
|
|
|
372
313
|
if (kind === 'removed') {
|
|
373
314
|
attachments.removeSession(s.id);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
screens.delete(s.id);
|
|
378
|
-
|
|
379
|
-
const pendingDetect = detectTimers.get(s.id);
|
|
315
|
+
runtimes.get(s.id)?.dispose();
|
|
316
|
+
runtimes.delete(s.id);
|
|
317
|
+
}
|
|
380
318
|
|
|
381
|
-
|
|
382
|
-
clearTimeout(pendingDetect);
|
|
319
|
+
const event = buildSessionEvent(mgr, kind, s);
|
|
383
320
|
|
|
384
|
-
|
|
385
|
-
|
|
321
|
+
if (event !== null) {
|
|
322
|
+
emitEvent(event);
|
|
386
323
|
}
|
|
387
|
-
|
|
388
|
-
const builders: Record<typeof kind, () => EventMsg> = {
|
|
389
|
-
added: () => ({ v: PROTOCOL_V, ev: 'session.added', session: getDescriptor(mgr, s.id) }),
|
|
390
|
-
state: () => ({
|
|
391
|
-
v: PROTOCOL_V,
|
|
392
|
-
ev: 'session.state',
|
|
393
|
-
s: s.id,
|
|
394
|
-
state: s.state,
|
|
395
|
-
unread: s.unread,
|
|
396
|
-
lastMsg: s.lastMsg,
|
|
397
|
-
kind: s.kind,
|
|
398
|
-
alive: s.pty !== null || (s.kind === 'headless' && s.state !== 'exited'),
|
|
399
|
-
...(s.agentSessionID === undefined ? {} : { agentSessionID: s.agentSessionID }),
|
|
400
|
-
agent: s.agent,
|
|
401
|
-
pinned: s.pinned,
|
|
402
|
-
lastAttachedAt: s.lastAttachedAt,
|
|
403
|
-
}),
|
|
404
|
-
renamed: () => ({
|
|
405
|
-
v: PROTOCOL_V,
|
|
406
|
-
ev: 'session.renamed',
|
|
407
|
-
s: s.id,
|
|
408
|
-
name: s.name,
|
|
409
|
-
namedBy: s.namedBy,
|
|
410
|
-
}),
|
|
411
|
-
removed: () => ({ v: PROTOCOL_V, ev: 'session.removed', s: s.id }),
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
emitEvent(builders[kind]());
|
|
415
324
|
};
|
|
416
325
|
|
|
417
326
|
const reporter = startHookServer((e) => {
|
|
418
327
|
if (e.event !== 'Statusline') {
|
|
419
|
-
store.recordEvent(e);
|
|
328
|
+
void store.recordEvent(e);
|
|
420
329
|
}
|
|
421
330
|
|
|
422
331
|
const kind = mgr.applyHook(e);
|
|
332
|
+
const runtime = runtimes.get(e.atcId);
|
|
423
333
|
|
|
424
334
|
if (kind === 'ended') {
|
|
425
|
-
|
|
335
|
+
runtime?.pendingEject?.();
|
|
426
336
|
}
|
|
427
337
|
|
|
428
338
|
// A revived session announcing itself is the cue a staggered restore
|
|
429
339
|
// waits on before booting the next one.
|
|
430
340
|
if (kind === 'started') {
|
|
431
|
-
|
|
341
|
+
runtime?.bootWaiter?.();
|
|
432
342
|
const started = mgr.sessions.find((s) => s.id === e.atcId);
|
|
433
343
|
|
|
434
|
-
if (started !== undefined && pendingLastUsed
|
|
435
|
-
|
|
344
|
+
if (started !== undefined && runtime !== undefined && runtime.pendingLastUsed) {
|
|
345
|
+
runtime.pendingLastUsed = false;
|
|
346
|
+
void store.writeLastUsedAgent(started.agent);
|
|
436
347
|
}
|
|
437
348
|
}
|
|
438
349
|
}, opts.reporterSocketPath);
|
|
@@ -446,11 +357,16 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
446
357
|
findAdapter: (kind) => mgr.findAdapter(kind),
|
|
447
358
|
spawnSession: (p) => {
|
|
448
359
|
const s = mgr.spawn(p.cwd, p.name, p.prompt, p.cols, p.rows, p.resume, p.namedBy, p.agent);
|
|
360
|
+
const runtime = runtimes.get(s.id);
|
|
361
|
+
|
|
362
|
+
if (runtime !== undefined) {
|
|
363
|
+
runtime.pendingLastUsed = true;
|
|
364
|
+
runtime.dims = { cols: p.cols, rows: p.rows };
|
|
365
|
+
|
|
366
|
+
runtime.screen = new ScreenModel(p.cols, p.rows);
|
|
367
|
+
}
|
|
449
368
|
|
|
450
|
-
|
|
451
|
-
ptyDims.set(s.id, { cols: p.cols, rows: p.rows });
|
|
452
|
-
screens.set(s.id, new ScreenModel(p.cols, p.rows));
|
|
453
|
-
store.recordSpawnDir(p.cwd);
|
|
369
|
+
void store.recordSpawnDir(p.cwd);
|
|
454
370
|
|
|
455
371
|
return getDescriptor(mgr, s.id);
|
|
456
372
|
},
|
|
@@ -459,18 +375,21 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
459
375
|
// The ok response for the quit request must flush before the sockets
|
|
460
376
|
// close under it.
|
|
461
377
|
setTimeout(() => {
|
|
462
|
-
|
|
463
|
-
|
|
378
|
+
void (async () => {
|
|
379
|
+
await stopDaemon?.();
|
|
380
|
+
|
|
381
|
+
opts.onQuit?.();
|
|
382
|
+
})();
|
|
464
383
|
}, 80);
|
|
465
384
|
},
|
|
466
|
-
killSession: (id) => {
|
|
385
|
+
killSession: async (id) => {
|
|
467
386
|
if (!mgr.sessions.some((s) => s.id === id)) {
|
|
468
387
|
return false;
|
|
469
388
|
}
|
|
470
389
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
mgr.kill(id);
|
|
390
|
+
runtimes.get(id)?.stopHeadlessRun();
|
|
391
|
+
|
|
392
|
+
await mgr.kill(id);
|
|
474
393
|
|
|
475
394
|
return true;
|
|
476
395
|
},
|
|
@@ -497,20 +416,19 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
497
416
|
return 'missing';
|
|
498
417
|
}
|
|
499
418
|
|
|
500
|
-
const
|
|
501
|
-
const timer = setTimeout(settled.resolve, opts.ejectSettleMs ?? 4000);
|
|
502
|
-
|
|
503
|
-
pendingEjects.set(id, settled.resolve);
|
|
504
|
-
|
|
505
|
-
void (async () => {
|
|
506
|
-
await settled.promise;
|
|
419
|
+
const runtime = runtimes.get(id);
|
|
507
420
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
421
|
+
if (runtime === undefined) {
|
|
422
|
+
return 'missing';
|
|
423
|
+
}
|
|
511
424
|
|
|
512
|
-
|
|
513
|
-
|
|
425
|
+
runEjectHandoff({
|
|
426
|
+
sessionID: id,
|
|
427
|
+
prompt,
|
|
428
|
+
settleMs: opts.ejectSettleMs ?? 4000,
|
|
429
|
+
runtime,
|
|
430
|
+
startHeadlessTurn: (sid, p) => startHeadlessTurn(mgr, findRuntime, sid, p),
|
|
431
|
+
});
|
|
514
432
|
|
|
515
433
|
return 'ok';
|
|
516
434
|
},
|
|
@@ -519,19 +437,19 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
519
437
|
return 'no_transcript';
|
|
520
438
|
}
|
|
521
439
|
|
|
522
|
-
|
|
523
|
-
headlessRuns.delete(id);
|
|
440
|
+
const runtime = runtimes.get(id);
|
|
524
441
|
|
|
442
|
+
runtime?.stopHeadlessRun();
|
|
525
443
|
const adopted = mgr.adoptTerminal(id, cols, rows);
|
|
526
444
|
|
|
527
445
|
if (adopted === null) {
|
|
528
446
|
return 'missing';
|
|
529
447
|
}
|
|
530
448
|
|
|
531
|
-
|
|
449
|
+
if (runtime !== undefined) {
|
|
450
|
+
runtime.dims = { cols, rows };
|
|
532
451
|
|
|
533
|
-
|
|
534
|
-
screens.set(id, new ScreenModel(cols, rows));
|
|
452
|
+
runtime.screen ??= new ScreenModel(cols, rows);
|
|
535
453
|
}
|
|
536
454
|
|
|
537
455
|
scheduleResize(id);
|
|
@@ -586,11 +504,11 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
586
504
|
}
|
|
587
505
|
|
|
588
506
|
if (s.kind === 'headless') {
|
|
589
|
-
if (
|
|
507
|
+
if ((runtimes.get(sessionID)?.headlessRun ?? null) !== null) {
|
|
590
508
|
return 'busy';
|
|
591
509
|
}
|
|
592
510
|
|
|
593
|
-
return startHeadlessTurn(sessionID, data.trimEnd()) ? 'ok' : 'dead';
|
|
511
|
+
return startHeadlessTurn(mgr, findRuntime, sessionID, data.trimEnd()) ? 'ok' : 'dead';
|
|
594
512
|
}
|
|
595
513
|
|
|
596
514
|
if (s.pty === null) {
|
|
@@ -613,77 +531,10 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
613
531
|
resyncClient: sendReplay,
|
|
614
532
|
...(opts.queueBytes === undefined ? {} : { queueBytes: opts.queueBytes }),
|
|
615
533
|
getEffectiveDims: (sessionID) =>
|
|
616
|
-
attachments.findEffectiveDims(sessionID) ??
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
(s) =>
|
|
621
|
-
s.agentSessionID === agentSessionID &&
|
|
622
|
-
(s.pty !== null || (s.kind === 'headless' && s.state !== 'exited')),
|
|
623
|
-
);
|
|
624
|
-
|
|
625
|
-
const recency = store.collectFleetRecency();
|
|
626
|
-
|
|
627
|
-
// Most recently active sessions revive first, so the ones the user was
|
|
628
|
-
// just working in come back before long-idle ones; entries that never
|
|
629
|
-
// reported an event keep their stored order at the end.
|
|
630
|
-
const entries = store
|
|
631
|
-
.loadFleet()
|
|
632
|
-
.filter((entry) => !hasSession(entry.agentSessionID))
|
|
633
|
-
.toSorted((a, b) =>
|
|
634
|
-
(recency.get(b.agentSessionID) ?? '').localeCompare(recency.get(a.agentSessionID) ?? ''),
|
|
635
|
-
);
|
|
636
|
-
|
|
637
|
-
// The whole fleet registers as terminal-less sessions up front, so the
|
|
638
|
-
// list shows every incoming session immediately instead of revealing
|
|
639
|
-
// them one boot at a time.
|
|
640
|
-
const queued = entries.map((entry) => mgr.restore(entry));
|
|
641
|
-
|
|
642
|
-
const adoptQueued = (s: Session): boolean => {
|
|
643
|
-
if (mgr.adoptTerminal(s.id, cols, rows) === null) {
|
|
644
|
-
return false;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
ptyDims.set(s.id, { cols, rows });
|
|
648
|
-
|
|
649
|
-
if (!screens.has(s.id)) {
|
|
650
|
-
screens.set(s.id, new ScreenModel(cols, rows));
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return true;
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
const [first, ...rest] = queued;
|
|
657
|
-
|
|
658
|
-
if (first === undefined) {
|
|
659
|
-
return 0;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// The first terminal attaches synchronously so a caller can attach at
|
|
663
|
-
// once; each later one waits for the previous session to report it has
|
|
664
|
-
// booted, so a heavy fleet comes up one process at a time instead of
|
|
665
|
-
// all at once. A per-session cap keeps a session that never reports
|
|
666
|
-
// from stalling the rest.
|
|
667
|
-
const cap = opts.restoreBootTimeoutMs ?? 0;
|
|
668
|
-
|
|
669
|
-
const adoptRest = async (previous: Session | null) => {
|
|
670
|
-
let prev = previous;
|
|
671
|
-
|
|
672
|
-
for (const s of rest) {
|
|
673
|
-
if (prev !== null) {
|
|
674
|
-
await waitForBoot(prev.id, cap);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
prev = adoptQueued(s) ? s : null;
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
const firstBooted = adoptQueued(first) ? first : null;
|
|
682
|
-
|
|
683
|
-
void adoptRest(firstBooted);
|
|
684
|
-
|
|
685
|
-
return queued.length;
|
|
686
|
-
},
|
|
534
|
+
attachments.findEffectiveDims(sessionID) ??
|
|
535
|
+
runtimes.get(sessionID)?.dims ?? { cols: 80, rows: 24 },
|
|
536
|
+
restoreFleet: (cols, rows) =>
|
|
537
|
+
restoreFleet({ mgr, store, findRuntime, cols, rows, capMs: opts.restoreBootTimeoutMs ?? 0 }),
|
|
687
538
|
};
|
|
688
539
|
|
|
689
540
|
try {
|
|
@@ -712,28 +563,18 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
712
563
|
},
|
|
713
564
|
});
|
|
714
565
|
|
|
715
|
-
stopDaemon = () => {
|
|
716
|
-
for (const timer of resizeTimers.values()) {
|
|
717
|
-
clearTimeout(timer);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
for (const timer of detectTimers.values()) {
|
|
721
|
-
clearTimeout(timer);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
for (const timer of restoreTimers) {
|
|
725
|
-
clearTimeout(timer);
|
|
726
|
-
}
|
|
727
|
-
|
|
566
|
+
stopDaemon = async () => {
|
|
728
567
|
server.stop(true);
|
|
729
568
|
reporter.stop(true);
|
|
730
569
|
mgr.killAll();
|
|
731
570
|
|
|
732
|
-
for (const
|
|
733
|
-
|
|
571
|
+
for (const runtime of runtimes.values()) {
|
|
572
|
+
runtime.dispose();
|
|
734
573
|
}
|
|
735
574
|
|
|
736
|
-
|
|
575
|
+
runtimes.clear();
|
|
576
|
+
|
|
577
|
+
await store.stop();
|
|
737
578
|
|
|
738
579
|
if (opts.pidPath !== undefined) {
|
|
739
580
|
try {
|
|
@@ -745,7 +586,7 @@ export function startDaemon(opts: DaemonOptions): DaemonHandle {
|
|
|
745
586
|
return { stop: stopDaemon };
|
|
746
587
|
}
|
|
747
588
|
|
|
748
|
-
function hasResumableTranscript(mgr: SessionManager, id:
|
|
589
|
+
function hasResumableTranscript(mgr: SessionManager, id: SessionID): boolean {
|
|
749
590
|
const s = mgr.sessions.find((x) => x.id === id);
|
|
750
591
|
|
|
751
592
|
if (s === undefined) {
|
|
@@ -764,7 +605,7 @@ function hasResumableTranscript(mgr: SessionManager, id: string): boolean {
|
|
|
764
605
|
});
|
|
765
606
|
}
|
|
766
607
|
|
|
767
|
-
function getDescriptor(mgr: SessionManager, id:
|
|
608
|
+
function getDescriptor(mgr: SessionManager, id: SessionID): SessionDescriptor {
|
|
768
609
|
const d = mgr.collectDescriptors().find((x) => x.id === id);
|
|
769
610
|
|
|
770
611
|
if (d === undefined) {
|