@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,21 +1,28 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { sql } from 'kysely';
|
|
2
3
|
import type { Generated, Kysely } from 'kysely';
|
|
3
4
|
import { DEFAULT_MIGRATION_TABLE, Migrator } from 'kysely/migration';
|
|
4
5
|
import type { Migration, MigrationProvider, MigrationResultSet } from 'kysely/migration';
|
|
6
|
+
import type { IdempotencyState } from './idempotency-record';
|
|
5
7
|
import type { MessageStatus } from './message-record';
|
|
6
8
|
|
|
7
9
|
interface FleetTable {
|
|
8
|
-
|
|
10
|
+
session_id: string;
|
|
11
|
+
agent_session_id: string | null;
|
|
9
12
|
name: string;
|
|
10
13
|
cwd: string;
|
|
11
14
|
pinned: number;
|
|
12
15
|
last_attached: number | null;
|
|
13
16
|
agent: string;
|
|
14
17
|
exited: number;
|
|
15
|
-
|
|
18
|
+
parent_session_id: string | null;
|
|
16
19
|
prompt: string | null;
|
|
17
20
|
result: string | null;
|
|
18
21
|
transcript_path: string | null;
|
|
22
|
+
model: string | null;
|
|
23
|
+
effort: string | null;
|
|
24
|
+
target: string | null;
|
|
25
|
+
target_identity: string | null;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
interface EventsTable {
|
|
@@ -39,6 +46,37 @@ interface PrefsTable {
|
|
|
39
46
|
value: string;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
// Which daemon owns each persisted session, and under which ownership
|
|
50
|
+
// epoch. A write from a daemon whose epoch is behind the stored one is
|
|
51
|
+
// stale and rejected.
|
|
52
|
+
interface SessionOwnerTable {
|
|
53
|
+
session_id: string;
|
|
54
|
+
daemon_id: string;
|
|
55
|
+
owner_epoch: Generated<number>;
|
|
56
|
+
updated_at: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One idempotency key per principal and operation: the hash of the payload
|
|
60
|
+
// it first arrived with, where the keyed effect stands, and the effect it
|
|
61
|
+
// names. A key whose outcome is unknown never expires, so a retry of it can
|
|
62
|
+
// never start a fresh effect.
|
|
63
|
+
interface IdempotencyTable {
|
|
64
|
+
principal: string;
|
|
65
|
+
operation: string;
|
|
66
|
+
key: string;
|
|
67
|
+
payload_hash: string;
|
|
68
|
+
state: IdempotencyState;
|
|
69
|
+
effect_ref: string;
|
|
70
|
+
result: string | null;
|
|
71
|
+
created_at: number;
|
|
72
|
+
updated_at: number;
|
|
73
|
+
|
|
74
|
+
// The target, and its identity, the completed effect's session was bound
|
|
75
|
+
// to; null for a key completed without one.
|
|
76
|
+
effect_target: string | null;
|
|
77
|
+
effect_target_identity: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
interface MessagesTable {
|
|
43
81
|
id: string;
|
|
44
82
|
atc_id: string;
|
|
@@ -66,11 +104,14 @@ export interface StateStoreSchema {
|
|
|
66
104
|
spawn_history: SpawnHistoryTable;
|
|
67
105
|
prefs: PrefsTable;
|
|
68
106
|
messages: MessagesTable;
|
|
107
|
+
session_owner: SessionOwnerTable;
|
|
108
|
+
idempotency: IdempotencyTable;
|
|
69
109
|
}
|
|
70
110
|
|
|
71
111
|
// Every shape the fleet table has shipped with: the oldest carries only
|
|
72
112
|
// agent_session_id under its Claude-era name plus name and cwd, and each
|
|
73
|
-
// later step adds one column the daemon grew to depend on
|
|
113
|
+
// later step adds one column the daemon grew to depend on, until the
|
|
114
|
+
// rebuild that keys the table by the atc session id. events later gains
|
|
74
115
|
// columns of its own, as does messages, while spawn_history and prefs have
|
|
75
116
|
// carried one shape since they were added.
|
|
76
117
|
const MIGRATIONS: Record<string, Migration> = {
|
|
@@ -232,6 +273,94 @@ const MIGRATIONS: Record<string, Migration> = {
|
|
|
232
273
|
.execute();
|
|
233
274
|
},
|
|
234
275
|
},
|
|
276
|
+
'014_add_fleet_model_effort': {
|
|
277
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
278
|
+
await db.schema.alterTable('fleet').addColumn('model', 'text').execute();
|
|
279
|
+
await db.schema.alterTable('fleet').addColumn('effort', 'text').execute();
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
'015_rebuild_fleet_keyed_by_session_id': {
|
|
283
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
284
|
+
// The ledger records this step only after it returns, so a crash in
|
|
285
|
+
// between leaves a rebuilt table behind a ledger that still lacks it.
|
|
286
|
+
const columns = await collectFleetColumns(db);
|
|
287
|
+
|
|
288
|
+
if (columns.has('session_id')) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// SQLite takes this DDL inside a transaction even though kysely's
|
|
293
|
+
// adapter declines to open one for it, so the rebuild lands whole or
|
|
294
|
+
// not at all.
|
|
295
|
+
await sql`BEGIN IMMEDIATE`.execute(db);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await updateFleetKeyToSessionID(db);
|
|
299
|
+
|
|
300
|
+
await sql`COMMIT`.execute(db);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
await sql`ROLLBACK`.execute(db);
|
|
303
|
+
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
'016_create_session_owner': {
|
|
309
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
310
|
+
await sql`BEGIN IMMEDIATE`.execute(db);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await createSessionOwners(db);
|
|
314
|
+
|
|
315
|
+
await sql`COMMIT`.execute(db);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
await sql`ROLLBACK`.execute(db);
|
|
318
|
+
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
'017_create_idempotency': {
|
|
324
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
325
|
+
await db.schema
|
|
326
|
+
.createTable('idempotency')
|
|
327
|
+
.ifNotExists()
|
|
328
|
+
.addColumn('principal', 'text', (c) => c.notNull())
|
|
329
|
+
.addColumn('operation', 'text', (c) => c.notNull())
|
|
330
|
+
.addColumn('key', 'text', (c) => c.notNull())
|
|
331
|
+
.addColumn('payload_hash', 'text', (c) => c.notNull())
|
|
332
|
+
.addColumn('state', 'text', (c) => c.notNull())
|
|
333
|
+
.addColumn('effect_ref', 'text', (c) => c.notNull())
|
|
334
|
+
.addColumn('result', 'text')
|
|
335
|
+
.addColumn('created_at', 'integer', (c) => c.notNull())
|
|
336
|
+
.addColumn('updated_at', 'integer', (c) => c.notNull())
|
|
337
|
+
.addPrimaryKeyConstraint('idempotency_pk', ['principal', 'operation', 'key'])
|
|
338
|
+
.execute();
|
|
339
|
+
|
|
340
|
+
await db.schema
|
|
341
|
+
.createIndex('idempotency_state_updated_at')
|
|
342
|
+
.ifNotExists()
|
|
343
|
+
.on('idempotency')
|
|
344
|
+
.columns(['state', 'updated_at'])
|
|
345
|
+
.execute();
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
'018_add_fleet_target': {
|
|
349
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
350
|
+
await db.schema.alterTable('fleet').addColumn('target', 'text').execute();
|
|
351
|
+
await db.schema.alterTable('fleet').addColumn('target_identity', 'text').execute();
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
'019_add_idempotency_effect_target': {
|
|
355
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
356
|
+
await db.schema.alterTable('idempotency').addColumn('effect_target', 'text').execute();
|
|
357
|
+
|
|
358
|
+
await db.schema
|
|
359
|
+
.alterTable('idempotency')
|
|
360
|
+
.addColumn('effect_target_identity', 'text')
|
|
361
|
+
.execute();
|
|
362
|
+
},
|
|
363
|
+
},
|
|
235
364
|
};
|
|
236
365
|
|
|
237
366
|
const PROVIDER: MigrationProvider = {
|
|
@@ -243,8 +372,8 @@ const PROVIDER: MigrationProvider = {
|
|
|
243
372
|
* `Migrator`, one additive step at a time. A database from before the
|
|
244
373
|
* ladder existed is recognized at whichever shape it stopped at by a
|
|
245
374
|
* baselining pass that records the steps its columns already satisfy, so
|
|
246
|
-
* only what is genuinely missing runs.
|
|
247
|
-
* a row.
|
|
375
|
+
* only what is genuinely missing runs. One step rebuilds the fleet table
|
|
376
|
+
* under a new key and carries every row across; no step drops a row.
|
|
248
377
|
*/
|
|
249
378
|
export async function runMigrations(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
250
379
|
// A baselined step can land out of order relative to a step that turned
|
|
@@ -314,8 +443,128 @@ async function recordLegacyBaseline(db: Kysely<StateStoreSchema>): Promise<void>
|
|
|
314
443
|
}
|
|
315
444
|
}
|
|
316
445
|
|
|
446
|
+
// Mints the store's daemon id when it has none, then records that daemon as
|
|
447
|
+
// the owner of every fleet row, at the first ownership epoch.
|
|
448
|
+
async function createSessionOwners(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
449
|
+
await db.schema
|
|
450
|
+
.createTable('session_owner')
|
|
451
|
+
.ifNotExists()
|
|
452
|
+
.addColumn('session_id', 'text', (c) => c.primaryKey())
|
|
453
|
+
.addColumn('daemon_id', 'text', (c) => c.notNull())
|
|
454
|
+
.addColumn('owner_epoch', 'integer', (c) => c.notNull().defaultTo(1))
|
|
455
|
+
.addColumn('updated_at', 'integer', (c) => c.notNull())
|
|
456
|
+
.execute();
|
|
457
|
+
|
|
458
|
+
await db
|
|
459
|
+
.insertInto('prefs')
|
|
460
|
+
.values({ key: 'daemon_id', value: randomUUID() })
|
|
461
|
+
.onConflict((oc) => oc.column('key').doNothing())
|
|
462
|
+
.execute();
|
|
463
|
+
|
|
464
|
+
await sql`
|
|
465
|
+
INSERT OR IGNORE INTO session_owner (session_id, daemon_id, updated_at)
|
|
466
|
+
SELECT fleet.session_id, prefs.value, ${Date.now()}
|
|
467
|
+
FROM fleet JOIN prefs ON prefs.key = 'daemon_id'
|
|
468
|
+
`.execute(db);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
interface LegacyFleetIDRow {
|
|
472
|
+
old_rowid: number;
|
|
473
|
+
agent_session_id: string | null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// The columns the rebuild writes by name. Any other column a legacy table
|
|
477
|
+
// carries moves across untouched, so the rebuild never loses data a step
|
|
478
|
+
// outside this ladder added.
|
|
479
|
+
const REBUILT_FLEET_COLUMNS: ReadonlySet<string> = new Set([
|
|
480
|
+
'agent_session_id',
|
|
481
|
+
'name',
|
|
482
|
+
'cwd',
|
|
483
|
+
'pinned',
|
|
484
|
+
'last_attached',
|
|
485
|
+
'agent',
|
|
486
|
+
'exited',
|
|
487
|
+
'parent',
|
|
488
|
+
'prompt',
|
|
489
|
+
'result',
|
|
490
|
+
'transcript_path',
|
|
491
|
+
'model',
|
|
492
|
+
'effort',
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
// Gives every row a minted atc session id and moves the sub-session link
|
|
496
|
+
// from the parent's agent session id to the parent's atc session id. A link
|
|
497
|
+
// to an agent session id no row holds becomes no link.
|
|
498
|
+
async function updateFleetKeyToSessionID(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
499
|
+
const columns = await sql<ColumnInfoRow>`PRAGMA table_info(fleet)`.execute(db);
|
|
500
|
+
|
|
501
|
+
const extras = columns.rows.filter((column) => !REBUILT_FLEET_COLUMNS.has(column.name));
|
|
502
|
+
|
|
503
|
+
// Keyed by rowid: SQLite lets a text primary key hold NULL, so a legacy
|
|
504
|
+
// row can lack the agent session id the mapping would otherwise key on.
|
|
505
|
+
await sql`CREATE TEMP TABLE fleet_ids (old_rowid INTEGER PRIMARY KEY, agent_session_id TEXT, session_id TEXT NOT NULL)`.execute(
|
|
506
|
+
db,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const rows =
|
|
510
|
+
await sql<LegacyFleetIDRow>`SELECT rowid AS old_rowid, agent_session_id FROM fleet`.execute(db);
|
|
511
|
+
|
|
512
|
+
for (const row of rows.rows) {
|
|
513
|
+
await sql`INSERT INTO fleet_ids (old_rowid, agent_session_id, session_id) VALUES (${row.old_rowid}, ${row.agent_session_id}, ${randomUUID()})`.execute(
|
|
514
|
+
db,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const extraDefinitions = extras.map(
|
|
519
|
+
(column) => sql`, ${sql.ref(column.name)} ${sql.raw(column.type)}`,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const extraTargets = extras.map((column) => sql`, ${sql.ref(column.name)}`);
|
|
523
|
+
const extraSources = extras.map((column) => sql`, ${sql.ref(`f.${column.name}`)}`);
|
|
524
|
+
|
|
525
|
+
await sql`
|
|
526
|
+
CREATE TABLE fleet_rebuilt (
|
|
527
|
+
session_id TEXT PRIMARY KEY,
|
|
528
|
+
agent_session_id TEXT UNIQUE,
|
|
529
|
+
name TEXT NOT NULL,
|
|
530
|
+
cwd TEXT NOT NULL,
|
|
531
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
532
|
+
last_attached INTEGER,
|
|
533
|
+
agent TEXT NOT NULL DEFAULT 'claude',
|
|
534
|
+
exited INTEGER NOT NULL DEFAULT 0,
|
|
535
|
+
parent_session_id TEXT,
|
|
536
|
+
prompt TEXT,
|
|
537
|
+
result TEXT,
|
|
538
|
+
transcript_path TEXT,
|
|
539
|
+
model TEXT,
|
|
540
|
+
effort TEXT
|
|
541
|
+
${sql.join(extraDefinitions, sql``)}
|
|
542
|
+
)
|
|
543
|
+
`.execute(db);
|
|
544
|
+
|
|
545
|
+
await sql`
|
|
546
|
+
INSERT INTO fleet_rebuilt (
|
|
547
|
+
session_id, agent_session_id, name, cwd, pinned, last_attached, agent, exited,
|
|
548
|
+
parent_session_id, prompt, result, transcript_path, model, effort
|
|
549
|
+
${sql.join(extraTargets, sql``)}
|
|
550
|
+
)
|
|
551
|
+
SELECT
|
|
552
|
+
ids.session_id, f.agent_session_id, f.name, f.cwd, f.pinned, f.last_attached, f.agent,
|
|
553
|
+
f.exited, parents.session_id, f.prompt, f.result, f.transcript_path, f.model, f.effort
|
|
554
|
+
${sql.join(extraSources, sql``)}
|
|
555
|
+
FROM fleet f
|
|
556
|
+
JOIN fleet_ids ids ON ids.old_rowid = f.rowid
|
|
557
|
+
LEFT JOIN fleet_ids parents ON parents.agent_session_id = f.parent
|
|
558
|
+
`.execute(db);
|
|
559
|
+
|
|
560
|
+
await sql`DROP TABLE fleet`.execute(db);
|
|
561
|
+
await sql`ALTER TABLE fleet_rebuilt RENAME TO fleet`.execute(db);
|
|
562
|
+
await sql`DROP TABLE fleet_ids`.execute(db);
|
|
563
|
+
}
|
|
564
|
+
|
|
317
565
|
interface ColumnInfoRow {
|
|
318
566
|
name: string;
|
|
567
|
+
type: string;
|
|
319
568
|
}
|
|
320
569
|
|
|
321
570
|
async function collectFleetColumns(db: Kysely<StateStoreSchema>): Promise<ReadonlySet<string>> {
|