@zgeoff/atc 2.11.0 → 2.12.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 +1 -1
- package/src/agents/agent-adapter.ts +51 -0
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/claude-adapter.ts +49 -2
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +43 -2
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +45 -2
- package/src/agents/grok-adapter.ts +19 -1
- package/src/cli.ts +18 -0
- package/src/client/daemon-client.ts +14 -2
- package/src/client/spawn-picker.ts +27 -4
- package/src/daemon/build-agent-list.ts +37 -1
- package/src/daemon/build-config-revision.ts +27 -0
- package/src/daemon/build-execution-targets.ts +34 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-scoped-context.ts +232 -0
- package/src/daemon/build-target-access.ts +33 -0
- package/src/daemon/build-target-forbidden-error.ts +13 -0
- package/src/daemon/build-target-identity.ts +22 -0
- package/src/daemon/build-target-list.ts +50 -0
- package/src/daemon/daemon-connection.ts +331 -95
- package/src/daemon/daemon.ts +452 -75
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +103 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/local-pty-provider.ts +83 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/restore-fleet.ts +66 -19
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +282 -65
- package/src/daemon/start-headless-run.ts +11 -0
- package/src/daemon/start-headless-turn.ts +11 -2
- package/src/daemon/target-access.ts +36 -0
- package/src/mcp/answer-mcp-request.ts +12 -4
- package/src/mcp/answer-rpc-request.ts +28 -1
- package/src/mcp/build-principal-caller.ts +15 -0
- package/src/mcp/build-spawn-descriptions.ts +45 -0
- package/src/mcp/build-tool-list.ts +32 -4
- package/src/mcp/mcp-tools.ts +98 -8
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +39 -7
- package/src/mcp/require-daemon-features.ts +7 -0
- package/src/mcp/run-tool.ts +63 -11
- package/src/mcp/types.ts +2 -0
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +24 -0
- package/src/protocol/protocol.ts +44 -11
- package/src/protocol/request-param-schemas.ts +60 -10
- package/src/shared/agent-session-id.ts +1 -1
- package/src/shared/collect-principals.ts +51 -0
- package/src/shared/collect-targets.ts +144 -0
- package/src/shared/config.ts +139 -14
- package/src/shared/daemon-id.ts +8 -0
- package/src/shared/format-json-kind.ts +21 -0
- package/src/shared/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/store/fleet-entry.ts +42 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/run-migrations.ts +254 -5
- package/src/store/state-store.ts +433 -31
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +239 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +103 -0
- package/src/workspace/sanitize-workspace-clone.ts +146 -0
- package/src/workspace/workspace-provenance.ts +11 -0
- package/src/workspace/workspace-source.ts +8 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import type { AgentAdapter, AgentID, SpawnOptions } from '../agents/agent-adapter';
|
|
2
|
+
import type { AgentAdapter, AgentID, SpawnOptions, SpawnOverrides } from '../agents/agent-adapter';
|
|
3
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
3
4
|
import { DAEMON_FEATURES } from '../protocol/daemon-features';
|
|
4
5
|
import { decodeCursor } from '../protocol/decode-cursor';
|
|
5
6
|
import { encodeCursor } from '../protocol/encode-cursor';
|
|
@@ -14,19 +15,29 @@ import {
|
|
|
14
15
|
encodeMessage,
|
|
15
16
|
} from '../protocol/protocol';
|
|
16
17
|
import type { ErrorCode, EventMsg, RequestMsg } from '../protocol/protocol';
|
|
18
|
+
import type { TargetConfigError } from '../shared/collect-targets';
|
|
19
|
+
import type { DaemonID } from '../shared/daemon-id';
|
|
17
20
|
import type { MessageID } from '../shared/message-id';
|
|
21
|
+
import { isRecord } from '../shared/report';
|
|
18
22
|
import type { SessionID } from '../shared/session-id';
|
|
23
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
19
24
|
import type { FleetEntry } from '../store/fleet-entry';
|
|
20
25
|
import type { MessageRecord } from '../store/message-record';
|
|
21
26
|
import type { Dims } from './attach-registry';
|
|
22
27
|
import type { AgentEntry } from './build-agent-list';
|
|
23
28
|
import type { FleetEvent } from './build-fleet-events';
|
|
29
|
+
import { buildPayloadHash } from './build-payload-hash';
|
|
30
|
+
import { buildScopedContext } from './build-scoped-context';
|
|
31
|
+
import type { TargetEntry } from './build-target-list';
|
|
32
|
+
import type { KeyedRequest } from './idempotency-ledger';
|
|
24
33
|
import type { TranscriptPage, TranscriptPosition } from './load-transcript-page';
|
|
34
|
+
import { parseSpawnOverrides } from './parse-spawn-overrides';
|
|
25
35
|
import type { AnswerResult } from './permission-registry';
|
|
26
36
|
import type { ScreenText } from './screen-model';
|
|
27
37
|
import type { SessionDescriptor } from './sessions';
|
|
38
|
+
import type { TargetAccess, TargetGrant } from './target-access';
|
|
28
39
|
|
|
29
|
-
interface SpawnParams {
|
|
40
|
+
export interface SpawnParams {
|
|
30
41
|
readonly cwd: string;
|
|
31
42
|
readonly name: string;
|
|
32
43
|
readonly prompt: string;
|
|
@@ -36,6 +47,8 @@ interface SpawnParams {
|
|
|
36
47
|
readonly namedBy: 'user' | 'auto';
|
|
37
48
|
readonly agent: AgentID;
|
|
38
49
|
readonly parent: SessionID | null;
|
|
50
|
+
readonly overrides: SpawnOverrides;
|
|
51
|
+
readonly target: string;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
interface SessionRecord {
|
|
@@ -55,13 +68,46 @@ interface SessionTranscriptRead {
|
|
|
55
68
|
|
|
56
69
|
export interface DaemonContext {
|
|
57
70
|
readonly build: string;
|
|
71
|
+
readonly daemonID: DaemonID;
|
|
58
72
|
readonly collectSessions: () => SessionDescriptor[];
|
|
59
73
|
readonly collectSpawnDirs: () => Promise<string[]>;
|
|
60
74
|
readonly collectAgents: () => AgentList;
|
|
61
75
|
readonly collectFleet: () => Promise<FleetEntry[]>;
|
|
62
76
|
readonly loadLastUsedAgent: () => Promise<AgentID>;
|
|
63
77
|
readonly findAdapter: (id: AgentID) => AgentAdapter | null;
|
|
64
|
-
|
|
78
|
+
|
|
79
|
+
// The targets a principal may use.
|
|
80
|
+
readonly buildTargetAccess: (principal: string) => TargetAccess;
|
|
81
|
+
|
|
82
|
+
// The target and identity a session is bound to, or null when no
|
|
83
|
+
// session holds the id.
|
|
84
|
+
readonly findSessionGrant: (id: SessionID) => TargetGrant | null;
|
|
85
|
+
|
|
86
|
+
// The identity a target holds now, or null when the config holds no such
|
|
87
|
+
// target.
|
|
88
|
+
readonly findTargetIdentity: (target: string) => string | null;
|
|
89
|
+
|
|
90
|
+
// The ids of a session's sub-sessions.
|
|
91
|
+
readonly collectChildIDs: (id: SessionID) => SessionID[];
|
|
92
|
+
|
|
93
|
+
// The session a permission request belongs to, answered or not, or null
|
|
94
|
+
// for an unknown request.
|
|
95
|
+
readonly findPermissionSession: (request: string) => SessionID | null;
|
|
96
|
+
|
|
97
|
+
// The target a spawn runs on: the one it names, else the default. Throws
|
|
98
|
+
// the refusal for a target the spawn cannot run on, and for a spawn
|
|
99
|
+
// without a target when the config gives no default.
|
|
100
|
+
readonly resolveSpawnTarget: (requested: string | undefined) => string;
|
|
101
|
+
|
|
102
|
+
// Runs the plan, which throws the refusal for a spawn it refuses, then
|
|
103
|
+
// spawns. Answers with the `session.spawn` ok payload, which a keyed
|
|
104
|
+
// retry replays as the first spawn answered it while the access, when
|
|
105
|
+
// there is one, still reaches the target the spawn's session was bound to.
|
|
106
|
+
readonly spawnSession: (
|
|
107
|
+
plan: () => SpawnParams,
|
|
108
|
+
keyed: KeyedRequest | null,
|
|
109
|
+
access: TargetAccess | null,
|
|
110
|
+
) => Promise<Readonly<Record<string, unknown>>>;
|
|
65
111
|
readonly killSession: (id: SessionID) => Promise<boolean>;
|
|
66
112
|
readonly updateSession: (id: SessionID, name?: string, pinned?: boolean) => boolean | 'child_pin';
|
|
67
113
|
readonly quitDaemon: () => void;
|
|
@@ -100,17 +146,25 @@ export interface DaemonContext {
|
|
|
100
146
|
from: TranscriptPosition | null,
|
|
101
147
|
limit: number,
|
|
102
148
|
) => Promise<SessionTranscriptRead | 'missing' | 'unsupported'>;
|
|
149
|
+
|
|
150
|
+
// Reads the trail, leaving out each event of a session outside the access
|
|
151
|
+
// when there is one.
|
|
103
152
|
readonly readEvents: (
|
|
104
153
|
afterID: number | null,
|
|
105
154
|
limit: number,
|
|
106
155
|
waitMs: number,
|
|
107
156
|
sessionID: SessionID | null,
|
|
157
|
+
access: TargetAccess | null,
|
|
108
158
|
) => Promise<EventsPage>;
|
|
159
|
+
|
|
160
|
+
// Answers with the `session.message` ok payload, which a keyed retry
|
|
161
|
+
// replays with the message's current status, or with the refusal.
|
|
109
162
|
readonly writeSessionMessage: (
|
|
110
163
|
sessionID: SessionID,
|
|
111
164
|
from: string,
|
|
112
165
|
text: string,
|
|
113
|
-
|
|
166
|
+
keyed: KeyedRequest | null,
|
|
167
|
+
) => Promise<Readonly<Record<string, unknown>> | MessageRefusal>;
|
|
114
168
|
readonly readMessage: (messageID: MessageID, waitMs: number) => Promise<MessageView | null>;
|
|
115
169
|
readonly attachTap: (client: TapClient, sessionID: SessionID) => 'ok' | 'missing' | 'unsupported';
|
|
116
170
|
readonly ackMessage: (
|
|
@@ -126,7 +180,10 @@ export interface OutputClient {
|
|
|
126
180
|
readonly sendOutput: (sessionID: SessionID, event: EventMsg, byteLength: number) => void;
|
|
127
181
|
}
|
|
128
182
|
|
|
129
|
-
// The `agents.list` answer: the host the daemon runs on,
|
|
183
|
+
// The `agents.list` answer: the host the daemon runs on, each agent, each
|
|
184
|
+
// execution target, what a spawn without either runs with, a digest of
|
|
185
|
+
// the target config, and the target config problems the daemon started
|
|
186
|
+
// with.
|
|
130
187
|
interface AgentList {
|
|
131
188
|
readonly daemon: {
|
|
132
189
|
readonly hostname: string;
|
|
@@ -135,6 +192,10 @@ interface AgentList {
|
|
|
135
192
|
readonly build: string;
|
|
136
193
|
};
|
|
137
194
|
readonly agents: readonly AgentEntry[];
|
|
195
|
+
readonly targets: readonly TargetEntry[];
|
|
196
|
+
readonly spawnDefaults: { readonly agent: AgentID; readonly target: string | null };
|
|
197
|
+
readonly configRevision: string;
|
|
198
|
+
readonly targetErrors: readonly TargetConfigError[];
|
|
138
199
|
}
|
|
139
200
|
|
|
140
201
|
// One `events.read` answer: the events, and whether more follow them.
|
|
@@ -151,11 +212,23 @@ interface MessageView {
|
|
|
151
212
|
readonly answeredWith: readonly MessageID[];
|
|
152
213
|
}
|
|
153
214
|
|
|
215
|
+
// Why a session refuses a message before the daemon accepts it.
|
|
216
|
+
export type MessageRefusal = 'missing' | 'dead' | 'unsupported' | 'no_tap';
|
|
217
|
+
|
|
154
218
|
// The slice of a connection a tap subscription needs.
|
|
155
219
|
export interface TapClient {
|
|
156
220
|
readonly sendEvent: (event: EventMsg) => void;
|
|
157
221
|
}
|
|
158
222
|
|
|
223
|
+
// The targets a request may use and the namespace of its idempotency keys.
|
|
224
|
+
interface RequestScope {
|
|
225
|
+
readonly access: TargetAccess;
|
|
226
|
+
readonly keyNamespace: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The requests that act on the whole daemon, which only its owner may make.
|
|
230
|
+
const OWNER_METHODS: ReadonlySet<string> = new Set(['daemon.quit', 'fleet.restore']);
|
|
231
|
+
|
|
159
232
|
interface PeerSocket extends SocketWriter {
|
|
160
233
|
readonly end: () => void;
|
|
161
234
|
}
|
|
@@ -180,6 +253,17 @@ export class DaemonConnection {
|
|
|
180
253
|
|
|
181
254
|
private readonly desynced = new Map<SessionID, number>();
|
|
182
255
|
|
|
256
|
+
// The principal the whole connection acts as, from its handshake, and the
|
|
257
|
+
// targets that principal may use; null for the daemon's owner, whose
|
|
258
|
+
// reach has no limit.
|
|
259
|
+
private principal: string | null = null;
|
|
260
|
+
|
|
261
|
+
private access: TargetAccess | null = null;
|
|
262
|
+
|
|
263
|
+
// The sessions and permission requests this limited connection was shown,
|
|
264
|
+
// so the events that end them reach it after the session is gone.
|
|
265
|
+
private readonly shown = new Set<string>();
|
|
266
|
+
|
|
183
267
|
constructor(peer: PeerSocket, ctx: DaemonContext) {
|
|
184
268
|
this.peer = peer;
|
|
185
269
|
this.ctx = ctx;
|
|
@@ -195,7 +279,11 @@ export class DaemonConnection {
|
|
|
195
279
|
}
|
|
196
280
|
|
|
197
281
|
sendEvent(event: EventMsg): void {
|
|
198
|
-
if (this.helloed
|
|
282
|
+
if (!this.helloed || !this.canSeeEvent(event)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!this.queue.send(encodeMessage(event))) {
|
|
199
287
|
this.peer.end();
|
|
200
288
|
}
|
|
201
289
|
}
|
|
@@ -298,18 +386,88 @@ export class DaemonConnection {
|
|
|
298
386
|
return false;
|
|
299
387
|
}
|
|
300
388
|
|
|
301
|
-
|
|
389
|
+
const scope = this.findRequestScope(req);
|
|
390
|
+
|
|
391
|
+
if (scope !== null && OWNER_METHODS.has(req.m)) {
|
|
392
|
+
this.sendErr(req.id, 'unauthorized', `${req.m} is open to the daemon's owner only`);
|
|
393
|
+
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const ctx =
|
|
398
|
+
scope === null ? this.ctx : buildScopedContext(this.ctx, scope.access, scope.keyNamespace);
|
|
399
|
+
|
|
400
|
+
this.answerAsync(req.id, () => this.applyRequest(req, ctx));
|
|
401
|
+
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The targets a request may use and the namespace its idempotency keys
|
|
406
|
+
// live in: the request's own principal, limited to what the connection's
|
|
407
|
+
// may use, else the connection's. Null is the daemon's owner. The owner
|
|
408
|
+
// may act as any principal, that principal's keys included; a connection
|
|
409
|
+
// that acts as a principal always keeps its own keys, whatever principal
|
|
410
|
+
// a request acts as.
|
|
411
|
+
private findRequestScope(req: RequestMsg): RequestScope | null {
|
|
412
|
+
if (req.as === undefined) {
|
|
413
|
+
return this.principal === null || this.access === null
|
|
414
|
+
? null
|
|
415
|
+
: { access: this.access, keyNamespace: `client:${this.principal}` };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const access = this.ctx.buildTargetAccess(req.as);
|
|
419
|
+
|
|
420
|
+
if (this.principal === null || this.access === null) {
|
|
421
|
+
return { access, keyNamespace: `client:${req.as}` };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { access: this.access.merge(access), keyNamespace: `client:${this.principal}` };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Whether this connection may see an event: always for the daemon's
|
|
428
|
+
// owner, and otherwise only for an event of a session the connection may
|
|
429
|
+
// see or was already shown.
|
|
430
|
+
private canSeeEvent(event: EventMsg): boolean {
|
|
431
|
+
if (this.access === null) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const sessionID = findEventSession(event);
|
|
436
|
+
const request = typeof event['request'] === 'string' ? event['request'] : null;
|
|
437
|
+
|
|
438
|
+
if (sessionID === null) {
|
|
439
|
+
return request !== null && this.shown.has(`request:${request}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const grant = this.ctx.findSessionGrant(sessionID);
|
|
443
|
+
|
|
444
|
+
if (grant === null || !this.access.canUse(grant)) {
|
|
445
|
+
return this.shown.has(`session:${sessionID}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.shown.add(`session:${sessionID}`);
|
|
449
|
+
|
|
450
|
+
if (request !== null) {
|
|
451
|
+
this.shown.add(`request:${request}`);
|
|
452
|
+
}
|
|
302
453
|
|
|
303
454
|
return true;
|
|
304
455
|
}
|
|
305
456
|
|
|
306
457
|
// A store query that rejects must end one request, never the daemon: a
|
|
307
|
-
// floating promise here would take the whole process down with it.
|
|
458
|
+
// floating promise here would take the whole process down with it. A
|
|
459
|
+
// rejection that carries a protocol error code answers with that code.
|
|
308
460
|
private answerAsync(id: number, answered: () => Promise<void>): void {
|
|
309
461
|
void (async () => {
|
|
310
462
|
try {
|
|
311
463
|
await answered();
|
|
312
464
|
} catch (error) {
|
|
465
|
+
if (error instanceof DaemonError) {
|
|
466
|
+
this.sendErr(id, error.code, error.message, error.data);
|
|
467
|
+
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
313
471
|
const reason = error instanceof Error ? error.message : String(error);
|
|
314
472
|
|
|
315
473
|
this.sendErr(id, 'internal', reason);
|
|
@@ -317,7 +475,7 @@ export class DaemonConnection {
|
|
|
317
475
|
})();
|
|
318
476
|
}
|
|
319
477
|
|
|
320
|
-
private async applyRequest(req: RequestMsg): Promise<void> {
|
|
478
|
+
private async applyRequest(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
321
479
|
await this.helloAnswered;
|
|
322
480
|
|
|
323
481
|
switch (req.m) {
|
|
@@ -327,33 +485,33 @@ export class DaemonConnection {
|
|
|
327
485
|
return;
|
|
328
486
|
}
|
|
329
487
|
case 'session.list': {
|
|
330
|
-
this.sendOk(req.id, { sessions:
|
|
488
|
+
this.sendOk(req.id, { sessions: ctx.collectSessions() });
|
|
331
489
|
|
|
332
490
|
return;
|
|
333
491
|
}
|
|
334
492
|
case 'agents.list': {
|
|
335
|
-
this.sendOk(req.id, { ...
|
|
493
|
+
this.sendOk(req.id, { ...ctx.collectAgents() });
|
|
336
494
|
|
|
337
495
|
return;
|
|
338
496
|
}
|
|
339
497
|
case 'dirs.list': {
|
|
340
|
-
this.sendOk(req.id, { dirs: await
|
|
498
|
+
this.sendOk(req.id, { dirs: await ctx.collectSpawnDirs() });
|
|
341
499
|
|
|
342
500
|
return;
|
|
343
501
|
}
|
|
344
502
|
case 'fleet.list': {
|
|
345
|
-
this.sendOk(req.id, { fleet: await
|
|
503
|
+
this.sendOk(req.id, { fleet: await ctx.collectFleet() });
|
|
346
504
|
|
|
347
505
|
return;
|
|
348
506
|
}
|
|
349
507
|
case 'session.spawn': {
|
|
350
|
-
this.applySpawn(req);
|
|
508
|
+
await this.applySpawn(req, ctx);
|
|
351
509
|
|
|
352
510
|
return;
|
|
353
511
|
}
|
|
354
512
|
case 'daemon.quit': {
|
|
355
513
|
this.sendOk(req.id, {});
|
|
356
|
-
|
|
514
|
+
ctx.quitDaemon();
|
|
357
515
|
|
|
358
516
|
return;
|
|
359
517
|
}
|
|
@@ -367,7 +525,7 @@ export class DaemonConnection {
|
|
|
367
525
|
}
|
|
368
526
|
|
|
369
527
|
const sessionID = parsed.data.session;
|
|
370
|
-
const updated =
|
|
528
|
+
const updated = ctx.updateSession(sessionID, parsed.data.name, parsed.data.pinned);
|
|
371
529
|
|
|
372
530
|
if (updated === 'child_pin') {
|
|
373
531
|
this.sendErr(
|
|
@@ -384,12 +542,12 @@ export class DaemonConnection {
|
|
|
384
542
|
return;
|
|
385
543
|
}
|
|
386
544
|
case 'session.kill': {
|
|
387
|
-
await this.applySessionVerb(req, 'session.kill',
|
|
545
|
+
await this.applySessionVerb(req, 'session.kill', ctx.killSession);
|
|
388
546
|
|
|
389
547
|
return;
|
|
390
548
|
}
|
|
391
549
|
case 'session.ack': {
|
|
392
|
-
await this.applySessionVerb(req, 'session.ack',
|
|
550
|
+
await this.applySessionVerb(req, 'session.ack', ctx.ackSession);
|
|
393
551
|
|
|
394
552
|
return;
|
|
395
553
|
}
|
|
@@ -403,7 +561,7 @@ export class DaemonConnection {
|
|
|
403
561
|
}
|
|
404
562
|
|
|
405
563
|
const id = parsed.data.session;
|
|
406
|
-
const command =
|
|
564
|
+
const command = ctx.buildResumeCommand(id);
|
|
407
565
|
|
|
408
566
|
if (command === null) {
|
|
409
567
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -424,7 +582,7 @@ export class DaemonConnection {
|
|
|
424
582
|
|
|
425
583
|
const id = parsed.data.session;
|
|
426
584
|
|
|
427
|
-
const screen = await
|
|
585
|
+
const screen = await ctx.readSessionScreen(id);
|
|
428
586
|
|
|
429
587
|
if (screen === 'missing') {
|
|
430
588
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -446,7 +604,7 @@ export class DaemonConnection {
|
|
|
446
604
|
}
|
|
447
605
|
|
|
448
606
|
const sessionID = parsed.data.session;
|
|
449
|
-
const result =
|
|
607
|
+
const result = ctx.ejectSession(sessionID, parsed.data.prompt);
|
|
450
608
|
|
|
451
609
|
if (result === 'ok') {
|
|
452
610
|
this.sendOk(req.id, {});
|
|
@@ -478,7 +636,7 @@ export class DaemonConnection {
|
|
|
478
636
|
}
|
|
479
637
|
|
|
480
638
|
const sessionID = parsed.data.session;
|
|
481
|
-
const adoptResult =
|
|
639
|
+
const adoptResult = ctx.adoptSession(sessionID, parsed.data.cols, parsed.data.rows);
|
|
482
640
|
|
|
483
641
|
if (adoptResult === 'ok') {
|
|
484
642
|
this.sendOk(req.id, {});
|
|
@@ -499,7 +657,7 @@ export class DaemonConnection {
|
|
|
499
657
|
return;
|
|
500
658
|
}
|
|
501
659
|
case 'session.attach': {
|
|
502
|
-
this.applyAttach(req);
|
|
660
|
+
this.applyAttach(req, ctx);
|
|
503
661
|
|
|
504
662
|
return;
|
|
505
663
|
}
|
|
@@ -512,43 +670,43 @@ export class DaemonConnection {
|
|
|
512
670
|
return;
|
|
513
671
|
}
|
|
514
672
|
|
|
515
|
-
|
|
673
|
+
ctx.detachSession(this, parsed.data.session);
|
|
516
674
|
this.sendOk(req.id, {});
|
|
517
675
|
|
|
518
676
|
return;
|
|
519
677
|
}
|
|
520
678
|
case 'session.input': {
|
|
521
|
-
this.applyInput(req);
|
|
679
|
+
this.applyInput(req, ctx);
|
|
522
680
|
|
|
523
681
|
return;
|
|
524
682
|
}
|
|
525
683
|
case 'session.resize': {
|
|
526
|
-
this.applyResize(req);
|
|
684
|
+
this.applyResize(req, ctx);
|
|
527
685
|
|
|
528
686
|
return;
|
|
529
687
|
}
|
|
530
688
|
case 'permission.respond': {
|
|
531
|
-
this.applyPermissionRespond(req);
|
|
689
|
+
this.applyPermissionRespond(req, ctx);
|
|
532
690
|
|
|
533
691
|
return;
|
|
534
692
|
}
|
|
535
693
|
case 'session.message': {
|
|
536
|
-
await this.applySessionMessage(req);
|
|
694
|
+
await this.applySessionMessage(req, ctx);
|
|
537
695
|
|
|
538
696
|
return;
|
|
539
697
|
}
|
|
540
698
|
case 'session.tap': {
|
|
541
|
-
this.applyTap(req);
|
|
699
|
+
this.applyTap(req, ctx);
|
|
542
700
|
|
|
543
701
|
return;
|
|
544
702
|
}
|
|
545
703
|
case 'message.get': {
|
|
546
|
-
await this.applyMessageGet(req);
|
|
704
|
+
await this.applyMessageGet(req, ctx);
|
|
547
705
|
|
|
548
706
|
return;
|
|
549
707
|
}
|
|
550
708
|
case 'message.ack': {
|
|
551
|
-
await this.applyMessageAck(req);
|
|
709
|
+
await this.applyMessageAck(req, ctx);
|
|
552
710
|
|
|
553
711
|
return;
|
|
554
712
|
}
|
|
@@ -562,23 +720,23 @@ export class DaemonConnection {
|
|
|
562
720
|
}
|
|
563
721
|
|
|
564
722
|
this.sendOk(req.id, {
|
|
565
|
-
restored: await
|
|
723
|
+
restored: await ctx.restoreFleet(parsed.data.cols, parsed.data.rows),
|
|
566
724
|
});
|
|
567
725
|
|
|
568
726
|
return;
|
|
569
727
|
}
|
|
570
728
|
case 'session.get': {
|
|
571
|
-
await this.applySessionGet(req);
|
|
729
|
+
await this.applySessionGet(req, ctx);
|
|
572
730
|
|
|
573
731
|
return;
|
|
574
732
|
}
|
|
575
733
|
case 'session.read': {
|
|
576
|
-
await this.applySessionRead(req);
|
|
734
|
+
await this.applySessionRead(req, ctx);
|
|
577
735
|
|
|
578
736
|
return;
|
|
579
737
|
}
|
|
580
738
|
case 'events.read': {
|
|
581
|
-
await this.applyEventsRead(req);
|
|
739
|
+
await this.applyEventsRead(req, ctx);
|
|
582
740
|
|
|
583
741
|
return;
|
|
584
742
|
}
|
|
@@ -588,7 +746,7 @@ export class DaemonConnection {
|
|
|
588
746
|
}
|
|
589
747
|
}
|
|
590
748
|
|
|
591
|
-
private applySpawn(req: RequestMsg): void {
|
|
749
|
+
private async applySpawn(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
592
750
|
const parsed = parseRequestParams('session.spawn', req.p);
|
|
593
751
|
|
|
594
752
|
if (!parsed.ok) {
|
|
@@ -597,48 +755,78 @@ export class DaemonConnection {
|
|
|
597
755
|
return;
|
|
598
756
|
}
|
|
599
757
|
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
758
|
+
const data = parsed.data;
|
|
759
|
+
|
|
760
|
+
// Every refusal is thrown from the plan, which runs only once a key is
|
|
761
|
+
// claimed: a retry of a held key answers from the key without checking
|
|
762
|
+
// anything again, and a refused spawn drops its claim.
|
|
763
|
+
const plan = (): SpawnParams => {
|
|
764
|
+
// The target goes first: a config or target problem is the cause a
|
|
765
|
+
// spawn reports, ahead of any agent check that problem can skew.
|
|
766
|
+
const target = ctx.resolveSpawnTarget(data.target);
|
|
767
|
+
const agent: AgentID = data.agent ?? 'claude';
|
|
768
|
+
const adapter = ctx.findAdapter(agent);
|
|
769
|
+
const entry = ctx.collectAgents().agents.find((candidate) => candidate.id === agent);
|
|
770
|
+
|
|
771
|
+
if (adapter === null || entry === undefined) {
|
|
772
|
+
throw new DaemonError('unsupported', `no adapter for agent '${agent}'`);
|
|
773
|
+
}
|
|
606
774
|
|
|
607
|
-
|
|
608
|
-
|
|
775
|
+
// A stand-in adapter declares no binary to check, so only a profiled
|
|
776
|
+
// agent's missing binary refuses the spawn.
|
|
777
|
+
if (adapter.profile !== undefined && !entry.installed) {
|
|
778
|
+
throw new DaemonError(
|
|
779
|
+
'unsupported',
|
|
780
|
+
`agent '${agent}' is registered but not installed on this host`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
609
783
|
|
|
610
|
-
|
|
784
|
+
const overrides = parseSpawnOverrides(entry, { model: data.model, effort: data.effort });
|
|
611
785
|
|
|
612
|
-
|
|
613
|
-
|
|
786
|
+
if (!overrides.ok) {
|
|
787
|
+
throw new DaemonError(overrides.code, overrides.message);
|
|
788
|
+
}
|
|
614
789
|
|
|
615
|
-
|
|
616
|
-
this.sendErr(req.id, 'no_such_session', `no session '${parsed.data.parent}'`);
|
|
790
|
+
let parent: SessionID | null = null;
|
|
617
791
|
|
|
618
|
-
|
|
619
|
-
|
|
792
|
+
if (data.parent !== undefined) {
|
|
793
|
+
const owner = ctx.collectSessions().find((s) => s.id === data.parent);
|
|
620
794
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
795
|
+
if (owner === undefined) {
|
|
796
|
+
throw new DaemonError('no_such_session', `no session '${data.parent}'`);
|
|
797
|
+
}
|
|
625
798
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
cols: parsed.data.cols,
|
|
631
|
-
rows: parsed.data.rows,
|
|
632
|
-
resume: parsed.data.resume,
|
|
633
|
-
namedBy: name === '' ? 'auto' : 'user',
|
|
634
|
-
agent,
|
|
635
|
-
parent,
|
|
636
|
-
});
|
|
799
|
+
// A sub-session spawning a sub-session of its own lands beside it,
|
|
800
|
+
// so a set stays one level deep.
|
|
801
|
+
parent = owner.parent ?? owner.id;
|
|
802
|
+
}
|
|
637
803
|
|
|
638
|
-
|
|
804
|
+
return {
|
|
805
|
+
cwd: data.cwd,
|
|
806
|
+
name: data.name === '' ? basename(data.cwd) : data.name,
|
|
807
|
+
prompt: data.prompt,
|
|
808
|
+
cols: data.cols,
|
|
809
|
+
rows: data.rows,
|
|
810
|
+
resume: data.resume,
|
|
811
|
+
namedBy: data.name === '' ? 'auto' : 'user',
|
|
812
|
+
agent,
|
|
813
|
+
parent,
|
|
814
|
+
overrides: overrides.overrides,
|
|
815
|
+
target,
|
|
816
|
+
};
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const keyed =
|
|
820
|
+
data.idempotencyKey === undefined
|
|
821
|
+
? null
|
|
822
|
+
: { key: data.idempotencyKey, payloadHash: buildPayloadHash(data) };
|
|
823
|
+
|
|
824
|
+
const spawned = await ctx.spawnSession(plan, keyed, null);
|
|
825
|
+
|
|
826
|
+
this.sendOk(req.id, spawned);
|
|
639
827
|
}
|
|
640
828
|
|
|
641
|
-
private applyAttach(req: RequestMsg): void {
|
|
829
|
+
private applyAttach(req: RequestMsg, ctx: DaemonContext): void {
|
|
642
830
|
const parsed = parseRequestParams('session.attach', req.p);
|
|
643
831
|
|
|
644
832
|
if (!parsed.ok) {
|
|
@@ -649,7 +837,7 @@ export class DaemonConnection {
|
|
|
649
837
|
|
|
650
838
|
const sessionID = parsed.data.session;
|
|
651
839
|
|
|
652
|
-
const result =
|
|
840
|
+
const result = ctx.attachSession(this, sessionID, {
|
|
653
841
|
cols: parsed.data.cols,
|
|
654
842
|
rows: parsed.data.rows,
|
|
655
843
|
});
|
|
@@ -666,12 +854,12 @@ export class DaemonConnection {
|
|
|
666
854
|
return;
|
|
667
855
|
}
|
|
668
856
|
|
|
669
|
-
const dims =
|
|
857
|
+
const dims = ctx.getEffectiveDims(sessionID);
|
|
670
858
|
|
|
671
859
|
this.sendOk(req.id, { cols: dims.cols, rows: dims.rows });
|
|
672
860
|
}
|
|
673
861
|
|
|
674
|
-
private applyInput(req: RequestMsg): void {
|
|
862
|
+
private applyInput(req: RequestMsg, ctx: DaemonContext): void {
|
|
675
863
|
const parsed = parseRequestParams('session.input', req.p);
|
|
676
864
|
|
|
677
865
|
if (!parsed.ok) {
|
|
@@ -681,7 +869,7 @@ export class DaemonConnection {
|
|
|
681
869
|
}
|
|
682
870
|
|
|
683
871
|
const sessionID = parsed.data.session;
|
|
684
|
-
const result =
|
|
872
|
+
const result = ctx.writeSessionInput(sessionID, parsed.data.d);
|
|
685
873
|
|
|
686
874
|
if (result === 'missing') {
|
|
687
875
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -708,7 +896,7 @@ export class DaemonConnection {
|
|
|
708
896
|
this.sendOk(req.id, {});
|
|
709
897
|
}
|
|
710
898
|
|
|
711
|
-
private applyResize(req: RequestMsg): void {
|
|
899
|
+
private applyResize(req: RequestMsg, ctx: DaemonContext): void {
|
|
712
900
|
const parsed = parseRequestParams('session.resize', req.p);
|
|
713
901
|
|
|
714
902
|
if (!parsed.ok) {
|
|
@@ -719,9 +907,7 @@ export class DaemonConnection {
|
|
|
719
907
|
|
|
720
908
|
const sessionID = parsed.data.session;
|
|
721
909
|
|
|
722
|
-
if (
|
|
723
|
-
!this.ctx.resizeSession(this, sessionID, { cols: parsed.data.cols, rows: parsed.data.rows })
|
|
724
|
-
) {
|
|
910
|
+
if (!ctx.resizeSession(this, sessionID, { cols: parsed.data.cols, rows: parsed.data.rows })) {
|
|
725
911
|
this.sendErr(req.id, 'bad_args', `not attached to session '${sessionID}'`);
|
|
726
912
|
|
|
727
913
|
return;
|
|
@@ -730,7 +916,7 @@ export class DaemonConnection {
|
|
|
730
916
|
this.sendOk(req.id, {});
|
|
731
917
|
}
|
|
732
918
|
|
|
733
|
-
private applyPermissionRespond(req: RequestMsg): void {
|
|
919
|
+
private applyPermissionRespond(req: RequestMsg, ctx: DaemonContext): void {
|
|
734
920
|
const parsed = parseRequestParams('permission.respond', req.p);
|
|
735
921
|
|
|
736
922
|
if (!parsed.ok) {
|
|
@@ -741,7 +927,7 @@ export class DaemonConnection {
|
|
|
741
927
|
|
|
742
928
|
const request = parsed.data.request;
|
|
743
929
|
const decision = parsed.data.decision;
|
|
744
|
-
const result =
|
|
930
|
+
const result = ctx.answerPermission(request, decision);
|
|
745
931
|
|
|
746
932
|
switch (result) {
|
|
747
933
|
case 'ok': {
|
|
@@ -765,7 +951,7 @@ export class DaemonConnection {
|
|
|
765
951
|
}
|
|
766
952
|
}
|
|
767
953
|
|
|
768
|
-
private async applySessionGet(req: RequestMsg): Promise<void> {
|
|
954
|
+
private async applySessionGet(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
769
955
|
const parsed = parseRequestParams('session.get', req.p);
|
|
770
956
|
|
|
771
957
|
if (!parsed.ok) {
|
|
@@ -776,7 +962,7 @@ export class DaemonConnection {
|
|
|
776
962
|
|
|
777
963
|
const id = parsed.data.session;
|
|
778
964
|
|
|
779
|
-
const record = await
|
|
965
|
+
const record = await ctx.readSessionRecord(id);
|
|
780
966
|
|
|
781
967
|
if (record === 'missing') {
|
|
782
968
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -787,7 +973,7 @@ export class DaemonConnection {
|
|
|
787
973
|
this.sendOk(req.id, { ...record });
|
|
788
974
|
}
|
|
789
975
|
|
|
790
|
-
private async applySessionRead(req: RequestMsg): Promise<void> {
|
|
976
|
+
private async applySessionRead(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
791
977
|
const parsed = parseRequestParams('session.read', req.p);
|
|
792
978
|
|
|
793
979
|
if (!parsed.ok) {
|
|
@@ -811,7 +997,7 @@ export class DaemonConnection {
|
|
|
811
997
|
from = { path: decoded.path, offset: decoded.offset };
|
|
812
998
|
}
|
|
813
999
|
|
|
814
|
-
const read = await
|
|
1000
|
+
const read = await ctx.loadSessionTranscript(id, from, parsed.data.limit);
|
|
815
1001
|
|
|
816
1002
|
if (read === 'missing') {
|
|
817
1003
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -830,7 +1016,7 @@ export class DaemonConnection {
|
|
|
830
1016
|
}
|
|
831
1017
|
}
|
|
832
1018
|
|
|
833
|
-
private async applyEventsRead(req: RequestMsg): Promise<void> {
|
|
1019
|
+
private async applyEventsRead(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
834
1020
|
const parsed = parseRequestParams('events.read', req.p);
|
|
835
1021
|
|
|
836
1022
|
if (!parsed.ok) {
|
|
@@ -853,11 +1039,12 @@ export class DaemonConnection {
|
|
|
853
1039
|
afterID = decoded.id;
|
|
854
1040
|
}
|
|
855
1041
|
|
|
856
|
-
const page = await
|
|
1042
|
+
const page = await ctx.readEvents(
|
|
857
1043
|
afterID,
|
|
858
1044
|
parsed.data.limit,
|
|
859
1045
|
parsed.data.waitMs,
|
|
860
1046
|
parsed.data.session ?? null,
|
|
1047
|
+
null,
|
|
861
1048
|
);
|
|
862
1049
|
|
|
863
1050
|
const last = page.events.at(-1);
|
|
@@ -870,7 +1057,7 @@ export class DaemonConnection {
|
|
|
870
1057
|
});
|
|
871
1058
|
}
|
|
872
1059
|
|
|
873
|
-
private async applySessionMessage(req: RequestMsg): Promise<void> {
|
|
1060
|
+
private async applySessionMessage(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
874
1061
|
const parsed = parseRequestParams('session.message', req.p);
|
|
875
1062
|
|
|
876
1063
|
if (!parsed.ok) {
|
|
@@ -881,10 +1068,16 @@ export class DaemonConnection {
|
|
|
881
1068
|
|
|
882
1069
|
const sessionID = parsed.data.session;
|
|
883
1070
|
|
|
884
|
-
const
|
|
1071
|
+
const keyed =
|
|
1072
|
+
parsed.data.idempotencyKey === undefined
|
|
1073
|
+
? null
|
|
1074
|
+
: { key: parsed.data.idempotencyKey, payloadHash: buildPayloadHash(parsed.data) };
|
|
1075
|
+
|
|
1076
|
+
const result = await ctx.writeSessionMessage(
|
|
885
1077
|
sessionID,
|
|
886
1078
|
parsed.data.from,
|
|
887
1079
|
parsed.data.text,
|
|
1080
|
+
keyed,
|
|
888
1081
|
);
|
|
889
1082
|
|
|
890
1083
|
if (result === 'missing') {
|
|
@@ -915,10 +1108,10 @@ export class DaemonConnection {
|
|
|
915
1108
|
return;
|
|
916
1109
|
}
|
|
917
1110
|
|
|
918
|
-
this.sendOk(req.id,
|
|
1111
|
+
this.sendOk(req.id, result);
|
|
919
1112
|
}
|
|
920
1113
|
|
|
921
|
-
private applyTap(req: RequestMsg): void {
|
|
1114
|
+
private applyTap(req: RequestMsg, ctx: DaemonContext): void {
|
|
922
1115
|
const parsed = parseRequestParams('session.tap', req.p);
|
|
923
1116
|
|
|
924
1117
|
if (!parsed.ok) {
|
|
@@ -928,7 +1121,7 @@ export class DaemonConnection {
|
|
|
928
1121
|
}
|
|
929
1122
|
|
|
930
1123
|
const sessionID = parsed.data.session;
|
|
931
|
-
const result =
|
|
1124
|
+
const result = ctx.attachTap(this, sessionID);
|
|
932
1125
|
|
|
933
1126
|
if (result === 'missing') {
|
|
934
1127
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -945,7 +1138,7 @@ export class DaemonConnection {
|
|
|
945
1138
|
this.sendOk(req.id, {});
|
|
946
1139
|
}
|
|
947
1140
|
|
|
948
|
-
private async applyMessageGet(req: RequestMsg): Promise<void> {
|
|
1141
|
+
private async applyMessageGet(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
949
1142
|
const parsed = parseRequestParams('message.get', req.p);
|
|
950
1143
|
|
|
951
1144
|
if (!parsed.ok) {
|
|
@@ -956,7 +1149,7 @@ export class DaemonConnection {
|
|
|
956
1149
|
|
|
957
1150
|
const messageID = parsed.data.message;
|
|
958
1151
|
|
|
959
|
-
const view = await
|
|
1152
|
+
const view = await ctx.readMessage(messageID, parsed.data.waitMs);
|
|
960
1153
|
|
|
961
1154
|
if (view === null) {
|
|
962
1155
|
this.sendErr(req.id, 'bad_args', `no message '${messageID}'`);
|
|
@@ -981,7 +1174,7 @@ export class DaemonConnection {
|
|
|
981
1174
|
});
|
|
982
1175
|
}
|
|
983
1176
|
|
|
984
|
-
private async applyMessageAck(req: RequestMsg): Promise<void> {
|
|
1177
|
+
private async applyMessageAck(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
985
1178
|
const parsed = parseRequestParams('message.ack', req.p);
|
|
986
1179
|
|
|
987
1180
|
if (!parsed.ok) {
|
|
@@ -993,7 +1186,7 @@ export class DaemonConnection {
|
|
|
993
1186
|
const sessionID = parsed.data.session;
|
|
994
1187
|
const messageID = parsed.data.message;
|
|
995
1188
|
|
|
996
|
-
const result = await
|
|
1189
|
+
const result = await ctx.ackMessage(this, sessionID, messageID);
|
|
997
1190
|
|
|
998
1191
|
if (result === 'not_tapping') {
|
|
999
1192
|
this.sendErr(req.id, 'bad_args', `this connection is not tapping session '${sessionID}'`);
|
|
@@ -1038,6 +1231,21 @@ export class DaemonConnection {
|
|
|
1038
1231
|
const parsedHello = parseRequestParams('daemon.hello', req.p);
|
|
1039
1232
|
const client = parsedHello.ok ? parsedHello.data.client : 'unknown client';
|
|
1040
1233
|
|
|
1234
|
+
// A principal the handshake cannot read would otherwise leave the
|
|
1235
|
+
// connection with the owner's reach.
|
|
1236
|
+
if (!parsedHello.ok && req.p?.['principal'] !== undefined) {
|
|
1237
|
+
this.sendErr(req.id, 'bad_args', parsedHello.message);
|
|
1238
|
+
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const principal = parsedHello.ok ? (parsedHello.data.principal ?? null) : null;
|
|
1243
|
+
|
|
1244
|
+
if (principal !== null) {
|
|
1245
|
+
this.principal = principal;
|
|
1246
|
+
this.access = this.ctx.buildTargetAccess(principal);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1041
1249
|
if (req.v !== PROTOCOL_V) {
|
|
1042
1250
|
this.sendErr(
|
|
1043
1251
|
req.id,
|
|
@@ -1059,6 +1267,7 @@ export class DaemonConnection {
|
|
|
1059
1267
|
private async sendHelloOk(id: number): Promise<void> {
|
|
1060
1268
|
this.sendOk(id, {
|
|
1061
1269
|
daemon: this.ctx.build,
|
|
1270
|
+
daemonID: this.ctx.daemonID,
|
|
1062
1271
|
limits: { maxLine: MAX_LINE, maxChunk: MAX_CHUNK },
|
|
1063
1272
|
features: DAEMON_FEATURES,
|
|
1064
1273
|
lastUsedAgent: await this.ctx.loadLastUsedAgent(),
|
|
@@ -1069,7 +1278,34 @@ export class DaemonConnection {
|
|
|
1069
1278
|
this.queue.send(encodeMessage({ v: PROTOCOL_V, id, ok }));
|
|
1070
1279
|
}
|
|
1071
1280
|
|
|
1072
|
-
private sendErr(
|
|
1073
|
-
|
|
1281
|
+
private sendErr(
|
|
1282
|
+
id: number,
|
|
1283
|
+
code: ErrorCode,
|
|
1284
|
+
msg: string,
|
|
1285
|
+
data?: Readonly<Record<string, unknown>>,
|
|
1286
|
+
): void {
|
|
1287
|
+
this.queue.send(
|
|
1288
|
+
encodeMessage({
|
|
1289
|
+
v: PROTOCOL_V,
|
|
1290
|
+
id,
|
|
1291
|
+
err: { code, msg, ...(data === undefined ? {} : { data }) },
|
|
1292
|
+
}),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// The session an event belongs to, from its session id or the session it
|
|
1298
|
+
// adds, or null for an event of no session.
|
|
1299
|
+
function findEventSession(event: EventMsg): SessionID | null {
|
|
1300
|
+
if (typeof event['s'] === 'string') {
|
|
1301
|
+
return toSessionID(event['s']);
|
|
1074
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
const session = event['session'];
|
|
1305
|
+
|
|
1306
|
+
if (isRecord(session) && typeof session['id'] === 'string') {
|
|
1307
|
+
return toSessionID(session['id']);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return null;
|
|
1075
1311
|
}
|