sortie-dogs 0.13.20 → 0.13.22
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 +7 -7
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/codex-mission.js +6 -1
- package/dist/codex/app-server.d.ts +8 -0
- package/dist/codex/app-server.js +37 -14
- package/dist/codex/mission-session.d.ts +7 -0
- package/dist/codex/mission-session.js +29 -4
- package/dist/core/operator-mission.d.ts +24 -1
- package/dist/core/operator-mission.js +127 -19
- package/dist/plugin/gate.d.ts +7 -0
- package/dist/plugin/gate.js +22 -2
- package/dist/plugin/profiled.js +261 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ evidence-backed result, not more agents for their own sake.
|
|
|
19
19
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
20
20
|
[](LICENSE)
|
|
21
21
|
|
|
22
|
-
**Current release: [v0.13.
|
|
23
|
-
· [Release notes](docs/release-v0.13.
|
|
22
|
+
**Current release: [v0.13.21](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.21)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.21.md)
|
|
24
24
|
|
|
25
25
|
[Quick start](#quick-start) · [Workflow](#mission-workflow) · [Models](#default-models) ·
|
|
26
26
|
[Measured results](#measured-results) · [Configuration](#configuration) · [Documentation](#documentation)
|
|
@@ -57,7 +57,7 @@ Use an existing Codex CLI signed in with **ChatGPT**. On Windows, PowerShell **7
|
|
|
57
57
|
available; POSIX uses bash. Sortie does not install Codex, start login or use a metered API fallback.
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
60
|
+
npm install --save-dev sortie-dogs@0.13.21
|
|
61
61
|
npx --no-install sortie-dogs codex init .
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -86,7 +86,7 @@ bridge. [Codex guide: SDK, permissions, progress, resume and manifest route](doc
|
|
|
86
86
|
Use **OpenCode V2**. In the target project:
|
|
87
87
|
|
|
88
88
|
```sh
|
|
89
|
-
npm install --save-dev sortie-dogs@0.13.
|
|
89
|
+
npm install --save-dev sortie-dogs@0.13.21
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -193,7 +193,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
193
193
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
194
194
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
195
195
|
|
|
196
|
-
All scores belong to their fixed candidates, **not v0.13.
|
|
196
|
+
All scores belong to their fixed candidates, **not v0.13.21**. SWE-bench remains an optional
|
|
197
197
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
198
198
|
|
|
199
199
|
## Configuration
|
|
@@ -216,7 +216,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
216
216
|
|
|
217
217
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
218
218
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
219
|
-
The current Mission asset marker is `0.13.
|
|
219
|
+
The current Mission asset marker is `0.13.21-operation-recovery-v1`; the unchanged Codex skill marker is
|
|
220
220
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
221
221
|
|
|
222
222
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -232,7 +232,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
232
232
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
233
233
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
234
234
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
235
|
-
- **Releases:** [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
235
|
+
- **Releases:** [v0.13.21](docs/release-v0.13.21.md) · [v0.13.20](docs/release-v0.13.20.md) · [v0.13.19](docs/release-v0.13.19.md) · [v0.13.18](docs/release-v0.13.18.md) · [v0.13.17](docs/release-v0.13.17.md) · [v0.13.16](docs/release-v0.13.16.md) · [v0.13.15](docs/release-v0.13.15.md) · [v0.13.14](docs/release-v0.13.14.md) · [v0.13.13](docs/release-v0.13.13.md) · [v0.13.12](docs/release-v0.13.12.md) · [v0.13.11](docs/release-v0.13.11.md) · [v0.13.10](docs/release-v0.13.10.md) ·
|
|
236
236
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
237
237
|
|
|
238
238
|
## Community
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.13.22-owned-task-lifecycle-v1";
|
|
7
7
|
export declare const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
8
8
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION | typeof CODEX_SKILL_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.13.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.13.22-owned-task-lifecycle-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -47,7 +47,7 @@ export async function runCodexMissionCommand(argv) {
|
|
|
47
47
|
const result = await adapter.run(prompt);
|
|
48
48
|
process.stdout.write(`${JSON.stringify({ status: result.accepted ? "succeeded" : "incomplete", root_session_id: result.rootSessionID,
|
|
49
49
|
thread_id: result.turn.threadID, turn_id: result.turn.turnID, native_status: result.turn.status,
|
|
50
|
-
response: result.turn.finalResponse, sessions: result.sessions, cost: null })}\n`);
|
|
50
|
+
response: result.turn.finalResponse, lifecycle: result.lifecycle, sessions: result.sessions, cost: null })}\n`);
|
|
51
51
|
return signalExit ?? (result.accepted ? 0 : 1);
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
@@ -66,6 +66,11 @@ export async function runCodexMissionCommand(argv) {
|
|
|
66
66
|
}
|
|
67
67
|
/** Bounded user-facing progress; do not dump control packets or hidden reasoning. */
|
|
68
68
|
export function codexMissionProgress(event) {
|
|
69
|
+
if (event.method === "turn/completed")
|
|
70
|
+
return { thread_id: event.threadId, phase: "native-turn-terminal", turn_id: event.params.turn?.id,
|
|
71
|
+
native_status: event.params.turn?.status };
|
|
72
|
+
if (event.method === "sortie/continuation")
|
|
73
|
+
return { thread_id: event.threadId, turn_id: event.params.turnId, phase: event.params.phase, calls: event.params.calls };
|
|
69
74
|
if (event.method === "sortie/modelRoute")
|
|
70
75
|
return { thread_id: event.threadId, phase: "model-route", agent: event.params.agent, model: event.params.model,
|
|
71
76
|
effort: event.params.effort, service_tier: event.params.serviceTier };
|
|
@@ -46,6 +46,14 @@ export interface CodexTurnResult {
|
|
|
46
46
|
readonly finalResponse?: string;
|
|
47
47
|
readonly items: readonly JsonObject[];
|
|
48
48
|
readonly usage?: JsonObject;
|
|
49
|
+
/** Host tools still running at native turn completion, drained without resending them. */
|
|
50
|
+
readonly deferredToolResults?: readonly CodexDeferredToolResult[];
|
|
51
|
+
}
|
|
52
|
+
export interface CodexDeferredToolResult {
|
|
53
|
+
readonly callID: string;
|
|
54
|
+
readonly tool: string;
|
|
55
|
+
readonly success: boolean;
|
|
56
|
+
readonly output: string;
|
|
49
57
|
}
|
|
50
58
|
export interface CodexAuthenticationState {
|
|
51
59
|
readonly type: string | null;
|
package/dist/codex/app-server.js
CHANGED
|
@@ -188,7 +188,7 @@ export class CodexAppServerHost {
|
|
|
188
188
|
if (!turnID)
|
|
189
189
|
throw new CodexHostError("protocol-invalid-response", "turn/start returned no turn id.");
|
|
190
190
|
return new Promise((resolve, reject) => {
|
|
191
|
-
this.active = { threadID, turnID, items: [], observer: Promise.resolve(), onEvent: options.onEvent, resolve, reject };
|
|
191
|
+
this.active = { threadID, turnID, items: [], observer: Promise.resolve(), tools: new Set(), onEvent: options.onEvent, resolve, reject };
|
|
192
192
|
const backlog = this.notificationBacklog.splice(0);
|
|
193
193
|
void backlog.reduce((prior, event) => prior.then(() => this.notification(event.method, event.params)), Promise.resolve())
|
|
194
194
|
.catch(error => { this.active?.reject(error); this.active = undefined; });
|
|
@@ -289,20 +289,34 @@ export class CodexAppServerHost {
|
|
|
289
289
|
const params = object(message.params) ? message.params : {};
|
|
290
290
|
if (method === "item/tool/call") {
|
|
291
291
|
const active = this.active;
|
|
292
|
-
|
|
292
|
+
if (!active || active.completing || params.threadId !== active.threadID || params.turnId !== active.turnID ||
|
|
293
|
+
(params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool) {
|
|
294
|
+
this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText",
|
|
295
|
+
text: "Dynamic tool call does not belong to an active Sortie turn." }], success: false } });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const call = { threadId: active.threadID, turnId: active.turnID, callId: params.callId, tool: params.tool, arguments: params.arguments };
|
|
299
|
+
const pending = Promise.resolve().then(async () => {
|
|
300
|
+
let output, success;
|
|
301
|
+
try {
|
|
302
|
+
output = await this.options.dynamicTool(call);
|
|
303
|
+
success = true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
output = error instanceof Error ? error.message : "Dynamic tool failed.";
|
|
307
|
+
success = false;
|
|
308
|
+
}
|
|
309
|
+
if (!this.closed && !this.fault)
|
|
310
|
+
this.transport.send({ id: message.id, result: { contentItems: [{ type: "inputText", text: output }], success } });
|
|
311
|
+
return { callID: call.callId, tool: call.tool, success, output };
|
|
312
|
+
});
|
|
313
|
+
active.tools.add(pending);
|
|
293
314
|
try {
|
|
294
|
-
|
|
295
|
-
(params.namespace != null) || typeof params.callId !== "string" || typeof params.tool !== "string" || !this.options.dynamicTool)
|
|
296
|
-
throw new Error("Dynamic tool call does not belong to an active Sortie turn.");
|
|
297
|
-
const output = await this.options.dynamicTool({ threadId: active.threadID, turnId: active.turnID,
|
|
298
|
-
callId: params.callId, tool: params.tool, arguments: params.arguments });
|
|
299
|
-
result = { contentItems: [{ type: "inputText", text: output }], success: true };
|
|
315
|
+
await pending;
|
|
300
316
|
}
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
finally {
|
|
318
|
+
active.tools.delete(pending);
|
|
303
319
|
}
|
|
304
|
-
if (!this.closed)
|
|
305
|
-
this.transport.send({ id: message.id, result });
|
|
306
320
|
return;
|
|
307
321
|
}
|
|
308
322
|
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
|
|
@@ -374,14 +388,23 @@ export class CodexAppServerHost {
|
|
|
374
388
|
// requests such as interrupt(), whose response must be read by this pump.
|
|
375
389
|
// Keep the completing turn active until observers settle so close() can
|
|
376
390
|
// reject it and a later turn cannot receive an interrupt meant for it.
|
|
377
|
-
|
|
391
|
+
const deferred = status === "completed" ? [...active.tools] : [];
|
|
392
|
+
// Native response completion is not host execution completion. Keep pumping
|
|
393
|
+
// command responses and child Tasks; explicit close/transport loss can still
|
|
394
|
+
// reject this turn. Failed/interrupted native turns never auto-continue.
|
|
395
|
+
void Promise.all([active.observer, Promise.all(deferred)]).then(([, tools]) => {
|
|
378
396
|
if (this.active !== active)
|
|
379
397
|
return;
|
|
380
398
|
this.active = undefined;
|
|
381
399
|
if (active.observerError)
|
|
382
400
|
active.reject(new CodexHostError("request-failed", "Codex turn event observer failed.", active.observerError));
|
|
383
401
|
else
|
|
384
|
-
active.resolve(result);
|
|
402
|
+
active.resolve({ ...result, ...(tools.length ? { deferredToolResults: tools } : {}) });
|
|
403
|
+
}).catch(error => {
|
|
404
|
+
if (this.active !== active)
|
|
405
|
+
return;
|
|
406
|
+
this.active = undefined;
|
|
407
|
+
active.reject(error);
|
|
385
408
|
});
|
|
386
409
|
}
|
|
387
410
|
}
|
|
@@ -67,6 +67,12 @@ export declare class CodexMissionSession {
|
|
|
67
67
|
rootSessionID: string;
|
|
68
68
|
turn: CodexTurnResult;
|
|
69
69
|
accepted: boolean;
|
|
70
|
+
lifecycle: {
|
|
71
|
+
root_turn_terminal: boolean;
|
|
72
|
+
owned_tasks_settled: boolean;
|
|
73
|
+
mission_phase: string | null;
|
|
74
|
+
mission_accepted: boolean;
|
|
75
|
+
};
|
|
70
76
|
sessions: readonly JsonObject[];
|
|
71
77
|
}>;
|
|
72
78
|
private stopHosts;
|
|
@@ -85,6 +91,7 @@ export declare class CodexMissionSession {
|
|
|
85
91
|
private restoreSession;
|
|
86
92
|
private tools;
|
|
87
93
|
private prompt;
|
|
94
|
+
private promptTurn;
|
|
88
95
|
private execute;
|
|
89
96
|
}
|
|
90
97
|
export {};
|
|
@@ -72,8 +72,11 @@ export class CodexMissionSession {
|
|
|
72
72
|
const previous = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
|
|
73
73
|
const turn = await this.prompt(session, prompt);
|
|
74
74
|
const mission = await new OperatorMissionRuntime(this.directory, profile).read(session.id);
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const ownedTasksSettled = [...this.sessions.values()].every(item => !["running", "unknown"].includes(item.outcome));
|
|
76
|
+
const accepted = turn.status === "completed" && ownedTasksSettled && mission?.phase === "completed" &&
|
|
77
|
+
(previous?.phase !== "completed" || previous.id !== mission.id);
|
|
78
|
+
return { rootSessionID: session.id, turn, accepted, lifecycle: { root_turn_terminal: ["completed", "failed", "interrupted", "cancelled"].includes(turn.status),
|
|
79
|
+
owned_tasks_settled: ownedTasksSettled, mission_phase: mission?.phase ?? null, mission_accepted: accepted }, sessions: [...this.sessions.values()].map(item => ({ thread_id: item.id, agent: item.agent, model: item.model, usage: item.usage ?? null, usage_scope: "native-thread-cumulative", cost: null })) };
|
|
77
80
|
}
|
|
78
81
|
async stopHosts() {
|
|
79
82
|
this.closed = true;
|
|
@@ -459,11 +462,33 @@ export class CodexMissionSession {
|
|
|
459
462
|
return tools;
|
|
460
463
|
}
|
|
461
464
|
async prompt(session, prompt, onDispatch) {
|
|
465
|
+
let result = await this.promptTurn(session, prompt, onDispatch);
|
|
466
|
+
while (result.status === "completed" && result.deferredToolResults?.length) {
|
|
467
|
+
if (this.closed)
|
|
468
|
+
throw new Error("Codex Mission adapter is closed.");
|
|
469
|
+
const mission = await new OperatorMissionRuntime(this.directory, profile).read(this.rootOf(session));
|
|
470
|
+
if (!mission || ["completed", "cancelled"].includes(mission.phase))
|
|
471
|
+
break;
|
|
472
|
+
await this.claimOwner(this.rootOf(session));
|
|
473
|
+
await this.options.onEvent?.({ method: "sortie/continuation", threadId: session.id,
|
|
474
|
+
params: { turnId: result.turnID, phase: "owned-tools-settled", calls: result.deferredToolResults.map(item => ({ callId: item.callID, tool: item.tool, success: item.success })) } });
|
|
475
|
+
result = await this.promptTurn(session, "The previous native response ended while existing host tools were still running. Those exact calls have now returned:\n" +
|
|
476
|
+
JSON.stringify(result.deferredToolResults) +
|
|
477
|
+
"\nContinue this same Mission from these results through the remaining validation, delivery, review and root acceptance. " +
|
|
478
|
+
"Do not resend the original request or redispatch the completed Tasks. Preserve failures, ownership and cumulative budget; use existing Mission status for the next action.", undefined, true);
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
async promptTurn(session, prompt, onDispatch, continuation = false) {
|
|
462
483
|
const startedAt = Date.now();
|
|
463
484
|
const messageID = randomUUID();
|
|
464
485
|
const message = { id: messageID, agent: session.agent, model: { ...session.model } };
|
|
465
|
-
const parts = [{ type: "text", text: prompt }];
|
|
466
|
-
|
|
486
|
+
const parts = [{ type: "text", text: prompt, ...(continuation ? { synthetic: true } : {}) }];
|
|
487
|
+
// Only this host's drained results can continue an already-admitted execution.
|
|
488
|
+
// Reclaiming that Task with the result prompt rejects Workers' exact handoff;
|
|
489
|
+
// treating it as a real root request also grants spurious user-turn authority.
|
|
490
|
+
if (!continuation)
|
|
491
|
+
await this.hooks["chat.message"]?.({ sessionID: session.id, messageID, agent: session.agent }, { message, parts });
|
|
467
492
|
// Shared Task routing may select a model; explicit adapter overrides still govern every native turn.
|
|
468
493
|
if (message.model.providerID !== "openai")
|
|
469
494
|
throw new Error("Codex Mission cannot use a non-OpenAI model route.");
|
|
@@ -101,16 +101,30 @@ export interface MissionTerminalRescue {
|
|
|
101
101
|
export interface MissionExecution {
|
|
102
102
|
commands: string[];
|
|
103
103
|
directory: string;
|
|
104
|
+
/** History-service availability is not a process outcome or an execution permission. */
|
|
105
|
+
recovery?: {
|
|
106
|
+
status: "unavailable";
|
|
107
|
+
errors: {
|
|
108
|
+
sessionID: string;
|
|
109
|
+
reason: string;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
104
112
|
observations: {
|
|
105
113
|
command: string;
|
|
106
114
|
directory: string;
|
|
107
115
|
callID: string;
|
|
108
116
|
sessionID: string;
|
|
109
117
|
startedAt: string;
|
|
118
|
+
/** Actual native execution start, distinct from a live hook's admission clock. */
|
|
119
|
+
nativeStartedAt?: string;
|
|
110
120
|
completedAt?: string;
|
|
111
121
|
exit?: number;
|
|
112
122
|
status?: "running" | "completed" | "error";
|
|
113
123
|
shellID?: string;
|
|
124
|
+
/** Contradictory native records retained for this same call, not a process outcome. */
|
|
125
|
+
conflicting?: boolean;
|
|
126
|
+
/** Latest actual start reported for a conflicting call; original startedAt is retained. */
|
|
127
|
+
latestConflictingStartedAt?: string;
|
|
114
128
|
outcome?: "not-started" | "execution-failed" | "executed";
|
|
115
129
|
result?: Record<string, unknown>;
|
|
116
130
|
}[];
|
|
@@ -314,10 +328,19 @@ export declare function missionReviewVerdict(text: string): "PASS" | "evidence-g
|
|
|
314
328
|
export declare function missionReviewAccepted(review: NonNullable<OperatorMission["review"]>): boolean;
|
|
315
329
|
/** A short native report, not a tag/hash-based second-review policy or an approval checklist. */
|
|
316
330
|
export declare function missionSelfRecheckReport(text: string, source: string, hostBound?: boolean): Pick<MissionSelfRecheck, "unresolvedFindings" | "residualMajor"> | undefined;
|
|
317
|
-
export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "running" | "execution-failed" | "executed";
|
|
331
|
+
export declare function missionExecutionStatus(mission: OperatorMission): "not-required" | "not-started" | "recovery-unavailable" | "recovery-blocked" | "running" | "execution-failed" | "executed";
|
|
332
|
+
/** Native Windows cwd case/separator spelling is not a different operation directory. */
|
|
333
|
+
export declare function missionOperationDirectoryMatch(actual: string, declared: string): boolean;
|
|
334
|
+
/** Match only the same declared operation, allowing its native PowerShell envelope and
|
|
335
|
+
* an absolute Node/script/path spelling resolved from the unchanged operation cwd. */
|
|
336
|
+
export declare function missionOperationCommandMatch(command: string, declared: readonly string[], directory: string): string | undefined;
|
|
337
|
+
/** Apply the existing no-decoration operation check to the same accepted native spelling. */
|
|
338
|
+
export declare function missionOperationCommandDecorated(command: string, declared: readonly string[], directory: string): boolean;
|
|
318
339
|
/** A terminal failure is a collected result, not a successful operation. Required success
|
|
319
340
|
* remains in the declared formal checks and Operator's original-request comparison. */
|
|
320
341
|
export declare function missionExecutionComplete(mission: OperatorMission): boolean;
|
|
342
|
+
/** Missing history cannot justify replay; adequate collected results remain usable. */
|
|
343
|
+
export declare function missionExecutionRecoveryAction(mission: OperatorMission): string | undefined;
|
|
321
344
|
/** Projection of existing observations only; never another check or inferred success. */
|
|
322
345
|
export declare function missionOperationSummary(mission: OperatorMission): Record<string, unknown>;
|
|
323
346
|
/** Observe the command's own terminal summary, never Worker/Reviewer prose. Scores are result data,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { join, resolve, win32 } from "node:path";
|
|
4
4
|
import { normalizeExecutionScope } from "./path.js";
|
|
5
5
|
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
6
6
|
import { profileAgent } from "./runtime-profile.js";
|
|
7
|
-
import { normalizeCommand } from "../plugin/gate.js";
|
|
7
|
+
import { normalizeCommand, shellCommandBase, shellCommandWords } from "../plugin/gate.js";
|
|
8
8
|
import { missionValidationCommand } from "./mission-validation-command.js";
|
|
9
9
|
import { retainMissionContext, missionContextView } from "./mission-context.js";
|
|
10
10
|
import { planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
@@ -111,22 +111,129 @@ export function missionExecutionStatus(mission) {
|
|
|
111
111
|
if (mission.kind !== "operation")
|
|
112
112
|
return "not-required";
|
|
113
113
|
const execution = mission.execution;
|
|
114
|
-
if (!execution || execution.commands.length === 0
|
|
114
|
+
if (!execution || execution.commands.length === 0)
|
|
115
115
|
return "not-started";
|
|
116
|
-
|
|
116
|
+
if (execution.observations.length === 0)
|
|
117
|
+
return execution.recovery ? "recovery-unavailable" : "not-started";
|
|
118
|
+
// Recovery may discover an older call after a newer live/failed observation was saved.
|
|
119
|
+
const orderingTime = (item) => Math.max(Date.parse(item.nativeStartedAt ?? item.startedAt), Date.parse(item.latestConflictingStartedAt ?? item.nativeStartedAt ?? item.startedAt));
|
|
120
|
+
const ordered = [...execution.observations].sort((left, right) => orderingTime(left) - orderingTime(right)).reverse();
|
|
121
|
+
const latest = execution.commands.map(command => ordered.find(item => item.command === command && item.directory === execution.directory));
|
|
122
|
+
if (latest.some(item => item?.conflicting || item && ["completed", "error"].includes(item.status ?? "") &&
|
|
123
|
+
!["executed", "execution-failed", "not-started"].includes(item.outcome ?? "")))
|
|
124
|
+
return "recovery-blocked";
|
|
117
125
|
if (latest.some(item => item && !item.completedAt))
|
|
118
126
|
return "running";
|
|
127
|
+
if (latest.some(item => !item) && execution.recovery)
|
|
128
|
+
return "recovery-unavailable";
|
|
119
129
|
if (latest.some(item => !item || !["executed", "execution-failed"].includes(item.outcome ?? "")))
|
|
120
130
|
return "not-started";
|
|
121
131
|
if (latest.some(item => item?.outcome === "execution-failed" || (item && (item.status === "error" || item.exit !== 0))))
|
|
122
132
|
return "execution-failed";
|
|
123
133
|
return "executed";
|
|
124
134
|
}
|
|
135
|
+
function stripMissionPowerShellPrefix(command) {
|
|
136
|
+
return command.trim().replace(/^\$ErrorActionPreference\s*=\s*(['"])Stop\1\s*;\s*/iu, "");
|
|
137
|
+
}
|
|
138
|
+
function missionCommandTokens(command) {
|
|
139
|
+
const tokens = shellCommandWords(stripMissionPowerShellPrefix(command));
|
|
140
|
+
if (tokens[0] === "&")
|
|
141
|
+
tokens.shift();
|
|
142
|
+
return tokens.length ? tokens : undefined;
|
|
143
|
+
}
|
|
144
|
+
function missionWindowsPath(value) {
|
|
145
|
+
return /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
|
146
|
+
}
|
|
147
|
+
function missionPathIdentity(value, directory, windows) {
|
|
148
|
+
if (!windows)
|
|
149
|
+
return resolve(directory, value);
|
|
150
|
+
return win32.normalize(win32.resolve(directory, value)).toLowerCase();
|
|
151
|
+
}
|
|
152
|
+
/** Native Windows cwd case/separator spelling is not a different operation directory. */
|
|
153
|
+
export function missionOperationDirectoryMatch(actual, declared) {
|
|
154
|
+
const windows = missionWindowsPath(actual) || missionWindowsPath(declared);
|
|
155
|
+
return missionPathIdentity(actual, declared, windows) === missionPathIdentity(declared, declared, windows);
|
|
156
|
+
}
|
|
157
|
+
function sameNodeOperation(declared, actual, directory) {
|
|
158
|
+
const expected = missionCommandTokens(declared), observed = missionCommandTokens(actual);
|
|
159
|
+
if (!expected || !observed || expected.length !== observed.length || expected.length < 2)
|
|
160
|
+
return false;
|
|
161
|
+
const windows = missionWindowsPath(directory);
|
|
162
|
+
const name = (value) => {
|
|
163
|
+
const basename = value.split(windows ? /[\\/]/u : /\//u).at(-1) ?? "";
|
|
164
|
+
return windows ? basename.toLowerCase() : basename;
|
|
165
|
+
};
|
|
166
|
+
const nodeNames = windows ? ["node", "node.exe"] : ["node"];
|
|
167
|
+
if (!nodeNames.includes(name(expected[0])) || !nodeNames.includes(name(observed[0])))
|
|
168
|
+
return false;
|
|
169
|
+
const executablePath = (value) => windows ? /[\\/]/u.test(value) : value.includes("/");
|
|
170
|
+
if (executablePath(expected[0]) && (!executablePath(observed[0]) ||
|
|
171
|
+
missionPathIdentity(expected[0], directory, windows) !== missionPathIdentity(observed[0], directory, windows)))
|
|
172
|
+
return false;
|
|
173
|
+
const script = expected[1];
|
|
174
|
+
if (!/\.(?:cjs|mjs|js)$/iu.test(script) || !/\.(?:cjs|mjs|js)$/iu.test(observed[1]))
|
|
175
|
+
return false;
|
|
176
|
+
if (missionPathIdentity(script, directory, windows) !== missionPathIdentity(observed[1], directory, windows))
|
|
177
|
+
return false;
|
|
178
|
+
for (let index = 2; index < expected.length; index += 1) {
|
|
179
|
+
const left = expected[index], right = observed[index];
|
|
180
|
+
if (left === right)
|
|
181
|
+
continue;
|
|
182
|
+
// Options and URI values are script inputs, not filesystem paths with Windows case rules.
|
|
183
|
+
const literal = (value) => value.startsWith("-") || !missionWindowsPath(value) && /^[A-Za-z][A-Za-z\d+.-]*:/u.test(value);
|
|
184
|
+
if (literal(left) || literal(right))
|
|
185
|
+
return false;
|
|
186
|
+
// Only the supported absolute-path substitution can change a positional argument.
|
|
187
|
+
// Two different relative opaque values must not collapse via path normalization.
|
|
188
|
+
const absolute = (value) => missionWindowsPath(value) || value.startsWith("/");
|
|
189
|
+
const pathArgument = absolute(left) !== absolute(right) || windows && missionWindowsPath(left) && missionWindowsPath(right);
|
|
190
|
+
if (pathArgument
|
|
191
|
+
? missionPathIdentity(left, directory, windows) !== missionPathIdentity(right, directory, windows)
|
|
192
|
+
: left !== right)
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/** Match only the same declared operation, allowing its native PowerShell envelope and
|
|
198
|
+
* an absolute Node/script/path spelling resolved from the unchanged operation cwd. */
|
|
199
|
+
export function missionOperationCommandMatch(command, declared, directory) {
|
|
200
|
+
const normalized = normalizeCommand(stripMissionPowerShellPrefix(command));
|
|
201
|
+
const exact = declared.filter(item => normalizeCommand(stripMissionPowerShellPrefix(item)) === normalized);
|
|
202
|
+
if (exact.length === 1)
|
|
203
|
+
return normalizeCommand(exact[0]);
|
|
204
|
+
if (exact.length > 1)
|
|
205
|
+
return undefined;
|
|
206
|
+
const equivalent = declared.filter(item => sameNodeOperation(item, command, directory));
|
|
207
|
+
return equivalent.length === 1 ? normalizeCommand(equivalent[0]) : undefined;
|
|
208
|
+
}
|
|
209
|
+
/** Apply the existing no-decoration operation check to the same accepted native spelling. */
|
|
210
|
+
export function missionOperationCommandDecorated(command, declared, directory) {
|
|
211
|
+
if (missionOperationCommandMatch(command, declared, directory) !== undefined)
|
|
212
|
+
return false;
|
|
213
|
+
const stripped = stripMissionPowerShellPrefix(command), actual = normalizeCommand(stripped);
|
|
214
|
+
if (declared.some(item => {
|
|
215
|
+
const expected = normalizeCommand(stripMissionPowerShellPrefix(item));
|
|
216
|
+
return actual.startsWith(expected) && /^\s*(?:\||\d?>|&&?|;)/u.test(actual.slice(expected.length));
|
|
217
|
+
}))
|
|
218
|
+
return true;
|
|
219
|
+
const base = shellCommandBase(stripped, stripped !== command.trim() || stripped.startsWith("&") ? "powershell" : "posix");
|
|
220
|
+
return base !== undefined && missionOperationCommandMatch(base, declared, directory) !== undefined;
|
|
221
|
+
}
|
|
125
222
|
/** A terminal failure is a collected result, not a successful operation. Required success
|
|
126
223
|
* remains in the declared formal checks and Operator's original-request comparison. */
|
|
127
224
|
export function missionExecutionComplete(mission) {
|
|
128
225
|
return ["not-required", "executed", "execution-failed"].includes(missionExecutionStatus(mission));
|
|
129
226
|
}
|
|
227
|
+
/** Missing history cannot justify replay; adequate collected results remain usable. */
|
|
228
|
+
export function missionExecutionRecoveryAction(mission) {
|
|
229
|
+
if (missionExecutionStatus(mission) === "recovery-blocked")
|
|
230
|
+
return "An admitted operation has missing or contradictory terminal evidence. Preserve existing observations; inspect the same native call's shell/progress and recover its actual result. " +
|
|
231
|
+
"Do not replay or rerun the operation. Report the actual recovery blocker if its result remains unknown.";
|
|
232
|
+
if (!mission.execution?.recovery || missionExecutionComplete(mission))
|
|
233
|
+
return undefined;
|
|
234
|
+
return "Native operation history could not be read. Preserve existing observations; inspect the admitted session's existing shell/progress or restore the history reader. " +
|
|
235
|
+
"Do not replay or rerun the operation. Report the actual recovery blocker if history remains unavailable.";
|
|
236
|
+
}
|
|
130
237
|
/** Projection of existing observations only; never another check or inferred success. */
|
|
131
238
|
export function missionOperationSummary(mission) {
|
|
132
239
|
const status = missionExecutionStatus(mission);
|
|
@@ -836,21 +943,22 @@ export function missionPacket(mission, run, includeWorkQuantity = true) {
|
|
|
836
943
|
: "Correction ready; reuse the native author for explicit self-recheck or select an independent Reviewer with review_mission when useful. Remaining authorized implementation may proceed with plan_units without acceptance. Known Major/Medium defects still require correction. Do not restart unchanged checks or investigation; CORRECTION_READY alone is not acceptance."
|
|
837
944
|
: mission.kind === "operation" && operationStatus === "running"
|
|
838
945
|
? "The declared operation is already running. Inspect its native shell/progress; do not start another Worker or run. Wait for a terminal result, or report the existing run as blocked if its completion cannot be observed."
|
|
839
|
-
:
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
946
|
+
: missionExecutionRecoveryAction(mission) !== undefined ? missionExecutionRecoveryAction(mission)
|
|
947
|
+
: run?.phase === "awaiting-decision" && mission.corrections?.some(item => item.runID === run.runID && item.status === "failed")
|
|
948
|
+
? "Correction validation or native execution failed. Reuse its context with repair_review, select another available Reviewer, or plan_units for Worker/direct correction. Retain findings, affected checks, requested delivery and cumulative spend. Failure is never acceptance."
|
|
949
|
+
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
950
|
+
? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
|
|
951
|
+
: run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && !unit.normalRemediationUsed)
|
|
952
|
+
? "Coordinator: one exact declared validation failed. Reconcile the returned Worker and cumulative budget, then call retry_mission_unit once for that unit before replanning. This preserves the same scope and acceptance."
|
|
953
|
+
: run.units.some(unit => unit.status === "failed" && unit.resultClass === "acceptance" && unit.failure?.outcome === "fail" && unit.normalRemediationUsed && !unit.terminalRescue)
|
|
954
|
+
? "Coordinator: the one ordinary remediation failed the declared validation again. If its native terminal, writer release, exact candidate and cumulative budget permit, call rescue_mission_unit once; inspect and report a non_rescue reason, then continue ordinary correction within budget. Rescue does not bypass validation, review or root acceptance."
|
|
955
|
+
: "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
|
|
956
|
+
: run?.phase === "awaiting-acceptance" && !operationComplete
|
|
957
|
+
? `Requested operation is ${operationStatus}. Continue the actual operation or report its blocker; auxiliary checks and review disposition cannot complete it.`
|
|
958
|
+
: run?.phase === "awaiting-acceptance" ? (reviewAccepted
|
|
959
|
+
? "Coordinator: review permits submission, not completion of remaining implementation. Continue authorized remaining work with plan_units, or submit when the original requirements are met. Retain review notes; do not repeat passed validation or review for evidence formatting. Operator final acceptance remains separate."
|
|
960
|
+
: "Coordinator: correct known Major/Medium findings, then obtain native author self-recheck or useful independent review (required for concrete residual Major risk). Remaining authorized implementation can proceed without submission/acceptance. Operator compares all requirements with source/evidence before complete_mission.")
|
|
961
|
+
: "Coordinator: continue the next useful unit within original requirements. Return only a completion candidate, user-only decision, or scope/budget extension." };
|
|
854
962
|
}
|
|
855
963
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
856
964
|
export function missionReviewTask(mission) {
|
package/dist/plugin/gate.d.ts
CHANGED
|
@@ -51,7 +51,14 @@ export declare function resolveProjectRoot(input: {
|
|
|
51
51
|
directory: string;
|
|
52
52
|
worktree?: string;
|
|
53
53
|
}): string;
|
|
54
|
+
/** Reuse the write-gate's quoted shell tokenization for non-authorizing command identity checks. */
|
|
55
|
+
export declare function shellCommandWords(command: string): string[];
|
|
56
|
+
type ShellDialect = "posix" | "powershell";
|
|
57
|
+
/** The command before an unquoted shell suffix, reusing the gate's existing syntax scanner. */
|
|
58
|
+
export declare function shellCommandBase(command: string, dialect: ShellDialect): string | undefined;
|
|
54
59
|
export declare function describeUnclassifiedCommand(tool: string, args: unknown): string | undefined;
|
|
60
|
+
/** Shell tool names already supported by the write gate, including native suffixes. */
|
|
61
|
+
export declare function isShellTool(tool: string): boolean;
|
|
55
62
|
/** Extract known write destinations; unknown shell executables fail closed as ambiguous. */
|
|
56
63
|
export declare function extractWritePaths(tool: string, args: unknown, toolDirectory?: string): Extraction;
|
|
57
64
|
/**
|
package/dist/plugin/gate.js
CHANGED
|
@@ -101,6 +101,10 @@ function unquote(value) {
|
|
|
101
101
|
function words(command) {
|
|
102
102
|
return command.match(/"(?:\\.|[^"])*"|'[^']*'|[^\s]+/gu)?.map(unquote) ?? [];
|
|
103
103
|
}
|
|
104
|
+
/** Reuse the write-gate's quoted shell tokenization for non-authorizing command identity checks. */
|
|
105
|
+
export function shellCommandWords(command) {
|
|
106
|
+
return words(command);
|
|
107
|
+
}
|
|
104
108
|
function operands(tokens) {
|
|
105
109
|
const result = [];
|
|
106
110
|
for (let index = 1; index < tokens.length; index += 1) {
|
|
@@ -411,6 +415,18 @@ function shellSegments(command, dialect) {
|
|
|
411
415
|
segments.push(command.slice(start));
|
|
412
416
|
return segments;
|
|
413
417
|
}
|
|
418
|
+
/** The command before an unquoted shell suffix, reusing the gate's existing syntax scanner. */
|
|
419
|
+
export function shellCommandBase(command, dialect) {
|
|
420
|
+
const masked = scanShellSyntax(command, dialect).masked;
|
|
421
|
+
for (let index = 0; index < masked.length; index += 1) {
|
|
422
|
+
const character = masked[index];
|
|
423
|
+
if (!";|><\r\n".includes(character) && !(character === "&" && masked.slice(0, index).trim()))
|
|
424
|
+
continue;
|
|
425
|
+
const base = command.slice(0, index);
|
|
426
|
+
return (character === ">" || character === "<" ? base.replace(/(?:^|\s)\d+$/u, "") : base).trim();
|
|
427
|
+
}
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
414
430
|
function depthOnePowerShellLiteral(source) {
|
|
415
431
|
const match = /^\s*(?:"[^"]*(?:pwsh|powershell)(?:\.exe)?"|[^\s"']*(?:pwsh|powershell)(?:\.exe)?)\s+-noprofile\s+-command\s+'((?:''|[^'])*)'\s*$/iu.exec(source);
|
|
416
432
|
return match?.[1].replaceAll("''", "'");
|
|
@@ -761,6 +777,10 @@ export function describeUnclassifiedCommand(tool, args) {
|
|
|
761
777
|
const extracted = extractWritePaths(tool, args);
|
|
762
778
|
return extracted.ambiguous && extracted.issue !== undefined ? issuePath(extracted.issue) : undefined;
|
|
763
779
|
}
|
|
780
|
+
/** Shell tool names already supported by the write gate, including native suffixes. */
|
|
781
|
+
export function isShellTool(tool) {
|
|
782
|
+
return /^(?:bash|shell|powershell|pwsh)(?:$|[_-])/u.test(tool.toLowerCase());
|
|
783
|
+
}
|
|
764
784
|
/** Extract known write destinations; unknown shell executables fail closed as ambiguous. */
|
|
765
785
|
export function extractWritePaths(tool, args, toolDirectory) {
|
|
766
786
|
const name = tool.toLowerCase();
|
|
@@ -774,7 +794,7 @@ export function extractWritePaths(tool, args, toolDirectory) {
|
|
|
774
794
|
const extracted = patch === undefined ? [] : patchPaths(patch);
|
|
775
795
|
return { applies: true, ambiguous: extracted.length === 0, paths: [...paths, ...extracted] };
|
|
776
796
|
}
|
|
777
|
-
if (
|
|
797
|
+
if (isShellTool(tool)) {
|
|
778
798
|
const command = isRecord(args) && typeof args.command === "string" ? args.command : undefined;
|
|
779
799
|
if (command === undefined) {
|
|
780
800
|
const ambiguous = paths.length === 0;
|
|
@@ -1036,7 +1056,7 @@ export function isKnownReadOnlyTool(tool, args, additionalReadOnlyTools = new Se
|
|
|
1036
1056
|
const name = tool.toLowerCase();
|
|
1037
1057
|
if (READ_ONLY_TOOLS.has(name) || additionalReadOnlyTools.has(name))
|
|
1038
1058
|
return true;
|
|
1039
|
-
if (
|
|
1059
|
+
if (!isShellTool(tool))
|
|
1040
1060
|
return false;
|
|
1041
1061
|
const extraction = extractWritePaths(tool, args);
|
|
1042
1062
|
return !extraction.applies && !extraction.ambiguous && extraction.remoteMutation !== true;
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -11,9 +11,9 @@ import { BUILT_IN_MODEL_CATALOG } from "./model-routing.js";
|
|
|
11
11
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
12
12
|
import { decoratePreviewHeadings, returnReportPanel } from "./receipt-presentation.js";
|
|
13
13
|
import { sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
14
|
-
import { normalizeCommand, canonicalObservedValidationMembers, extractWritePaths } from "./gate.js";
|
|
14
|
+
import { normalizeCommand, canonicalObservedValidationMembers, extractWritePaths, isShellTool } from "./gate.js";
|
|
15
15
|
import { normalizeExecutionScope, normalizeManifestScope, normalizeRelativePath } from "../core/path.js";
|
|
16
|
-
import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionComplete, missionExecutionStatus, missionOperationSummary, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
|
|
16
|
+
import { OperatorMissionRuntime, missionAcceptanceSummary, missionPacket, missionPlan, missionReviewAccepted, missionReviewIndependent, missionReviewScope, missionReviewTask, missionCommandOutcome, missionConversationContext, missionExecutionComplete, missionExecutionRecoveryAction, missionExecutionStatus, missionOperationCommandMatch, missionOperationCommandDecorated, missionOperationDirectoryMatch, missionOperationSummary, missionValidationCommand, missionReviewTraces, missionReviewVerdict, missionSelfRecheckReport } from "../core/operator-mission.js";
|
|
17
17
|
import { publishMissionProgress } from "./mission-progress.js";
|
|
18
18
|
import { formatWorkQuantity, planWorkQuantity } from "../core/work-quantity.js";
|
|
19
19
|
import { cachedWorkProgress, forgetWorkSession, observeActorWork, workRootForActor } from "../core/work-quantity-observations.js";
|
|
@@ -242,8 +242,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
242
242
|
return observedMissionValidationSummary(validation, child, typeof nativeSession?.messages === "function"
|
|
243
243
|
? () => session("messages", { path: { id: child }, query: { directory: input.directory } }) : undefined, notBefore, notBefore === undefined ? undefined : input.directory, direct?.unit.directExecution?.validationCwd ?? direct?.unit.unit.validation_cwd);
|
|
244
244
|
}
|
|
245
|
-
async function reviewMessages(id, notBefore) {
|
|
246
|
-
const
|
|
245
|
+
async function reviewMessages(id, notBefore, requireAvailable = false) {
|
|
246
|
+
const method = typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages";
|
|
247
|
+
if (requireAvailable && typeof nativeSession?.[method] !== "function")
|
|
248
|
+
throw new Error("native-operation-history-api-unavailable");
|
|
249
|
+
const response = await session(method, { path: { id }, query: { directory: input.directory, ...(notBefore ? { notBefore } : {}) } });
|
|
250
|
+
const result = payload(response);
|
|
251
|
+
if (requireAvailable && (record(response) && response.error || !Array.isArray(result)))
|
|
252
|
+
throw new Error("native-operation-history-response-unavailable");
|
|
247
253
|
return Array.isArray(result) ? result.filter(record) : [];
|
|
248
254
|
}
|
|
249
255
|
async function observedSessionModel(id) {
|
|
@@ -1617,6 +1623,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1617
1623
|
if (mission) {
|
|
1618
1624
|
const budget = await control.currentBudget(root);
|
|
1619
1625
|
const run = await operators.read(root);
|
|
1626
|
+
mission = await reconcileMissionExecution(root, mission, run);
|
|
1620
1627
|
const completion = run?.phase === "awaiting-acceptance" ? await control.completionReadiness(root) : undefined;
|
|
1621
1628
|
const acceptanceSummary = await missionAcceptanceSummary(mission, run, operators, acceptanceValidationObservation);
|
|
1622
1629
|
const packet = { acceptance_summary: acceptanceSummary, ...missionDispatchPacket(mission, run), budget,
|
|
@@ -1635,6 +1642,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1635
1642
|
delete packet.task;
|
|
1636
1643
|
delete packet.coordinator_dispatch;
|
|
1637
1644
|
}
|
|
1645
|
+
const recoveryAction = missionExecutionRecoveryAction(mission);
|
|
1646
|
+
if (recoveryAction)
|
|
1647
|
+
packet.next_action = recoveryAction;
|
|
1638
1648
|
if (view === "progress")
|
|
1639
1649
|
return serializeOperatorStatus(missionProgress(mission, run, budget, packet));
|
|
1640
1650
|
return serializeOperatorStatus(view === "full" ? packet : compactMissionEvidence(packet, run, root));
|
|
@@ -1766,10 +1776,12 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1766
1776
|
await requireRoot(context.sessionID);
|
|
1767
1777
|
const state = await operators.required(context.sessionID);
|
|
1768
1778
|
let mission = await missions.read(context.sessionID);
|
|
1779
|
+
if (mission)
|
|
1780
|
+
mission = await reconcileMissionExecution(context.sessionID, mission, state);
|
|
1769
1781
|
if (mission?.kind === "operation" && !missionExecutionComplete(mission))
|
|
1770
1782
|
return JSON.stringify({
|
|
1771
1783
|
status: "not-ready", operation_status: missionExecutionStatus(mission),
|
|
1772
|
-
next_action: "The requested operation has not completed. Preserve its real outcome; auxiliary checks and a bounded review do not complete it. Continue the declared operation or report its blocker.",
|
|
1784
|
+
next_action: missionExecutionRecoveryAction(mission) ?? "The requested operation has not completed. Preserve its real outcome; auxiliary checks and a bounded review do not complete it. Continue the declared operation or report its blocker.",
|
|
1773
1785
|
packet: missionPacket(mission, state)
|
|
1774
1786
|
});
|
|
1775
1787
|
if (mission && !["completed", "cancelled"].includes(mission.phase))
|
|
@@ -2518,6 +2530,225 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2518
2530
|
const commands = [...new Set(execution.commands.map(missionValidationCommand).map(normalizeCommand))];
|
|
2519
2531
|
return { commands, directory: resolve(input.directory, execution.directory), observations: operation?.observations ?? [] };
|
|
2520
2532
|
}
|
|
2533
|
+
async function reconcileMissionExecution(root, mission, run) {
|
|
2534
|
+
const execution = mission.execution;
|
|
2535
|
+
if (mission.kind !== "operation" || !execution || !run || run.runID !== mission.runID ||
|
|
2536
|
+
["completed", "cancelled"].includes(mission.phase))
|
|
2537
|
+
return mission;
|
|
2538
|
+
const owners = new Map();
|
|
2539
|
+
const recoveryErrors = [];
|
|
2540
|
+
const addOwner = (actor, window) => owners.set(actor, [...(owners.get(actor) ?? []), window]);
|
|
2541
|
+
for (const unit of run.units) {
|
|
2542
|
+
if (unit.directExecution?.actor) {
|
|
2543
|
+
addOwner(unit.directExecution.actor, { notBefore: unit.directExecution.startedAt,
|
|
2544
|
+
...(unit.directExecution.finishedAt ? { notAfter: unit.directExecution.finishedAt } : {}) });
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const child = unit.childSessionID;
|
|
2548
|
+
if (!child || !unit.callID)
|
|
2549
|
+
continue;
|
|
2550
|
+
const attempt = mission.attempts?.find(item => item.runID === run.runID && item.unitID === unit.unit.id &&
|
|
2551
|
+
item.callID === unit.callID && item.childSessionID === child);
|
|
2552
|
+
if (!attempt || attempt.status === "pending")
|
|
2553
|
+
continue;
|
|
2554
|
+
let info;
|
|
2555
|
+
try {
|
|
2556
|
+
const response = await session("get", { path: { id: child }, query: { directory: input.directory } });
|
|
2557
|
+
const value = payload(response);
|
|
2558
|
+
if (!record(value) || record(response) && response.error)
|
|
2559
|
+
throw new Error("native-operation-session-unavailable");
|
|
2560
|
+
info = value;
|
|
2561
|
+
}
|
|
2562
|
+
catch (error) {
|
|
2563
|
+
recoveryErrors.push({ sessionID: child, reason: error instanceof Error ? error.message : String(error) });
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
if (info.id !== undefined && info.id !== child || info.parentID !== (run.operatorSessionID ?? root))
|
|
2567
|
+
continue;
|
|
2568
|
+
const created = record(info.time) ? info.time.created : undefined;
|
|
2569
|
+
const createdAt = typeof created === "number" && Number.isFinite(created) ? created
|
|
2570
|
+
: typeof created === "string" ? Date.parse(created) : NaN;
|
|
2571
|
+
// A reusable correction author belongs to this admission, not every earlier turn
|
|
2572
|
+
// since the conversation was created. Ordinary Workers have a new native session.
|
|
2573
|
+
const admittedAt = Date.parse(unit.reviewerCorrection?.admittedAt ?? run.createdAt);
|
|
2574
|
+
const settled = unit.reviewerCorrection && ["succeeded", "failed", "cancelled"].includes(unit.status)
|
|
2575
|
+
? [...mission.progress].reverse().find(item => item.unit === `${run.runID}/${unit.unit.id}` &&
|
|
2576
|
+
["succeeded", "failed", "cancelled"].includes(item.status) && Number.isFinite(Date.parse(item.at)))?.at
|
|
2577
|
+
: undefined;
|
|
2578
|
+
// The correction session is reusable after settlement; its later turns are
|
|
2579
|
+
// not this unit's operation. Reuse the existing recorded settlement boundary.
|
|
2580
|
+
if (unit.reviewerCorrection && unit.status !== "running" && !settled)
|
|
2581
|
+
continue;
|
|
2582
|
+
if (Number.isFinite(createdAt) && Number.isFinite(admittedAt)) {
|
|
2583
|
+
addOwner(child, { notBefore: new Date(Math.max(createdAt, admittedAt)).toISOString(),
|
|
2584
|
+
...(settled ? { notAfter: settled } : {}) });
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (owners.size === 0 && recoveryErrors.length === 0)
|
|
2588
|
+
return mission;
|
|
2589
|
+
const recovered = [];
|
|
2590
|
+
const observedInputs = [];
|
|
2591
|
+
const conflictingCalls = [];
|
|
2592
|
+
const laterStart = (saved, observed) => !saved || Date.parse(observed) > Date.parse(saved) ? observed : saved;
|
|
2593
|
+
for (const [sessionID, windows] of owners) {
|
|
2594
|
+
// A direct session has its own native admission time; a delegated session is bounded
|
|
2595
|
+
// by its native creation time when exposed by the existing session API.
|
|
2596
|
+
const notBefore = windows.map(window => window.notBefore).sort()[0];
|
|
2597
|
+
if (!notBefore)
|
|
2598
|
+
continue;
|
|
2599
|
+
let history;
|
|
2600
|
+
try {
|
|
2601
|
+
history = await reviewMessages(sessionID, notBefore, true);
|
|
2602
|
+
}
|
|
2603
|
+
catch (error) {
|
|
2604
|
+
recoveryErrors.push({ sessionID, reason: error instanceof Error ? error.message : String(error) });
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const calls = new Map();
|
|
2608
|
+
const conflicts = new Set();
|
|
2609
|
+
const nativeStarts = new Map();
|
|
2610
|
+
const nativeInputs = new Map();
|
|
2611
|
+
for (const message of history) {
|
|
2612
|
+
const info = record(message.info) ? message.info : message;
|
|
2613
|
+
if (info.role !== "assistant" || info.sessionID !== sessionID || !Array.isArray(message.parts))
|
|
2614
|
+
continue;
|
|
2615
|
+
for (const part of message.parts) {
|
|
2616
|
+
if (!record(part) || part.type !== "tool" || !isShellTool(String(part.tool)) ||
|
|
2617
|
+
(part.sessionID !== undefined && part.sessionID !== sessionID) || typeof part.callID !== "string" ||
|
|
2618
|
+
!record(part.state) || !record(part.state.input))
|
|
2619
|
+
continue;
|
|
2620
|
+
const state = part.state;
|
|
2621
|
+
const nativeInput = state.input;
|
|
2622
|
+
if (typeof nativeInput.command !== "string")
|
|
2623
|
+
continue;
|
|
2624
|
+
const time = record(state.time) ? state.time : record(part.time) ? part.time : {};
|
|
2625
|
+
const date = (value) => typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString()
|
|
2626
|
+
: typeof value === "string" && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : undefined;
|
|
2627
|
+
const startedAt = date(time.start ?? time.ran);
|
|
2628
|
+
if (!startedAt)
|
|
2629
|
+
continue;
|
|
2630
|
+
// A boundary page may include tools from a later reusable-session turn.
|
|
2631
|
+
// Ignore them before comparing call input or terminal state as well.
|
|
2632
|
+
if (!windows.some(window => Date.parse(startedAt) >= Date.parse(window.notBefore) &&
|
|
2633
|
+
(!window.notAfter || Date.parse(startedAt) <= Date.parse(window.notAfter))))
|
|
2634
|
+
continue;
|
|
2635
|
+
const nativeDirectory = resolve(input.directory, typeof nativeInput.workdir === "string" ? nativeInput.workdir : ".");
|
|
2636
|
+
const directory = missionOperationDirectoryMatch(nativeDirectory, execution.directory) ? execution.directory : nativeDirectory;
|
|
2637
|
+
const key = part.callID;
|
|
2638
|
+
nativeStarts.set(key, laterStart(nativeStarts.get(key), startedAt));
|
|
2639
|
+
const nativeInputIdentity = { command: nativeInput.command, directory };
|
|
2640
|
+
const priorInput = nativeInputs.get(key);
|
|
2641
|
+
if (priorInput && (priorInput.command !== nativeInput.command || priorInput.directory !== directory)) {
|
|
2642
|
+
conflicts.add(key);
|
|
2643
|
+
}
|
|
2644
|
+
nativeInputs.set(key, nativeInputIdentity);
|
|
2645
|
+
const command = missionOperationCommandMatch(nativeInput.command, execution.commands, execution.directory);
|
|
2646
|
+
// Compare admitted inputs with persisted same-call observations even when this
|
|
2647
|
+
// record is no longer a matching command/cwd and will not yield a new result.
|
|
2648
|
+
observedInputs.push({ sessionID, callID: key, command, directory, startedAt });
|
|
2649
|
+
if (directory !== execution.directory)
|
|
2650
|
+
continue;
|
|
2651
|
+
if (!command)
|
|
2652
|
+
continue;
|
|
2653
|
+
const metadata = record(state.metadata) ? state.metadata : {};
|
|
2654
|
+
const nativeStatus = String(state.status ?? "").toLowerCase();
|
|
2655
|
+
const resultStatus = String(metadata.status ?? "").toLowerCase();
|
|
2656
|
+
if ((nativeStatus === "error" && resultStatus === "running") || (nativeStatus === "running" && resultStatus === "error")) {
|
|
2657
|
+
conflicts.add(key);
|
|
2658
|
+
}
|
|
2659
|
+
const running = nativeStatus === "running" || resultStatus === "running";
|
|
2660
|
+
const failed = nativeStatus === "error" || resultStatus === "error";
|
|
2661
|
+
if (!["completed", "error", "running"].includes(nativeStatus))
|
|
2662
|
+
continue;
|
|
2663
|
+
if (!running && !failed && nativeStatus !== "completed")
|
|
2664
|
+
continue;
|
|
2665
|
+
const completedAt = running ? undefined : date(time.end ?? time.completed);
|
|
2666
|
+
const exit = Number.isSafeInteger(metadata.exit) ? metadata.exit : undefined;
|
|
2667
|
+
const output = typeof state.output === "string" ? state.output : typeof part.output === "string" ? part.output : "";
|
|
2668
|
+
const status = failed ? "error" : running ? "running" : "completed";
|
|
2669
|
+
const outcome = conflicts.has(key) || running || !completedAt || exit === undefined ? undefined : missionCommandOutcome(output, exit, status);
|
|
2670
|
+
// A completed tool envelope without an exit/result does not prove a successful
|
|
2671
|
+
// process. Keep it unclassified instead of turning missing metadata into success.
|
|
2672
|
+
if (nativeInput.background === true && !running)
|
|
2673
|
+
continue;
|
|
2674
|
+
const observation = { command, directory: execution.directory, callID: key, sessionID, startedAt, nativeStartedAt: startedAt,
|
|
2675
|
+
...(completedAt ? { completedAt } : {}), ...(exit === undefined ? {} : { exit }), status,
|
|
2676
|
+
...(typeof metadata.shellID === "string" ? { shellID: metadata.shellID } : {}), ...(outcome ?? {}) };
|
|
2677
|
+
const prior = calls.get(key);
|
|
2678
|
+
if (prior && (prior.command !== command || prior.directory !== directory || prior.observation.startedAt !== startedAt ||
|
|
2679
|
+
prior.observation.completedAt && observation.completedAt && (prior.observation.status !== status ||
|
|
2680
|
+
prior.observation.completedAt !== observation.completedAt ||
|
|
2681
|
+
prior.observation.exit !== undefined && exit !== undefined && prior.observation.exit !== exit ||
|
|
2682
|
+
prior.observation.outcome !== undefined && observation.outcome !== undefined && JSON.stringify(prior.observation) !== JSON.stringify(observation)))) {
|
|
2683
|
+
conflicts.add(key);
|
|
2684
|
+
}
|
|
2685
|
+
else if (!prior || !conflicts.has(key) && (prior.observation.outcome === undefined || observation.outcome !== undefined)) {
|
|
2686
|
+
calls.set(key, { command, directory, observation });
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
for (const [callID, item] of calls)
|
|
2691
|
+
recovered.push({ ...item.observation, ...(conflicts.has(callID)
|
|
2692
|
+
? { conflicting: true, latestConflictingStartedAt: nativeStarts.get(callID) } : {}) });
|
|
2693
|
+
for (const callID of conflicts)
|
|
2694
|
+
conflictingCalls.push({ sessionID, callID, latestConflictingStartedAt: nativeStarts.get(callID) });
|
|
2695
|
+
}
|
|
2696
|
+
if (recovered.length === 0 && conflictingCalls.length === 0 && observedInputs.length === 0 && recoveryErrors.length === 0 && !execution.recovery)
|
|
2697
|
+
return mission;
|
|
2698
|
+
return missions.update(root, current => {
|
|
2699
|
+
if (current.runID !== mission.runID || current.kind !== "operation" || !current.execution ||
|
|
2700
|
+
["completed", "cancelled"].includes(current.phase) ||
|
|
2701
|
+
current.execution.directory !== execution.directory || JSON.stringify(current.execution.commands) !== JSON.stringify(execution.commands))
|
|
2702
|
+
return;
|
|
2703
|
+
if (recoveryErrors.length)
|
|
2704
|
+
current.execution.recovery = { status: "unavailable", errors: recoveryErrors };
|
|
2705
|
+
else
|
|
2706
|
+
delete current.execution.recovery;
|
|
2707
|
+
for (const nativeInput of observedInputs) {
|
|
2708
|
+
const prior = current.execution.observations.find(item => item.callID === nativeInput.callID && item.sessionID === nativeInput.sessionID);
|
|
2709
|
+
if (prior && (prior.command !== nativeInput.command || prior.directory !== nativeInput.directory)) {
|
|
2710
|
+
prior.conflicting = true;
|
|
2711
|
+
prior.latestConflictingStartedAt = laterStart(prior.latestConflictingStartedAt ?? prior.nativeStartedAt, nativeInput.startedAt);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
for (const observation of recovered) {
|
|
2715
|
+
const prior = current.execution.observations.find(item => item.callID === observation.callID && item.sessionID === observation.sessionID);
|
|
2716
|
+
if (!prior) {
|
|
2717
|
+
current.execution.observations.push(observation);
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
if (prior.command !== observation.command || prior.directory !== observation.directory)
|
|
2721
|
+
continue;
|
|
2722
|
+
if (observation.conflicting || prior.nativeStartedAt !== undefined && prior.nativeStartedAt !== observation.nativeStartedAt ||
|
|
2723
|
+
prior.completedAt && observation.completedAt && (prior.status !== observation.status ||
|
|
2724
|
+
prior.exit !== undefined && observation.exit !== undefined && prior.exit !== observation.exit ||
|
|
2725
|
+
prior.outcome !== undefined && observation.outcome !== undefined && prior.outcome !== observation.outcome)) {
|
|
2726
|
+
prior.conflicting = true;
|
|
2727
|
+
}
|
|
2728
|
+
if (prior.conflicting)
|
|
2729
|
+
prior.latestConflictingStartedAt = laterStart(prior.latestConflictingStartedAt ?? prior.nativeStartedAt, observation.latestConflictingStartedAt ?? observation.nativeStartedAt);
|
|
2730
|
+
prior.nativeStartedAt ??= observation.nativeStartedAt;
|
|
2731
|
+
if (prior.outcome !== undefined)
|
|
2732
|
+
continue;
|
|
2733
|
+
const latestConflictingStartedAt = prior.latestConflictingStartedAt;
|
|
2734
|
+
const nativeStartedAt = prior.nativeStartedAt;
|
|
2735
|
+
delete prior.completedAt;
|
|
2736
|
+
delete prior.exit;
|
|
2737
|
+
delete prior.outcome;
|
|
2738
|
+
delete prior.result;
|
|
2739
|
+
Object.assign(prior, observation, { startedAt: prior.startedAt,
|
|
2740
|
+
...(nativeStartedAt ? { nativeStartedAt } : {}),
|
|
2741
|
+
...(latestConflictingStartedAt ? { latestConflictingStartedAt } : {}) });
|
|
2742
|
+
}
|
|
2743
|
+
for (const conflict of conflictingCalls) {
|
|
2744
|
+
const prior = current.execution.observations.find(item => item.callID === conflict.callID && item.sessionID === conflict.sessionID);
|
|
2745
|
+
if (prior) {
|
|
2746
|
+
prior.conflicting = true;
|
|
2747
|
+
prior.latestConflictingStartedAt = laterStart(prior.latestConflictingStartedAt ?? prior.nativeStartedAt, conflict.latestConflictingStartedAt);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2521
2752
|
async function declareMissionUnits(root, actor, mission, raw, reason, execution, planningCallID, handoffNotes, workQuantities) {
|
|
2522
2753
|
await control.currentBudget(root);
|
|
2523
2754
|
mission = await missions.required(root);
|
|
@@ -3186,13 +3417,14 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3186
3417
|
args: { status: { type: "string", enum: ["ready", "needs-decision", "blocked"] }, summary: stringSchema }, execute: async (args, context) => {
|
|
3187
3418
|
const { root } = await missionAuthority(context.sessionID);
|
|
3188
3419
|
return serializeDispatchTransition(root, async () => {
|
|
3189
|
-
|
|
3420
|
+
let mission = await missions.required(root);
|
|
3421
|
+
mission = await reconcileMissionExecution(root, mission, await operators.read(root));
|
|
3190
3422
|
if (args.status === "ready") {
|
|
3191
3423
|
if (mission.kind === "operation" && !missionExecutionComplete(mission)) {
|
|
3192
3424
|
return JSON.stringify({ ...missionPacket(mission, await operators.read(root)), status: "operation-incomplete",
|
|
3193
|
-
next_action: missionExecutionStatus(mission) === "running"
|
|
3425
|
+
next_action: missionExecutionRecoveryAction(mission) ?? (missionExecutionStatus(mission) === "running"
|
|
3194
3426
|
? "The declared operation is already running. Inspect its native shell/progress; do not start another Worker or run. Wait for a terminal result, or report the existing run as blocked if its completion cannot be observed."
|
|
3195
|
-
: "Continue the requested operation or submit its actual blocker. A successful setup/NO_START check does not authorize ready." });
|
|
3427
|
+
: "Continue the requested operation or submit its actual blocker. A successful setup/NO_START check does not authorize ready.") });
|
|
3196
3428
|
}
|
|
3197
3429
|
const run = await operators.required(root);
|
|
3198
3430
|
if (run.phase !== "awaiting-acceptance")
|
|
@@ -3215,7 +3447,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3215
3447
|
...compactMissionEvidence(missionPacket(updated, submittedRun), submittedRun, root),
|
|
3216
3448
|
next_action: args.status === "ready"
|
|
3217
3449
|
? "Return this result and acceptance_summary to Operator now. Compare the original requests with the candidate and actual evidence; inspect concrete gaps only, then complete_mission if satisfied. No routine archive search or full source reread. Completion still requires its final comparison and complete_mission receipt."
|
|
3218
|
-
: "Return the saved blocker or user-only decision to Operator now. This is incomplete delivery, not acceptance; retain the candidate, failed checks and cumulative spend. Do not repeat unchanged work or dispatch a replacement merely to finalize this result."
|
|
3450
|
+
: ["Return the saved blocker or user-only decision to Operator now. This is incomplete delivery, not acceptance; retain the candidate, failed checks and cumulative spend. Do not repeat unchanged work or dispatch a replacement merely to finalize this result.",
|
|
3451
|
+
missionExecutionRecoveryAction(updated)].filter(Boolean).join(" ") });
|
|
3219
3452
|
});
|
|
3220
3453
|
} };
|
|
3221
3454
|
tools[completeMission] = { description: "Operator only: after comparing the original request, source, actual checks and current review disposition (explicit native author self-recheck is not independent PASS), explicitly accept the whole mission. A collected terminal failure can satisfy a run-once/report request, not a request for successful execution. Returns operation outcome separately from the measured 🐾 Mission receipt; never treat Worker start, CORRECTION_READY or one passing check as completion.",
|
|
@@ -3872,17 +4105,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3872
4105
|
const operationArgs = record(mapped.args) ? mapped.args : {};
|
|
3873
4106
|
// A decorated launch would spend on the real operation but leave the native mission observation empty.
|
|
3874
4107
|
// Ask the same Worker to correct its shell input before that expensive side effect.
|
|
3875
|
-
if (writer &&
|
|
4108
|
+
if (writer && isShellTool(request.tool) && mission?.execution &&
|
|
3876
4109
|
typeof operationArgs.command === "string" &&
|
|
3877
|
-
resolve(input.directory, typeof operationArgs.workdir === "string" ? operationArgs.workdir : ".")
|
|
3878
|
-
const
|
|
3879
|
-
if (operationArgs.background === true &&
|
|
4110
|
+
missionOperationDirectoryMatch(resolve(input.directory, typeof operationArgs.workdir === "string" ? operationArgs.workdir : "."), mission.execution.directory)) {
|
|
4111
|
+
const declaredOperation = missionOperationCommandMatch(operationArgs.command, mission.execution.commands, mission.execution.directory);
|
|
4112
|
+
if (operationArgs.background === true && declaredOperation !== undefined) {
|
|
3880
4113
|
throw new Error("mission-operation-background: run the declared command in foreground with a suitable timeout. " +
|
|
3881
4114
|
"A background shell reports only launch, not process exit; no run was started by this refused call. " +
|
|
3882
4115
|
"Correct this same Worker command without a new plan or approval.");
|
|
3883
4116
|
}
|
|
3884
|
-
const decorated = mission.execution.commands
|
|
3885
|
-
/^\s*(?:\||\d?>|&&?|;)/u.test(actual.slice(declared.length)));
|
|
4117
|
+
const decorated = missionOperationCommandDecorated(operationArgs.command, mission.execution.commands, mission.execution.directory);
|
|
3886
4118
|
if (decorated)
|
|
3887
4119
|
throw new Error("mission-operation-command-not-observed: run the declared operation command exactly; " +
|
|
3888
4120
|
"do not append a pipe, tee, redirection or chained command. Capture its tool output or write a separate result file afterward. " +
|
|
@@ -3926,16 +4158,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
3926
4158
|
}
|
|
3927
4159
|
else
|
|
3928
4160
|
Object.assign(output, outward);
|
|
3929
|
-
if (writer &&
|
|
3930
|
-
typeof args.command === "string" &&
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
4161
|
+
if (writer && isShellTool(request.tool) && mission?.execution &&
|
|
4162
|
+
typeof args.command === "string" && missionOperationDirectoryMatch(resolve(input.directory, typeof args.workdir === "string" ? args.workdir : "."), mission.execution.directory)) {
|
|
4163
|
+
const command = missionOperationCommandMatch(args.command, mission.execution.commands, mission.execution.directory);
|
|
4164
|
+
if (command !== undefined)
|
|
4165
|
+
await missions.update(root, item => {
|
|
4166
|
+
if (!item.execution.observations.some(observation => observation.callID === request.callID)) {
|
|
4167
|
+
item.execution.observations.push({ command, directory: item.execution.directory,
|
|
4168
|
+
callID: request.callID, sessionID: request.sessionID, startedAt: new Date().toISOString() });
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
3939
4171
|
}
|
|
3940
4172
|
}
|
|
3941
4173
|
catch (error) {
|
|
@@ -4192,7 +4424,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
4192
4424
|
await core["tool.execute.after"]?.({ ...translate(request, false),
|
|
4193
4425
|
...(ownership ? { sessionID: ownership.root } : {}) }, mapped);
|
|
4194
4426
|
Object.assign(output, translate(mapped, true));
|
|
4195
|
-
if (!ownership &&
|
|
4427
|
+
if (!ownership && isShellTool(request.tool)) {
|
|
4196
4428
|
const root = await rootFor(id);
|
|
4197
4429
|
const mission = root ? await missions.read(root) : undefined;
|
|
4198
4430
|
if (root && mission?.execution?.observations.some(item => item.callID === request.callID && item.sessionID === id)) {
|
|
@@ -4211,7 +4443,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
4211
4443
|
observation.status = output.status === "error" || metadata.status === "error" ? "error" : "completed";
|
|
4212
4444
|
if (Number.isSafeInteger(metadata.exit))
|
|
4213
4445
|
observation.exit = metadata.exit;
|
|
4214
|
-
|
|
4446
|
+
if (Number.isSafeInteger(metadata.exit)) {
|
|
4447
|
+
Object.assign(observation, missionCommandOutcome(returnedOutput, metadata.exit, observation.status));
|
|
4448
|
+
}
|
|
4215
4449
|
});
|
|
4216
4450
|
}
|
|
4217
4451
|
}
|