@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,3 +1,5 @@
|
|
|
1
|
+
import type { SessionID } from '../shared/session-id';
|
|
2
|
+
|
|
1
3
|
export interface Dims {
|
|
2
4
|
readonly cols: number;
|
|
3
5
|
readonly rows: number;
|
|
@@ -10,16 +12,16 @@ export interface Dims {
|
|
|
10
12
|
* the whole screen.
|
|
11
13
|
*/
|
|
12
14
|
export class AttachRegistry<TClient> {
|
|
13
|
-
private readonly bySession = new Map<
|
|
15
|
+
private readonly bySession = new Map<SessionID, Map<TClient, Dims>>();
|
|
14
16
|
|
|
15
|
-
attach(sessionID:
|
|
17
|
+
attach(sessionID: SessionID, client: TClient, dims: Dims): void {
|
|
16
18
|
const clients = this.bySession.get(sessionID) ?? new Map<TClient, Dims>();
|
|
17
19
|
|
|
18
20
|
clients.set(client, dims);
|
|
19
21
|
this.bySession.set(sessionID, clients);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
detach(sessionID:
|
|
24
|
+
detach(sessionID: SessionID, client: TClient): void {
|
|
23
25
|
const clients = this.bySession.get(sessionID);
|
|
24
26
|
|
|
25
27
|
clients?.delete(client);
|
|
@@ -29,8 +31,8 @@ export class AttachRegistry<TClient> {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
detachAll(client: TClient):
|
|
33
|
-
const affected:
|
|
34
|
+
detachAll(client: TClient): SessionID[] {
|
|
35
|
+
const affected: SessionID[] = [];
|
|
34
36
|
|
|
35
37
|
for (const [sessionID, clients] of this.bySession) {
|
|
36
38
|
if (clients.delete(client)) {
|
|
@@ -45,15 +47,15 @@ export class AttachRegistry<TClient> {
|
|
|
45
47
|
return affected;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
removeSession(sessionID:
|
|
50
|
+
removeSession(sessionID: SessionID): void {
|
|
49
51
|
this.bySession.delete(sessionID);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
hasClient(sessionID:
|
|
54
|
+
hasClient(sessionID: SessionID, client: TClient): boolean {
|
|
53
55
|
return this.bySession.get(sessionID)?.has(client) ?? false;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
updateDims(sessionID:
|
|
58
|
+
updateDims(sessionID: SessionID, client: TClient, dims: Dims): boolean {
|
|
57
59
|
const clients = this.bySession.get(sessionID);
|
|
58
60
|
|
|
59
61
|
if (clients === undefined || !clients.has(client)) {
|
|
@@ -65,11 +67,11 @@ export class AttachRegistry<TClient> {
|
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
collectClients(sessionID:
|
|
70
|
+
collectClients(sessionID: SessionID): TClient[] {
|
|
69
71
|
return [...(this.bySession.get(sessionID)?.keys() ?? [])];
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
findEffectiveDims(sessionID:
|
|
74
|
+
findEffectiveDims(sessionID: SessionID): Dims | null {
|
|
73
75
|
const clients = this.bySession.get(sessionID);
|
|
74
76
|
|
|
75
77
|
if (clients === undefined || clients.size === 0) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PROTOCOL_V } from '../protocol/protocol';
|
|
2
|
+
import type { EventMsg } from '../protocol/protocol';
|
|
3
|
+
import type { SessionID } from '../shared/session-id';
|
|
4
|
+
import type { Session, SessionDescriptor, SessionEventKind, SessionManager } from './sessions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the wire event for a session-manager notification, or null when the
|
|
8
|
+
* notification carries no descriptor to send: `added` and `state` fold in
|
|
9
|
+
* the current descriptor and skip emitting if the session is gone by the
|
|
10
|
+
* time the descriptor is collected.
|
|
11
|
+
*/
|
|
12
|
+
export function buildSessionEvent(
|
|
13
|
+
mgr: SessionManager,
|
|
14
|
+
kind: SessionEventKind,
|
|
15
|
+
s: Session,
|
|
16
|
+
): EventMsg | null {
|
|
17
|
+
const builders: Record<SessionEventKind, () => EventMsg | null> = {
|
|
18
|
+
added: () => {
|
|
19
|
+
const session = findDescriptor(mgr, s.id);
|
|
20
|
+
|
|
21
|
+
return session === null ? null : { v: PROTOCOL_V, ev: 'session.added', session };
|
|
22
|
+
},
|
|
23
|
+
state: () => {
|
|
24
|
+
const session = findDescriptor(mgr, s.id);
|
|
25
|
+
|
|
26
|
+
return session === null ? null : { v: PROTOCOL_V, ev: 'session.state', session };
|
|
27
|
+
},
|
|
28
|
+
renamed: () => ({
|
|
29
|
+
v: PROTOCOL_V,
|
|
30
|
+
ev: 'session.renamed',
|
|
31
|
+
s: s.id,
|
|
32
|
+
name: s.name,
|
|
33
|
+
namedBy: s.namedBy,
|
|
34
|
+
}),
|
|
35
|
+
removed: () => ({ v: PROTOCOL_V, ev: 'session.removed', s: s.id }),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return builders[kind]();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findDescriptor(mgr: SessionManager, id: SessionID): SessionDescriptor | null {
|
|
42
|
+
return mgr.collectDescriptors().find((x) => x.id === id) ?? null;
|
|
43
|
+
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import type { AgentAdapter,
|
|
2
|
+
import type { AgentAdapter, AgentID, SpawnOptions } from '../agents/agent-adapter';
|
|
3
|
+
import { OutboundQueue } from '../protocol/outbound-queue';
|
|
4
|
+
import type { SocketWriter } from '../protocol/outbound-queue';
|
|
5
|
+
import { parseRequestParams } from '../protocol/parse-request-params';
|
|
6
|
+
import {
|
|
7
|
+
MAX_CHUNK,
|
|
8
|
+
MAX_LINE,
|
|
9
|
+
PROTOCOL_V,
|
|
10
|
+
decodeMessage,
|
|
11
|
+
encodeMessage,
|
|
12
|
+
} from '../protocol/protocol';
|
|
13
|
+
import type { ErrorCode, EventMsg, RequestMsg } from '../protocol/protocol';
|
|
14
|
+
import type { SessionID } from '../shared/session-id';
|
|
15
|
+
import type { FleetEntry } from '../store/fleet-entry';
|
|
3
16
|
import type { Dims } from './attach-registry';
|
|
4
|
-
import { OutboundQueue } from './outbound-queue';
|
|
5
|
-
import type { SocketWriter } from './outbound-queue';
|
|
6
17
|
import type { AnswerResult } from './permission-registry';
|
|
7
|
-
import {
|
|
8
|
-
import type { ErrorCode, EventMsg, RequestMsg } from './protocol';
|
|
9
|
-
import type { FleetEntry, SessionDescriptor } from './sessions';
|
|
18
|
+
import type { SessionDescriptor } from './sessions';
|
|
10
19
|
|
|
11
20
|
interface SpawnParams {
|
|
12
21
|
readonly cwd: string;
|
|
@@ -14,56 +23,56 @@ interface SpawnParams {
|
|
|
14
23
|
readonly prompt: string;
|
|
15
24
|
readonly cols: number;
|
|
16
25
|
readonly rows: number;
|
|
17
|
-
readonly resume:
|
|
26
|
+
readonly resume: SpawnOptions['resume'];
|
|
18
27
|
readonly namedBy: 'user' | 'auto';
|
|
19
|
-
readonly agent:
|
|
28
|
+
readonly agent: AgentID;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
export interface DaemonContext {
|
|
23
32
|
readonly build: string;
|
|
24
33
|
readonly collectSessions: () => SessionDescriptor[];
|
|
25
|
-
readonly collectSpawnDirs: () => string[]
|
|
26
|
-
readonly collectFleet: () => FleetEntry[]
|
|
27
|
-
readonly loadLastUsedAgent: () =>
|
|
28
|
-
readonly findAdapter: (
|
|
34
|
+
readonly collectSpawnDirs: () => Promise<string[]>;
|
|
35
|
+
readonly collectFleet: () => Promise<FleetEntry[]>;
|
|
36
|
+
readonly loadLastUsedAgent: () => Promise<AgentID>;
|
|
37
|
+
readonly findAdapter: (id: AgentID) => AgentAdapter | null;
|
|
29
38
|
readonly spawnSession: (p: SpawnParams) => SessionDescriptor;
|
|
30
|
-
readonly killSession: (id:
|
|
31
|
-
readonly updateSession: (id:
|
|
39
|
+
readonly killSession: (id: SessionID) => Promise<boolean>;
|
|
40
|
+
readonly updateSession: (id: SessionID, name?: string, pinned?: boolean) => boolean;
|
|
32
41
|
readonly quitDaemon: () => void;
|
|
33
|
-
readonly ackSession: (id:
|
|
34
|
-
readonly buildResumeCommand: (id:
|
|
42
|
+
readonly ackSession: (id: SessionID) => boolean;
|
|
43
|
+
readonly buildResumeCommand: (id: SessionID) => string | null;
|
|
35
44
|
readonly answerPermission: (request: string, decision: string) => AnswerResult;
|
|
36
|
-
readonly restoreFleet: (cols: number, rows: number) => number
|
|
45
|
+
readonly restoreFleet: (cols: number, rows: number) => Promise<number>;
|
|
37
46
|
readonly attachSession: (
|
|
38
47
|
client: OutputClient,
|
|
39
|
-
sessionID:
|
|
48
|
+
sessionID: SessionID,
|
|
40
49
|
dims: Dims,
|
|
41
50
|
) => 'ok' | 'missing' | 'dead';
|
|
42
|
-
readonly detachSession: (client: OutputClient, sessionID:
|
|
51
|
+
readonly detachSession: (client: OutputClient, sessionID: SessionID) => void;
|
|
43
52
|
readonly detachClient: (client: OutputClient) => void;
|
|
44
53
|
readonly writeSessionInput: (
|
|
45
|
-
sessionID:
|
|
54
|
+
sessionID: SessionID,
|
|
46
55
|
data: string,
|
|
47
56
|
) => 'busy' | 'ok' | 'missing' | 'dead';
|
|
48
57
|
readonly ejectSession: (
|
|
49
|
-
id:
|
|
58
|
+
id: SessionID,
|
|
50
59
|
prompt: string,
|
|
51
60
|
) => 'ok' | 'missing' | 'unsupported' | 'no_transcript';
|
|
52
61
|
readonly adoptSession: (
|
|
53
|
-
id:
|
|
62
|
+
id: SessionID,
|
|
54
63
|
cols: number,
|
|
55
64
|
rows: number,
|
|
56
65
|
) => 'ok' | 'missing' | 'no_transcript';
|
|
57
|
-
readonly resizeSession: (client: OutputClient, sessionID:
|
|
58
|
-
readonly resyncClient: (sessionID:
|
|
66
|
+
readonly resizeSession: (client: OutputClient, sessionID: SessionID, dims: Dims) => boolean;
|
|
67
|
+
readonly resyncClient: (sessionID: SessionID, client: OutputClient) => Promise<void>;
|
|
59
68
|
readonly queueBytes?: number;
|
|
60
|
-
readonly getEffectiveDims: (sessionID:
|
|
69
|
+
readonly getEffectiveDims: (sessionID: SessionID) => Dims;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
// The slice of a connection the attach bookkeeping needs: identity plus the
|
|
64
73
|
// ability to receive output events.
|
|
65
74
|
export interface OutputClient {
|
|
66
|
-
readonly sendOutput: (sessionID:
|
|
75
|
+
readonly sendOutput: (sessionID: SessionID, event: EventMsg, byteLength: number) => void;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
interface PeerSocket extends SocketWriter {
|
|
@@ -81,7 +90,12 @@ export class DaemonConnection {
|
|
|
81
90
|
|
|
82
91
|
private helloed = false;
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
// The handshake answer is written before any other response on this
|
|
94
|
+
// connection, even though building it reads the store: a request that
|
|
95
|
+
// arrives while that read is in flight waits behind it.
|
|
96
|
+
private helloAnswered: Promise<void> = Promise.resolve();
|
|
97
|
+
|
|
98
|
+
private readonly desynced = new Map<SessionID, number>();
|
|
85
99
|
|
|
86
100
|
constructor(peer: PeerSocket, ctx: DaemonContext) {
|
|
87
101
|
this.peer = peer;
|
|
@@ -100,7 +114,7 @@ export class DaemonConnection {
|
|
|
100
114
|
// this client and resynchronizes with a repaint once the queue drains. An
|
|
101
115
|
// intermediate chunk is never dropped without that resync, because a byte
|
|
102
116
|
// stream cut mid-escape corrupts the client's terminal state.
|
|
103
|
-
sendOutput(sessionID:
|
|
117
|
+
sendOutput(sessionID: SessionID, event: EventMsg, byteLength: number): void {
|
|
104
118
|
if (!this.helloed) {
|
|
105
119
|
return;
|
|
106
120
|
}
|
|
@@ -187,12 +201,28 @@ export class DaemonConnection {
|
|
|
187
201
|
return false;
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
this.applyRequest(req);
|
|
204
|
+
this.answerAsync(req.id, () => this.applyRequest(req));
|
|
191
205
|
|
|
192
206
|
return true;
|
|
193
207
|
}
|
|
194
208
|
|
|
195
|
-
|
|
209
|
+
// A store query that rejects must end one request, never the daemon: a
|
|
210
|
+
// floating promise here would take the whole process down with it.
|
|
211
|
+
private answerAsync(id: number, answered: () => Promise<void>): void {
|
|
212
|
+
void (async () => {
|
|
213
|
+
try {
|
|
214
|
+
await answered();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
217
|
+
|
|
218
|
+
this.sendErr(id, 'internal', reason);
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async applyRequest(req: RequestMsg): Promise<void> {
|
|
224
|
+
await this.helloAnswered;
|
|
225
|
+
|
|
196
226
|
switch (req.m) {
|
|
197
227
|
case 'daemon.ping': {
|
|
198
228
|
this.sendOk(req.id, {});
|
|
@@ -205,12 +235,12 @@ export class DaemonConnection {
|
|
|
205
235
|
return;
|
|
206
236
|
}
|
|
207
237
|
case 'dirs.list': {
|
|
208
|
-
this.sendOk(req.id, { dirs: this.ctx.collectSpawnDirs() });
|
|
238
|
+
this.sendOk(req.id, { dirs: await this.ctx.collectSpawnDirs() });
|
|
209
239
|
|
|
210
240
|
return;
|
|
211
241
|
}
|
|
212
242
|
case 'fleet.list': {
|
|
213
|
-
this.sendOk(req.id, { fleet: this.ctx.collectFleet() });
|
|
243
|
+
this.sendOk(req.id, { fleet: await this.ctx.collectFleet() });
|
|
214
244
|
|
|
215
245
|
return;
|
|
216
246
|
}
|
|
@@ -226,11 +256,17 @@ export class DaemonConnection {
|
|
|
226
256
|
return;
|
|
227
257
|
}
|
|
228
258
|
case 'session.update': {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
259
|
+
const parsed = parseRequestParams('session.update', req.p);
|
|
260
|
+
|
|
261
|
+
if (!parsed.ok) {
|
|
262
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
263
|
+
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const sessionID = parsed.data.session;
|
|
232
268
|
|
|
233
|
-
if (this.ctx.updateSession(sessionID, name, pinned)) {
|
|
269
|
+
if (this.ctx.updateSession(sessionID, parsed.data.name, parsed.data.pinned)) {
|
|
234
270
|
this.sendOk(req.id, {});
|
|
235
271
|
} else {
|
|
236
272
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -239,17 +275,25 @@ export class DaemonConnection {
|
|
|
239
275
|
return;
|
|
240
276
|
}
|
|
241
277
|
case 'session.kill': {
|
|
242
|
-
this.applySessionVerb(req, this.ctx.killSession);
|
|
278
|
+
await this.applySessionVerb(req, 'session.kill', this.ctx.killSession);
|
|
243
279
|
|
|
244
280
|
return;
|
|
245
281
|
}
|
|
246
282
|
case 'session.ack': {
|
|
247
|
-
this.applySessionVerb(req, this.ctx.ackSession);
|
|
283
|
+
await this.applySessionVerb(req, 'session.ack', this.ctx.ackSession);
|
|
248
284
|
|
|
249
285
|
return;
|
|
250
286
|
}
|
|
251
287
|
case 'session.resumeCommand': {
|
|
252
|
-
const
|
|
288
|
+
const parsed = parseRequestParams('session.resumeCommand', req.p);
|
|
289
|
+
|
|
290
|
+
if (!parsed.ok) {
|
|
291
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
292
|
+
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const id = parsed.data.session;
|
|
253
297
|
const command = this.ctx.buildResumeCommand(id);
|
|
254
298
|
|
|
255
299
|
if (command === null) {
|
|
@@ -261,14 +305,16 @@ export class DaemonConnection {
|
|
|
261
305
|
return;
|
|
262
306
|
}
|
|
263
307
|
case 'session.eject': {
|
|
264
|
-
const
|
|
308
|
+
const parsed = parseRequestParams('session.eject', req.p);
|
|
265
309
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
310
|
+
if (!parsed.ok) {
|
|
311
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
312
|
+
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
270
315
|
|
|
271
|
-
const
|
|
316
|
+
const sessionID = parsed.data.session;
|
|
317
|
+
const result = this.ctx.ejectSession(sessionID, parsed.data.prompt);
|
|
272
318
|
|
|
273
319
|
if (result === 'ok') {
|
|
274
320
|
this.sendOk(req.id, {});
|
|
@@ -291,10 +337,16 @@ export class DaemonConnection {
|
|
|
291
337
|
return;
|
|
292
338
|
}
|
|
293
339
|
case 'session.adopt': {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
340
|
+
const parsed = parseRequestParams('session.adopt', req.p);
|
|
341
|
+
|
|
342
|
+
if (!parsed.ok) {
|
|
343
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
344
|
+
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const sessionID = parsed.data.session;
|
|
349
|
+
const adoptResult = this.ctx.adoptSession(sessionID, parsed.data.cols, parsed.data.rows);
|
|
298
350
|
|
|
299
351
|
if (adoptResult === 'ok') {
|
|
300
352
|
this.sendOk(req.id, {});
|
|
@@ -320,9 +372,15 @@ export class DaemonConnection {
|
|
|
320
372
|
return;
|
|
321
373
|
}
|
|
322
374
|
case 'session.detach': {
|
|
323
|
-
const
|
|
375
|
+
const parsed = parseRequestParams('session.detach', req.p);
|
|
324
376
|
|
|
325
|
-
|
|
377
|
+
if (!parsed.ok) {
|
|
378
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
379
|
+
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.ctx.detachSession(this, parsed.data.session);
|
|
326
384
|
this.sendOk(req.id, {});
|
|
327
385
|
|
|
328
386
|
return;
|
|
@@ -343,10 +401,17 @@ export class DaemonConnection {
|
|
|
343
401
|
return;
|
|
344
402
|
}
|
|
345
403
|
case 'fleet.restore': {
|
|
346
|
-
const
|
|
347
|
-
|
|
404
|
+
const parsed = parseRequestParams('fleet.restore', req.p);
|
|
405
|
+
|
|
406
|
+
if (!parsed.ok) {
|
|
407
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
348
408
|
|
|
349
|
-
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.sendOk(req.id, {
|
|
413
|
+
restored: await this.ctx.restoreFleet(parsed.data.cols, parsed.data.rows),
|
|
414
|
+
});
|
|
350
415
|
|
|
351
416
|
return;
|
|
352
417
|
}
|
|
@@ -357,36 +422,17 @@ export class DaemonConnection {
|
|
|
357
422
|
}
|
|
358
423
|
|
|
359
424
|
private applySpawn(req: RequestMsg): void {
|
|
360
|
-
const
|
|
425
|
+
const parsed = parseRequestParams('session.spawn', req.p);
|
|
361
426
|
|
|
362
|
-
if (
|
|
363
|
-
this.sendErr(req.id, 'bad_args',
|
|
427
|
+
if (!parsed.ok) {
|
|
428
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
364
429
|
|
|
365
430
|
return;
|
|
366
431
|
}
|
|
367
432
|
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (typeof rawResume === 'boolean' || typeof rawResume === 'string') {
|
|
373
|
-
resume = rawResume;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const rawAgent = req.p?.['agent'];
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
rawAgent !== undefined &&
|
|
380
|
-
rawAgent !== 'claude' &&
|
|
381
|
-
rawAgent !== 'grok' &&
|
|
382
|
-
rawAgent !== 'codex'
|
|
383
|
-
) {
|
|
384
|
-
this.sendErr(req.id, 'bad_args', "session.spawn agent must be 'claude', 'grok', or 'codex'");
|
|
385
|
-
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const agent: AgentKind = rawAgent === 'grok' || rawAgent === 'codex' ? rawAgent : 'claude';
|
|
433
|
+
const cwd = parsed.data.cwd;
|
|
434
|
+
const name = parsed.data.name;
|
|
435
|
+
const agent: AgentID = parsed.data.agent ?? 'claude';
|
|
390
436
|
|
|
391
437
|
if (this.ctx.findAdapter(agent) === null) {
|
|
392
438
|
this.sendErr(req.id, 'unsupported', `no adapter for agent '${agent}'`);
|
|
@@ -397,10 +443,10 @@ export class DaemonConnection {
|
|
|
397
443
|
const session = this.ctx.spawnSession({
|
|
398
444
|
cwd,
|
|
399
445
|
name: name === '' ? basename(cwd) : name,
|
|
400
|
-
prompt:
|
|
401
|
-
cols:
|
|
402
|
-
rows:
|
|
403
|
-
resume,
|
|
446
|
+
prompt: parsed.data.prompt,
|
|
447
|
+
cols: parsed.data.cols,
|
|
448
|
+
rows: parsed.data.rows,
|
|
449
|
+
resume: parsed.data.resume,
|
|
404
450
|
namedBy: name === '' ? 'auto' : 'user',
|
|
405
451
|
agent,
|
|
406
452
|
});
|
|
@@ -409,10 +455,20 @@ export class DaemonConnection {
|
|
|
409
455
|
}
|
|
410
456
|
|
|
411
457
|
private applyAttach(req: RequestMsg): void {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
458
|
+
const parsed = parseRequestParams('session.attach', req.p);
|
|
459
|
+
|
|
460
|
+
if (!parsed.ok) {
|
|
461
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
462
|
+
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const sessionID = parsed.data.session;
|
|
467
|
+
|
|
468
|
+
const result = this.ctx.attachSession(this, sessionID, {
|
|
469
|
+
cols: parsed.data.cols,
|
|
470
|
+
rows: parsed.data.rows,
|
|
471
|
+
});
|
|
416
472
|
|
|
417
473
|
if (result === 'missing') {
|
|
418
474
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -432,9 +488,16 @@ export class DaemonConnection {
|
|
|
432
488
|
}
|
|
433
489
|
|
|
434
490
|
private applyInput(req: RequestMsg): void {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
491
|
+
const parsed = parseRequestParams('session.input', req.p);
|
|
492
|
+
|
|
493
|
+
if (!parsed.ok) {
|
|
494
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
495
|
+
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const sessionID = parsed.data.session;
|
|
500
|
+
const result = this.ctx.writeSessionInput(sessionID, parsed.data.d);
|
|
438
501
|
|
|
439
502
|
if (result === 'missing') {
|
|
440
503
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -462,17 +525,19 @@ export class DaemonConnection {
|
|
|
462
525
|
}
|
|
463
526
|
|
|
464
527
|
private applyResize(req: RequestMsg): void {
|
|
465
|
-
const
|
|
466
|
-
const cols = typeof req.p?.['cols'] === 'number' ? req.p['cols'] : 0;
|
|
467
|
-
const rows = typeof req.p?.['rows'] === 'number' ? req.p['rows'] : 0;
|
|
528
|
+
const parsed = parseRequestParams('session.resize', req.p);
|
|
468
529
|
|
|
469
|
-
if (
|
|
470
|
-
this.sendErr(req.id, 'bad_args',
|
|
530
|
+
if (!parsed.ok) {
|
|
531
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
471
532
|
|
|
472
533
|
return;
|
|
473
534
|
}
|
|
474
535
|
|
|
475
|
-
|
|
536
|
+
const sessionID = parsed.data.session;
|
|
537
|
+
|
|
538
|
+
if (
|
|
539
|
+
!this.ctx.resizeSession(this, sessionID, { cols: parsed.data.cols, rows: parsed.data.rows })
|
|
540
|
+
) {
|
|
476
541
|
this.sendErr(req.id, 'bad_args', `not attached to session '${sessionID}'`);
|
|
477
542
|
|
|
478
543
|
return;
|
|
@@ -482,15 +547,16 @@ export class DaemonConnection {
|
|
|
482
547
|
}
|
|
483
548
|
|
|
484
549
|
private applyPermissionRespond(req: RequestMsg): void {
|
|
485
|
-
const
|
|
486
|
-
const decision = typeof req.p?.['decision'] === 'string' ? req.p['decision'] : '';
|
|
550
|
+
const parsed = parseRequestParams('permission.respond', req.p);
|
|
487
551
|
|
|
488
|
-
if (
|
|
489
|
-
this.sendErr(req.id, 'bad_args',
|
|
552
|
+
if (!parsed.ok) {
|
|
553
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
490
554
|
|
|
491
555
|
return;
|
|
492
556
|
}
|
|
493
557
|
|
|
558
|
+
const request = parsed.data.request;
|
|
559
|
+
const decision = parsed.data.decision;
|
|
494
560
|
const result = this.ctx.answerPermission(request, decision);
|
|
495
561
|
|
|
496
562
|
switch (result) {
|
|
@@ -515,10 +581,24 @@ export class DaemonConnection {
|
|
|
515
581
|
}
|
|
516
582
|
}
|
|
517
583
|
|
|
518
|
-
private applySessionVerb(
|
|
519
|
-
|
|
584
|
+
private async applySessionVerb(
|
|
585
|
+
req: RequestMsg,
|
|
586
|
+
method: 'session.kill' | 'session.ack',
|
|
587
|
+
verb: (id: SessionID) => boolean | Promise<boolean>,
|
|
588
|
+
): Promise<void> {
|
|
589
|
+
const parsed = parseRequestParams(method, req.p);
|
|
590
|
+
|
|
591
|
+
if (!parsed.ok) {
|
|
592
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
593
|
+
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const id = parsed.data.session;
|
|
520
598
|
|
|
521
|
-
|
|
599
|
+
const ok = await verb(id);
|
|
600
|
+
|
|
601
|
+
if (ok) {
|
|
522
602
|
this.sendOk(req.id, {});
|
|
523
603
|
} else {
|
|
524
604
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -526,7 +606,8 @@ export class DaemonConnection {
|
|
|
526
606
|
}
|
|
527
607
|
|
|
528
608
|
private applyHello(req: RequestMsg): boolean {
|
|
529
|
-
const
|
|
609
|
+
const parsedHello = parseRequestParams('daemon.hello', req.p);
|
|
610
|
+
const client = parsedHello.ok ? parsedHello.data.client : 'unknown client';
|
|
530
611
|
|
|
531
612
|
if (req.v !== PROTOCOL_V) {
|
|
532
613
|
this.sendErr(
|
|
@@ -539,14 +620,19 @@ export class DaemonConnection {
|
|
|
539
620
|
}
|
|
540
621
|
|
|
541
622
|
this.helloed = true;
|
|
623
|
+
this.helloAnswered = this.sendHelloOk(req.id);
|
|
624
|
+
|
|
625
|
+
this.answerAsync(req.id, () => this.helloAnswered);
|
|
542
626
|
|
|
543
|
-
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async sendHelloOk(id: number): Promise<void> {
|
|
631
|
+
this.sendOk(id, {
|
|
544
632
|
daemon: this.ctx.build,
|
|
545
633
|
limits: { maxLine: MAX_LINE, maxChunk: MAX_CHUNK },
|
|
546
|
-
lastUsedAgent: this.ctx.loadLastUsedAgent(),
|
|
634
|
+
lastUsedAgent: await this.ctx.loadLastUsedAgent(),
|
|
547
635
|
});
|
|
548
|
-
|
|
549
|
-
return true;
|
|
550
636
|
}
|
|
551
637
|
|
|
552
638
|
private sendOk(id: number, ok: Readonly<Record<string, unknown>>): void {
|