@zgeoff/atc 2.10.3 → 2.11.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 +3 -3
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +21 -0
- package/src/agents/atc-bridge-files.ts +1 -1
- package/src/agents/claude-adapter.ts +4 -0
- package/src/agents/codex-adapter.ts +4 -0
- package/src/agents/gateway-adapter.ts +31 -0
- package/src/agents/grok-adapter.ts +4 -0
- package/src/cli.ts +4 -0
- package/src/client/boot-daemon.ts +6 -0
- package/src/daemon/build-agent-list.ts +58 -0
- package/src/daemon/daemon-connection.ts +44 -8
- package/src/daemon/daemon.ts +92 -22
- package/src/daemon/parse-report.ts +29 -3
- package/src/daemon/sessions.ts +5 -0
- package/src/mcp/answer-rpc-request.ts +13 -6
- package/src/mcp/build-tool-list.ts +60 -7
- package/src/mcp/mcp-tools.ts +151 -20
- package/src/mcp/reconnecting-caller.ts +42 -12
- package/src/mcp/require-daemon-features.ts +33 -0
- package/src/mcp/run-tool.ts +75 -23
- package/src/mcp/start-mcp-http-server.ts +130 -24
- package/src/mcp/types.ts +7 -1
- package/src/mcp-http-server.ts +6 -1
- package/src/mcp-server.ts +13 -1
- package/src/protocol/daemon-features.ts +23 -0
- package/src/protocol/parse-daemon-features.ts +19 -0
- package/src/protocol/request-param-schemas.ts +18 -2
- package/src/report.ts +20 -3
- package/src/store/message-record.ts +3 -0
- package/src/store/run-migrations.ts +18 -2
- package/src/store/state-store.ts +77 -10
|
@@ -50,6 +50,10 @@ interface MessagesTable {
|
|
|
50
50
|
delivered_at: number | null;
|
|
51
51
|
answered_at: number | null;
|
|
52
52
|
answer: string | null;
|
|
53
|
+
|
|
54
|
+
// The turn whose final reply is the answer; null for a message answered
|
|
55
|
+
// without one.
|
|
56
|
+
turn_id: string | null;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -67,8 +71,8 @@ export interface StateStoreSchema {
|
|
|
67
71
|
// Every shape the fleet table has shipped with: the oldest carries only
|
|
68
72
|
// agent_session_id under its Claude-era name plus name and cwd, and each
|
|
69
73
|
// later step adds one column the daemon grew to depend on. events later gains
|
|
70
|
-
// columns of its own, while spawn_history
|
|
71
|
-
// shape since they were added.
|
|
74
|
+
// columns of its own, as does messages, while spawn_history and prefs have
|
|
75
|
+
// carried one shape since they were added.
|
|
72
76
|
const MIGRATIONS: Record<string, Migration> = {
|
|
73
77
|
'001_create_initial_schema': {
|
|
74
78
|
async up(db: Kysely<StateStoreSchema>) {
|
|
@@ -216,6 +220,18 @@ const MIGRATIONS: Record<string, Migration> = {
|
|
|
216
220
|
.execute();
|
|
217
221
|
},
|
|
218
222
|
},
|
|
223
|
+
'013_add_messages_turn_id': {
|
|
224
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
225
|
+
await db.schema.alterTable('messages').addColumn('turn_id', 'text').execute();
|
|
226
|
+
|
|
227
|
+
await db.schema
|
|
228
|
+
.createIndex('messages_turn_id')
|
|
229
|
+
.ifNotExists()
|
|
230
|
+
.on('messages')
|
|
231
|
+
.column('turn_id')
|
|
232
|
+
.execute();
|
|
233
|
+
},
|
|
234
|
+
},
|
|
219
235
|
};
|
|
220
236
|
|
|
221
237
|
const PROVIDER: MigrationProvider = {
|
package/src/store/state-store.ts
CHANGED
|
@@ -23,6 +23,16 @@ import type { TrailEntry } from './trail-entry';
|
|
|
23
23
|
// Spelled as the partial index's predicate so SQLite can match them.
|
|
24
24
|
const TRAIL_FILTER = sql<boolean>`kind IS NOT NULL AND kind != 'heartbeat'`;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* One session's slice of the event trail: rows under its atc id, plus rows
|
|
28
|
+
* under its agent session id, since a restore re-mints the atc id while the
|
|
29
|
+
* agent session id carries on.
|
|
30
|
+
*/
|
|
31
|
+
export interface EventScope {
|
|
32
|
+
readonly atcID: SessionID;
|
|
33
|
+
readonly agentSessionID: AgentSessionID | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
export interface StoredEvent {
|
|
27
37
|
readonly id: number;
|
|
28
38
|
|
|
@@ -238,12 +248,17 @@ export class StateStore {
|
|
|
238
248
|
.execute();
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
async collectEventsAfter(
|
|
251
|
+
async collectEventsAfter(
|
|
252
|
+
afterID: number,
|
|
253
|
+
limit: number,
|
|
254
|
+
scope: EventScope | null = null,
|
|
255
|
+
): Promise<StoredEvent[]> {
|
|
242
256
|
const rows = await this.db
|
|
243
257
|
.selectFrom('events')
|
|
244
258
|
.select(['id', 'ts', 'atc_id', 'session_id', 'kind', 'detail', 'message'])
|
|
245
259
|
.where('id', '>', afterID)
|
|
246
260
|
.where(TRAIL_FILTER)
|
|
261
|
+
.where((eb) => buildScopeMatch(eb, scope))
|
|
247
262
|
.orderBy('id', 'asc')
|
|
248
263
|
.limit(limit)
|
|
249
264
|
.execute();
|
|
@@ -251,11 +266,15 @@ export class StateStore {
|
|
|
251
266
|
return buildStoredEvents(rows);
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
async collectLatestEvents(
|
|
269
|
+
async collectLatestEvents(
|
|
270
|
+
limit: number,
|
|
271
|
+
scope: EventScope | null = null,
|
|
272
|
+
): Promise<StoredEvent[]> {
|
|
255
273
|
const rows = await this.db
|
|
256
274
|
.selectFrom('events')
|
|
257
275
|
.select(['id', 'ts', 'atc_id', 'session_id', 'kind', 'detail', 'message'])
|
|
258
276
|
.where(TRAIL_FILTER)
|
|
277
|
+
.where((eb) => buildScopeMatch(eb, scope))
|
|
259
278
|
.orderBy('id', 'desc')
|
|
260
279
|
.limit(limit)
|
|
261
280
|
.execute();
|
|
@@ -375,6 +394,31 @@ export class StateStore {
|
|
|
375
394
|
return row === undefined ? null : toMessageRecord(row);
|
|
376
395
|
}
|
|
377
396
|
|
|
397
|
+
// The other messages of the same session the given message's turn
|
|
398
|
+
// answered, oldest first; none when the message has no turn.
|
|
399
|
+
async collectTurnSiblings(record: MessageRecord): Promise<MessageID[]> {
|
|
400
|
+
if (record.turn === undefined) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const owner: MessageOwner =
|
|
405
|
+
record.agentSessionID === undefined
|
|
406
|
+
? { atcID: record.atcID }
|
|
407
|
+
: { atcID: record.atcID, agentSessionID: record.agentSessionID };
|
|
408
|
+
|
|
409
|
+
const rows = await this.db
|
|
410
|
+
.selectFrom('messages')
|
|
411
|
+
.select('id')
|
|
412
|
+
.where('turn_id', '=', record.turn)
|
|
413
|
+
.where('id', '!=', record.id)
|
|
414
|
+
.where((eb) => buildOwnerFilter(eb, owner))
|
|
415
|
+
.orderBy('sent_at', 'asc')
|
|
416
|
+
.orderBy(sql`rowid`, 'asc')
|
|
417
|
+
.execute();
|
|
418
|
+
|
|
419
|
+
return rows.map((row) => toMessageID(row.id));
|
|
420
|
+
}
|
|
421
|
+
|
|
378
422
|
async updateMessageDelivered(
|
|
379
423
|
id: MessageID,
|
|
380
424
|
owner: MessageOwner,
|
|
@@ -392,22 +436,30 @@ export class StateStore {
|
|
|
392
436
|
return row === undefined ? null : toMessageRecord(row);
|
|
393
437
|
}
|
|
394
438
|
|
|
395
|
-
|
|
396
|
-
|
|
439
|
+
// Answers every given message the owner holds unanswered in one statement,
|
|
440
|
+
// so a reader sees all of one turn's messages answered or none of them.
|
|
441
|
+
// Returns the messages it answered, oldest first.
|
|
442
|
+
async updateMessagesAnswered(
|
|
443
|
+
ids: readonly MessageID[],
|
|
397
444
|
owner: MessageOwner,
|
|
398
445
|
answer: string,
|
|
399
446
|
at: number,
|
|
400
|
-
|
|
401
|
-
|
|
447
|
+
turn: string | null = null,
|
|
448
|
+
): Promise<MessageRecord[]> {
|
|
449
|
+
if (ids.length === 0) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const rows = await this.db
|
|
402
454
|
.updateTable('messages')
|
|
403
|
-
.set({ status: 'answered', answered_at: at, answer })
|
|
404
|
-
.where('id', '
|
|
455
|
+
.set({ status: 'answered', answered_at: at, answer, turn_id: turn })
|
|
456
|
+
.where('id', 'in', ids)
|
|
405
457
|
.where('status', 'in', ['accepted', 'delivered'])
|
|
406
458
|
.where((eb) => buildOwnerFilter(eb, owner))
|
|
407
459
|
.returningAll()
|
|
408
|
-
.
|
|
460
|
+
.execute();
|
|
409
461
|
|
|
410
|
-
return row
|
|
462
|
+
return rows.map((row) => toMessageRecord(row)).toSorted((a, b) => a.sentAt - b.sentAt);
|
|
411
463
|
}
|
|
412
464
|
|
|
413
465
|
// Messages sent before the agent reported its session id carry no agent
|
|
@@ -482,6 +534,20 @@ export class StateStore {
|
|
|
482
534
|
}
|
|
483
535
|
}
|
|
484
536
|
|
|
537
|
+
function buildScopeMatch(
|
|
538
|
+
eb: ExpressionBuilder<StateStoreSchema, 'events'>, // oxlint-disable-line prefer-readonly-parameter-types -- a kysely expression builder bound to a live query; not meaningfully freezable
|
|
539
|
+
scope: EventScope | null,
|
|
540
|
+
): Expression<SqlBool> {
|
|
541
|
+
if (scope === null) {
|
|
542
|
+
return eb.lit(true);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return eb.or([
|
|
546
|
+
eb('atc_id', '=', scope.atcID),
|
|
547
|
+
...(scope.agentSessionID === undefined ? [] : [eb('session_id', '=', scope.agentSessionID)]),
|
|
548
|
+
]);
|
|
549
|
+
}
|
|
550
|
+
|
|
485
551
|
interface EventRow {
|
|
486
552
|
readonly id: number;
|
|
487
553
|
readonly ts: string;
|
|
@@ -562,5 +628,6 @@ function toMessageRecord(row: Readonly<StateStoreSchema['messages']>): MessageRe
|
|
|
562
628
|
...(row.delivered_at === null ? {} : { deliveredAt: row.delivered_at }),
|
|
563
629
|
...(row.answered_at === null ? {} : { answeredAt: row.answered_at }),
|
|
564
630
|
...(row.answer === null ? {} : { answer: row.answer }),
|
|
631
|
+
...(row.turn_id === null ? {} : { turn: row.turn_id }),
|
|
565
632
|
};
|
|
566
633
|
}
|