@zgeoff/atc 2.10.4 → 2.12.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 +72 -0
- package/src/agents/atc-bridge-files.ts +1 -1
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/claude-adapter.ts +52 -1
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +46 -1
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +75 -1
- package/src/agents/grok-adapter.ts +22 -0
- package/src/cli.ts +22 -0
- package/src/client/boot-daemon.ts +6 -0
- package/src/client/daemon-client.ts +14 -2
- package/src/client/spawn-picker.ts +27 -4
- package/src/daemon/build-agent-list.ts +94 -0
- package/src/daemon/build-config-revision.ts +27 -0
- package/src/daemon/build-execution-targets.ts +34 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-scoped-context.ts +232 -0
- package/src/daemon/build-target-access.ts +33 -0
- package/src/daemon/build-target-forbidden-error.ts +13 -0
- package/src/daemon/build-target-identity.ts +22 -0
- package/src/daemon/build-target-list.ts +50 -0
- package/src/daemon/daemon-connection.ts +371 -99
- package/src/daemon/daemon.ts +537 -90
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +103 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/local-pty-provider.ts +83 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-report.ts +29 -3
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/restore-fleet.ts +66 -19
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +287 -65
- package/src/daemon/start-headless-run.ts +11 -0
- package/src/daemon/start-headless-turn.ts +11 -2
- package/src/daemon/target-access.ts +36 -0
- package/src/mcp/answer-mcp-request.ts +12 -4
- package/src/mcp/answer-rpc-request.ts +40 -6
- package/src/mcp/build-principal-caller.ts +15 -0
- package/src/mcp/build-spawn-descriptions.ts +45 -0
- package/src/mcp/build-tool-list.ts +88 -7
- package/src/mcp/mcp-tools.ts +245 -24
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +77 -15
- package/src/mcp/require-daemon-features.ts +40 -0
- package/src/mcp/run-tool.ts +139 -35
- package/src/mcp/start-mcp-http-server.ts +130 -24
- package/src/mcp/types.ts +9 -1
- package/src/mcp-http-server.ts +6 -1
- package/src/mcp-server.ts +13 -1
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +47 -0
- package/src/protocol/parse-daemon-features.ts +19 -0
- package/src/protocol/protocol.ts +44 -11
- package/src/protocol/request-param-schemas.ts +78 -12
- package/src/report.ts +20 -3
- package/src/shared/agent-session-id.ts +1 -1
- package/src/shared/collect-principals.ts +51 -0
- package/src/shared/collect-targets.ts +144 -0
- package/src/shared/config.ts +139 -14
- package/src/shared/daemon-id.ts +8 -0
- package/src/shared/format-json-kind.ts +21 -0
- package/src/shared/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/store/fleet-entry.ts +42 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/message-record.ts +3 -0
- package/src/store/run-migrations.ts +272 -7
- package/src/store/state-store.ts +503 -34
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +239 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +103 -0
- package/src/workspace/sanitize-workspace-clone.ts +146 -0
- package/src/workspace/workspace-provenance.ts +11 -0
- package/src/workspace/workspace-source.ts +8 -0
package/src/shared/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { buildOptionalString } from './build-optional-string';
|
|
5
5
|
import { buildOptionalStringArray } from './build-optional-string-array';
|
|
@@ -8,6 +8,11 @@ import { collectGateways } from './collect-gateways';
|
|
|
8
8
|
import type { GatewayConfig } from './collect-gateways';
|
|
9
9
|
import { collectHooks } from './collect-hooks';
|
|
10
10
|
import type { HooksConfig } from './collect-hooks';
|
|
11
|
+
import { collectPrincipals } from './collect-principals';
|
|
12
|
+
import { collectTargets } from './collect-targets';
|
|
13
|
+
import type { TargetConfig, TargetConfigError } from './collect-targets';
|
|
14
|
+
import { formatJSONKind } from './format-json-kind';
|
|
15
|
+
import { isRecord } from './report';
|
|
11
16
|
import { resolveHomeDir } from './resolve-home-dir';
|
|
12
17
|
|
|
13
18
|
export interface Config {
|
|
@@ -21,6 +26,20 @@ export interface Config {
|
|
|
21
26
|
gateways: GatewayConfig[];
|
|
22
27
|
hooks: HooksConfig;
|
|
23
28
|
leader: LeaderKey;
|
|
29
|
+
|
|
30
|
+
// Where sessions run, and the one a spawn without a target runs on: null
|
|
31
|
+
// when the config gives none it can use.
|
|
32
|
+
targets: readonly TargetConfig[];
|
|
33
|
+
defaultTarget: string | null;
|
|
34
|
+
|
|
35
|
+
// The target config problems that leave a target, or every target,
|
|
36
|
+
// unusable.
|
|
37
|
+
targetErrors: readonly TargetConfigError[];
|
|
38
|
+
|
|
39
|
+
// The targets each principal may use: null when the config has no
|
|
40
|
+
// principals, which leaves every principal the implicit local target.
|
|
41
|
+
principals: ReadonlyMap<string, readonly string[]> | null;
|
|
42
|
+
principalErrors: readonly string[];
|
|
24
43
|
}
|
|
25
44
|
|
|
26
45
|
/**
|
|
@@ -47,6 +66,11 @@ const DEFAULTS: Config = {
|
|
|
47
66
|
gateways: [],
|
|
48
67
|
hooks: {},
|
|
49
68
|
leader: { code: 0, label: '^Space' },
|
|
69
|
+
targets: [{ id: 'local', provider: 'local-pty', options: {} }],
|
|
70
|
+
defaultTarget: 'local',
|
|
71
|
+
targetErrors: [],
|
|
72
|
+
principals: null,
|
|
73
|
+
principalErrors: [],
|
|
50
74
|
};
|
|
51
75
|
|
|
52
76
|
const configDir = join(resolveHomeDir(), '.config', 'atc');
|
|
@@ -80,38 +104,132 @@ const CONFIG_SCHEMA = z.object({
|
|
|
80
104
|
gateways: z.unknown().optional(),
|
|
81
105
|
hooks: z.unknown().optional(),
|
|
82
106
|
leader: buildOptionalString(),
|
|
107
|
+
targets: z.unknown().optional(),
|
|
108
|
+
defaultTarget: z.unknown().optional(),
|
|
109
|
+
principals: z.unknown().optional(),
|
|
83
110
|
});
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Reads and parses config.json. Only an absent file means every default,
|
|
114
|
+
* the implicit `local` target included, and a first run writes those
|
|
115
|
+
* defaults out. A file that exists but cannot be read or parsed loads as
|
|
116
|
+
* unusable: every default but the targets, which it leaves empty with the
|
|
117
|
+
* problem as the one target error, so nothing runs until the file is fixed.
|
|
118
|
+
* Never throws.
|
|
119
|
+
*/
|
|
120
|
+
export function loadConfig(file: string = configFile): Config {
|
|
121
|
+
// Every atc process writes under the state directory; a failure here
|
|
122
|
+
// comes back from the first write into it.
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(stateDir, { recursive: true });
|
|
125
|
+
} catch {}
|
|
126
|
+
|
|
127
|
+
let text: string;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
text = readFileSync(file, 'utf8');
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const code: unknown = error instanceof Error ? Reflect.get(error, 'code') : undefined;
|
|
133
|
+
|
|
134
|
+
if (code !== 'ENOENT') {
|
|
135
|
+
const detail = typeof code === 'string' ? code : 'the file could not be read';
|
|
88
136
|
|
|
89
|
-
|
|
137
|
+
return buildUnusableConfig('config_unreadable', file, detail);
|
|
138
|
+
}
|
|
90
139
|
|
|
91
|
-
|
|
92
|
-
writeFileSync(file, `${JSON.stringify(DEFAULTS, null, 2)}\n`);
|
|
140
|
+
tryWriteDefaultConfig(file);
|
|
93
141
|
|
|
94
142
|
return { ...DEFAULTS };
|
|
95
143
|
}
|
|
96
144
|
|
|
145
|
+
let raw: unknown;
|
|
146
|
+
|
|
97
147
|
try {
|
|
98
|
-
|
|
148
|
+
raw = JSON.parse(text);
|
|
99
149
|
} catch {
|
|
100
|
-
|
|
150
|
+
// The parser's own message can quote the file's text, so the detail is
|
|
151
|
+
// a fixed phrase instead.
|
|
152
|
+
return buildUnusableConfig('config_malformed', file, 'the file is not valid JSON');
|
|
101
153
|
}
|
|
154
|
+
|
|
155
|
+
return parseConfig(raw, file);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A config for a file that exists but cannot be used: every default but the
|
|
160
|
+
* targets, which are empty, with no default target and the file's problem
|
|
161
|
+
* as the one target error, and the principals, which get no target, since
|
|
162
|
+
* the file's own principals cannot be read.
|
|
163
|
+
*/
|
|
164
|
+
function buildUnusableConfig(
|
|
165
|
+
problem: 'config_malformed' | 'config_unreadable',
|
|
166
|
+
path: string,
|
|
167
|
+
detail: string,
|
|
168
|
+
): Config {
|
|
169
|
+
return {
|
|
170
|
+
...DEFAULTS,
|
|
171
|
+
targets: [],
|
|
172
|
+
defaultTarget: null,
|
|
173
|
+
targetErrors: [{ scope: 'config', problem, path, detail }],
|
|
174
|
+
principals: new Map(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Writes the first-run config, never over a file that appeared since the
|
|
180
|
+
* read. A failure leaves the defaults in effect for this run, so it is not
|
|
181
|
+
* an error.
|
|
182
|
+
*/
|
|
183
|
+
function tryWriteDefaultConfig(file: string): void {
|
|
184
|
+
try {
|
|
185
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
186
|
+
writeFileSync(file, renderDefaultConfig(), { flag: 'wx' });
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The config.json text a first run writes. It leaves out the targets and
|
|
192
|
+
* principals, so the file holds the one implicit `local` target and no
|
|
193
|
+
* principals until the user sets their own, and the errors a parse
|
|
194
|
+
* reports, which belong to no file.
|
|
195
|
+
*/
|
|
196
|
+
export function renderDefaultConfig(): string {
|
|
197
|
+
const {
|
|
198
|
+
targets: _targets,
|
|
199
|
+
defaultTarget: _default,
|
|
200
|
+
targetErrors: _errors,
|
|
201
|
+
principals: _principals,
|
|
202
|
+
principalErrors: _principalErrors,
|
|
203
|
+
...written
|
|
204
|
+
} = DEFAULTS;
|
|
205
|
+
|
|
206
|
+
return `${JSON.stringify(written, null, 2)}\n`;
|
|
102
207
|
}
|
|
103
208
|
|
|
104
209
|
/**
|
|
105
210
|
* Parses a user-written config.json's already-decoded JSON value into a
|
|
106
211
|
* Config, applying every default a malformed or absent field falls back to.
|
|
107
|
-
*
|
|
108
|
-
*
|
|
212
|
+
* A root that is not an object leaves the config unusable, with no targets,
|
|
213
|
+
* and `file` is the path its error holds. Total: no shape of `raw` throws,
|
|
214
|
+
* so a hand-edited config never stops atc starting.
|
|
109
215
|
*/
|
|
110
|
-
export function parseConfig(raw: unknown): Config {
|
|
216
|
+
export function parseConfig(raw: unknown, file: string = configFile): Config {
|
|
217
|
+
if (!isRecord(raw) || Array.isArray(raw)) {
|
|
218
|
+
return buildUnusableConfig(
|
|
219
|
+
'config_malformed',
|
|
220
|
+
file,
|
|
221
|
+
`the root is ${formatJSONKind(raw)}, not an object`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
111
225
|
const parsed = CONFIG_SCHEMA.safeParse(raw);
|
|
112
226
|
|
|
113
227
|
if (!parsed.success) {
|
|
114
|
-
return
|
|
228
|
+
return buildUnusableConfig(
|
|
229
|
+
'config_malformed',
|
|
230
|
+
file,
|
|
231
|
+
'the file does not match the config schema',
|
|
232
|
+
);
|
|
115
233
|
}
|
|
116
234
|
|
|
117
235
|
const claudeBin = parsed.data.claudeBin ?? DEFAULTS.claudeBin;
|
|
@@ -123,6 +241,8 @@ export function parseConfig(raw: unknown): Config {
|
|
|
123
241
|
const dirs = { roots: collectDirRoots(parsed.data.dirs) };
|
|
124
242
|
const gateways = collectGateways(parsed.data.gateways, claudeBin, claudeArgs);
|
|
125
243
|
const hooks = collectHooks(parsed.data.hooks);
|
|
244
|
+
const targets = collectTargets(parsed.data.targets, parsed.data.defaultTarget);
|
|
245
|
+
const principals = collectPrincipals(parsed.data.principals);
|
|
126
246
|
|
|
127
247
|
const leader =
|
|
128
248
|
(parsed.data.leader === undefined ? null : decodeLeader(parsed.data.leader)) ?? DEFAULTS.leader;
|
|
@@ -138,6 +258,11 @@ export function parseConfig(raw: unknown): Config {
|
|
|
138
258
|
gateways,
|
|
139
259
|
hooks,
|
|
140
260
|
leader,
|
|
261
|
+
targets: targets.targets,
|
|
262
|
+
defaultTarget: targets.defaultTarget,
|
|
263
|
+
targetErrors: targets.errors,
|
|
264
|
+
principals: principals.principals,
|
|
265
|
+
principalErrors: principals.errors,
|
|
141
266
|
};
|
|
142
267
|
}
|
|
143
268
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Tagged } from 'type-fest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The id a daemon holds for its whole life, minted once into its state
|
|
5
|
+
* store. It names the daemon in `daemon.hello`, in every session locator,
|
|
6
|
+
* and in the ownership row of each session the daemon persists.
|
|
7
|
+
*/
|
|
8
|
+
export type DaemonID = Tagged<string, 'DaemonID'>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kind of a decoded JSON value, as an error detail: "null", "an array",
|
|
3
|
+
* "an object", "a string", "a number", or "a boolean". It never includes
|
|
4
|
+
* the value itself, so a config diagnostic built from it holds nothing the
|
|
5
|
+
* file holds.
|
|
6
|
+
*/
|
|
7
|
+
export function formatJSONKind(raw: unknown): string {
|
|
8
|
+
if (raw === null) {
|
|
9
|
+
return 'null';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(raw)) {
|
|
13
|
+
return 'an array';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof raw === 'object') {
|
|
17
|
+
return 'an object';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `a ${typeof raw}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a JSON value with every object's keys in sorted order, at every
|
|
3
|
+
* depth, so two values that differ only in key order serialize the same.
|
|
4
|
+
*/
|
|
5
|
+
export function sortJSONKeys(value: unknown): unknown {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map((item) => sortJSONKeys(item));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof value === 'object' && value !== null) {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
Object.entries(value)
|
|
13
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
14
|
+
.map(([key, item]) => [key, sortJSONKeys(item)]),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DaemonID } from './daemon-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Trusts a string as a daemon id. This is the one point in the codebase
|
|
5
|
+
* where a plain string becomes a `DaemonID`, and it adds no runtime check:
|
|
6
|
+
* the caller asserts the string is the id a daemon minted for itself.
|
|
7
|
+
*/
|
|
8
|
+
export function toDaemonID(id: string): DaemonID {
|
|
9
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- the one point where a string is trusted as a DaemonID
|
|
10
|
+
return id as DaemonID;
|
|
11
|
+
}
|
package/src/store/fleet-entry.ts
CHANGED
|
@@ -4,20 +4,28 @@ import type { AgentID } from '../agents/agent-adapter';
|
|
|
4
4
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
5
5
|
import { buildOptionalBoolean } from '../shared/build-optional-boolean';
|
|
6
6
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
7
|
+
import type { DaemonID } from '../shared/daemon-id';
|
|
7
8
|
import { isRecord } from '../shared/report';
|
|
9
|
+
import type { SessionID } from '../shared/session-id';
|
|
8
10
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
9
11
|
|
|
12
|
+
// One fleet row. The atc session id keys it and stays the same for the
|
|
13
|
+
// session's whole life, across daemon restarts and fleet restores.
|
|
10
14
|
export interface FleetEntry {
|
|
15
|
+
readonly sessionID: SessionID;
|
|
11
16
|
readonly name: string;
|
|
12
17
|
readonly cwd: string;
|
|
13
|
-
|
|
18
|
+
|
|
19
|
+
// Absent until the agent reports its own session id; a row without one
|
|
20
|
+
// restores as an exited session, since there is nothing to resume.
|
|
21
|
+
readonly agentSessionID?: AgentSessionID;
|
|
14
22
|
readonly agent: AgentID;
|
|
15
23
|
readonly pinned?: boolean;
|
|
16
24
|
readonly lastAttachedAt?: number;
|
|
17
25
|
readonly exited?: boolean;
|
|
18
26
|
|
|
19
|
-
// The
|
|
20
|
-
readonly parent?:
|
|
27
|
+
// The atc session id of the session this one is a sub-session of.
|
|
28
|
+
readonly parent?: SessionID;
|
|
21
29
|
|
|
22
30
|
// The prompt the session was spawned with.
|
|
23
31
|
readonly prompt?: string;
|
|
@@ -27,15 +35,26 @@ export interface FleetEntry {
|
|
|
27
35
|
|
|
28
36
|
// The transcript file the agent's hooks last reported for the session.
|
|
29
37
|
readonly transcriptPath?: string;
|
|
38
|
+
|
|
39
|
+
// The model and effort the session was spawned with; absent for a session
|
|
40
|
+
// that runs on the agent's configured default.
|
|
41
|
+
readonly model?: string;
|
|
42
|
+
readonly effort?: string;
|
|
43
|
+
|
|
44
|
+
// The execution target the session runs on; a row without one restores
|
|
45
|
+
// on the `local` target.
|
|
46
|
+
readonly target?: string;
|
|
47
|
+
|
|
48
|
+
// The identity the target had when the session started on it; a row
|
|
49
|
+
// without one is bound to the implicit `local` target's identity.
|
|
50
|
+
readonly targetIdentity?: string;
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
export interface FleetStore {
|
|
54
|
+
readonly daemonID: DaemonID;
|
|
33
55
|
readonly loadFleet: () => Promise<FleetEntry[]>;
|
|
34
56
|
readonly writeFleet: (entries: readonly FleetEntry[]) => Promise<void>;
|
|
35
|
-
readonly updateFleetEntry: (
|
|
36
|
-
agentSessionID: AgentSessionID,
|
|
37
|
-
fields: FleetEntryUpdate,
|
|
38
|
-
) => Promise<void>;
|
|
57
|
+
readonly updateFleetEntry: (sessionID: SessionID, fields: FleetEntryUpdate) => Promise<void>;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
// The fields a session rewrites on its own row while it runs, without
|
|
@@ -45,7 +64,21 @@ export interface FleetEntryUpdate {
|
|
|
45
64
|
readonly transcriptPath?: string;
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
//
|
|
67
|
+
// One entry of a legacy fleet.json, from before the fleet moved into the
|
|
68
|
+
// state store: keyed by the agent session id, with the parent link held as
|
|
69
|
+
// the parent's agent session id.
|
|
70
|
+
export interface LegacyFleetEntry {
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly cwd: string;
|
|
73
|
+
readonly agentSessionID: AgentSessionID;
|
|
74
|
+
readonly agent: AgentID;
|
|
75
|
+
readonly pinned?: boolean;
|
|
76
|
+
readonly lastAttachedAt?: number;
|
|
77
|
+
readonly exited?: boolean;
|
|
78
|
+
readonly parent?: AgentSessionID;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A legacy fleet.json entry's keys. name, cwd, and the resolved agentSessionID are
|
|
49
82
|
// required: a row missing any of them cannot restore a session, so the whole
|
|
50
83
|
// row parses to undefined rather than a half-built entry. Fleet files
|
|
51
84
|
// written before the id key was agent-neutral carry it under its Claude-era
|
|
@@ -71,7 +104,7 @@ const FLEET_ENTRY_SCHEMA = z.preprocess(
|
|
|
71
104
|
}),
|
|
72
105
|
);
|
|
73
106
|
|
|
74
|
-
export function parseFleetEntry(raw: unknown):
|
|
107
|
+
export function parseFleetEntry(raw: unknown): LegacyFleetEntry | undefined {
|
|
75
108
|
const parsed = FLEET_ENTRY_SCHEMA.safeParse(raw);
|
|
76
109
|
|
|
77
110
|
if (!parsed.success) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a keyed effect stands. `in_progress` holds while the daemon that
|
|
3
|
+
* claimed the key runs the effect; `completed` once the effect is durable;
|
|
4
|
+
* `outcome_unknown` when a daemon stopped mid-effect and its start-up
|
|
5
|
+
* reconciliation found no trace of the effect.
|
|
6
|
+
*/
|
|
7
|
+
export type IdempotencyState = 'in_progress' | 'completed' | 'outcome_unknown';
|
|
8
|
+
|
|
9
|
+
// One idempotency key and the effect it names.
|
|
10
|
+
export interface IdempotencyRecord {
|
|
11
|
+
readonly principal: string;
|
|
12
|
+
readonly operation: string;
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly payloadHash: string;
|
|
15
|
+
readonly state: IdempotencyState;
|
|
16
|
+
|
|
17
|
+
// The id the effect was minted before it ran: the session a spawn
|
|
18
|
+
// created, or the message a send wrote.
|
|
19
|
+
readonly effectRef: string;
|
|
20
|
+
|
|
21
|
+
// The effect's answer as JSON, once completed by the daemon that ran it.
|
|
22
|
+
readonly result: string | null;
|
|
23
|
+
|
|
24
|
+
// The target the completed effect's session was bound to, as it stood
|
|
25
|
+
// then; null for a key completed without one.
|
|
26
|
+
readonly effectTarget: EffectTarget | null;
|
|
27
|
+
readonly createdAt: number;
|
|
28
|
+
readonly updatedAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A target name and the identity it held, which a replay of the key is
|
|
33
|
+
* authorized against.
|
|
34
|
+
*/
|
|
35
|
+
export interface EffectTarget {
|
|
36
|
+
readonly target: string;
|
|
37
|
+
readonly targetIdentity: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The claim a keyed request makes before its effect runs.
|
|
41
|
+
export interface IdempotencyClaim {
|
|
42
|
+
readonly principal: string;
|
|
43
|
+
readonly operation: string;
|
|
44
|
+
readonly key: string;
|
|
45
|
+
readonly payloadHash: string;
|
|
46
|
+
readonly effectRef: string;
|
|
47
|
+
readonly at: number;
|
|
48
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AgentSessionID } from '../shared/agent-session-id';
|
|
|
2
2
|
import type { SessionID } from '../shared/session-id';
|
|
3
3
|
|
|
4
4
|
// Which session a message belongs to: the atc id it was sent to, or the
|
|
5
|
-
// agent session id
|
|
5
|
+
// agent session id, which links rows written under an earlier atc id.
|
|
6
6
|
export interface MessageOwner {
|
|
7
7
|
readonly atcID: SessionID;
|
|
8
8
|
readonly agentSessionID?: AgentSessionID;
|