@zgeoff/atc 2.15.0 → 2.15.2

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.
Files changed (67) hide show
  1. package/package.json +2 -1
  2. package/src/agents/agent-adapter.ts +3 -38
  3. package/src/agents/build-hook-settings.ts +1 -1
  4. package/src/agents/claude-adapter.ts +3 -3
  5. package/src/agents/codex-adapter.ts +3 -3
  6. package/src/agents/gateway-adapter.ts +3 -3
  7. package/src/agents/grok-adapter.ts +3 -3
  8. package/src/agents/start-claude-headless-run.ts +1 -1
  9. package/src/client/boot-daemon.ts +2 -2
  10. package/src/client/collect-agent-picks.ts +1 -1
  11. package/src/client/daemon-client.ts +2 -1
  12. package/src/client/format-overlay-agent-mark.ts +1 -1
  13. package/src/client/index.ts +4 -2
  14. package/src/client/pick-tab-target.ts +2 -2
  15. package/src/client/spawn-picker.ts +1 -1
  16. package/src/client/to-mirror-session.ts +3 -3
  17. package/src/client/ui.ts +3 -3
  18. package/src/daemon/build-fleet-events.ts +10 -4
  19. package/src/daemon/build-grant-from-fleet-entry.ts +14 -0
  20. package/src/daemon/build-message-trail-entry.ts +1 -1
  21. package/src/daemon/build-report-trail-entry.ts +1 -1
  22. package/src/daemon/build-report-view.ts +10 -4
  23. package/src/daemon/build-scoped-context.ts +64 -77
  24. package/src/daemon/build-session-message-event.ts +1 -1
  25. package/src/daemon/daemon-connection.ts +206 -288
  26. package/src/daemon/daemon-context.ts +291 -0
  27. package/src/daemon/daemon.ts +250 -94
  28. package/src/daemon/drain-inbox.ts +85 -0
  29. package/src/daemon/execution-provider.ts +12 -0
  30. package/src/daemon/hooks.ts +1 -7
  31. package/src/daemon/idempotency-ledger.ts +78 -18
  32. package/src/daemon/imp-harness.ts +41 -0
  33. package/src/daemon/is-tree-in-reach.ts +32 -0
  34. package/src/daemon/local-pty-provider.ts +45 -0
  35. package/src/daemon/parse-hook-line.ts +1 -1
  36. package/src/daemon/sessions.ts +175 -123
  37. package/src/daemon/start-headless-turn.ts +3 -3
  38. package/src/daemon/start-session-bridge.ts +2 -2
  39. package/src/daemon/tap-registry.ts +43 -5
  40. package/src/mcp/collect-clients.ts +1 -1
  41. package/src/mcp/collect-grants.ts +1 -1
  42. package/src/mcp/find-client-name.ts +1 -1
  43. package/src/mcp/mcp-auth-schema.ts +37 -0
  44. package/src/mcp/open-mcp-auth.ts +2 -2
  45. package/src/mcp/reconnecting-caller.ts +15 -6
  46. package/src/mcp/remove-client.ts +1 -1
  47. package/src/mcp/revoke-grant.ts +1 -1
  48. package/src/mcp/types.ts +0 -38
  49. package/src/mcp-http-server.ts +2 -1
  50. package/src/protocol/adapter-event.ts +25 -0
  51. package/src/protocol/count-session-states.ts +16 -0
  52. package/src/protocol/daemon-channel.ts +17 -0
  53. package/src/protocol/hook-event.ts +11 -0
  54. package/src/protocol/pinned-group-key.ts +2 -0
  55. package/src/protocol/session-state.ts +5 -0
  56. package/src/protocol/sort-grouped-session-views.ts +29 -0
  57. package/src/protocol/sort-session-views.ts +41 -0
  58. package/src/protocol/sortable-session-view.ts +13 -0
  59. package/src/shared/agent-id.ts +6 -0
  60. package/src/shared/collect-gateways.ts +1 -1
  61. package/src/shared/to-agent-id.ts +11 -0
  62. package/src/store/fleet-entry.ts +2 -2
  63. package/src/store/run-migrations.ts +55 -4
  64. package/src/store/state-store.ts +49 -13
  65. /package/src/{store → shared}/bun-sqlite-driver.ts +0 -0
  66. /package/src/{agents → shared}/truncate-detail.ts +0 -0
  67. /package/src/{daemon → shared}/truncate-summary.ts +0 -0
@@ -0,0 +1,291 @@
1
+ import type { AgentAdapter, SpawnOptions, SpawnOverrides } from '../agents/agent-adapter';
2
+ import type { EventMsg } from '../protocol/protocol';
3
+ import type { SpawnWorkspaceSource } from '../protocol/request-param-schemas';
4
+ import type { AgentID } from '../shared/agent-id';
5
+ import type { TargetConfigError } from '../shared/collect-targets';
6
+ import type { DaemonID } from '../shared/daemon-id';
7
+ import type { MessageID } from '../shared/message-id';
8
+ import type { SessionID } from '../shared/session-id';
9
+ import type { FleetEntry } from '../store/fleet-entry';
10
+ import type { MessageRecord } from '../store/message-record';
11
+ import type { TurnSibling } from '../store/state-store';
12
+ import type { Dims } from './attach-registry';
13
+ import type { AgentEntry } from './build-agent-list';
14
+ import type { FleetEvent } from './build-fleet-events';
15
+ import type { ReportView } from './build-report-view';
16
+ import type { TargetEntry } from './build-target-list';
17
+ import type { KeyedRequest } from './idempotency-ledger';
18
+ import type { TranscriptPage, TranscriptPosition } from './load-transcript-page';
19
+ import type { AnswerResult } from './permission-registry';
20
+ import type { ScreenText } from './screen-model';
21
+ import type { SessionDescriptor } from './sessions';
22
+ import type { TargetAccess, TargetGrant } from './target-access';
23
+
24
+ export interface SpawnParams {
25
+ readonly cwd: string;
26
+ readonly name: string;
27
+ readonly prompt: string;
28
+ readonly cols: number;
29
+ readonly rows: number;
30
+ readonly resume: SpawnOptions['resume'];
31
+ readonly namedBy: 'user' | 'auto';
32
+ readonly agent: AgentID;
33
+ readonly parent: SessionID | null;
34
+ readonly overrides: SpawnOverrides;
35
+ readonly target: string;
36
+
37
+ // Where the session's working directory comes from; null runs the
38
+ // session in cwd as it stands.
39
+ readonly workspace: SpawnWorkspaceSource | null;
40
+ }
41
+
42
+ interface SessionRecord {
43
+ readonly session: SessionDescriptor;
44
+ readonly prompt: string | null;
45
+ readonly lastActivityAt: number;
46
+ readonly pending: { readonly message: string } | null;
47
+ readonly result: string | null;
48
+ }
49
+
50
+ /**
51
+ * What `session.forget` answers: the token a forget that destroys a host
52
+ * must carry, with the time it stops being taken; or the forgotten session,
53
+ * with whether its host was destroyed.
54
+ */
55
+ type ForgetResult =
56
+ | { readonly confirmToken: string; readonly expiresAt: number }
57
+ | { readonly forgotten: true; readonly destroyed: boolean };
58
+
59
+ // A transcript page with the file it came from, so a cursor into a replaced
60
+ // transcript is distinguishable from one into a grown transcript.
61
+ interface SessionTranscriptRead {
62
+ readonly path: string;
63
+ readonly page: TranscriptPage;
64
+ }
65
+
66
+ // The `agents.list` answer: the host the daemon runs on, each agent, each
67
+ // execution target, what a spawn without either runs with, a digest of
68
+ // the target config, and the target config problems the daemon started
69
+ // with.
70
+ interface AgentList {
71
+ readonly daemon: {
72
+ readonly hostname: string;
73
+ readonly platform: string;
74
+ readonly arch: string;
75
+ readonly build: string;
76
+ };
77
+ readonly agents: readonly AgentEntry[];
78
+ readonly targets: readonly TargetEntry[];
79
+ readonly spawnDefaults: { readonly agent: AgentID; readonly target: string | null };
80
+ readonly configRevision: string;
81
+ readonly targetErrors: readonly TargetConfigError[];
82
+ }
83
+
84
+ // The slice of a connection the attach bookkeeping needs: identity plus the
85
+ // ability to receive output events.
86
+ export interface OutputClient {
87
+ readonly sendOutput: (sessionID: SessionID, event: EventMsg, byteLength: number) => void;
88
+ }
89
+
90
+ // One `events.read` answer: the events, and whether more follow them.
91
+ interface EventsPage {
92
+ readonly events: readonly FleetEvent[];
93
+ readonly more: boolean;
94
+ }
95
+
96
+ // One message as `message.get` reports it: the session it belongs to now,
97
+ // every field of the record, and the other messages its turn answered with
98
+ // the atc id each was sent to.
99
+ interface MessageView {
100
+ readonly session: SessionID;
101
+ readonly record: MessageRecord;
102
+ readonly answeredWith: readonly TurnSibling[];
103
+ }
104
+
105
+ // Why a session refuses a message before the daemon accepts it.
106
+ export type MessageRefusal = 'missing' | 'dead' | 'unsupported' | 'no_tap';
107
+
108
+ // The slice of a connection a tap subscription needs.
109
+ export interface TapClient {
110
+ readonly sendEvent: (event: EventMsg) => void;
111
+ }
112
+
113
+ export interface DaemonContext {
114
+ readonly build: string;
115
+ readonly daemonID: DaemonID;
116
+ readonly collectSessions: () => SessionDescriptor[];
117
+
118
+ // The directories spawns ran in, most recent first, leaving out each one
119
+ // spawned only on targets outside the access when there is one.
120
+ readonly collectSpawnDirs: (access: TargetAccess | null) => Promise<string[]>;
121
+ readonly collectAgents: () => AgentList;
122
+ readonly collectFleet: () => Promise<FleetEntry[]>;
123
+ readonly loadLastUsedAgent: () => Promise<AgentID>;
124
+ readonly findAdapter: (id: AgentID) => AgentAdapter | null;
125
+
126
+ // The targets a principal may use.
127
+ readonly buildTargetAccess: (principal: string) => TargetAccess;
128
+
129
+ // The target and identity a session is bound to, or null when no
130
+ // session holds the id.
131
+ readonly findSessionGrant: (id: SessionID) => TargetGrant | null;
132
+
133
+ // The identity a target holds now, or null when the config holds no such
134
+ // target.
135
+ readonly findTargetIdentity: (target: string) => string | null;
136
+
137
+ // Whether the access reaches every session in the given session's tree:
138
+ // its top-level session and each sub-session of that one. False for an
139
+ // unknown session.
140
+ readonly canSeeSession: (id: SessionID, access: TargetAccess) => boolean;
141
+
142
+ // Whether the request this context serves may still see the session: any
143
+ // session for the daemon's owner, and for a principal only a session whose
144
+ // whole tree it reaches. A read answers only after asking this, in the
145
+ // same step as it sends, so no await lies between the check and the send.
146
+ readonly isSessionVisible: (id: SessionID) => boolean;
147
+
148
+ // The session a permission request belongs to, answered or not, or null
149
+ // for an unknown request.
150
+ readonly findPermissionSession: (request: string) => SessionID | null;
151
+
152
+ // The session a spawn under the given session lands under: that session,
153
+ // or its own parent for a sub-session, so a set stays one level deep.
154
+ // Null spawns the session top-level; 'missing' for an unknown session.
155
+ readonly resolveSpawnParent: (id: SessionID) => SessionID | null | 'missing';
156
+
157
+ // The target a spawn runs on: the one it names, else the default. Throws
158
+ // the refusal for a target the spawn cannot run on, and for a spawn
159
+ // without a target when the config gives no default.
160
+ readonly resolveSpawnTarget: (requested: string | undefined) => string;
161
+
162
+ // Throws the refusal for a target that cannot materialize a workspace:
163
+ // one whose provider cannot both transfer an archive and run a command.
164
+ readonly requireWorkspaceTarget: (target: string) => void;
165
+
166
+ // Runs the plan, which throws the refusal for a spawn it refuses, then
167
+ // spawns. Answers with the `session.spawn` ok payload, which a keyed
168
+ // retry replays as the first spawn answered it while the access, when
169
+ // there is one, still reaches the target the spawn's session was bound to.
170
+ readonly spawnSession: (
171
+ plan: () => SpawnParams,
172
+ keyed: KeyedRequest | null,
173
+ access: TargetAccess | null,
174
+ ) => Promise<Readonly<Record<string, unknown>>>;
175
+ readonly killSession: (id: SessionID) => Promise<boolean>;
176
+
177
+ // Forgets a session, or answers with the token a forget that destroys a
178
+ // host must carry. Throws the refusal for a token it does not take.
179
+ readonly forgetSession: (
180
+ id: SessionID,
181
+ confirmToken: string | undefined,
182
+ ) => Promise<ForgetResult | 'missing'>;
183
+ readonly updateSession: (id: SessionID, name?: string, pinned?: boolean) => boolean | 'child_pin';
184
+ readonly quitDaemon: () => void;
185
+ readonly ackSession: (id: SessionID) => boolean;
186
+ readonly buildResumeCommand: (id: SessionID) => string | null;
187
+ readonly readSessionScreen: (id: SessionID) => Promise<ScreenText | 'missing' | 'no_screen'>;
188
+ readonly answerPermission: (request: string, decision: string) => AnswerResult;
189
+ readonly restoreFleet: (cols: number, rows: number) => Promise<number>;
190
+ readonly attachSession: (
191
+ client: OutputClient,
192
+ sessionID: SessionID,
193
+ dims: Dims,
194
+ ) => 'ok' | 'missing' | 'dead';
195
+ readonly detachSession: (client: OutputClient, sessionID: SessionID) => void;
196
+ readonly detachClient: (client: OutputClient) => void;
197
+ readonly writeSessionInput: (
198
+ sessionID: SessionID,
199
+ data: string,
200
+ ) => 'busy' | 'ok' | 'missing' | 'dead';
201
+
202
+ // Types a line into the session and submits it the way the session's
203
+ // agent accepts a line, as opposed to the raw bytes the input write takes.
204
+ readonly writeSessionLine: (
205
+ sessionID: SessionID,
206
+ text: string,
207
+ ) => 'busy' | 'ok' | 'missing' | 'dead';
208
+ readonly ejectSession: (
209
+ id: SessionID,
210
+ prompt: string,
211
+ ) => 'ok' | 'missing' | 'unsupported' | 'no_transcript';
212
+
213
+ // Starts a harness for a dead or headless session. Under an access, the
214
+ // adopt answers 'missing' once the session's tree leaves it, however late.
215
+ readonly adoptSession: (
216
+ id: SessionID,
217
+ cols: number,
218
+ rows: number,
219
+ access: TargetAccess | null,
220
+ ) => Promise<'ok' | 'missing' | 'no_transcript'>;
221
+ readonly resizeSession: (client: OutputClient, sessionID: SessionID, dims: Dims) => boolean;
222
+ readonly resyncClient: (sessionID: SessionID, client: OutputClient) => Promise<void>;
223
+ readonly queueBytes?: number;
224
+ readonly getEffectiveDims: (sessionID: SessionID) => Dims;
225
+
226
+ // The session's record. Its activity time counts only the session's own
227
+ // trail rows when there is an access.
228
+ readonly readSessionRecord: (
229
+ id: SessionID,
230
+ access: TargetAccess | null,
231
+ ) => Promise<SessionRecord | 'missing'>;
232
+ readonly loadSessionTranscript: (
233
+ id: SessionID,
234
+ from: TranscriptPosition | null,
235
+ limit: number,
236
+ ) => Promise<SessionTranscriptRead | 'missing' | 'unsupported'>;
237
+
238
+ // Reads the trail, leaving out each event of a session outside the access
239
+ // when there is one.
240
+ readonly readEvents: (
241
+ afterID: number | null,
242
+ limit: number,
243
+ waitMs: number,
244
+ sessionID: SessionID | null,
245
+ access: TargetAccess | null,
246
+ ) => Promise<EventsPage>;
247
+
248
+ // One report by the trail id of its event, with the atc id of the session
249
+ // that sent it, or null for a trail id that holds no report, or whose
250
+ // report's session is outside the access when there is one. The view may
251
+ // name the report by another session; who may read it is checked against
252
+ // the sender.
253
+ readonly readReport: (
254
+ id: number,
255
+ access: TargetAccess | null,
256
+ ) => Promise<{ readonly owner: SessionID; readonly view: ReportView } | null>;
257
+
258
+ // Answers with the `session.message` ok payload, which a keyed retry
259
+ // replays with the message's current status, or with the refusal. Under
260
+ // an access, a session whose tree leaves it before the write refuses as
261
+ // 'missing'.
262
+ readonly writeSessionMessage: (
263
+ sessionID: SessionID,
264
+ from: string,
265
+ text: string,
266
+ keyed: KeyedRequest | null,
267
+ access: TargetAccess | null,
268
+ ) => Promise<Readonly<Record<string, unknown>> | MessageRefusal>;
269
+ readonly readMessage: (messageID: MessageID, waitMs: number) => Promise<MessageView | null>;
270
+
271
+ // Makes the client the session's inbox tap. Under an access, the tap
272
+ // takes only the messages sent to the session's atc id.
273
+ readonly attachTap: (
274
+ client: TapClient,
275
+ sessionID: SessionID,
276
+ access: TargetAccess | null,
277
+ ) => 'ok' | 'missing' | 'unsupported';
278
+
279
+ // Lets go of the session's inbox tap when the client holds it, so its
280
+ // messages wait for another tap.
281
+ readonly detachTap: (client: TapClient, sessionID: SessionID) => void;
282
+
283
+ // Marks a tapped message delivered. Under an access, only a message sent
284
+ // to the session's atc id counts as the session's.
285
+ readonly ackMessage: (
286
+ client: TapClient,
287
+ sessionID: SessionID,
288
+ messageID: MessageID,
289
+ access: TargetAccess | null,
290
+ ) => Promise<MessageRecord | 'not_tapping' | 'unknown'>;
291
+ }