@ours.network/fleet 0.17.8 → 0.17.10
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/dist/build-info.json +4 -4
- package/dist/cli.js +8 -0
- package/dist/config.js +15 -8
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +27 -2
- package/dist/harness/claude-code.js +198 -7
- package/dist/harness/codex.js +10 -1
- package/dist/harness/types.d.ts +50 -1
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/ops.js +1 -1
- package/dist/owner-channel/channel.d.ts +10 -0
- package/dist/owner-channel/channel.js +28 -13
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +220 -92
- package/dist/session/acp.d.ts +46 -0
- package/dist/session/acp.js +62 -7
- package/dist/session/types.d.ts +11 -0
- package/dist/spawn.js +29 -17
- package/package.json +1 -1
package/dist/harness/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@agentclientprotocol/sdk';
|
|
1
2
|
import type { CommonPermissions, FleetPermissionMode, ResolvedRole } from '../config.js';
|
|
2
3
|
export interface PrereqCheck {
|
|
3
4
|
name: string;
|
|
@@ -21,7 +22,32 @@ export interface SessionPrep {
|
|
|
21
22
|
env: Record<string, string>;
|
|
22
23
|
/** Optional launcher selected after runtime prerequisite probing. */
|
|
23
24
|
command?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The settings overlay prepareSession wrote, if it wrote one.
|
|
27
|
+
*
|
|
28
|
+
* The tmux launch delivers this as `--settings <path>` in `argv`; an ACP agent
|
|
29
|
+
* takes no flags, so it needs the PATH rather than the flag. Recorded here so
|
|
30
|
+
* the two deliveries read one value instead of each re-deriving the filename.
|
|
31
|
+
*/
|
|
32
|
+
settingsOverlay?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The MCP config file prepareSession wrote for `harness_options.mcp_servers`,
|
|
35
|
+
* if the role declared any. Same reason as `settingsOverlay`: the tmux launch
|
|
36
|
+
* passes the file, the ACP launch has to send the servers themselves.
|
|
37
|
+
*/
|
|
38
|
+
mcpConfigFile?: string;
|
|
24
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* One MCP server as ACP's `session/new` declares it.
|
|
42
|
+
*
|
|
43
|
+
* ⚠ THE PROTOCOL'S OWN TYPE, DELIBERATELY NOT A LOCAL RESTATEMENT. `mcpServers`
|
|
44
|
+
* goes onto the wire unchanged, so a hand-written near-copy would compile while
|
|
45
|
+
* being subtly wrong — `env` and `headers` are REQUIRED arrays, and the stdio
|
|
46
|
+
* variant is the one with no `type` field at all. Aliasing it also keeps
|
|
47
|
+
* `session/new`'s response type inferable, which a structural stand-in silently
|
|
48
|
+
* broke (every field of the result degraded to `unknown`).
|
|
49
|
+
*/
|
|
50
|
+
export type AcpMcpServer = McpServer;
|
|
25
51
|
export interface Launch {
|
|
26
52
|
argv: string[];
|
|
27
53
|
env: Record<string, string>;
|
|
@@ -96,7 +122,13 @@ export interface HarnessAdapter {
|
|
|
96
122
|
id: string;
|
|
97
123
|
supportsResume: boolean;
|
|
98
124
|
checkPrereqs(): Promise<PrereqReport>;
|
|
99
|
-
|
|
125
|
+
/**
|
|
126
|
+
* `role` is the SESSION-AWARE half: some harness options can only be honoured
|
|
127
|
+
* on some session types, and an option that is silently dropped is worse than
|
|
128
|
+
* one that is refused. Optional so an adapter that has nothing session-specific
|
|
129
|
+
* to say keeps its one-argument implementation.
|
|
130
|
+
*/
|
|
131
|
+
validateOptions(opts: unknown, role?: ResolvedRole): ValidationError[];
|
|
100
132
|
prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
|
|
101
133
|
buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
|
|
102
134
|
buildAcpLaunch?(role: ResolvedRole, prep: SessionPrep): AcpLaunch;
|
|
@@ -106,6 +138,23 @@ export interface HarnessAdapter {
|
|
|
106
138
|
* agent's default. Omit for a harness whose ACP agent has no modes.
|
|
107
139
|
*/
|
|
108
140
|
acpPermissionModeId?(role: ResolvedRole): string | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* The MCP servers this role declares, for the `mcpServers` array of ACP's
|
|
143
|
+
* `session/new` / `resume` / `load`. Empty (or omitted) leaves the agent's own
|
|
144
|
+
* configuration alone, which is what fleet has always sent.
|
|
145
|
+
*/
|
|
146
|
+
acpMcpServers?(role: ResolvedRole): AcpMcpServer[];
|
|
147
|
+
/**
|
|
148
|
+
* Agent-specific `_meta` for `session/new` — how a capability the CLI takes as
|
|
149
|
+
* a flag reaches an ACP agent that accepts no flags.
|
|
150
|
+
*
|
|
151
|
+
* ⚠ THIS IS A PER-AGENT VOCABULARY, NOT PROTOCOL. `_meta` is free-form in ACP,
|
|
152
|
+
* so what an adapter puts here is only honoured by the agent it was written
|
|
153
|
+
* for. An adapter must therefore return nothing for an ACP command it did not
|
|
154
|
+
* choose, and the options that depend on it must be refused at validation for
|
|
155
|
+
* such a role rather than sent and silently ignored.
|
|
156
|
+
*/
|
|
157
|
+
acpSessionMeta?(role: ResolvedRole, prep: SessionPrep): Record<string, unknown> | undefined;
|
|
109
158
|
/** Effective portable policy and harness-native approval mode after native overrides win. */
|
|
110
159
|
effectivePermissionMode?(role: ResolvedRole): {
|
|
111
160
|
fleetMode: FleetPermissionMode;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ResolvedRole } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which environment variable a harness reads to pin the model it RUNS.
|
|
4
|
+
*
|
|
5
|
+
* This is not a convenience: for `claude-code` it is the only channel that
|
|
6
|
+
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
7
|
+
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
8
|
+
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
9
|
+
* `settings.model`, then a resumed session's live model, then its first
|
|
10
|
+
* catalogue entry. A role's declared model was therefore invisible to every
|
|
11
|
+
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
12
|
+
* an explicitly requested one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
|
|
15
|
+
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
16
|
+
export declare function modelEnvVar(harness: string | undefined): string | undefined;
|
|
17
|
+
export interface RoleModelEnvInput {
|
|
18
|
+
harness: string;
|
|
19
|
+
/** Already resolved by `resolveRoleModel` — may come from the fleet default. */
|
|
20
|
+
model: string | undefined;
|
|
21
|
+
/** True when the role (or `--model`) named a model, including `model: null`. */
|
|
22
|
+
modelWasExplicit: boolean;
|
|
23
|
+
defaultsEnv?: Record<string, string>;
|
|
24
|
+
roleEnv?: Record<string, string>;
|
|
25
|
+
authProxyBaseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RoleModelEnv {
|
|
28
|
+
env: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* The model the harness will actually run. Equal to `env[pin]` for a harness
|
|
31
|
+
* that pins by env, so anything reporting this value reports the runtime.
|
|
32
|
+
*/
|
|
33
|
+
model: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
37
|
+
* never disagree.
|
|
38
|
+
*
|
|
39
|
+
* Precedence, highest first:
|
|
40
|
+
* 1. an explicit `model:` / `--model` on the role
|
|
41
|
+
* 2. the role's own `env:` pin
|
|
42
|
+
* 3. the fleet `defaults.model`
|
|
43
|
+
* 4. the fleet `defaults.env` pin
|
|
44
|
+
*
|
|
45
|
+
* Inheriting the fleet default remains correct when the role names no model
|
|
46
|
+
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
47
|
+
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
48
|
+
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
49
|
+
*
|
|
50
|
+
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
51
|
+
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveRoleModelEnv(input: RoleModelEnvInput, describe?: (message: string) => Error): RoleModelEnv;
|
|
54
|
+
/**
|
|
55
|
+
* The model a role will actually run, read back from the environment it was
|
|
56
|
+
* resolved with. Use this wherever a model is reported to a human.
|
|
57
|
+
*/
|
|
58
|
+
export declare function effectiveRoleModel(role: ResolvedRole): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
61
|
+
* role runs after resolution — model-chain recovery is the live example — must
|
|
62
|
+
* go through this, or it changes only the label.
|
|
63
|
+
*/
|
|
64
|
+
export declare function repinModelEnv(role: ResolvedRole, model: string | undefined): Record<string, string> | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Last line of defence, at the exact point a child's environment is composed:
|
|
67
|
+
* refuse to launch a role whose child would run a model other than the one the
|
|
68
|
+
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
69
|
+
* agreement must fail loudly, not start and be believed.
|
|
70
|
+
*/
|
|
71
|
+
export declare function assertModelPinReachesChild(role: ResolvedRole, childEnv: Record<string, string | undefined>): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which environment variable a harness reads to pin the model it RUNS.
|
|
3
|
+
*
|
|
4
|
+
* This is not a convenience: for `claude-code` it is the only channel that
|
|
5
|
+
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
6
|
+
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
7
|
+
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
8
|
+
* `settings.model`, then a resumed session's live model, then its first
|
|
9
|
+
* catalogue entry. A role's declared model was therefore invisible to every
|
|
10
|
+
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
11
|
+
* an explicitly requested one.
|
|
12
|
+
*/
|
|
13
|
+
export const MODEL_ENV_BY_HARNESS = {
|
|
14
|
+
'claude-code': 'ANTHROPIC_MODEL',
|
|
15
|
+
};
|
|
16
|
+
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
17
|
+
export function modelEnvVar(harness) {
|
|
18
|
+
return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
22
|
+
* never disagree.
|
|
23
|
+
*
|
|
24
|
+
* Precedence, highest first:
|
|
25
|
+
* 1. an explicit `model:` / `--model` on the role
|
|
26
|
+
* 2. the role's own `env:` pin
|
|
27
|
+
* 3. the fleet `defaults.model`
|
|
28
|
+
* 4. the fleet `defaults.env` pin
|
|
29
|
+
*
|
|
30
|
+
* Inheriting the fleet default remains correct when the role names no model
|
|
31
|
+
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
32
|
+
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
33
|
+
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
34
|
+
*
|
|
35
|
+
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
36
|
+
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveRoleModelEnv(input, describe = message => new Error(message)) {
|
|
39
|
+
const env = {
|
|
40
|
+
...(input.defaultsEnv ?? {}),
|
|
41
|
+
...(input.roleEnv ?? {}),
|
|
42
|
+
...(input.authProxyBaseUrl ? { ANTHROPIC_BASE_URL: input.authProxyBaseUrl } : {}),
|
|
43
|
+
};
|
|
44
|
+
const pin = modelEnvVar(input.harness);
|
|
45
|
+
if (!pin)
|
|
46
|
+
return { env, model: input.model };
|
|
47
|
+
const rolePin = input.roleEnv?.[pin];
|
|
48
|
+
if (input.modelWasExplicit) {
|
|
49
|
+
if (rolePin !== undefined && rolePin !== input.model)
|
|
50
|
+
throw describe(`model '${input.model ?? '(none)'}' contradicts env.${pin} '${rolePin}'; `
|
|
51
|
+
+ `remove one — ${pin} is what the harness actually runs`);
|
|
52
|
+
if (input.model === undefined)
|
|
53
|
+
delete env[pin];
|
|
54
|
+
else
|
|
55
|
+
env[pin] = input.model;
|
|
56
|
+
return { env, model: input.model };
|
|
57
|
+
}
|
|
58
|
+
// Not explicit: a role-level pin is the most specific thing said about this
|
|
59
|
+
// role, so it decides — and the reported model follows it.
|
|
60
|
+
if (rolePin !== undefined)
|
|
61
|
+
return { env, model: rolePin };
|
|
62
|
+
if (input.model !== undefined)
|
|
63
|
+
env[pin] = input.model;
|
|
64
|
+
return { env, model: input.model ?? env[pin] };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The model a role will actually run, read back from the environment it was
|
|
68
|
+
* resolved with. Use this wherever a model is reported to a human.
|
|
69
|
+
*/
|
|
70
|
+
export function effectiveRoleModel(role) {
|
|
71
|
+
const pin = modelEnvVar(role.harness);
|
|
72
|
+
return (pin ? role.env?.[pin] : undefined) ?? role.model;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
76
|
+
* role runs after resolution — model-chain recovery is the live example — must
|
|
77
|
+
* go through this, or it changes only the label.
|
|
78
|
+
*/
|
|
79
|
+
export function repinModelEnv(role, model) {
|
|
80
|
+
const pin = modelEnvVar(role.harness);
|
|
81
|
+
if (!pin)
|
|
82
|
+
return role.env;
|
|
83
|
+
const env = { ...(role.env ?? {}) };
|
|
84
|
+
if (model === undefined)
|
|
85
|
+
delete env[pin];
|
|
86
|
+
else
|
|
87
|
+
env[pin] = model;
|
|
88
|
+
return Object.keys(env).length ? env : undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Last line of defence, at the exact point a child's environment is composed:
|
|
92
|
+
* refuse to launch a role whose child would run a model other than the one the
|
|
93
|
+
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
94
|
+
* agreement must fail loudly, not start and be believed.
|
|
95
|
+
*/
|
|
96
|
+
export function assertModelPinReachesChild(role, childEnv) {
|
|
97
|
+
const pin = modelEnvVar(role.harness);
|
|
98
|
+
if (!pin || role.model === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const actual = childEnv[pin];
|
|
101
|
+
if (actual === role.model)
|
|
102
|
+
return;
|
|
103
|
+
throw new Error(`[${role.name}] refusing to launch: role model is '${role.model}' but the child's `
|
|
104
|
+
+ `${pin} is ${actual === undefined ? 'unset' : `'${actual}'`} — the session would run a `
|
|
105
|
+
+ 'different model than the one reported');
|
|
106
|
+
}
|
package/dist/ops.js
CHANGED
|
@@ -16,7 +16,7 @@ import { realExec } from './exec.js';
|
|
|
16
16
|
/** Materialize a role's state dir from config: briefing + markers. Returns the dir. */
|
|
17
17
|
export function applyRole(role, opts = {}) {
|
|
18
18
|
const adapter = getAdapter(role.harness);
|
|
19
|
-
const errs = adapter.validateOptions(role.harness_options);
|
|
19
|
+
const errs = adapter.validateOptions(role.harness_options, role);
|
|
20
20
|
if (errs.length)
|
|
21
21
|
throw new Error(`role '${role.name}': ` + errs.map(e => `${e.path}: ${e.message}`).join('; '));
|
|
22
22
|
const dir = agentDir(role.name, opts.temp === true);
|
|
@@ -238,6 +238,16 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
238
238
|
private warnOwnerOfUnauthorizedSender;
|
|
239
239
|
private effectiveOwners;
|
|
240
240
|
private authorizationIntegrity;
|
|
241
|
+
/**
|
|
242
|
+
* Report what the session actually did with the prompt, not what the config
|
|
243
|
+
* asked for. `interrupt: true` used to be reported as "your request
|
|
244
|
+
* interrupted the previous task" unconditionally; the session now answers
|
|
245
|
+
* whether anything was cancelled, whether the request is queued behind
|
|
246
|
+
* earlier prompts, or whether it is held until the current task reaches a
|
|
247
|
+
* safe stopping point. Backends that report no delivery state keep the old
|
|
248
|
+
* queuedBehind-based wording.
|
|
249
|
+
*/
|
|
250
|
+
private acceptanceNotice;
|
|
241
251
|
private complete;
|
|
242
252
|
private commentsState;
|
|
243
253
|
/** Model-authored commentary only; raw protocol/tool data never reaches here. */
|
|
@@ -715,11 +715,7 @@ export class OwnerChannel {
|
|
|
715
715
|
origin: { kind: 'owner', requestId,
|
|
716
716
|
...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
|
|
717
717
|
});
|
|
718
|
-
const accepted = this.
|
|
719
|
-
? ownerNotices.receivedInterrupting()
|
|
720
|
-
: queued.queuedBehind > 0
|
|
721
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
722
|
-
: ownerNotices.receivedStarted();
|
|
718
|
+
const accepted = this.acceptanceNotice(queued);
|
|
723
719
|
handledWireIds.forEach(wire => this.inFlight.add(wire));
|
|
724
720
|
const receipt = this.send(sender.id, accepted, originWireId).then(() => undefined).catch(error => {
|
|
725
721
|
this.logError(`attachment request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -860,14 +856,7 @@ export class OwnerChannel {
|
|
|
860
856
|
this.state.remember(wireId);
|
|
861
857
|
return true;
|
|
862
858
|
}
|
|
863
|
-
|
|
864
|
-
// accepted into the ACP queue. Never claim this request is running while
|
|
865
|
-
// the session itself says earlier work remains ahead of it.
|
|
866
|
-
const accepted = queued.queuedBehind > 0
|
|
867
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
868
|
-
: this.options.config.interrupt
|
|
869
|
-
? ownerNotices.receivedInterrupting()
|
|
870
|
-
: ownerNotices.receivedStarted();
|
|
859
|
+
const accepted = this.acceptanceNotice(queued);
|
|
871
860
|
this.inFlight.add(wireId);
|
|
872
861
|
const receipt = this.send(sender.id, accepted, wireId).then(() => undefined).catch(error => {
|
|
873
862
|
this.logError(`request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -1253,6 +1242,32 @@ export class OwnerChannel {
|
|
|
1253
1242
|
authorizationIntegrity() {
|
|
1254
1243
|
return this.options.config.agent ? { ok: true } : this.authorizations.integrity();
|
|
1255
1244
|
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Report what the session actually did with the prompt, not what the config
|
|
1247
|
+
* asked for. `interrupt: true` used to be reported as "your request
|
|
1248
|
+
* interrupted the previous task" unconditionally; the session now answers
|
|
1249
|
+
* whether anything was cancelled, whether the request is queued behind
|
|
1250
|
+
* earlier prompts, or whether it is held until the current task reaches a
|
|
1251
|
+
* safe stopping point. Backends that report no delivery state keep the old
|
|
1252
|
+
* queuedBehind-based wording.
|
|
1253
|
+
*/
|
|
1254
|
+
acceptanceNotice(queued) {
|
|
1255
|
+
switch (queued.delivery) {
|
|
1256
|
+
case 'interrupted': return ownerNotices.receivedInterrupting();
|
|
1257
|
+
case 'deferred': return ownerNotices.receivedDeferred();
|
|
1258
|
+
case 'queued': return ownerNotices.receivedQueued(Math.max(1, queued.queuedBehind));
|
|
1259
|
+
case 'started': return ownerNotices.receivedStarted();
|
|
1260
|
+
default:
|
|
1261
|
+
// Interrupting the live turn does not remove prompts which were already
|
|
1262
|
+
// accepted into the ACP queue. Never claim this request is running while
|
|
1263
|
+
// the session itself says earlier work remains ahead of it.
|
|
1264
|
+
return queued.queuedBehind > 0
|
|
1265
|
+
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
1266
|
+
: this.options.config.interrupt
|
|
1267
|
+
? ownerNotices.receivedInterrupting()
|
|
1268
|
+
: ownerNotices.receivedStarted();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1256
1271
|
async complete(active, outbox, queued, activityCursor) {
|
|
1257
1272
|
const progressMs = this.options.config.progress_interval_ms;
|
|
1258
1273
|
let lastSeq = activityCursor;
|
|
@@ -22,6 +22,13 @@ export declare const ownerNotices: {
|
|
|
22
22
|
receivedStarted: () => string;
|
|
23
23
|
receivedQueued: (queuedBehind: number) => string;
|
|
24
24
|
receivedInterrupting: () => string;
|
|
25
|
+
/**
|
|
26
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
27
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
28
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
29
|
+
* request.
|
|
30
|
+
*/
|
|
31
|
+
receivedDeferred: () => string;
|
|
25
32
|
status: (role: string, snapshot: SessionSnapshot) => string;
|
|
26
33
|
interrupted: (role: string) => string;
|
|
27
34
|
/** The turn IS cancelled — say how, without implying the owner must retry. */
|
|
@@ -26,6 +26,15 @@ export const ownerNotices = {
|
|
|
26
26
|
receivedInterrupting: () => "ℹ️ Message received. The agent's previous task was interrupted to prioritize "
|
|
27
27
|
+ 'this request, and it is now working on a response. '
|
|
28
28
|
+ 'The response will arrive in this channel when ready.',
|
|
29
|
+
/**
|
|
30
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
31
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
32
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
33
|
+
* request.
|
|
34
|
+
*/
|
|
35
|
+
receivedDeferred: () => 'ℹ️ Message received and held. The agent is in the middle of a task that '
|
|
36
|
+
+ 'cannot be interrupted safely; this request starts as soon as that work '
|
|
37
|
+
+ 'reaches a stopping point. The response will arrive in this channel when ready.',
|
|
29
38
|
status: (role, snapshot) => `📊 ${role} status: ${snapshot.readiness}; session is ${snapshot.alive ? 'online' : 'offline'}.`,
|
|
30
39
|
interrupted: (role) => `🛑 Interrupt sent to ${role}'s active turn.`,
|
|
31
40
|
/** The turn IS cancelled — say how, without implying the owner must retry. */
|
package/dist/runner.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ export interface RunnerDeps {
|
|
|
35
35
|
}
|
|
36
36
|
/** Environment injected only into the managed harness process. */
|
|
37
37
|
export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* The environment a managed harness child actually receives, checked at the one
|
|
40
|
+
* point where it is composed. `role.env` deliberately wins over harness prep,
|
|
41
|
+
* which is exactly how a stale fleet-wide model pin used to outrank the model
|
|
42
|
+
* the role was spawned with — so the model pin is verified here rather than
|
|
43
|
+
* trusted, and a disagreement stops the launch instead of being reported as a
|
|
44
|
+
* success (see src/model-env.ts).
|
|
45
|
+
*/
|
|
46
|
+
export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record<string, string> | undefined, stateDir: string): Record<string, string>;
|
|
38
47
|
/**
|
|
39
48
|
* Record who owns wake delivery for this run. Returning true means a fleet
|
|
40
49
|
* monitor is taking ownership back from a native harness and must start at the
|
|
@@ -61,6 +70,23 @@ export declare function readExitRecord(path: string): ExitRecord | null;
|
|
|
61
70
|
export declare const RESTART_LEDGER_FILE = ".restart-ledger.json";
|
|
62
71
|
/** Consecutive immediate failures tolerated before the agent is held down. */
|
|
63
72
|
export declare const RESTART_FAIL_THRESHOLD = 5;
|
|
73
|
+
/**
|
|
74
|
+
* How the previous supervisor process ended.
|
|
75
|
+
*
|
|
76
|
+
* `abrupt` is the case the ledger used to miss entirely: an OOM-kill or any
|
|
77
|
+
* other external signal takes the supervisor down before it can write anything,
|
|
78
|
+
* the service manager restarts the unit, and every durable indicator still
|
|
79
|
+
* describes the run that died. A health check reading them reported "no
|
|
80
|
+
* restarts" for a role that had died and come back.
|
|
81
|
+
*/
|
|
82
|
+
export interface TerminationRecord {
|
|
83
|
+
class: 'clean' | 'abrupt' | 'unknown';
|
|
84
|
+
detail: string;
|
|
85
|
+
/** When the SURVIVING process observed it, not when it happened. */
|
|
86
|
+
observedAt: string;
|
|
87
|
+
/** Start time of the run that ended, when it was recorded. */
|
|
88
|
+
runStartedAt?: string;
|
|
89
|
+
}
|
|
64
90
|
export interface RestartLedger {
|
|
65
91
|
version: 1;
|
|
66
92
|
consecutiveImmediateFailures: number;
|
|
@@ -72,7 +98,29 @@ export interface RestartLedger {
|
|
|
72
98
|
updatedAt: string;
|
|
73
99
|
/** When the circuit opened, for the held-down status line. */
|
|
74
100
|
openedAt?: string;
|
|
101
|
+
/** How the previous supervisor process ended, including abnormal exits. */
|
|
102
|
+
lastTermination?: TerminationRecord;
|
|
103
|
+
/** Supervisor processes that died without closing their run marker. */
|
|
104
|
+
abruptTerminations?: number;
|
|
105
|
+
/** Start of the supervisor run that owns this state directory now. */
|
|
106
|
+
supervisorStartedAt?: string;
|
|
75
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Carried across a supervisor process's life so its successor can tell an
|
|
110
|
+
* orderly exit from a kill. Present on disk == "a supervisor believed it was
|
|
111
|
+
* running"; the next start finding one that is not its own is proof the
|
|
112
|
+
* previous process died without getting to write anything.
|
|
113
|
+
*/
|
|
114
|
+
export declare const RUN_MARKER_FILE = ".supervisor-run.json";
|
|
115
|
+
/**
|
|
116
|
+
* Claim this state directory for the current supervisor process and report how
|
|
117
|
+
* the previous one ended. Runs BEFORE the first attempt, which is the whole
|
|
118
|
+
* point: after an abrupt kill nothing else writes until an attempt finishes,
|
|
119
|
+
* and an attempt can take minutes.
|
|
120
|
+
*/
|
|
121
|
+
export declare function claimSupervisorRun(dir: string, startedAt: string, pid?: number): TerminationRecord;
|
|
122
|
+
/** Orderly exit: the successor must not read this run as a kill. */
|
|
123
|
+
export declare function releaseSupervisorRun(dir: string): void;
|
|
76
124
|
/** Bounded exponential backoff for the nth consecutive immediate failure. */
|
|
77
125
|
export declare function backoffFor(consecutiveFailures: number): number;
|
|
78
126
|
/** Read a role's restart ledger; a missing or corrupt one starts clean. */
|