@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.
- package/package.json +2 -1
- package/src/agents/agent-adapter.ts +3 -38
- package/src/agents/build-hook-settings.ts +1 -1
- package/src/agents/claude-adapter.ts +3 -3
- package/src/agents/codex-adapter.ts +3 -3
- package/src/agents/gateway-adapter.ts +3 -3
- package/src/agents/grok-adapter.ts +3 -3
- package/src/agents/start-claude-headless-run.ts +1 -1
- package/src/client/boot-daemon.ts +2 -2
- package/src/client/collect-agent-picks.ts +1 -1
- package/src/client/daemon-client.ts +2 -1
- package/src/client/format-overlay-agent-mark.ts +1 -1
- package/src/client/index.ts +4 -2
- package/src/client/pick-tab-target.ts +2 -2
- package/src/client/spawn-picker.ts +1 -1
- package/src/client/to-mirror-session.ts +3 -3
- package/src/client/ui.ts +3 -3
- package/src/daemon/build-fleet-events.ts +10 -4
- package/src/daemon/build-grant-from-fleet-entry.ts +14 -0
- package/src/daemon/build-message-trail-entry.ts +1 -1
- package/src/daemon/build-report-trail-entry.ts +1 -1
- package/src/daemon/build-report-view.ts +10 -4
- package/src/daemon/build-scoped-context.ts +64 -77
- package/src/daemon/build-session-message-event.ts +1 -1
- package/src/daemon/daemon-connection.ts +206 -288
- package/src/daemon/daemon-context.ts +291 -0
- package/src/daemon/daemon.ts +250 -94
- package/src/daemon/drain-inbox.ts +85 -0
- package/src/daemon/execution-provider.ts +12 -0
- package/src/daemon/hooks.ts +1 -7
- package/src/daemon/idempotency-ledger.ts +78 -18
- package/src/daemon/imp-harness.ts +41 -0
- package/src/daemon/is-tree-in-reach.ts +32 -0
- package/src/daemon/local-pty-provider.ts +45 -0
- package/src/daemon/parse-hook-line.ts +1 -1
- package/src/daemon/sessions.ts +175 -123
- package/src/daemon/start-headless-turn.ts +3 -3
- package/src/daemon/start-session-bridge.ts +2 -2
- package/src/daemon/tap-registry.ts +43 -5
- package/src/mcp/collect-clients.ts +1 -1
- package/src/mcp/collect-grants.ts +1 -1
- package/src/mcp/find-client-name.ts +1 -1
- package/src/mcp/mcp-auth-schema.ts +37 -0
- package/src/mcp/open-mcp-auth.ts +2 -2
- package/src/mcp/reconnecting-caller.ts +15 -6
- package/src/mcp/remove-client.ts +1 -1
- package/src/mcp/revoke-grant.ts +1 -1
- package/src/mcp/types.ts +0 -38
- package/src/mcp-http-server.ts +2 -1
- package/src/protocol/adapter-event.ts +25 -0
- package/src/protocol/count-session-states.ts +16 -0
- package/src/protocol/daemon-channel.ts +17 -0
- package/src/protocol/hook-event.ts +11 -0
- package/src/protocol/pinned-group-key.ts +2 -0
- package/src/protocol/session-state.ts +5 -0
- package/src/protocol/sort-grouped-session-views.ts +29 -0
- package/src/protocol/sort-session-views.ts +41 -0
- package/src/protocol/sortable-session-view.ts +13 -0
- package/src/shared/agent-id.ts +6 -0
- package/src/shared/collect-gateways.ts +1 -1
- package/src/shared/to-agent-id.ts +11 -0
- package/src/store/fleet-entry.ts +2 -2
- package/src/store/run-migrations.ts +55 -4
- package/src/store/state-store.ts +49 -13
- /package/src/{store → shared}/bun-sqlite-driver.ts +0 -0
- /package/src/{agents → shared}/truncate-detail.ts +0 -0
- /package/src/{daemon → shared}/truncate-summary.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import type { DaemonChannel } from '../protocol/daemon-channel';
|
|
3
3
|
import type { DaemonFeature } from '../protocol/daemon-features';
|
|
4
4
|
import { parseDaemonFeatures } from '../protocol/parse-daemon-features';
|
|
5
5
|
import { requireDaemonFeatures } from './require-daemon-features';
|
|
@@ -30,7 +30,7 @@ const KEYED_METHODS: ReadonlyMap<string, DaemonFeature> = new Map<string, Daemon
|
|
|
30
30
|
|
|
31
31
|
// One handshaken connection and the features its daemon announced.
|
|
32
32
|
interface DaemonConnection {
|
|
33
|
-
readonly client:
|
|
33
|
+
readonly client: DaemonChannel;
|
|
34
34
|
readonly features: ReadonlySet<DaemonFeature>;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -48,13 +48,22 @@ export class ReconnectingCaller implements FleetCaller {
|
|
|
48
48
|
|
|
49
49
|
private readonly build: string;
|
|
50
50
|
|
|
51
|
+
private readonly openChannel: (socketPath: string) => Promise<DaemonChannel>;
|
|
52
|
+
|
|
51
53
|
private client: Promise<DaemonConnection> | null = null;
|
|
52
54
|
|
|
53
|
-
private readonly closed = new WeakSet<
|
|
55
|
+
private readonly closed = new WeakSet<DaemonChannel>();
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
// The last argument connects to the daemon at a socket path; each channel
|
|
58
|
+
// it returns is handshaken here before a request rides it.
|
|
59
|
+
constructor(
|
|
60
|
+
socketPath: string,
|
|
61
|
+
build: string,
|
|
62
|
+
openChannel: (socketPath: string) => Promise<DaemonChannel>,
|
|
63
|
+
) {
|
|
56
64
|
this.socketPath = socketPath;
|
|
57
65
|
this.build = build;
|
|
66
|
+
this.openChannel = openChannel;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// The required features are checked against each connection right before
|
|
@@ -141,10 +150,10 @@ export class ReconnectingCaller implements FleetCaller {
|
|
|
141
150
|
}
|
|
142
151
|
};
|
|
143
152
|
|
|
144
|
-
let client:
|
|
153
|
+
let client: DaemonChannel;
|
|
145
154
|
|
|
146
155
|
try {
|
|
147
|
-
client = await
|
|
156
|
+
client = await this.openChannel(this.socketPath);
|
|
148
157
|
} catch (error) {
|
|
149
158
|
resetClient();
|
|
150
159
|
throw error;
|
package/src/mcp/remove-client.ts
CHANGED
package/src/mcp/revoke-grant.ts
CHANGED
package/src/mcp/types.ts
CHANGED
|
@@ -25,44 +25,6 @@ export interface ToolContext {
|
|
|
25
25
|
readonly sender: { readonly kind: 'fixed' | 'default'; readonly name: string };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* The columns of the authorization server's database that atc reads and
|
|
30
|
-
* writes itself. better-auth owns the tables and their migrations; dates are
|
|
31
|
-
* ISO 8601 strings and string lists are JSON arrays.
|
|
32
|
-
*/
|
|
33
|
-
export interface MCPAuthSchema {
|
|
34
|
-
readonly oauthClient: {
|
|
35
|
-
readonly id: string;
|
|
36
|
-
readonly clientId: string;
|
|
37
|
-
readonly name: string | null;
|
|
38
|
-
readonly redirectUris: string;
|
|
39
|
-
readonly createdAt: string | null;
|
|
40
|
-
};
|
|
41
|
-
readonly oauthAccessToken: {
|
|
42
|
-
readonly id: string;
|
|
43
|
-
readonly token: string;
|
|
44
|
-
readonly clientId: string;
|
|
45
|
-
readonly sessionId: string | null;
|
|
46
|
-
readonly authorizationCodeId: string | null;
|
|
47
|
-
};
|
|
48
|
-
readonly oauthRefreshToken: {
|
|
49
|
-
readonly id: string;
|
|
50
|
-
readonly clientId: string;
|
|
51
|
-
readonly sessionId: string | null;
|
|
52
|
-
readonly authorizationCodeId: string | null;
|
|
53
|
-
readonly scopes: string;
|
|
54
|
-
readonly createdAt: string;
|
|
55
|
-
readonly expiresAt: string;
|
|
56
|
-
readonly revoked: string | null;
|
|
57
|
-
};
|
|
58
|
-
readonly oauthConsent: { readonly id: string; readonly clientId: string };
|
|
59
|
-
readonly oauthResource: { readonly identifier: string };
|
|
60
|
-
readonly session: { readonly id: string; readonly expiresAt: string };
|
|
61
|
-
|
|
62
|
-
// When each grant last reached /mcp, keyed by the grant's authorization code id.
|
|
63
|
-
readonly atc_grant_use: { readonly grant_id: string; readonly last_used_at: string };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
28
|
/**
|
|
67
29
|
* The authorization server's store: the better-auth instance and the
|
|
68
30
|
* database under it.
|
package/src/mcp-http-server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
2
|
import { bootDaemonClient } from './client/boot-daemon';
|
|
3
|
+
import { DaemonClient } from './client/daemon-client';
|
|
3
4
|
import { collectClients } from './mcp/collect-clients';
|
|
4
5
|
import { openMCPAuth } from './mcp/open-mcp-auth';
|
|
5
6
|
import { ReconnectingCaller } from './mcp/reconnecting-caller';
|
|
@@ -27,7 +28,7 @@ export async function runMCPHTTPServer(build: string, flags: MCPHTTPFlags): Prom
|
|
|
27
28
|
|
|
28
29
|
mkdirSync(stateDir, { recursive: true });
|
|
29
30
|
|
|
30
|
-
const caller = new ReconnectingCaller(boot.socketPath, build);
|
|
31
|
+
const caller = new ReconnectingCaller(boot.socketPath, build, (path) => DaemonClient.open(path));
|
|
31
32
|
|
|
32
33
|
const server = await startMCPHTTPServer({
|
|
33
34
|
caller,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What an adapter makes of one hook event: the agent-neutral kind and the
|
|
5
|
+
* fields the daemon and the store act on.
|
|
6
|
+
*/
|
|
7
|
+
export interface AdapterEvent {
|
|
8
|
+
kind: 'started' | 'needs-input' | 'turn-done' | 'prompt-submitted' | 'ended' | 'heartbeat';
|
|
9
|
+
agentSessionID?: AgentSessionID;
|
|
10
|
+
message?: string;
|
|
11
|
+
|
|
12
|
+
// Fuller activity text than message: what the agent last said or was
|
|
13
|
+
// asked, for briefing. Bounded by the adapter.
|
|
14
|
+
detail?: string;
|
|
15
|
+
|
|
16
|
+
// Opaque handle the adapter can later pull a session name from.
|
|
17
|
+
nameSource?: string;
|
|
18
|
+
|
|
19
|
+
// Claude resume-existence path. Distinct from nameSource: a naming
|
|
20
|
+
// handle is not a resume gate.
|
|
21
|
+
transcriptSource?: string;
|
|
22
|
+
|
|
23
|
+
// The agent's whole final message for a finished turn; detail holds a bounded preview of it.
|
|
24
|
+
result?: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionState } from './session-state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How many of the sessions are in each state.
|
|
5
|
+
*/
|
|
6
|
+
export function countSessionStates(
|
|
7
|
+
list: readonly { readonly state: SessionState }[],
|
|
8
|
+
): Record<SessionState, number> {
|
|
9
|
+
const c = { needs_you: 0, running: 0, done: 0, exited: 0 };
|
|
10
|
+
|
|
11
|
+
for (const s of list) {
|
|
12
|
+
c[s.state]++;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return c;
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One open protocol connection to a daemon, as a caller that does not own
|
|
3
|
+
* the transport drives it: the handshake, correlated requests, a callback
|
|
4
|
+
* for when the connection ends, and closing it.
|
|
5
|
+
*/
|
|
6
|
+
export interface DaemonChannel {
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
readonly sendHello: (build: string) => Promise<Readonly<Record<string, unknown>>>;
|
|
9
|
+
|
|
10
|
+
// A request with a principal acts as that principal.
|
|
11
|
+
readonly sendRequest: (
|
|
12
|
+
m: string,
|
|
13
|
+
p?: Readonly<Record<string, unknown>>,
|
|
14
|
+
as?: string,
|
|
15
|
+
) => Promise<Readonly<Record<string, unknown>>>;
|
|
16
|
+
readonly stop: () => void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionID } from '../shared/session-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One line a hook reporter sends: the atc session it reports on, the agent's
|
|
5
|
+
* hook event name, and the hook's payload as the agent gave it.
|
|
6
|
+
*/
|
|
7
|
+
export interface HookEvent {
|
|
8
|
+
atcId: SessionID;
|
|
9
|
+
event: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PINNED_GROUP_KEY } from './pinned-group-key';
|
|
2
|
+
import { sortSessionViews } from './sort-session-views';
|
|
3
|
+
import type { SortableSessionView } from './sortable-session-view';
|
|
4
|
+
|
|
5
|
+
// Overlay display order for the grouped view: the flat sort with each
|
|
6
|
+
// repository's sessions pulled together at the position of its best-ranked
|
|
7
|
+
// member, so the renderer's adjacency-based headers appear once per group.
|
|
8
|
+
// Pinned sessions form their own leading group. A sub-session keys by its
|
|
9
|
+
// parent, so a set never splits across groups.
|
|
10
|
+
export function sortGroupedSessionViews<
|
|
11
|
+
T extends SortableSessionView & { readonly repoRoot: string },
|
|
12
|
+
>(list: readonly T[]): T[] {
|
|
13
|
+
const byID = new Map(list.map((s) => [s.id, s]));
|
|
14
|
+
const buckets = new Map<string, T[]>();
|
|
15
|
+
|
|
16
|
+
for (const s of sortSessionViews(list)) {
|
|
17
|
+
const owner = (s.parent === null ? undefined : byID.get(s.parent)) ?? s;
|
|
18
|
+
const key = owner.pinned ? PINNED_GROUP_KEY : owner.repoRoot;
|
|
19
|
+
const bucket = buckets.get(key);
|
|
20
|
+
|
|
21
|
+
if (bucket === undefined) {
|
|
22
|
+
buckets.set(key, [s]);
|
|
23
|
+
} else {
|
|
24
|
+
bucket.push(s);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [...buckets.values()].flat();
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SessionState } from './session-state';
|
|
2
|
+
import type { SortableSessionView } from './sortable-session-view';
|
|
3
|
+
|
|
4
|
+
// Overlay order: pinned sessions first in most-recently-attached order, then
|
|
5
|
+
// everyone else by urgency — who needs you, finished turns, busy, dead —
|
|
6
|
+
// with most-recently-attached breaking ties inside each state. A
|
|
7
|
+
// sub-session sits directly under its parent, ranked among its siblings
|
|
8
|
+
// alone, so its attention never moves the parent's row; a sub-session whose
|
|
9
|
+
// parent is not listed ranks as a top-level row.
|
|
10
|
+
export function sortSessionViews<T extends SortableSessionView>(list: readonly T[]): T[] {
|
|
11
|
+
const rank: Record<SessionState, number> = {
|
|
12
|
+
needs_you: 0,
|
|
13
|
+
done: 1,
|
|
14
|
+
running: 2,
|
|
15
|
+
exited: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ranked = [...list].toSorted((a, b) => {
|
|
19
|
+
if (a.pinned !== b.pinned) {
|
|
20
|
+
return a.pinned ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const recency = b.lastAttachedAt - a.lastAttachedAt || b.createdAt - a.createdAt;
|
|
24
|
+
|
|
25
|
+
return a.pinned ? recency : rank[a.state] - rank[b.state] || recency;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const listed = new Set(ranked.map((s) => s.id));
|
|
29
|
+
|
|
30
|
+
const sorted: T[] = [];
|
|
31
|
+
|
|
32
|
+
for (const s of ranked) {
|
|
33
|
+
if (s.parent !== null && listed.has(s.parent)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
sorted.push(s, ...ranked.filter((child) => child.parent === s.id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return sorted;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionState } from './session-state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The fields of a session row that the overlay orders by.
|
|
5
|
+
*/
|
|
6
|
+
export interface SortableSessionView {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly parent: string | null;
|
|
9
|
+
readonly state: SessionState;
|
|
10
|
+
readonly pinned: boolean;
|
|
11
|
+
readonly lastAttachedAt: number;
|
|
12
|
+
readonly createdAt: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentID } from './agent-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Missing and empty values become Claude so a fleet written before the agent
|
|
5
|
+
* column still restores as Claude. Any other string is returned as it stands,
|
|
6
|
+
* registered or not: an id whose adapter is gone must reach the caller intact
|
|
7
|
+
* so the session can be shown and refused, never quietly run as Claude.
|
|
8
|
+
*/
|
|
9
|
+
export function toAgentID(raw: unknown): AgentID {
|
|
10
|
+
return typeof raw === 'string' && raw !== '' ? raw : 'claude';
|
|
11
|
+
}
|
package/src/store/fleet-entry.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import type { AgentID } from '../agents/agent-adapter';
|
|
2
|
+
import type { AgentID } from '../shared/agent-id';
|
|
4
3
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
5
4
|
import { buildOptionalBoolean } from '../shared/build-optional-boolean';
|
|
6
5
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
7
6
|
import type { DaemonID } from '../shared/daemon-id';
|
|
8
7
|
import { isRecord } from '../shared/report';
|
|
9
8
|
import type { SessionID } from '../shared/session-id';
|
|
9
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
10
10
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
11
11
|
import type { SessionWorkspace } from './workspace-materialization';
|
|
12
12
|
|
|
@@ -56,6 +56,8 @@ interface EventsTable {
|
|
|
56
56
|
|
|
57
57
|
interface SpawnHistoryTable {
|
|
58
58
|
cwd: string;
|
|
59
|
+
target: string;
|
|
60
|
+
target_identity: string;
|
|
59
61
|
last_spawn: number;
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -152,8 +154,9 @@ export interface StateStoreSchema {
|
|
|
152
154
|
// agent_session_id under its Claude-era name plus name and cwd, and each
|
|
153
155
|
// later step adds one column the daemon grew to depend on, until the
|
|
154
156
|
// rebuild that keys the table by the atc session id. events later gains
|
|
155
|
-
// columns of its own, as does messages,
|
|
156
|
-
//
|
|
157
|
+
// columns of its own, as does messages, spawn_history is rebuilt to key each
|
|
158
|
+
// directory by the target it was spawned on, and prefs has carried one shape
|
|
159
|
+
// since it was added.
|
|
157
160
|
const MIGRATIONS: Record<string, Migration> = {
|
|
158
161
|
'001_create_initial_schema': {
|
|
159
162
|
async up(db: Kysely<StateStoreSchema>) {
|
|
@@ -446,6 +449,29 @@ const MIGRATIONS: Record<string, Migration> = {
|
|
|
446
449
|
await db.schema.alterTable('events').addColumn('report_text', 'text').execute();
|
|
447
450
|
},
|
|
448
451
|
},
|
|
452
|
+
'024_rebuild_spawn_history_keyed_by_target': {
|
|
453
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
454
|
+
// The ledger records this step only after it returns, so a crash in
|
|
455
|
+
// between leaves a rebuilt table behind a ledger that still lacks it.
|
|
456
|
+
const columns = await sql<ColumnInfoRow>`PRAGMA table_info(spawn_history)`.execute(db);
|
|
457
|
+
|
|
458
|
+
if (columns.rows.some((column) => column.name === 'target')) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
await sql`BEGIN IMMEDIATE`.execute(db);
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
await updateSpawnHistoryKeyToTarget(db);
|
|
466
|
+
|
|
467
|
+
await sql`COMMIT`.execute(db);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
await sql`ROLLBACK`.execute(db);
|
|
470
|
+
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
},
|
|
449
475
|
};
|
|
450
476
|
|
|
451
477
|
const PROVIDER: MigrationProvider = {
|
|
@@ -457,8 +483,9 @@ const PROVIDER: MigrationProvider = {
|
|
|
457
483
|
* `Migrator`, one additive step at a time. A database from before the
|
|
458
484
|
* ladder existed is recognized at whichever shape it stopped at by a
|
|
459
485
|
* baselining pass that records the steps its columns already satisfy, so
|
|
460
|
-
* only what is genuinely missing runs.
|
|
461
|
-
*
|
|
486
|
+
* only what is genuinely missing runs. Two steps rebuild a table under a
|
|
487
|
+
* new key, the fleet and the spawn history, and carry every row across; no
|
|
488
|
+
* step drops a row.
|
|
462
489
|
*/
|
|
463
490
|
export async function runMigrations(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
464
491
|
// A baselined step can land out of order relative to a step that turned
|
|
@@ -652,6 +679,30 @@ interface ColumnInfoRow {
|
|
|
652
679
|
type: string;
|
|
653
680
|
}
|
|
654
681
|
|
|
682
|
+
// The identity of the local target with no options. Every spawn from before
|
|
683
|
+
// spawns recorded their target ran there.
|
|
684
|
+
const LEGACY_SPAWN_TARGET_IDENTITY = 'local-pty:44136fa355b3678a';
|
|
685
|
+
|
|
686
|
+
async function updateSpawnHistoryKeyToTarget(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
687
|
+
await sql`
|
|
688
|
+
CREATE TABLE spawn_history_rebuilt (
|
|
689
|
+
cwd TEXT NOT NULL,
|
|
690
|
+
target TEXT NOT NULL,
|
|
691
|
+
target_identity TEXT NOT NULL,
|
|
692
|
+
last_spawn INTEGER NOT NULL,
|
|
693
|
+
PRIMARY KEY (cwd, target, target_identity)
|
|
694
|
+
)
|
|
695
|
+
`.execute(db);
|
|
696
|
+
|
|
697
|
+
await sql`
|
|
698
|
+
INSERT INTO spawn_history_rebuilt (cwd, target, target_identity, last_spawn)
|
|
699
|
+
SELECT cwd, 'local', ${LEGACY_SPAWN_TARGET_IDENTITY}, last_spawn FROM spawn_history
|
|
700
|
+
`.execute(db);
|
|
701
|
+
|
|
702
|
+
await sql`DROP TABLE spawn_history`.execute(db);
|
|
703
|
+
await sql`ALTER TABLE spawn_history_rebuilt RENAME TO spawn_history`.execute(db);
|
|
704
|
+
}
|
|
705
|
+
|
|
655
706
|
async function collectFleetColumns(db: Kysely<StateStoreSchema>): Promise<ReadonlySet<string>> {
|
|
656
707
|
const result = await sql<ColumnInfoRow>`PRAGMA table_info(fleet)`.execute(db);
|
|
657
708
|
|
package/src/store/state-store.ts
CHANGED
|
@@ -3,19 +3,20 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, sql } from 'kysely';
|
|
5
5
|
import type { Expression, ExpressionBuilder, SqlBool, Transaction } from 'kysely';
|
|
6
|
-
import {
|
|
7
|
-
import type { AdapterEvent, AgentID } from '../agents/agent-adapter';
|
|
8
|
-
import type { HookEvent } from '../daemon/hooks';
|
|
6
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
9
7
|
import { DaemonError } from '../protocol/daemon-error';
|
|
8
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
9
|
+
import type { AgentID } from '../shared/agent-id';
|
|
10
10
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
11
|
+
import { BunSqliteDriver } from '../shared/bun-sqlite-driver';
|
|
11
12
|
import type { DaemonID } from '../shared/daemon-id';
|
|
12
13
|
import type { MessageID } from '../shared/message-id';
|
|
13
14
|
import type { SessionID } from '../shared/session-id';
|
|
15
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
14
16
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
15
17
|
import { toDaemonID } from '../shared/to-daemon-id';
|
|
16
18
|
import { toMessageID } from '../shared/to-message-id';
|
|
17
19
|
import { toSessionID } from '../shared/to-session-id';
|
|
18
|
-
import { BunSqliteDriver } from './bun-sqlite-driver';
|
|
19
20
|
import { parseFleetEntry } from './fleet-entry';
|
|
20
21
|
import type { FleetEntry, FleetEntryUpdate, LegacyFleetEntry } from './fleet-entry';
|
|
21
22
|
import type { EffectTarget, IdempotencyClaim, IdempotencyRecord } from './idempotency-record';
|
|
@@ -63,6 +64,27 @@ export interface StoredEvent {
|
|
|
63
64
|
readonly label?: string;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// The target a spawn ran on: its name and the identity the spawned session
|
|
68
|
+
// was bound to there.
|
|
69
|
+
interface SpawnTarget {
|
|
70
|
+
readonly target: string;
|
|
71
|
+
readonly targetIdentity: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A directory a spawn ran in and the target it ran on.
|
|
75
|
+
export interface SpawnDir {
|
|
76
|
+
readonly cwd: string;
|
|
77
|
+
readonly grant: SpawnTarget;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Another message one turn answered, and the atc id it was sent to.
|
|
82
|
+
*/
|
|
83
|
+
export interface TurnSibling {
|
|
84
|
+
readonly id: MessageID;
|
|
85
|
+
readonly atcID: SessionID;
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
/**
|
|
67
89
|
* One report as the trail holds it. A report recorded before the trail kept
|
|
68
90
|
* whole texts has only its preview, so its text is that preview and
|
|
@@ -512,24 +534,38 @@ export class StateStore {
|
|
|
512
534
|
return row?.ts === null || row?.ts === undefined ? null : Date.parse(row.ts);
|
|
513
535
|
}
|
|
514
536
|
|
|
515
|
-
async recordSpawnDir(cwd: string): Promise<void> {
|
|
537
|
+
async recordSpawnDir(cwd: string, grant: SpawnTarget): Promise<void> {
|
|
516
538
|
await this.db
|
|
517
539
|
.insertInto('spawn_history')
|
|
518
|
-
.values({
|
|
540
|
+
.values({
|
|
541
|
+
cwd,
|
|
542
|
+
target: grant.target,
|
|
543
|
+
target_identity: grant.targetIdentity,
|
|
544
|
+
last_spawn: Date.now(),
|
|
545
|
+
})
|
|
519
546
|
.onConflict((oc) =>
|
|
520
|
-
oc
|
|
547
|
+
oc
|
|
548
|
+
.columns(['cwd', 'target', 'target_identity'])
|
|
549
|
+
.doUpdateSet((eb) => ({ last_spawn: eb.ref('excluded.last_spawn') })),
|
|
521
550
|
)
|
|
522
551
|
.execute();
|
|
523
552
|
}
|
|
524
553
|
|
|
525
|
-
|
|
554
|
+
/**
|
|
555
|
+
* Each directory a spawn ran in, with the target it ran on, most recent
|
|
556
|
+
* first. A directory spawned on several targets has one entry for each.
|
|
557
|
+
*/
|
|
558
|
+
async collectSpawnDirs(): Promise<SpawnDir[]> {
|
|
526
559
|
const rows = await this.db
|
|
527
560
|
.selectFrom('spawn_history')
|
|
528
|
-
.select('cwd')
|
|
561
|
+
.select(['cwd', 'target', 'target_identity'])
|
|
529
562
|
.orderBy('last_spawn', 'desc')
|
|
530
563
|
.execute();
|
|
531
564
|
|
|
532
|
-
return rows.map((row) =>
|
|
565
|
+
return rows.map((row) => ({
|
|
566
|
+
cwd: row.cwd,
|
|
567
|
+
grant: { target: row.target, targetIdentity: row.target_identity },
|
|
568
|
+
}));
|
|
533
569
|
}
|
|
534
570
|
|
|
535
571
|
async loadLastUsedAgent(): Promise<AgentID> {
|
|
@@ -606,7 +642,7 @@ export class StateStore {
|
|
|
606
642
|
|
|
607
643
|
// The other messages of the same session the given message's turn
|
|
608
644
|
// answered, oldest first; none when the message has no turn.
|
|
609
|
-
async collectTurnSiblings(record: MessageRecord): Promise<
|
|
645
|
+
async collectTurnSiblings(record: MessageRecord): Promise<TurnSibling[]> {
|
|
610
646
|
if (record.turn === undefined) {
|
|
611
647
|
return [];
|
|
612
648
|
}
|
|
@@ -618,7 +654,7 @@ export class StateStore {
|
|
|
618
654
|
|
|
619
655
|
const rows = await this.db
|
|
620
656
|
.selectFrom('messages')
|
|
621
|
-
.select('id')
|
|
657
|
+
.select(['id', 'atc_id'])
|
|
622
658
|
.where('turn_id', '=', record.turn)
|
|
623
659
|
.where('id', '!=', record.id)
|
|
624
660
|
.where((eb) => buildOwnerFilter(eb, owner))
|
|
@@ -626,7 +662,7 @@ export class StateStore {
|
|
|
626
662
|
.orderBy(sql`rowid`, 'asc')
|
|
627
663
|
.execute();
|
|
628
664
|
|
|
629
|
-
return rows.map((row) => toMessageID(row.id));
|
|
665
|
+
return rows.map((row) => ({ id: toMessageID(row.id), atcID: toSessionID(row.atc_id) }));
|
|
630
666
|
}
|
|
631
667
|
|
|
632
668
|
async updateMessageDelivered(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|