@zgeoff/atc 2.33.0 → 2.35.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 +14 -2
- package/src/agents/build-claude-bundle-settings.ts +91 -0
- package/src/agents/build-claude-guest-launch.ts +21 -6
- package/src/agents/claude-adapter.ts +45 -9
- package/src/agents/claude-config-bundle-folder.ts +8 -0
- package/src/agents/load-claude-config-bundle.ts +169 -0
- package/src/cli.ts +2 -0
- package/src/client/index.ts +8 -0
- package/src/client/spawn-picker.ts +58 -6
- package/src/daemon/build-default-workspace-dir.ts +70 -0
- package/src/daemon/build-tar-archive.ts +33 -8
- package/src/daemon/daemon-connection.ts +28 -5
- package/src/daemon/daemon-context.ts +13 -0
- package/src/daemon/daemon.ts +77 -5
- package/src/daemon/materialize-workspace.ts +83 -11
- package/src/daemon/restore-fleet.ts +66 -2
- package/src/daemon/sessions.ts +80 -13
- package/src/mcp/mcp-tools.ts +8 -2
- package/src/mcp/require-daemon-features.ts +2 -0
- package/src/mcp/run-tool.ts +16 -1
- package/src/protocol/daemon-features.ts +8 -0
- package/src/protocol/request-param-schemas.ts +13 -1
- package/src/{client → shared}/build-workspace-destination.ts +19 -8
- package/src/shared/config.ts +9 -0
- package/src/statusline.ts +4 -3
- package/src/store/fleet-entry.ts +5 -0
- package/src/store/run-migrations.ts +12 -0
- package/src/store/state-store.ts +35 -0
- package/src/store/workspace-materialization.ts +4 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
interface DestinationParts {
|
|
2
|
-
// The root the checkout lands under
|
|
2
|
+
// The root the checkout lands under: absolute on its target, or relative
|
|
3
|
+
// to the target user's home, where an empty root is the home itself.
|
|
3
4
|
readonly root: string;
|
|
4
5
|
|
|
5
6
|
// The repository URL the checkout comes from, as the daemon resolved it.
|
|
@@ -7,15 +8,18 @@ interface DestinationParts {
|
|
|
7
8
|
|
|
8
9
|
// The branch or tag the commit was chosen by, or null for a bare commit.
|
|
9
10
|
readonly ref: string | null;
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
// The commit checked out, or null when only the ref picks it.
|
|
13
|
+
readonly sha: string | null;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* The default directory a git workspace lands in: `<repo>-<ref>-<short
|
|
15
|
-
* sha>` under the root, with the ref left out for a bare commit
|
|
16
|
-
* is the URL's last path segment
|
|
17
|
-
* `[A-Za-z0-9._-]`, with `/`
|
|
18
|
-
* segment on any target
|
|
18
|
+
* sha>` under the root, with the ref left out for a bare commit and the
|
|
19
|
+
* short sha for a ref alone. The repo is the URL's last path segment
|
|
20
|
+
* without `.git`, and each part keeps only `[A-Za-z0-9._-]`, with `/`
|
|
21
|
+
* turned into `-`, so the name is one path segment on any target; a name
|
|
22
|
+
* with nothing left is `workspace`.
|
|
19
23
|
*/
|
|
20
24
|
export function buildWorkspaceDestination(parts: DestinationParts): string {
|
|
21
25
|
const repo =
|
|
@@ -25,12 +29,19 @@ export function buildWorkspaceDestination(parts: DestinationParts): string {
|
|
|
25
29
|
.at(-1)
|
|
26
30
|
?.replace(/\.git$/u, '') ?? '';
|
|
27
31
|
|
|
28
|
-
const name = [
|
|
32
|
+
const name = [
|
|
33
|
+
repo,
|
|
34
|
+
...(parts.ref === null ? [] : [parts.ref]),
|
|
35
|
+
...(parts.sha === null ? [] : [parts.sha.slice(0, 7)]),
|
|
36
|
+
]
|
|
29
37
|
.map((part) => toSafeSegment(part))
|
|
30
38
|
.filter((part) => part !== '')
|
|
31
39
|
.join('-');
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
const segment = name === '' ? 'workspace' : name;
|
|
42
|
+
const root = parts.root.replace(/\/+$/u, '');
|
|
43
|
+
|
|
44
|
+
return parts.root === '' ? segment : `${root}/${segment}`;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
function toSafeSegment(part: string): string {
|
package/src/shared/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { buildOptionalBoolean } from './build-optional-boolean';
|
|
4
5
|
import { buildOptionalString } from './build-optional-string';
|
|
5
6
|
import { buildOptionalStringArray } from './build-optional-string-array';
|
|
6
7
|
import { collectAuthProfiles } from './collect-auth-profiles';
|
|
@@ -65,6 +66,11 @@ export interface Config {
|
|
|
65
66
|
|
|
66
67
|
// The workspace config problems, one line each.
|
|
67
68
|
workspaceErrors: readonly string[];
|
|
69
|
+
|
|
70
|
+
// Whether a fleet restore after a daemon restart sends a session that
|
|
71
|
+
// was mid-turn one message to carry on; a spawn can override it per
|
|
72
|
+
// session.
|
|
73
|
+
resumeInterruptedTurns: boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/**
|
|
@@ -109,6 +115,7 @@ const DEFAULTS: Config = {
|
|
|
109
115
|
principals: null,
|
|
110
116
|
principalErrors: [],
|
|
111
117
|
workspaceErrors: [],
|
|
118
|
+
resumeInterruptedTurns: false,
|
|
112
119
|
};
|
|
113
120
|
|
|
114
121
|
const configDir = join(resolveHomeDir(), '.config', 'atc');
|
|
@@ -148,6 +155,7 @@ const CONFIG_SCHEMA = z.object({
|
|
|
148
155
|
targets: z.unknown().optional(),
|
|
149
156
|
defaultTarget: z.unknown().optional(),
|
|
150
157
|
principals: z.unknown().optional(),
|
|
158
|
+
resumeInterruptedTurns: buildOptionalBoolean(),
|
|
151
159
|
});
|
|
152
160
|
|
|
153
161
|
/**
|
|
@@ -336,6 +344,7 @@ export function parseConfig(raw: unknown, file: string = configFile): Config {
|
|
|
336
344
|
principals: principals.principals,
|
|
337
345
|
principalErrors: principals.errors,
|
|
338
346
|
workspaceErrors: workspaces.errors,
|
|
347
|
+
resumeInterruptedTurns: parsed.data.resumeInterruptedTurns ?? DEFAULTS.resumeInterruptedTurns,
|
|
339
348
|
};
|
|
340
349
|
}
|
|
341
350
|
|
package/src/statusline.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { resolveAgentHome } from './agents/resolve-agent-home';
|
|
4
4
|
import { sendBridgeRequest } from './protocol/send-bridge-request';
|
|
5
5
|
import { statusFile } from './shared/config';
|
|
6
6
|
import { isRecord, sendReport } from './shared/report';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Runs as the statusLine command injected into wrangled sessions. Chains the
|
|
10
|
-
* user's own statusline (from
|
|
10
|
+
* user's own statusline (from settings.json in the Claude config folder,
|
|
11
|
+
* `$CLAUDE_CONFIG_DIR` or ~/.claude), appends the atc fleet
|
|
11
12
|
* segment, and heartbeats the session id back to the atc socket, with the
|
|
12
13
|
* agent id the command gave it. Always exits 0 so it never breaks the
|
|
13
14
|
* session it renders for.
|
|
@@ -37,7 +38,7 @@ export async function runStatusline(agent: string): Promise<void> {
|
|
|
37
38
|
let chained = '';
|
|
38
39
|
|
|
39
40
|
try {
|
|
40
|
-
const settingsPath = join(
|
|
41
|
+
const settingsPath = join(resolveAgentHome('CLAUDE_CONFIG_DIR', '.claude'), 'settings.json');
|
|
41
42
|
const rawSettings = readFileSync(settingsPath, 'utf8');
|
|
42
43
|
const settings: unknown = JSON.parse(rawSettings);
|
|
43
44
|
const statusLine = isRecord(settings) ? settings['statusLine'] : undefined;
|
package/src/store/fleet-entry.ts
CHANGED
|
@@ -67,6 +67,11 @@ export interface FleetEntry {
|
|
|
67
67
|
// parent's when the two share one host. A row without one runs on a host
|
|
68
68
|
// of its own.
|
|
69
69
|
readonly hostKey?: SessionID;
|
|
70
|
+
|
|
71
|
+
// Whether a fleet restore after a daemon restart sends the session one
|
|
72
|
+
// message to carry on a turn the restart cut off; a row without one
|
|
73
|
+
// follows the daemon's config.
|
|
74
|
+
readonly resumeInterruptedTurns?: boolean;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface FleetStore {
|
|
@@ -34,6 +34,10 @@ interface FleetTable {
|
|
|
34
34
|
// its parent's when the two share one host. Null for a row from before
|
|
35
35
|
// hosts were recorded, which runs on its own.
|
|
36
36
|
host_key: string | null;
|
|
37
|
+
|
|
38
|
+
// Whether a restore sends the session one message to carry on an
|
|
39
|
+
// interrupted turn: 1 or 0 as the spawn chose, null to follow the config.
|
|
40
|
+
resume_interrupted_turns: number | null;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
interface EventsTable {
|
|
@@ -549,6 +553,14 @@ const MIGRATIONS: Record<string, Migration> = {
|
|
|
549
553
|
.execute();
|
|
550
554
|
},
|
|
551
555
|
},
|
|
556
|
+
'026_add_fleet_resume_interrupted_turns': {
|
|
557
|
+
async up(db: Kysely<StateStoreSchema>) {
|
|
558
|
+
await db.schema
|
|
559
|
+
.alterTable('fleet')
|
|
560
|
+
.addColumn('resume_interrupted_turns', 'integer')
|
|
561
|
+
.execute();
|
|
562
|
+
},
|
|
563
|
+
},
|
|
552
564
|
};
|
|
553
565
|
|
|
554
566
|
const PROVIDER: MigrationProvider = {
|
package/src/store/state-store.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
|
|
39
39
|
// Spelled as the partial index's predicate so SQLite can match them.
|
|
40
40
|
const TRAIL_FILTER = sql<boolean>`kind IS NOT NULL AND kind != 'heartbeat'`;
|
|
41
|
+
const TURN_KINDS = ['prompt-submitted', 'turn-done', 'needs-input', 'ended'];
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Some sessions' slice of the event trail: rows under their atc ids, plus
|
|
@@ -210,6 +211,7 @@ export class StateStore {
|
|
|
210
211
|
'workspace.withheld_env',
|
|
211
212
|
'fleet.desired',
|
|
212
213
|
'fleet.host_key',
|
|
214
|
+
'fleet.resume_interrupted_turns',
|
|
213
215
|
])
|
|
214
216
|
.execute();
|
|
215
217
|
|
|
@@ -239,6 +241,9 @@ export class StateStore {
|
|
|
239
241
|
...buildWithheldEnvField(row.withheld_env),
|
|
240
242
|
...(row.desired === 'sleep' || row.desired === 'stop' ? { desired: row.desired } : {}),
|
|
241
243
|
...(row.host_key === null ? {} : { hostKey: toSessionID(row.host_key) }),
|
|
244
|
+
...(row.resume_interrupted_turns === null
|
|
245
|
+
? {}
|
|
246
|
+
: { resumeInterruptedTurns: row.resume_interrupted_turns !== 0 }),
|
|
242
247
|
});
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -346,6 +351,10 @@ export class StateStore {
|
|
|
346
351
|
target_identity: entry.targetIdentity ?? null,
|
|
347
352
|
desired: entry.desired ?? null,
|
|
348
353
|
host_key: entry.hostKey ?? null,
|
|
354
|
+
resume_interrupted_turns:
|
|
355
|
+
entry.resumeInterruptedTurns === undefined
|
|
356
|
+
? null
|
|
357
|
+
: Number(entry.resumeInterruptedTurns),
|
|
349
358
|
})
|
|
350
359
|
.execute();
|
|
351
360
|
|
|
@@ -539,6 +548,31 @@ export class StateStore {
|
|
|
539
548
|
return row?.ts === null || row?.ts === undefined ? null : Date.parse(row.ts);
|
|
540
549
|
}
|
|
541
550
|
|
|
551
|
+
// The kind of the session's latest event that moves a turn: a prompt
|
|
552
|
+
// submitted, a turn done, a request for input, or the session's end.
|
|
553
|
+
// Null when the trail holds none. Matches the session by either id, as
|
|
554
|
+
// the activity read does.
|
|
555
|
+
async findLatestTurnKind(
|
|
556
|
+
atcID: SessionID,
|
|
557
|
+
agentSessionID: AgentSessionID | undefined,
|
|
558
|
+
): Promise<string | null> {
|
|
559
|
+
const row = await this.db
|
|
560
|
+
.selectFrom('events')
|
|
561
|
+
.select('kind')
|
|
562
|
+
.where('kind', 'in', TURN_KINDS)
|
|
563
|
+
.where((eb) =>
|
|
564
|
+
eb.or([
|
|
565
|
+
eb('atc_id', '=', atcID),
|
|
566
|
+
...(agentSessionID === undefined ? [] : [eb('session_id', '=', agentSessionID)]),
|
|
567
|
+
]),
|
|
568
|
+
)
|
|
569
|
+
.orderBy('id', 'desc')
|
|
570
|
+
.limit(1)
|
|
571
|
+
.executeTakeFirst();
|
|
572
|
+
|
|
573
|
+
return row?.kind ?? null;
|
|
574
|
+
}
|
|
575
|
+
|
|
542
576
|
async recordSpawnDir(cwd: string, grant: SpawnTarget): Promise<void> {
|
|
543
577
|
await this.db
|
|
544
578
|
.insertInto('spawn_history')
|
|
@@ -939,6 +973,7 @@ export class StateStore {
|
|
|
939
973
|
.updateTable('workspace_materialization')
|
|
940
974
|
.set({
|
|
941
975
|
phase: fields.phase,
|
|
976
|
+
...(fields.dir === undefined ? {} : { dir: fields.dir }),
|
|
942
977
|
...(fields.repoURL === undefined ? {} : { repo_url: fields.repoURL }),
|
|
943
978
|
...(fields.sha === undefined ? {} : { sha: fields.sha }),
|
|
944
979
|
...(fields.ref === undefined ? {} : { ref: fields.ref }),
|
|
@@ -50,6 +50,10 @@ export interface WorkspaceMaterialization {
|
|
|
50
50
|
// The fields a materialization rewrites as it moves through its phases.
|
|
51
51
|
export interface MaterializationUpdate {
|
|
52
52
|
readonly phase: MaterializationPhase;
|
|
53
|
+
|
|
54
|
+
// The directory the workspace landed in, once a directory the daemon
|
|
55
|
+
// picked is claimed.
|
|
56
|
+
readonly dir?: string;
|
|
53
57
|
readonly repoURL?: string;
|
|
54
58
|
readonly sha?: string;
|
|
55
59
|
readonly ref?: string | null;
|