@zgeoff/atc 2.13.0 → 2.15.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 +2 -1
- package/package.json +1 -1
- package/src/client/boot-daemon.ts +47 -21
- package/src/client/daemon-client.ts +5 -22
- package/src/client/format-protocol-mismatch.ts +37 -0
- package/src/client/index.ts +54 -1
- package/src/daemon/build-imp-provider.ts +70 -23
- package/src/daemon/build-report-trail-entry.ts +4 -2
- package/src/daemon/build-report-view.ts +41 -0
- package/src/daemon/build-scoped-context.ts +5 -0
- package/src/daemon/daemon-connection.ts +42 -16
- package/src/daemon/daemon.ts +8 -1
- package/src/daemon/hooks.ts +5 -17
- package/src/daemon/imp-client-port.ts +83 -27
- package/src/daemon/imp-provider.ts +5 -11
- package/src/daemon/materialize-workspace.ts +5 -2
- package/src/daemon/read-imp-token.ts +30 -0
- package/src/daemon/sessions.ts +18 -3
- package/src/mcp/mcp-tools.ts +32 -1
- package/src/mcp/reconnecting-caller.ts +1 -0
- package/src/mcp/require-daemon-features.ts +1 -0
- package/src/mcp/run-tool.ts +5 -0
- package/src/mcp-server.ts +3 -14
- package/src/protocol/daemon-features.ts +3 -0
- package/src/protocol/line-decoder.ts +34 -0
- package/src/{shared → protocol}/open-bridge-socket.ts +4 -7
- package/src/protocol/request-param-schemas.ts +6 -0
- package/src/report.ts +1 -1
- package/src/run-bridge-tap.ts +2 -2
- package/src/statusline.ts +1 -1
- package/src/store/fleet-entry.ts +4 -1
- package/src/store/run-migrations.ts +9 -0
- package/src/store/state-store.ts +119 -15
- package/src/store/trail-entry.ts +3 -0
- /package/src/{shared → protocol}/send-bridge-request.ts +0 -0
|
@@ -48,6 +48,10 @@ interface EventsTable {
|
|
|
48
48
|
// The id a remote session's reporter gave a report row, unique so a
|
|
49
49
|
// resent report lands once; null on every other row.
|
|
50
50
|
report_id: string | null;
|
|
51
|
+
|
|
52
|
+
// A report row's whole text, which detail previews; null on every other
|
|
53
|
+
// row, and on a report row written before the column existed.
|
|
54
|
+
report_text: string | null;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
interface SpawnHistoryTable {
|
|
@@ -437,6 +441,11 @@ const MIGRATIONS: Record<string, Migration> = {
|
|
|
437
441
|
.execute();
|
|
438
442
|
},
|
|
439
443
|
},
|
|
444
|
+
'023_add_events_report_text': {
|
|
445
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
446
|
+
await db.schema.alterTable('events').addColumn('report_text', 'text').execute();
|
|
447
|
+
},
|
|
448
|
+
},
|
|
440
449
|
};
|
|
441
450
|
|
|
442
451
|
const PROVIDER: MigrationProvider = {
|
package/src/store/state-store.ts
CHANGED
|
@@ -63,6 +63,23 @@ export interface StoredEvent {
|
|
|
63
63
|
readonly label?: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* One report as the trail holds it. A report recorded before the trail kept
|
|
68
|
+
* whole texts has only its preview, so its text is that preview and
|
|
69
|
+
* `complete` is false.
|
|
70
|
+
*/
|
|
71
|
+
export interface StoredReport {
|
|
72
|
+
readonly id: number;
|
|
73
|
+
|
|
74
|
+
// Epoch ms the report arrived.
|
|
75
|
+
readonly at: number;
|
|
76
|
+
readonly atcID: SessionID;
|
|
77
|
+
readonly agentSessionID: AgentSessionID | null;
|
|
78
|
+
readonly label: string;
|
|
79
|
+
readonly text: string;
|
|
80
|
+
readonly complete: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
/**
|
|
67
84
|
* Daemon state in one SQLite store: the restorable fleet, the event trail
|
|
68
85
|
* (hook events, message status changes, and reports) that events.read and
|
|
@@ -224,12 +241,22 @@ export class StateStore {
|
|
|
224
241
|
return recency;
|
|
225
242
|
}
|
|
226
243
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
244
|
+
// Rewrites this daemon's rows for the given entries and drops its rows
|
|
245
|
+
// for the removed sessions, plus any row of its own that an entry
|
|
246
|
+
// replaces by holding the same agent session id. Every other row stays as
|
|
247
|
+
// it stands, so a row the daemon never loaded survives a write. A session
|
|
248
|
+
// another daemon owns, or one whose stored ownership epoch has moved past
|
|
249
|
+
// this daemon's, rejects the whole write with stale_epoch.
|
|
250
|
+
async writeFleet(
|
|
251
|
+
entries: readonly FleetEntry[],
|
|
252
|
+
removed: readonly SessionID[] = [],
|
|
253
|
+
): Promise<void> {
|
|
232
254
|
const kept = buildFleetWithoutReplacedRows(entries);
|
|
255
|
+
const sessionIDs = [...entries.map((entry) => entry.sessionID), ...removed];
|
|
256
|
+
|
|
257
|
+
const agentSessionIDs = entries.flatMap((entry) =>
|
|
258
|
+
entry.agentSessionID === undefined ? [] : [entry.agentSessionID],
|
|
259
|
+
);
|
|
233
260
|
|
|
234
261
|
await this.db.transaction().execute(async (trx) => {
|
|
235
262
|
await requireCurrentOwnership(
|
|
@@ -238,17 +265,35 @@ export class StateStore {
|
|
|
238
265
|
entries.map((entry) => entry.sessionID),
|
|
239
266
|
);
|
|
240
267
|
|
|
241
|
-
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
268
|
+
const replaced =
|
|
269
|
+
agentSessionIDs.length === 0
|
|
270
|
+
? []
|
|
271
|
+
: await trx
|
|
272
|
+
.selectFrom('fleet')
|
|
273
|
+
.select(['session_id', 'agent_session_id'])
|
|
274
|
+
.where('agent_session_id', 'in', agentSessionIDs)
|
|
275
|
+
.execute();
|
|
276
|
+
|
|
277
|
+
const dropped = [...new Set([...sessionIDs, ...replaced.map((row) => row.session_id)])];
|
|
278
|
+
|
|
279
|
+
if (dropped.length > 0) {
|
|
280
|
+
await trx
|
|
281
|
+
.deleteFrom('fleet')
|
|
282
|
+
.where('session_id', 'in', dropped)
|
|
283
|
+
.where('session_id', 'in', (eb) =>
|
|
284
|
+
eb
|
|
285
|
+
.selectFrom('session_owner')
|
|
286
|
+
.select('session_id')
|
|
287
|
+
.where('daemon_id', '=', this.daemonID),
|
|
288
|
+
)
|
|
289
|
+
.execute();
|
|
250
290
|
|
|
251
|
-
|
|
291
|
+
await trx
|
|
292
|
+
.deleteFrom('session_owner')
|
|
293
|
+
.where('daemon_id', '=', this.daemonID)
|
|
294
|
+
.where('session_id', 'in', dropped)
|
|
295
|
+
.execute();
|
|
296
|
+
}
|
|
252
297
|
|
|
253
298
|
const updatedAt = Date.now();
|
|
254
299
|
|
|
@@ -287,6 +332,38 @@ export class StateStore {
|
|
|
287
332
|
})
|
|
288
333
|
.execute();
|
|
289
334
|
}
|
|
335
|
+
|
|
336
|
+
// A stored row this write leaves in place follows its replaced parent
|
|
337
|
+
// to the session that replaced it, or to that session's own parent
|
|
338
|
+
// when the replacement is itself a sub-session.
|
|
339
|
+
const survivors = new Map(
|
|
340
|
+
kept.flatMap((entry) =>
|
|
341
|
+
entry.agentSessionID === undefined ? [] : [[entry.agentSessionID, entry] as const],
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
for (const row of replaced) {
|
|
346
|
+
const survivor =
|
|
347
|
+
row.agent_session_id === null
|
|
348
|
+
? undefined
|
|
349
|
+
: survivors.get(toAgentSessionID(row.agent_session_id));
|
|
350
|
+
|
|
351
|
+
if (survivor === undefined || survivor.sessionID === row.session_id) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await trx
|
|
356
|
+
.updateTable('fleet')
|
|
357
|
+
.set({ parent_session_id: survivor.parent ?? survivor.sessionID })
|
|
358
|
+
.where('parent_session_id', '=', row.session_id)
|
|
359
|
+
.where('session_id', 'in', (eb) =>
|
|
360
|
+
eb
|
|
361
|
+
.selectFrom('session_owner')
|
|
362
|
+
.select('session_id')
|
|
363
|
+
.where('daemon_id', '=', this.daemonID),
|
|
364
|
+
)
|
|
365
|
+
.execute();
|
|
366
|
+
}
|
|
290
367
|
});
|
|
291
368
|
}
|
|
292
369
|
|
|
@@ -347,6 +424,7 @@ export class StateStore {
|
|
|
347
424
|
kind: entry.kind,
|
|
348
425
|
detail: entry.detail,
|
|
349
426
|
report_id: entry.kind === 'report' ? (entry.reportID ?? null) : null,
|
|
427
|
+
report_text: entry.kind === 'report' ? entry.text : null,
|
|
350
428
|
})
|
|
351
429
|
.onConflict((oc) => oc.column('report_id').doNothing())
|
|
352
430
|
.executeTakeFirst();
|
|
@@ -372,6 +450,32 @@ export class StateStore {
|
|
|
372
450
|
return buildStoredEvents(rows);
|
|
373
451
|
}
|
|
374
452
|
|
|
453
|
+
// A row that is not a report, or that lies outside the scope, misses as a
|
|
454
|
+
// row the trail never held does.
|
|
455
|
+
async findReport(id: number, scope: EventScope | null = null): Promise<StoredReport | null> {
|
|
456
|
+
const row = await this.db
|
|
457
|
+
.selectFrom('events')
|
|
458
|
+
.select(['id', 'ts', 'atc_id', 'session_id', 'message', 'detail', 'report_text'])
|
|
459
|
+
.where('id', '=', id)
|
|
460
|
+
.where('kind', '=', 'report')
|
|
461
|
+
.where((eb) => buildScopeMatch(eb, scope))
|
|
462
|
+
.executeTakeFirst();
|
|
463
|
+
|
|
464
|
+
if (row === undefined) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
id: row.id,
|
|
470
|
+
at: Date.parse(row.ts),
|
|
471
|
+
atcID: toSessionID(row.atc_id),
|
|
472
|
+
agentSessionID: row.session_id === null ? null : toAgentSessionID(row.session_id),
|
|
473
|
+
label: row.message ?? '',
|
|
474
|
+
text: row.report_text ?? row.detail ?? '',
|
|
475
|
+
complete: row.report_text !== null,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
375
479
|
async collectLatestEvents(
|
|
376
480
|
limit: number,
|
|
377
481
|
scope: EventScope | null = null,
|
package/src/store/trail-entry.ts
CHANGED
|
@@ -19,6 +19,9 @@ interface ReportTrailEntry extends TrailEntryBase {
|
|
|
19
19
|
readonly kind: 'report';
|
|
20
20
|
readonly label: string;
|
|
21
21
|
|
|
22
|
+
// The report's whole text, which the detail previews.
|
|
23
|
+
readonly text: string;
|
|
24
|
+
|
|
22
25
|
// The id a remote session's reporter gave the report, so a resent
|
|
23
26
|
// report is stored once; absent for a report that carries none.
|
|
24
27
|
readonly reportID?: string;
|
|
File without changes
|