@zgeoff/atc 0.1.12 → 1.0.1
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 +50 -12
- package/package.json +7 -2
- package/src/{agent-adapter.ts → agents/agent-adapter.ts} +31 -14
- package/src/{build-cli-command.ts → agents/build-cli-command.ts} +1 -1
- package/src/agents/build-hook-settings.ts +59 -0
- package/src/{claude-adapter.ts → agents/claude-adapter.ts} +41 -80
- package/src/{codex-adapter.ts → agents/codex-adapter.ts} +41 -30
- package/src/agents/gateway-adapter.ts +106 -0
- package/src/{grok-adapter.ts → agents/grok-adapter.ts} +83 -46
- package/src/agents/resolve-agent-home.ts +13 -0
- package/src/agents/truncate-detail.ts +7 -0
- package/src/agents/write-hook-settings.ts +37 -0
- package/src/cli.ts +25 -14
- package/src/{boot-daemon.ts → client/boot-daemon.ts} +8 -7
- package/src/client/build-client-machine.ts +119 -0
- package/src/client/collect-agent-picks.ts +31 -0
- package/src/{daemon-client.ts → client/daemon-client.ts} +4 -4
- package/src/{dirs.ts → client/dirs.ts} +8 -0
- package/src/client/format-overlay-agent-mark.ts +15 -0
- package/src/{index.ts → client/index.ts} +181 -550
- package/src/client/keys.ts +111 -0
- package/src/client/parse-daemon-event.ts +114 -0
- package/src/{pick-tab-target.ts → client/pick-tab-target.ts} +2 -2
- package/src/client/plan-vacated-rows.ts +26 -0
- package/src/client/spawn-picker.ts +305 -0
- package/src/client/to-mirror-session.ts +65 -0
- package/src/{ui.ts → client/ui.ts} +27 -8
- package/src/{attach-registry.ts → daemon/attach-registry.ts} +12 -10
- package/src/daemon/build-session-event.ts +43 -0
- package/src/{daemon-connection.ts → daemon/daemon-connection.ts} +194 -108
- package/src/{daemon.ts → daemon/daemon.ts} +148 -313
- package/src/{hooks.ts → daemon/hooks.ts} +6 -4
- package/src/daemon/mint-session-id.ts +12 -0
- package/src/{permission-registry.ts → daemon/permission-registry.ts} +5 -3
- package/src/daemon/restore-fleet.ts +141 -0
- package/src/daemon/run-eject-handoff.ts +33 -0
- package/src/{screen-model.ts → daemon/screen-model.ts} +1 -1
- package/src/daemon/session-runtime.ts +64 -0
- package/src/{sessions.ts → daemon/sessions.ts} +77 -133
- package/src/{start-headless-run.ts → daemon/start-headless-run.ts} +9 -3
- package/src/daemon/start-headless-turn.ts +57 -0
- package/src/hook-report.ts +2 -2
- package/src/mcp-server.ts +33 -44
- package/src/protocol/parse-request-params.ts +31 -0
- package/src/{protocol.ts → protocol/protocol.ts} +1 -1
- package/src/protocol/request-param-schemas.ts +109 -0
- package/src/shared/agent-session-id.ts +8 -0
- package/src/shared/build-optional-boolean.ts +10 -0
- package/src/shared/build-optional-string-array.ts +14 -0
- package/src/shared/build-optional-string.ts +10 -0
- package/src/shared/collect-gateways.ts +113 -0
- package/src/{config.ts → shared/config.ts} +47 -31
- package/src/{get-build.ts → shared/get-build.ts} +21 -6
- package/src/shared/get-record.ts +20 -0
- package/src/shared/session-id.ts +8 -0
- package/src/shared/to-agent-session-id.ts +12 -0
- package/src/shared/to-session-id.ts +11 -0
- package/src/shared/to-shell-arg.ts +7 -0
- package/src/statusline.ts +2 -2
- package/src/store/bun-sqlite-driver.ts +96 -0
- package/src/store/fleet-entry.ts +72 -0
- package/src/store/run-migrations.ts +242 -0
- package/src/store/state-store.ts +226 -0
- package/src/format-overlay-agent-mark.ts +0 -17
- package/src/state-store.ts +0 -236
- /package/src/{normalize-hook-event.ts → agents/normalize-hook-event.ts} +0 -0
- /package/src/{print-codex-hook-file.ts → agents/print-codex-hook-file.ts} +0 -0
- /package/src/{print-grok-hook-file.ts → agents/print-grok-hook-file.ts} +0 -0
- /package/src/{build-leader-chords.ts → client/build-leader-chords.ts} +0 -0
- /package/src/{daemon-error.ts → protocol/daemon-error.ts} +0 -0
- /package/src/{outbound-queue.ts → protocol/outbound-queue.ts} +0 -0
- /package/src/{collect-clean-env.ts → shared/collect-clean-env.ts} +0 -0
- /package/src/{report.ts → shared/report.ts} +0 -0
- /package/src/{reset-input-modes.ts → shared/reset-input-modes.ts} +0 -0
- /package/src/{resolve-repo-root.ts → shared/resolve-repo-root.ts} +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import type { Generated, Kysely } from 'kysely';
|
|
3
|
+
import { DEFAULT_MIGRATION_TABLE, Migrator } from 'kysely/migration';
|
|
4
|
+
import type { Migration, MigrationProvider, MigrationResultSet } from 'kysely/migration';
|
|
5
|
+
|
|
6
|
+
interface FleetTable {
|
|
7
|
+
agent_session_id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
pinned: number;
|
|
11
|
+
last_attached: number | null;
|
|
12
|
+
agent: string;
|
|
13
|
+
exited: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface EventsTable {
|
|
17
|
+
id: Generated<number>;
|
|
18
|
+
ts: string;
|
|
19
|
+
atc_id: string;
|
|
20
|
+
event: string;
|
|
21
|
+
message: string | null;
|
|
22
|
+
session_id: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SpawnHistoryTable {
|
|
26
|
+
cwd: string;
|
|
27
|
+
last_spawn: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PrefsTable {
|
|
31
|
+
key: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The state store's schema: what the query builder and the migration
|
|
37
|
+
* ladder both build against.
|
|
38
|
+
*/
|
|
39
|
+
export interface StateStoreSchema {
|
|
40
|
+
fleet: FleetTable;
|
|
41
|
+
events: EventsTable;
|
|
42
|
+
spawn_history: SpawnHistoryTable;
|
|
43
|
+
prefs: PrefsTable;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Every shape the fleet table has shipped with: the oldest carries only
|
|
47
|
+
// agent_session_id under its Claude-era name plus name and cwd, and each
|
|
48
|
+
// later step adds one column the daemon grew to depend on. events,
|
|
49
|
+
// spawn_history, and prefs have carried one shape since they were added, so
|
|
50
|
+
// they need only their initial creation.
|
|
51
|
+
const MIGRATIONS: Record<string, Migration> = {
|
|
52
|
+
'001_create_initial_schema': {
|
|
53
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
54
|
+
await db.schema
|
|
55
|
+
.createTable('fleet')
|
|
56
|
+
.ifNotExists()
|
|
57
|
+
.addColumn('claude_id', 'text', (c) => c.primaryKey())
|
|
58
|
+
.addColumn('name', 'text', (c) => c.notNull())
|
|
59
|
+
.addColumn('cwd', 'text', (c) => c.notNull())
|
|
60
|
+
.execute();
|
|
61
|
+
|
|
62
|
+
await db.schema
|
|
63
|
+
.createTable('events')
|
|
64
|
+
.ifNotExists()
|
|
65
|
+
.addColumn('id', 'integer', (c) => c.primaryKey().autoIncrement())
|
|
66
|
+
.addColumn('ts', 'text', (c) => c.notNull())
|
|
67
|
+
.addColumn('atc_id', 'text', (c) => c.notNull())
|
|
68
|
+
.addColumn('event', 'text', (c) => c.notNull())
|
|
69
|
+
.addColumn('message', 'text')
|
|
70
|
+
.addColumn('session_id', 'text')
|
|
71
|
+
.execute();
|
|
72
|
+
|
|
73
|
+
await db.schema
|
|
74
|
+
.createTable('spawn_history')
|
|
75
|
+
.ifNotExists()
|
|
76
|
+
.addColumn('cwd', 'text', (c) => c.primaryKey())
|
|
77
|
+
.addColumn('last_spawn', 'integer', (c) => c.notNull())
|
|
78
|
+
.execute();
|
|
79
|
+
|
|
80
|
+
await db.schema
|
|
81
|
+
.createTable('prefs')
|
|
82
|
+
.ifNotExists()
|
|
83
|
+
.addColumn('key', 'text', (c) => c.primaryKey())
|
|
84
|
+
.addColumn('value', 'text', (c) => c.notNull())
|
|
85
|
+
.execute();
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
'002_rename_fleet_claude_id_to_agent_session_id': {
|
|
89
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
90
|
+
await db.schema.alterTable('fleet').renameColumn('claude_id', 'agent_session_id').execute();
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
'003_add_fleet_pinned': {
|
|
94
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
95
|
+
await db.schema
|
|
96
|
+
.alterTable('fleet')
|
|
97
|
+
.addColumn('pinned', 'integer', (c) => c.notNull().defaultTo(0))
|
|
98
|
+
.execute();
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
'004_add_fleet_last_attached': {
|
|
102
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
103
|
+
await db.schema.alterTable('fleet').addColumn('last_attached', 'integer').execute();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
'005_add_fleet_agent': {
|
|
107
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
108
|
+
await db.schema
|
|
109
|
+
.alterTable('fleet')
|
|
110
|
+
.addColumn('agent', 'text', (c) => c.notNull().defaultTo('claude'))
|
|
111
|
+
.execute();
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
'006_add_fleet_exited': {
|
|
115
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
116
|
+
await db.schema
|
|
117
|
+
.alterTable('fleet')
|
|
118
|
+
.addColumn('exited', 'integer', (c) => c.notNull().defaultTo(0))
|
|
119
|
+
.execute();
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const PROVIDER: MigrationProvider = {
|
|
125
|
+
getMigrations: () => Promise.resolve(MIGRATIONS),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Brings a state-store database up to the current schema through kysely's
|
|
130
|
+
* `Migrator`, one additive step at a time. A database from before the
|
|
131
|
+
* ladder existed is recognized at whichever shape it stopped at by a
|
|
132
|
+
* baselining pass that records the steps its columns already satisfy, so
|
|
133
|
+
* only what is genuinely missing runs. Nothing here ever drops a column or
|
|
134
|
+
* a row.
|
|
135
|
+
*/
|
|
136
|
+
export async function runMigrations(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
137
|
+
// A baselined step can land out of order relative to a step that turned
|
|
138
|
+
// out to still be missing beside it, so the ledger the baselining pass
|
|
139
|
+
// writes is not guaranteed to be a contiguous prefix of the ladder.
|
|
140
|
+
const migrator = new Migrator({ db, provider: PROVIDER, allowUnorderedMigrations: true });
|
|
141
|
+
|
|
142
|
+
// Baselining writes straight into the ledger, so the ledger has to exist
|
|
143
|
+
// first, and only the opening step can create it. A database that already
|
|
144
|
+
// has a ledger has been baselined once and goes straight to the latest
|
|
145
|
+
// step — running the opening step against a full ledger would read as a
|
|
146
|
+
// request to migrate back down to it.
|
|
147
|
+
if (!(await hasMigrationLedger(db))) {
|
|
148
|
+
const opened = await migrator.migrateTo('001_create_initial_schema');
|
|
149
|
+
|
|
150
|
+
requireMigrated(opened);
|
|
151
|
+
|
|
152
|
+
await recordLegacyBaseline(db);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const latest = await migrator.migrateToLatest();
|
|
156
|
+
|
|
157
|
+
requireMigrated(latest);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function hasMigrationLedger(db: Kysely<StateStoreSchema>): Promise<boolean> {
|
|
161
|
+
const result = await sql<{
|
|
162
|
+
name: string;
|
|
163
|
+
}>`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${DEFAULT_MIGRATION_TABLE}`.execute(
|
|
164
|
+
db,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return result.rows.length > 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requireMigrated(result: MigrationResultSet): void {
|
|
171
|
+
if (result.error === undefined) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (result.error instanceof Error) {
|
|
176
|
+
throw result.error;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new Error('kysely migration failed', { cause: result.error });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The migrator's own ledger has no concept of a database that already
|
|
183
|
+
// carries a later step's outcome from before the ladder existed. Recording
|
|
184
|
+
// those steps here, straight into the ledger, is what lets the migrator run
|
|
185
|
+
// only the steps a legacy database's fleet table genuinely still needs.
|
|
186
|
+
async function recordLegacyBaseline(db: Kysely<StateStoreSchema>): Promise<void> {
|
|
187
|
+
const columns = await collectFleetColumns(db);
|
|
188
|
+
|
|
189
|
+
const steps = pickBaselineSteps(columns);
|
|
190
|
+
|
|
191
|
+
if (steps.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const appliedAt = new Date().toISOString();
|
|
196
|
+
|
|
197
|
+
for (const name of steps) {
|
|
198
|
+
await sql`INSERT OR IGNORE INTO ${sql.table(DEFAULT_MIGRATION_TABLE)} (name, timestamp) VALUES (${name}, ${appliedAt})`.execute(
|
|
199
|
+
db,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface ColumnInfoRow {
|
|
205
|
+
name: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function collectFleetColumns(db: Kysely<StateStoreSchema>): Promise<ReadonlySet<string>> {
|
|
209
|
+
const result = await sql<ColumnInfoRow>`PRAGMA table_info(fleet)`.execute(db);
|
|
210
|
+
|
|
211
|
+
return new Set(result.rows.map((row) => row.name));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Each step is judged solely by whether its own column is already there —
|
|
215
|
+
// never by whether an earlier step's column is — because a database from
|
|
216
|
+
// before the ladder existed can carry any one of these columns without the
|
|
217
|
+
// others.
|
|
218
|
+
function pickBaselineSteps(columns: ReadonlySet<string>): readonly string[] {
|
|
219
|
+
const steps: string[] = [];
|
|
220
|
+
|
|
221
|
+
if (columns.has('agent_session_id')) {
|
|
222
|
+
steps.push('002_rename_fleet_claude_id_to_agent_session_id');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (columns.has('pinned')) {
|
|
226
|
+
steps.push('003_add_fleet_pinned');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (columns.has('last_attached')) {
|
|
230
|
+
steps.push('004_add_fleet_last_attached');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (columns.has('agent')) {
|
|
234
|
+
steps.push('005_add_fleet_agent');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (columns.has('exited')) {
|
|
238
|
+
steps.push('006_add_fleet_exited');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return steps;
|
|
242
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, sql } from 'kysely';
|
|
4
|
+
import { toAgentID } from '../agents/agent-adapter';
|
|
5
|
+
import type { AgentID } from '../agents/agent-adapter';
|
|
6
|
+
import type { HookEvent } from '../daemon/hooks';
|
|
7
|
+
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
8
|
+
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
9
|
+
import { BunSqliteDriver } from './bun-sqlite-driver';
|
|
10
|
+
import { parseFleetEntry } from './fleet-entry';
|
|
11
|
+
import type { FleetEntry } from './fleet-entry';
|
|
12
|
+
import { runMigrations } from './run-migrations';
|
|
13
|
+
import type { StateStoreSchema } from './run-migrations';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Daemon state in one SQLite store: the restorable fleet, the hook-event
|
|
17
|
+
* debug trail, and the spawn-directory history. The statusline contract
|
|
18
|
+
* file (status.json) stays a plain file because reporters inside wrangled
|
|
19
|
+
* sessions read it without speaking to the daemon. An existing fleet.json
|
|
20
|
+
* seeds the fleet table once, so upgrading never loses a restorable fleet.
|
|
21
|
+
* Every query runs through kysely against the schema the migration ladder
|
|
22
|
+
* maintains, executed by a driver over the one bun:sqlite connection this
|
|
23
|
+
* store owns. That makes every method here asynchronous; the driver's
|
|
24
|
+
* connection mutex is what keeps every write ordered regardless.
|
|
25
|
+
*/
|
|
26
|
+
export class StateStore {
|
|
27
|
+
private readonly sqlite: Database;
|
|
28
|
+
|
|
29
|
+
private readonly db: Kysely<StateStoreSchema>;
|
|
30
|
+
|
|
31
|
+
private constructor(sqlite: Database, db: Kysely<StateStoreSchema>) {
|
|
32
|
+
this.sqlite = sqlite;
|
|
33
|
+
this.db = db;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Migrations run statements that cannot happen inside a constructor, so
|
|
37
|
+
// opening a store is this factory instead of `new`.
|
|
38
|
+
static async open(dbPath: string, legacyFleetPath?: string): Promise<StateStore> {
|
|
39
|
+
const sqlite = new Database(dbPath, { create: true });
|
|
40
|
+
|
|
41
|
+
sqlite.run('PRAGMA journal_mode = WAL;');
|
|
42
|
+
|
|
43
|
+
const db = new Kysely<StateStoreSchema>({
|
|
44
|
+
dialect: {
|
|
45
|
+
createAdapter: () => new SqliteAdapter(),
|
|
46
|
+
createDriver: () => new BunSqliteDriver(sqlite),
|
|
47
|
+
createIntrospector: (kysely) => new SqliteIntrospector(kysely),
|
|
48
|
+
createQueryCompiler: () => new SqliteQueryCompiler(),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await runMigrations(db);
|
|
53
|
+
|
|
54
|
+
const store = new StateStore(sqlite, db);
|
|
55
|
+
|
|
56
|
+
if (legacyFleetPath !== undefined) {
|
|
57
|
+
await store.adoptLegacyFleet(legacyFleetPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return store;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async loadFleet(): Promise<FleetEntry[]> {
|
|
64
|
+
const rows = await this.db
|
|
65
|
+
.selectFrom('fleet')
|
|
66
|
+
.select(['agent_session_id', 'name', 'cwd', 'pinned', 'last_attached', 'agent', 'exited'])
|
|
67
|
+
.execute();
|
|
68
|
+
|
|
69
|
+
const entries: FleetEntry[] = [];
|
|
70
|
+
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
entries.push({
|
|
73
|
+
agentSessionID: toAgentSessionID(row.agent_session_id),
|
|
74
|
+
name: row.name,
|
|
75
|
+
cwd: row.cwd,
|
|
76
|
+
agent: toAgentID(row.agent),
|
|
77
|
+
...(row.pinned === 0 ? {} : { pinned: true }),
|
|
78
|
+
...(row.last_attached === null ? {} : { lastAttachedAt: row.last_attached }),
|
|
79
|
+
...(row.exited === 0 ? {} : { exited: true }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The latest hook-event timestamp per agent session id, from the event
|
|
87
|
+
// trail. Sessions that never reported an event are absent.
|
|
88
|
+
async collectFleetRecency(): Promise<Map<AgentSessionID, string>> {
|
|
89
|
+
const rows = await this.db
|
|
90
|
+
.selectFrom('events')
|
|
91
|
+
.select(['session_id', sql<string>`MAX(ts)`.as('ts')])
|
|
92
|
+
.where('session_id', 'is not', null)
|
|
93
|
+
.groupBy('session_id')
|
|
94
|
+
.execute();
|
|
95
|
+
|
|
96
|
+
const recency = new Map<AgentSessionID, string>();
|
|
97
|
+
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
if (row.session_id === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
recency.set(toAgentSessionID(row.session_id), row.ts);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return recency;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async writeFleet(entries: readonly FleetEntry[]): Promise<void> {
|
|
110
|
+
await this.db.transaction().execute(async (trx) => {
|
|
111
|
+
await trx.deleteFrom('fleet').execute();
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
await trx
|
|
115
|
+
.insertInto('fleet')
|
|
116
|
+
.values({
|
|
117
|
+
agent_session_id: entry.agentSessionID,
|
|
118
|
+
name: entry.name,
|
|
119
|
+
cwd: entry.cwd,
|
|
120
|
+
pinned: entry.pinned === true ? 1 : 0,
|
|
121
|
+
last_attached: entry.lastAttachedAt ?? null,
|
|
122
|
+
agent: entry.agent,
|
|
123
|
+
exited: entry.exited === true ? 1 : 0,
|
|
124
|
+
})
|
|
125
|
+
.orReplace()
|
|
126
|
+
.execute();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async recordEvent(e: HookEvent): Promise<void> {
|
|
132
|
+
const rawMessage = e.payload['message'];
|
|
133
|
+
const rawSessionID = e.payload['session_id'] ?? e.payload['sessionId'];
|
|
134
|
+
const message = typeof rawMessage === 'string' ? rawMessage : null;
|
|
135
|
+
const sessionID = typeof rawSessionID === 'string' ? rawSessionID : null;
|
|
136
|
+
const atcID: string = e.atcId;
|
|
137
|
+
|
|
138
|
+
await this.db
|
|
139
|
+
.insertInto('events')
|
|
140
|
+
.values({
|
|
141
|
+
ts: new Date().toISOString(),
|
|
142
|
+
atc_id: atcID,
|
|
143
|
+
event: e.event,
|
|
144
|
+
message,
|
|
145
|
+
session_id: sessionID,
|
|
146
|
+
})
|
|
147
|
+
.execute();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async recordSpawnDir(cwd: string): Promise<void> {
|
|
151
|
+
await this.db
|
|
152
|
+
.insertInto('spawn_history')
|
|
153
|
+
.values({ cwd, last_spawn: Date.now() })
|
|
154
|
+
.onConflict((oc) =>
|
|
155
|
+
oc.column('cwd').doUpdateSet((eb) => ({ last_spawn: eb.ref('excluded.last_spawn') })),
|
|
156
|
+
)
|
|
157
|
+
.execute();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async collectSpawnDirs(): Promise<string[]> {
|
|
161
|
+
const rows = await this.db
|
|
162
|
+
.selectFrom('spawn_history')
|
|
163
|
+
.select('cwd')
|
|
164
|
+
.orderBy('last_spawn', 'desc')
|
|
165
|
+
.execute();
|
|
166
|
+
|
|
167
|
+
return rows.map((row) => row.cwd);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async loadLastUsedAgent(): Promise<AgentID> {
|
|
171
|
+
const row = await this.db
|
|
172
|
+
.selectFrom('prefs')
|
|
173
|
+
.select('value')
|
|
174
|
+
.where('key', '=', 'last_used_agent')
|
|
175
|
+
.executeTakeFirst();
|
|
176
|
+
|
|
177
|
+
return toAgentID(row?.value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async writeLastUsedAgent(agent: AgentID): Promise<void> {
|
|
181
|
+
await this.db
|
|
182
|
+
.insertInto('prefs')
|
|
183
|
+
.values({ key: 'last_used_agent', value: agent })
|
|
184
|
+
.onConflict((oc) =>
|
|
185
|
+
oc.column('key').doUpdateSet((eb) => ({ value: eb.ref('excluded.value') })),
|
|
186
|
+
)
|
|
187
|
+
.execute();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async stop(): Promise<void> {
|
|
191
|
+
await this.db.destroy();
|
|
192
|
+
|
|
193
|
+
this.sqlite.close();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async adoptLegacyFleet(legacyFleetPath: string): Promise<void> {
|
|
197
|
+
const count = await this.db
|
|
198
|
+
.selectFrom('fleet')
|
|
199
|
+
.select(this.db.fn.countAll<number>().as('n'))
|
|
200
|
+
.executeTakeFirst();
|
|
201
|
+
|
|
202
|
+
if (count === undefined || count.n > 0 || !existsSync(legacyFleetPath)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const parsed: unknown = JSON.parse(readFileSync(legacyFleetPath, 'utf8'));
|
|
208
|
+
|
|
209
|
+
if (!Array.isArray(parsed)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const entries: FleetEntry[] = [];
|
|
214
|
+
|
|
215
|
+
for (const entry of parsed) {
|
|
216
|
+
const parsedEntry = parseFleetEntry(entry);
|
|
217
|
+
|
|
218
|
+
if (parsedEntry !== undefined) {
|
|
219
|
+
entries.push(parsedEntry);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.writeFleet(entries);
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { AgentKind } from './agent-adapter';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The reserved overlay column after unread: a `g` for Grok, a space for
|
|
5
|
-
* Claude, so session names stay aligned across a mixed fleet.
|
|
6
|
-
*/
|
|
7
|
-
export function formatOverlayAgentMark(agent: AgentKind): string {
|
|
8
|
-
if (agent === 'grok') {
|
|
9
|
-
return 'g';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (agent === 'codex') {
|
|
13
|
-
return 'x';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return ' ';
|
|
17
|
-
}
|