sortie-dogs 0.13.17 → 0.13.18
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/core/goal-bound.d.ts +10 -0
- package/dist/core/goal-bound.js +20 -7
- package/dist/core/operator-mission.d.ts +7 -0
- package/dist/core/operator-runtime.d.ts +6 -1
- package/dist/core/operator-runtime.js +13 -4
- package/dist/core/run-flight-ledger.js +8 -0
- package/dist/core/validation-budget.js +3 -1
- package/dist/plugin/index.js +103 -7
- package/dist/plugin/profiled.js +167 -95
- package/dist/plugin/runtime-bridge.d.ts +10 -1
- package/dist/plugin/v2.js +16 -6
- 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.18](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.18)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.18.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.18
|
|
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.18
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -188,7 +188,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
188
188
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
189
189
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
190
190
|
|
|
191
|
-
All scores belong to their fixed candidates, **not v0.13.
|
|
191
|
+
All scores belong to their fixed candidates, **not v0.13.18**. SWE-bench remains an optional
|
|
192
192
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
193
193
|
|
|
194
194
|
## Configuration
|
|
@@ -211,7 +211,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
211
211
|
|
|
212
212
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
213
213
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
214
|
-
The current Mission asset marker is `0.13.
|
|
214
|
+
The current Mission asset marker is `0.13.18-direct-settlement-v1`; the unchanged Codex skill marker is
|
|
215
215
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
216
216
|
|
|
217
217
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -227,7 +227,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
227
227
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
228
228
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
229
229
|
[Windows tests](docs/windows-tests.md) · [Release routine](docs/release-batch.md).
|
|
230
|
-
- **Releases:** [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) ·
|
|
230
|
+
- **Releases:** [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) ·
|
|
231
231
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
232
232
|
|
|
233
233
|
## 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.18-direct-settlement-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.18-direct-settlement-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -111,6 +111,15 @@ export interface GoalTerminalReceipt {
|
|
|
111
111
|
interface GoalEventBase {
|
|
112
112
|
readonly at: string;
|
|
113
113
|
}
|
|
114
|
+
/** Host verification of an existing direct finish receipt, not a new execution. */
|
|
115
|
+
export interface GoalDirectFinishRecovery {
|
|
116
|
+
readonly call_id: string;
|
|
117
|
+
readonly actor_session_id: string;
|
|
118
|
+
readonly finish_call_id: string;
|
|
119
|
+
readonly started_at: string;
|
|
120
|
+
readonly finished_at: string;
|
|
121
|
+
readonly validation_call_ids: readonly string[];
|
|
122
|
+
}
|
|
114
123
|
export interface GoalValidationRetryAuthorization {
|
|
115
124
|
readonly authority: "operator-repair-validation-retry";
|
|
116
125
|
readonly operator_run_id: string;
|
|
@@ -218,6 +227,7 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
218
227
|
readonly unit_id: string;
|
|
219
228
|
readonly previous_receipt_id: string;
|
|
220
229
|
readonly evidence: readonly GoalEvidence[];
|
|
230
|
+
readonly direct_finish?: GoalDirectFinishRecovery;
|
|
221
231
|
}) | (GoalEventBase & {
|
|
222
232
|
readonly kind: "validation.admission";
|
|
223
233
|
readonly goal_id: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -133,7 +133,7 @@ export function reduceGoalFlight(records) {
|
|
|
133
133
|
let replanRevision = null;
|
|
134
134
|
let legacyResetRevision = false;
|
|
135
135
|
let previous = null;
|
|
136
|
-
const
|
|
136
|
+
const recoverableSettlements = new Map();
|
|
137
137
|
const unitCosts = new Map();
|
|
138
138
|
const validationAdmissions = new Map();
|
|
139
139
|
const validationSettlements = new Map();
|
|
@@ -160,7 +160,7 @@ export function reduceGoalFlight(records) {
|
|
|
160
160
|
session_ids: [event.origin_session_id] };
|
|
161
161
|
replanRevision = null;
|
|
162
162
|
legacyResetRevision = false;
|
|
163
|
-
|
|
163
|
+
recoverableSettlements.clear();
|
|
164
164
|
validationAdmissions.clear();
|
|
165
165
|
validationSettlements.clear();
|
|
166
166
|
reopenedValidationEvidence.clear();
|
|
@@ -255,8 +255,8 @@ export function reduceGoalFlight(records) {
|
|
|
255
255
|
unit_ids: addUnique(state.unit_ids, event.unit_id),
|
|
256
256
|
evidence_refs: event.evidence.reduce((refs, evidence) => addUnique(refs, evidence.evidence_id), state.evidence_refs),
|
|
257
257
|
satisfied_criteria: event.evidence.reduce((ids, evidence) => evidence.measurement.criterion_ids.reduce(addUnique, ids), state.satisfied_criteria) };
|
|
258
|
-
if (event.
|
|
259
|
-
|
|
258
|
+
if (event.evidence.length === 0 && event.disposition !== "succeeded") {
|
|
259
|
+
recoverableSettlements.set(event.receipt_id, { unitID: event.unit_id, revision: state.revision, epoch: state.scope_epoch, settlement: event });
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
else if (event.kind === "unit.usage-reconciled") {
|
|
@@ -271,11 +271,24 @@ export function reduceGoalFlight(records) {
|
|
|
271
271
|
: costs.reduce((sum, item) => sum + item.cost, 0) };
|
|
272
272
|
}
|
|
273
273
|
else if (event.kind === "unit.evidence-reconciled") {
|
|
274
|
-
const old =
|
|
274
|
+
const old = recoverableSettlements.get(event.previous_receipt_id);
|
|
275
|
+
const direct = event.direct_finish, settled = old?.settlement;
|
|
276
|
+
const directFinish = direct !== undefined && text(direct.call_id) && direct.call_id.startsWith("direct-") &&
|
|
277
|
+
text(direct.actor_session_id) && text(direct.finish_call_id) && instant(direct.started_at) && instant(direct.finished_at) &&
|
|
278
|
+
Date.parse(direct.finished_at) >= Date.parse(direct.started_at) && Array.isArray(direct.validation_call_ids) && direct.validation_call_ids.length > 0 &&
|
|
279
|
+
direct.validation_call_ids.every(text) && settled?.native_session_id === direct.actor_session_id &&
|
|
280
|
+
settled.native_started_at !== undefined && Math.abs(Date.parse(settled.native_started_at) - Date.parse(direct.started_at)) <= 1000 &&
|
|
281
|
+
Math.abs(Date.parse(settled.at) - Date.parse(direct.finished_at)) <= 1000 &&
|
|
282
|
+
settled.reservation_id === goalFingerprint({ goal_id: event.goal_id, unit_id: event.unit_id, call_id: direct.call_id }) &&
|
|
283
|
+
settled.receipt_id === goalFingerprint({ call_id: direct.call_id,
|
|
284
|
+
output: "Host-observed direct unit finished; controller session remains active." }) &&
|
|
285
|
+
(settled.disposition === "cancelled" && settled.result_class === "interrupted" ||
|
|
286
|
+
settled.disposition === "failed" && ["process-defect", "acceptance"].includes(settled.result_class ?? ""));
|
|
287
|
+
const processDefect = direct === undefined && settled?.disposition !== "cancelled" && settled?.result_class === "process-defect";
|
|
275
288
|
requireState(state.phase === "active" && state.receipt === null && old?.unitID === event.unit_id &&
|
|
276
|
-
old.revision === state.revision && old.epoch === state.scope_epoch && state.outstanding_reservations.length === 0, "transition", "Evidence reconciliation requires a current settled process defect.");
|
|
289
|
+
old.revision === state.revision && old.epoch === state.scope_epoch && state.outstanding_reservations.length === 0 && (processDefect || directFinish), "transition", "Evidence reconciliation requires a current settled process defect or verified direct finish defect.");
|
|
277
290
|
requireState(event.evidence.length > 0 && event.evidence.every(entry => validGoalEvidence(entry, state) && entry.execution.units.includes(event.unit_id)), "evidence", "Reconciled evidence must retain the current goal and unit identity.");
|
|
278
|
-
|
|
291
|
+
recoverableSettlements.delete(event.previous_receipt_id);
|
|
279
292
|
state = { ...state, evidence_refs: event.evidence.reduce((refs, evidence) => addUnique(refs, evidence.evidence_id), state.evidence_refs),
|
|
280
293
|
satisfied_criteria: event.evidence.reduce((ids, evidence) => evidence.measurement.criterion_ids.reduce(addUnique, ids), state.satisfied_criteria) };
|
|
281
294
|
}
|
|
@@ -76,6 +76,13 @@ export interface MissionAttempt {
|
|
|
76
76
|
code: string;
|
|
77
77
|
};
|
|
78
78
|
resultClass?: string;
|
|
79
|
+
/** Retained erroneous finish projection; the original goal receipt remains append-only. */
|
|
80
|
+
registrationRecovery?: {
|
|
81
|
+
at: string;
|
|
82
|
+
previousStatus: MissionAttempt["status"];
|
|
83
|
+
previousResultClass?: string;
|
|
84
|
+
previousFailure?: MissionAttempt["failure"];
|
|
85
|
+
};
|
|
79
86
|
selectedModel?: string;
|
|
80
87
|
}
|
|
81
88
|
export interface MissionTerminalRescue {
|
|
@@ -131,6 +131,11 @@ interface UnitState {
|
|
|
131
131
|
actor: string;
|
|
132
132
|
startedAt: string;
|
|
133
133
|
finishedAt?: string;
|
|
134
|
+
registrationRecovery?: {
|
|
135
|
+
at: string;
|
|
136
|
+
previousStatus: "failed" | "cancelled";
|
|
137
|
+
previousResultClass: string | null;
|
|
138
|
+
};
|
|
134
139
|
checks: import("../plugin/runtime-bridge.js").ReviewerCorrectionCheck[];
|
|
135
140
|
validationCwd?: Readonly<Record<string, string>>;
|
|
136
141
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
@@ -430,7 +435,7 @@ export declare class OperatorRuntime {
|
|
|
430
435
|
private repairValidationRetryBinding;
|
|
431
436
|
canRetryRepairValidation(state: OperatorState): boolean;
|
|
432
437
|
resumeRepairValidation(root: string, runID: string, authorize: (source: OperatorRepairValidationRetrySource) => Promise<void>): Promise<OperatorState>;
|
|
433
|
-
resume(root: string, runID: string, recovered: ReadonlyMap<string, readonly GoalEvidence[]>, unstarted?: ReadonlySet<string>): Promise<OperatorState>;
|
|
438
|
+
resume(root: string, runID: string, recovered: ReadonlyMap<string, readonly GoalEvidence[]>, unstarted?: ReadonlySet<string>, recoveredDirect?: ReadonlySet<string>): Promise<OperatorState>;
|
|
434
439
|
requireAcceptanceRemediation(root: string, runID: string): Promise<OperatorState>;
|
|
435
440
|
draftStatus(root: string): Promise<unknown | undefined>;
|
|
436
441
|
completionGoalFingerprint(state: OperatorState): Promise<string>;
|
|
@@ -2091,16 +2091,17 @@ export class OperatorRuntime {
|
|
|
2091
2091
|
return state;
|
|
2092
2092
|
});
|
|
2093
2093
|
}
|
|
2094
|
-
resume(root, runID, recovered, unstarted = new Set()) {
|
|
2094
|
+
resume(root, runID, recovered, unstarted = new Set(), recoveredDirect = new Set()) {
|
|
2095
2095
|
return this.serial(root, async () => {
|
|
2096
2096
|
const state = await this.required(root);
|
|
2097
2097
|
if (state.contractRepair !== null)
|
|
2098
2098
|
throw new Error("operator-resume-contract-repair-required");
|
|
2099
|
-
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2099
|
+
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2100
|
+
unit.status === "cancelled" && !recoveredDirect.has(unit.unit.id))) {
|
|
2100
2101
|
throw new Error("operator-resume-not-ready");
|
|
2101
2102
|
}
|
|
2102
2103
|
for (const unit of state.units) {
|
|
2103
|
-
if (unit.status !== "failed")
|
|
2104
|
+
if (unit.status !== "failed" && unit.status !== "cancelled")
|
|
2104
2105
|
continue;
|
|
2105
2106
|
if (unstarted.has(unit.unit.id)) {
|
|
2106
2107
|
if (unit.resultClass !== "process-defect" || unit.childSessionID !== null || state.decision !== "dispatch-admission-rejected")
|
|
@@ -2111,10 +2112,18 @@ export class OperatorRuntime {
|
|
|
2111
2112
|
continue;
|
|
2112
2113
|
}
|
|
2113
2114
|
const evidence = recovered.get(unit.unit.id);
|
|
2115
|
+
const direct = recoveredDirect.has(unit.unit.id) && unit.directExecution?.actor === unit.childSessionID &&
|
|
2116
|
+
!!unit.directExecution.finishedAt && !!unit.callID && !!evidence?.length;
|
|
2117
|
+
await this.verifyControls(unit);
|
|
2114
2118
|
const ids = new Set(evidence?.flatMap(item => item.measurement.criterion_ids) ?? []);
|
|
2115
|
-
if (unit.resultClass !== "process-defect" || !evidence?.length || !unit.unit.acceptance_indices.every(index => state.acceptanceProof[index].some(id => ids.has(id)))) {
|
|
2119
|
+
if (unit.resultClass !== "process-defect" && !direct || !evidence?.length || !unit.unit.acceptance_indices.every(index => state.acceptanceProof[index].some(id => ids.has(id)))) {
|
|
2116
2120
|
throw new Error("operator-resume-proof-incomplete");
|
|
2117
2121
|
}
|
|
2122
|
+
if (unit.status === "cancelled" && !direct)
|
|
2123
|
+
throw new Error("operator-resume-proof-incomplete");
|
|
2124
|
+
if (direct)
|
|
2125
|
+
unit.directExecution.registrationRecovery = { at: new Date().toISOString(),
|
|
2126
|
+
previousStatus: unit.status, previousResultClass: unit.resultClass };
|
|
2118
2127
|
unit.status = "succeeded";
|
|
2119
2128
|
unit.resultClass = "acceptance";
|
|
2120
2129
|
unit.evidence = evidence;
|
|
@@ -925,6 +925,14 @@ export class RunFlightLedger {
|
|
|
925
925
|
stored.reservation_id === event.reservation_id && stored.native_session_id === event.native_session_id &&
|
|
926
926
|
stored.cost_usd === event.cost_usd))
|
|
927
927
|
return reduceGoalFlight(records);
|
|
928
|
+
if (event.kind === "unit.evidence-reconciled" && event.direct_finish !== undefined) {
|
|
929
|
+
const existing = records.find(record => record.event.kind === "unit.evidence-reconciled" &&
|
|
930
|
+
record.event.previous_receipt_id === event.previous_receipt_id)?.event;
|
|
931
|
+
if (existing?.kind === "unit.evidence-reconciled" && existing.goal_id === event.goal_id && existing.unit_id === event.unit_id &&
|
|
932
|
+
goalFingerprint(existing.evidence) === goalFingerprint(event.evidence) &&
|
|
933
|
+
goalFingerprint(existing.direct_finish) === goalFingerprint(event.direct_finish))
|
|
934
|
+
return reduceGoalFlight(records);
|
|
935
|
+
}
|
|
928
936
|
if (event.kind === "goal.accepted") {
|
|
929
937
|
const prior = records.find(({ event: stored }) => stored.kind === "goal.accepted" &&
|
|
930
938
|
stored.origin_user_message_id === event.origin_user_message_id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { CONTRACT_TEXT_LIMITS } from "./contract-limits.js";
|
|
2
3
|
export const VALIDATION_PROFILES = ["fast", "balanced", "assurance"];
|
|
3
4
|
/** A real failed process can be rerun after setup repair without changing source or command identity. */
|
|
4
5
|
export function retryableValidationFailure(outcome, exitCode) {
|
|
@@ -38,6 +39,7 @@ export function validationEvidenceState(events) {
|
|
|
38
39
|
return { reusable: [...reusable], blocked: [...blocked] };
|
|
39
40
|
}
|
|
40
41
|
const id = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096;
|
|
42
|
+
const commandText = (value) => typeof value === "string" && value.length > 0 && value.length <= CONTRACT_TEXT_LIMITS.command;
|
|
41
43
|
const scopes = new Set(["static", "targeted", "related", "canonical", "full-suite", "full"]);
|
|
42
44
|
const normalizedScope = (scope) => scope === "full" ? "canonical" : scope;
|
|
43
45
|
export function validationOwner(scope) {
|
|
@@ -94,7 +96,7 @@ export function decideValidationBudget(request, state) {
|
|
|
94
96
|
const environment = value.environment;
|
|
95
97
|
const marginal = value.marginal_value;
|
|
96
98
|
const valid = id(value.run_id) && id(value.operation_id) && id(value.source_snapshot) && id(value.candidate) &&
|
|
97
|
-
value.command.length > 0 && value.command.every(
|
|
99
|
+
value.command.length > 0 && value.command.every(commandText) && scopes.has(value.scope) &&
|
|
98
100
|
environment !== null && typeof environment === "object" && id(environment.platform) && id(environment.arch) &&
|
|
99
101
|
id(environment.runtime) && (environment.toolchain === undefined || id(environment.toolchain)) &&
|
|
100
102
|
(value.owner === "worker" || value.owner === "coordinator") &&
|
package/dist/plugin/index.js
CHANGED
|
@@ -50,6 +50,7 @@ import { AdaptiveRemediationRuntime, AdaptiveRunFlightLineage } from "../core/ad
|
|
|
50
50
|
import { DEFAULT_ADAPTIVE_REMEDIATION_MODEL, GitAdaptiveRemediationHost, OpenCodeAdaptiveRemediationProvider } from "./adaptive-remediation-host.js";
|
|
51
51
|
import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
52
52
|
import { STABLE_RUNTIME_PROFILE } from "../core/runtime-profile.js";
|
|
53
|
+
import { reviewerCorrectionValidationFresh } from "./mission-review.js";
|
|
53
54
|
import { evidenceFromObservedExecution } from "../core/observed-goal-evidence.js";
|
|
54
55
|
import { receiptBoundTerminalText } from "./receipt-presentation.js";
|
|
55
56
|
import { operationInputSnapshot, protectedSnapshot, refreshProtectedSnapshot, validatedSourceSnapshot, validationInputSnapshot } from "./protected-snapshot.js";
|
|
@@ -2369,11 +2370,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2369
2370
|
const failed = [...hostGoalExecutions.values()].find(execution => execution.root === reservation.root &&
|
|
2370
2371
|
execution.sessionID === child && execution.endedAt !== undefined && execution.outcome === "fail" &&
|
|
2371
2372
|
Date.parse(execution.startedAt) >= reservation.started - 1000);
|
|
2372
|
-
|
|
2373
|
+
const result = { rootSessionID: reservation.root, callID, unitID: settled.unit_id,
|
|
2373
2374
|
...(child ? { childSessionID: child } : {}), disposition: settled.disposition, evidence: settled.evidence,
|
|
2374
|
-
resultClass: settled.result_class ?? "process-defect",
|
|
2375
|
+
resultClass: settled.result_class ?? "process-defect",
|
|
2376
|
+
...(directEvidence !== undefined ? {} : { nativeOutcome: output.status === "error" || output.status === "cancelled" ? "failed" : "completed" }),
|
|
2375
2377
|
...(settled.result_class === "acceptance" && failed ? { failure: { command: failed.command.slice(0, 8),
|
|
2376
|
-
outcome: "fail", exitCode: failed.exitCode ?? null } } : {}) }
|
|
2378
|
+
outcome: "fail", exitCode: failed.exitCode ?? null } } : {}) };
|
|
2379
|
+
await input.runtimeBridge?.onSerialSettlement?.(result);
|
|
2380
|
+
goalReservations.delete(callID);
|
|
2381
|
+
return result;
|
|
2377
2382
|
}
|
|
2378
2383
|
goalReservations.delete(callID);
|
|
2379
2384
|
return;
|
|
@@ -2405,7 +2410,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2405
2410
|
// Revalidation can succeed for an already-satisfied criterion after a candidate change.
|
|
2406
2411
|
// New criterion coverage controls progress accounting, not the validation disposition.
|
|
2407
2412
|
const metadata = isRecord(output.metadata) ? output.metadata : undefined;
|
|
2408
|
-
|
|
2413
|
+
// A direct unit owns this invocation's checks, not the reusable controller
|
|
2414
|
+
// conversation's last native Task outcome. Empty direct evidence still denotes
|
|
2415
|
+
// direct failure; explicit cancellation/error below remains authoritative.
|
|
2416
|
+
const child = directEvidence === undefined && childSessionID ? await input.client?.session?.get?.({ path: { id: childSessionID }, query: { directory: input.directory } }).catch(() => undefined) : undefined;
|
|
2409
2417
|
const childInfo = isRecord(child) && "data" in child ? child.data : child;
|
|
2410
2418
|
const childOutcome = isRecord(childInfo) ? childInfo.outcome : undefined;
|
|
2411
2419
|
const terminalFailed = nativeOutcome === "failed" || metadata?.status === "error" || output.status === "error" ||
|
|
@@ -2432,7 +2440,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2432
2440
|
evidence: settlementEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started),
|
|
2433
2441
|
cost_usd: await nativeUnitCost(childSessionID, new Date(reservation.started).toISOString()),
|
|
2434
2442
|
...(childSessionID ? { native_session_id: childSessionID, native_started_at: new Date(reservation.started).toISOString() } : {}) });
|
|
2435
|
-
|
|
2443
|
+
const result = {
|
|
2436
2444
|
rootSessionID: reservation.root, callID, unitID: reservation.unitID,
|
|
2437
2445
|
...(childSessionID === undefined ? {} : { childSessionID }),
|
|
2438
2446
|
disposition: validated ? "succeeded" : interrupted ? "cancelled" : "failed",
|
|
@@ -2442,7 +2450,8 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2442
2450
|
command: failedAcceptanceExecution.command.slice(0, 8), outcome: "fail",
|
|
2443
2451
|
exitCode: failedAcceptanceExecution.exitCode ?? null,
|
|
2444
2452
|
} } : {}),
|
|
2445
|
-
}
|
|
2453
|
+
};
|
|
2454
|
+
await input.runtimeBridge?.onSerialSettlement?.(result);
|
|
2446
2455
|
goalReservations.delete(callID);
|
|
2447
2456
|
if (childSessionID !== undefined) {
|
|
2448
2457
|
goalValidationDefects.delete(childSessionID);
|
|
@@ -2451,6 +2460,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2451
2460
|
hostGoalExecutions.delete(executionCallID);
|
|
2452
2461
|
}
|
|
2453
2462
|
}
|
|
2463
|
+
return result;
|
|
2454
2464
|
}
|
|
2455
2465
|
async function settleRejectedGoalDispatch(rootSessionID, callID) {
|
|
2456
2466
|
const reservation = goalReservations.get(callID);
|
|
@@ -2472,6 +2482,89 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2472
2482
|
if (goal.acceptance_fingerprint !== request.goalFingerprint || goal.phase !== "active" || goal.receipt !== null || goal.outstanding_reservations.length) {
|
|
2473
2483
|
throw new Error("operator-recovery-goal-not-ready");
|
|
2474
2484
|
}
|
|
2485
|
+
if (request.direct) {
|
|
2486
|
+
const direct = request.direct;
|
|
2487
|
+
const defect = [...snapshot.records].reverse().map(record => record.event).find(event => event.kind === "unit.settled" &&
|
|
2488
|
+
event.unit_id === request.unitID && event.evidence.length === 0 && event.native_session_id === request.childSessionID &&
|
|
2489
|
+
event.reservation_id === goalFingerprint({ goal_id: goal.goal_id, unit_id: request.unitID, call_id: direct.callID }) &&
|
|
2490
|
+
event.receipt_id === goalFingerprint({ call_id: direct.callID,
|
|
2491
|
+
output: "Host-observed direct unit finished; controller session remains active." }));
|
|
2492
|
+
if (defect?.kind !== "unit.settled" || Math.abs(Date.parse(defect.at) - Date.parse(direct.finishedAt)) > 1000) {
|
|
2493
|
+
throw new Error("operator-recovery-direct-finish-receipt-missing");
|
|
2494
|
+
}
|
|
2495
|
+
const identity = await hostSessionIdentity(request.childSessionID);
|
|
2496
|
+
if (!identity || request.childSessionID !== root && identity.parentID !== root &&
|
|
2497
|
+
coordinatorRootForSession(request.childSessionID) !== root)
|
|
2498
|
+
throw new Error("operator-recovery-direct-owner-mismatch");
|
|
2499
|
+
const sessionAPI = input.client?.session;
|
|
2500
|
+
const historyAPI = sessionAPI;
|
|
2501
|
+
const messages = historyAPI?.reviewMessages ?? sessionAPI?.messages;
|
|
2502
|
+
if (!messages)
|
|
2503
|
+
throw new Error("operator-recovery-host-evidence-unavailable");
|
|
2504
|
+
const historyRequest = { path: { id: request.childSessionID }, query: { directory: input.directory,
|
|
2505
|
+
notBefore: new Date(Date.parse(direct.startedAt) - 1000).toISOString() } };
|
|
2506
|
+
const response = await messages.call(input.client.session, historyRequest);
|
|
2507
|
+
const payload = isRecord(response) && "data" in response ? response.data : response;
|
|
2508
|
+
if (!Array.isArray(payload))
|
|
2509
|
+
throw new Error("operator-recovery-host-evidence-invalid");
|
|
2510
|
+
const history = payload.filter(isRecord);
|
|
2511
|
+
const parts = history.filter(message => isRecord(message.info) && message.info.role === "assistant" &&
|
|
2512
|
+
message.info.sessionID === request.childSessionID).flatMap(message => Array.isArray(message.parts) ? message.parts.filter(isRecord) : []);
|
|
2513
|
+
const finish = parts.find(part => part.type === "tool" && ["sortie_finish_direct_unit", `${runtimeProfile.toolPrefix}finish_direct_unit`].includes(String(part.tool)) &&
|
|
2514
|
+
isRecord(part.state) && part.state.status === "completed" && isRecord(part.state.time) &&
|
|
2515
|
+
typeof part.state.time.start === "number" && typeof part.state.time.end === "number" &&
|
|
2516
|
+
part.state.time.start <= Date.parse(defect.at) && part.state.time.end >= Date.parse(defect.at));
|
|
2517
|
+
if (!finish || typeof finish.callID !== "string")
|
|
2518
|
+
throw new Error("operator-recovery-direct-finish-observation-missing");
|
|
2519
|
+
if (parts.some(part => part.type === "tool" && ["cancel_operator", "cancel_mission"].some(name => part.tool === `sortie_${name}` || part.tool === `${runtimeProfile.toolPrefix}${name}`) && isRecord(part.state) && part.state.status === "completed" &&
|
|
2520
|
+
isRecord(part.state.time) && typeof part.state.time.end === "number" && part.state.time.end >= Date.parse(direct.startedAt))) {
|
|
2521
|
+
throw new Error("operator-recovery-direct-explicit-cancellation");
|
|
2522
|
+
}
|
|
2523
|
+
const checked = await reviewerCorrectionValidationFresh(direct.validation, request.childSessionID, history, Date.parse(direct.startedAt), direct.checks, input.directory, direct.directories, direct.registeredAt);
|
|
2524
|
+
if (!checked.ready)
|
|
2525
|
+
throw new Error(`operator-recovery-proof-unavailable-or-stale:${checked.reason}`);
|
|
2526
|
+
const checks = direct.checks.filter(check => checked.matched?.some(item => item.callID === check.callID));
|
|
2527
|
+
if (checks.some(check => check.dispatchCallID !== direct.callID || check.childSessionID !== request.childSessionID ||
|
|
2528
|
+
resolve(input.directory, check.binding.manifest_path) !== resolve(request.manifestPath) ||
|
|
2529
|
+
Date.parse(check.endedAt) > Date.parse(defect.at) + 1000))
|
|
2530
|
+
throw new Error("operator-recovery-direct-check-identity-mismatch");
|
|
2531
|
+
const evidence = checks.flatMap(check => observedHostGoalEvidence({ ...check, owner: "coordinator", outcome: "pass",
|
|
2532
|
+
immutableRef: goalFingerprint({ direct_check: check }),
|
|
2533
|
+
correction: { taskID: request.unitID, dispatchCallID: direct.callID, commands: check.command } }, goal, request.unitID));
|
|
2534
|
+
for (const check of checks.filter(check => evidence.some(entry => check.command.includes(entry.execution.command[0])))) {
|
|
2535
|
+
// The persisted host check already binds this exact native call to its
|
|
2536
|
+
// Task/cwd/recipe. Its post-check validation recipe can be narrower than
|
|
2537
|
+
// the admission snapshot; do not relabel or recompute that historical key.
|
|
2538
|
+
const admission = snapshot.records.map(record => record.event).find(event => event.kind === "validation.admission" &&
|
|
2539
|
+
event.goal_id === goal.goal_id && event.operation_id === check.callID && event.decision === "ALLOW" &&
|
|
2540
|
+
Date.parse(event.at) >= Date.parse(direct.startedAt) - 1000 && Date.parse(event.at) <= Date.parse(check.endedAt) + 1000);
|
|
2541
|
+
if (admission?.kind !== "validation.admission" || !snapshot.records.some(({ event }) => event.kind === "validation.settled" &&
|
|
2542
|
+
event.reservation_id === admission.reservation_id && event.operation_id === check.callID &&
|
|
2543
|
+
event.goal_id === goal.goal_id && event.evidence_key === admission.evidence_key && event.outcome === "passed" && event.exit_code === 0)) {
|
|
2544
|
+
throw new Error("operator-recovery-direct-validation-settlement-missing");
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (!evidence.length || evidence.some(entry => !validGoalEvidence(entry, goal)))
|
|
2548
|
+
throw new Error("operator-recovery-proof-unavailable-or-stale");
|
|
2549
|
+
const proof = { call_id: direct.callID, actor_session_id: request.childSessionID, finish_call_id: finish.callID,
|
|
2550
|
+
started_at: direct.startedAt, finished_at: direct.finishedAt, validation_call_ids: checks.map(check => check.callID) };
|
|
2551
|
+
const prior = snapshot.records.map(record => record.event).find(event => event.kind === "unit.evidence-reconciled" &&
|
|
2552
|
+
event.previous_receipt_id === defect.receipt_id);
|
|
2553
|
+
if (prior?.kind === "unit.evidence-reconciled") {
|
|
2554
|
+
if (goalFingerprint(prior.direct_finish) !== goalFingerprint(proof) || goalFingerprint(prior.evidence) !== goalFingerprint(evidence)) {
|
|
2555
|
+
throw new Error("operator-recovery-direct-reconciliation-mismatch");
|
|
2556
|
+
}
|
|
2557
|
+
return prior.evidence;
|
|
2558
|
+
}
|
|
2559
|
+
// Recheck immediately before the existing append path; no reserve/admit/validation
|
|
2560
|
+
// execution or spend settlement occurs when repairing this registration.
|
|
2561
|
+
const final = await reviewerCorrectionValidationFresh(direct.validation, request.childSessionID, history, Date.parse(direct.startedAt), direct.checks, input.directory, direct.directories, direct.registeredAt);
|
|
2562
|
+
if (!final.ready)
|
|
2563
|
+
throw new Error(`operator-recovery-proof-unavailable-or-stale:${final.reason}`);
|
|
2564
|
+
await ledger.appendGoal({ kind: "unit.evidence-reconciled", at: new Date().toISOString(), goal_id: goal.goal_id, unit_id: request.unitID,
|
|
2565
|
+
previous_receipt_id: defect.receipt_id, evidence, direct_finish: proof });
|
|
2566
|
+
return evidence;
|
|
2567
|
+
}
|
|
2475
2568
|
const defect = [...snapshot.records].reverse().map(record => record.event).find(event => event.kind === "unit.settled" &&
|
|
2476
2569
|
event.unit_id === request.unitID && event.result_class === "process-defect" && !event.evidence.length);
|
|
2477
2570
|
if (defect?.kind !== "unit.settled")
|
|
@@ -8290,10 +8383,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
8290
8383
|
const evidence = checks.flatMap(check => observedHostGoalEvidence({ ...check, owner: "coordinator",
|
|
8291
8384
|
outcome: check.exitCode === 0 ? "pass" : "fail", immutableRef: goalFingerprint({ direct_check: check }),
|
|
8292
8385
|
correction: { taskID: direct.taskID, dispatchCallID: direct.callID, commands: check.command } }, snapshot.state, direct.taskID));
|
|
8293
|
-
await settleGoalDispatch(direct.callID, { output: "Host-observed direct unit finished; controller session remains active.",
|
|
8386
|
+
const settled = await settleGoalDispatch(direct.callID, { output: "Host-observed direct unit finished; controller session remains active.",
|
|
8294
8387
|
metadata: { sessionId: actor } }, undefined, evidence);
|
|
8388
|
+
if (!settled)
|
|
8389
|
+
throw new Error("mission-direct-settlement-unavailable");
|
|
8295
8390
|
await releaseWriteGate(actor);
|
|
8296
8391
|
releaseSessionEnforcement(actor);
|
|
8392
|
+
return settled;
|
|
8297
8393
|
},
|
|
8298
8394
|
releaseDirectUnit: async (actor) => {
|
|
8299
8395
|
await releaseWriteGate(actor);
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -240,8 +240,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
240
240
|
return observedMissionValidationSummary(validation, child, typeof nativeSession?.messages === "function"
|
|
241
241
|
? () => 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);
|
|
242
242
|
}
|
|
243
|
-
async function reviewMessages(id) {
|
|
244
|
-
const result = payload(await session(typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages", { path: { id }, query: { directory: input.directory } }));
|
|
243
|
+
async function reviewMessages(id, notBefore) {
|
|
244
|
+
const result = payload(await session(typeof nativeSession?.reviewMessages === "function" ? "reviewMessages" : "messages", { path: { id }, query: { directory: input.directory, ...(notBefore ? { notBefore } : {}) } }));
|
|
245
245
|
return Array.isArray(result) ? result.filter(record) : [];
|
|
246
246
|
}
|
|
247
247
|
async function observedSessionModel(id) {
|
|
@@ -1814,107 +1814,171 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
1814
1814
|
...(panel ? { return_report: panel,
|
|
1815
1815
|
return_report_instruction: "The host-authored receipt is retained in this tool result. Give the user a concise outcome and key checks; do not transcribe this card, recalculate it or request another model turn for presentation." } : {}) });
|
|
1816
1816
|
} };
|
|
1817
|
-
tools[resume] = { description: "Reconcile a finished process-defect
|
|
1817
|
+
tools[resume] = { description: "Reconcile a finished process-defect or diagnosed direct finish-registration defect from retained native checks and currently unchanged source. Direct recovery preserves the same unit and all spend without rerunning checks; explicit cancellation, failed/incomplete/stale validation and ordinary Worker terminal decisions remain authoritative. For the exact first validation-only contract-repair continuation process defect, unchanged durable controls/candidate and available validation budget permit one same-worker validation-only retry. Never reimplements a unit or resets implementation/goal spend.",
|
|
1818
1818
|
args: { run_id: stringSchema, acceptance_fingerprint: stringSchema }, execute: async (args, context) => {
|
|
1819
1819
|
await requireRoot(context.sessionID);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
throw new Error(
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const last = assistants.at(-1), info = record(last?.info) ? last.info : last;
|
|
1836
|
-
const tail = history.at(-1), tailInfo = record(tail?.info) ? tail.info : tail;
|
|
1837
|
-
if (!info || tailInfo?.role !== "assistant" || (!isRecordTimeComplete(info) && info.finish !== "stop") || history.some(message => Array.isArray(message.parts) && message.parts.some(part => record(part) && part.type === "tool" && record(part.state) && ["pending", "running"].includes(String(part.state.status))))) {
|
|
1838
|
-
throw new Error("operator-resume-host-still-active");
|
|
1820
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
1821
|
+
let state = await operators.required(context.sessionID);
|
|
1822
|
+
const replayDirect = new Set(state.units.filter(unit => unit.status === "succeeded" && !!unit.directExecution?.registrationRecovery)
|
|
1823
|
+
.map(unit => unit.unit.id));
|
|
1824
|
+
const replay = replayDirect.size > 0 && ["awaiting-acceptance", "awaiting-decision", "prepared"].includes(state.phase) &&
|
|
1825
|
+
!state.units.some(unit => ["failed", "cancelled", "running"].includes(unit.status));
|
|
1826
|
+
if (state.runID !== args.run_id || state.acceptanceFingerprint !== args.acceptance_fingerprint ||
|
|
1827
|
+
state.phase !== "awaiting-decision" && !replay)
|
|
1828
|
+
throw new Error("operator-resume-identity-mismatch");
|
|
1829
|
+
await operators.verifyContinuityControls(state);
|
|
1830
|
+
if (state.decision === "operator-acceptance-remediation-required" &&
|
|
1831
|
+
!state.units.some(unit => unit.status === "failed" && !!unit.directExecution?.finishedAt)) {
|
|
1832
|
+
throw new Error(state.repairGeneration === 1
|
|
1833
|
+
? "operator-resume-unit-not-recoverable"
|
|
1834
|
+
: "operator-resume-acceptance-remediation-required");
|
|
1839
1835
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
await control.assertActiveGoal(context.sessionID, goalFingerprint);
|
|
1853
|
-
await restorePriorAcceptance(context.sessionID, state);
|
|
1854
|
-
const recovered = new Map();
|
|
1855
|
-
const unstarted = new Set();
|
|
1856
|
-
for (const unit of state.units) {
|
|
1857
|
-
if (unit.status !== "failed")
|
|
1858
|
-
continue;
|
|
1859
|
-
const taskID = /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1];
|
|
1860
|
-
if (!taskID)
|
|
1861
|
-
throw new Error("operator-resume-task-id-missing");
|
|
1862
|
-
if (unit.resultClass === "process-defect" && unit.childSessionID === null && unit.callID && state.decision === "dispatch-admission-rejected") {
|
|
1863
|
-
const history = await messages(state.units.length > 1 ? delegate : context.sessionID);
|
|
1864
|
-
const matching = history.flatMap(message => Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) &&
|
|
1865
|
-
part.type === "tool" && ["task", "subagent"].includes(String(part.tool)) && part.callID === unit.callID && record(part.state) && part.state.status === "error" &&
|
|
1866
|
-
record(part.state.input) && operators.matchesRecordedWorkerTask(state, unit.unit.id, part.state.input) &&
|
|
1867
|
-
taskChildSessionID({ metadata: part.state.metadata, output: typeof part.state.output === "string" ? part.state.output : "" }) === undefined);
|
|
1868
|
-
if (matching.length !== 1 || !await control.hasNoGoalReservation(context.sessionID, taskID, unit.callID))
|
|
1869
|
-
throw new Error("operator-resume-unstarted-proof-missing");
|
|
1870
|
-
unstarted.add(unit.unit.id);
|
|
1871
|
-
continue;
|
|
1836
|
+
async function finishedSession(id, fullHistory = false, notBefore) {
|
|
1837
|
+
const statuses = payload(await session("status", { query: { directory: input.directory } }));
|
|
1838
|
+
const observed = record(statuses) ? statuses[id] : undefined;
|
|
1839
|
+
if (record(observed) && observed.type !== "idle")
|
|
1840
|
+
throw new Error("operator-resume-host-still-active");
|
|
1841
|
+
const history = fullHistory ? await reviewMessages(id, notBefore) : await messages(id);
|
|
1842
|
+
const assistants = history.filter(message => (record(message.info) ? message.info.role : message.role) === "assistant");
|
|
1843
|
+
const last = assistants.at(-1), info = record(last?.info) ? last.info : last;
|
|
1844
|
+
const tail = history.at(-1), tailInfo = record(tail?.info) ? tail.info : tail;
|
|
1845
|
+
if (!info || tailInfo?.role !== "assistant" || (!isRecordTimeComplete(info) && info.finish !== "stop") || history.some(message => Array.isArray(message.parts) && message.parts.some(part => record(part) && part.type === "tool" && record(part.state) && ["pending", "running"].includes(String(part.state.status))))) {
|
|
1846
|
+
throw new Error("operator-resume-host-still-active");
|
|
1847
|
+
}
|
|
1872
1848
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1849
|
+
function isRecordTimeComplete(info) { return record(info.time) && typeof info.time.completed === "number"; }
|
|
1850
|
+
const delegate = state.operatorSessionID;
|
|
1851
|
+
if (!replay && state.units.length > 1 && state.units.some(unit => unit.status === "failed" && !unit.directExecution)) {
|
|
1852
|
+
const owner = delegate ? await identity(delegate) : undefined;
|
|
1853
|
+
if (!delegate || owner?.parent !== context.sessionID || owner.role !== "dog-operator")
|
|
1854
|
+
throw new Error("operator-resume-delegate-owner-mismatch");
|
|
1855
|
+
await finishedSession(delegate);
|
|
1856
|
+
operatorParents.set(delegate, context.sessionID);
|
|
1881
1857
|
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1858
|
+
const goalFingerprint = await operators.completionGoalFingerprint(state);
|
|
1859
|
+
await relinkRegisteredGoal(context.sessionID, state, goalFingerprint);
|
|
1860
|
+
await control.assertActiveGoal(context.sessionID, goalFingerprint);
|
|
1861
|
+
await restorePriorAcceptance(context.sessionID, state);
|
|
1862
|
+
const recovered = new Map();
|
|
1863
|
+
const recoveredDirect = new Set();
|
|
1864
|
+
const unstarted = new Set();
|
|
1865
|
+
for (const unit of state.units) {
|
|
1866
|
+
if (unit.status !== "failed" && unit.status !== "cancelled" && !(replay && replayDirect.has(unit.unit.id)))
|
|
1867
|
+
continue;
|
|
1868
|
+
const taskID = /^task_id: (.+)$/m.exec(unit.task.prompt)?.[1];
|
|
1869
|
+
if (!taskID)
|
|
1870
|
+
throw new Error("operator-resume-task-id-missing");
|
|
1871
|
+
if (unit.directExecution) {
|
|
1872
|
+
const direct = unit.directExecution;
|
|
1873
|
+
const mission = await missions.required(context.sessionID);
|
|
1874
|
+
if (["cancelled", "completed"].includes(mission.phase) || mission.runID !== state.runID || !unit.callID ||
|
|
1875
|
+
!direct.finishedAt || direct.actor !== unit.childSessionID ||
|
|
1876
|
+
direct.actor !== (mission.coordinator ?? context.sessionID) && unit.reviewerCorrection?.author !== direct.actor) {
|
|
1877
|
+
throw new Error("operator-resume-direct-not-recoverable");
|
|
1889
1878
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1879
|
+
// A reusable root executes this very recovery tool. Its native conversation
|
|
1880
|
+
// need not end like a Worker Task; other actors still must be inactive.
|
|
1881
|
+
if (direct.actor !== context.sessionID)
|
|
1882
|
+
await finishedSession(direct.actor, true, direct.startedAt);
|
|
1883
|
+
recovered.set(unit.unit.id, await control.recoverUnitEvidence(context.sessionID, { unitID: taskID,
|
|
1884
|
+
childSessionID: direct.actor, manifestPath: unit.manifestPath, manifestHash: unit.hashes[1], goalFingerprint,
|
|
1885
|
+
direct: { callID: unit.callID, startedAt: direct.startedAt, finishedAt: direct.finishedAt,
|
|
1886
|
+
checks: unit.reviewerCorrection?.checks ?? direct.checks, validation: unit.unit.validation,
|
|
1887
|
+
directories: direct.validationCwd ?? unit.unit.validation_cwd,
|
|
1888
|
+
registeredAt: unit.reviewerCorrection?.validationRegisteredAt ?? direct.validationRegisteredAt } }));
|
|
1889
|
+
recoveredDirect.add(unit.unit.id);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (unit.resultClass === "process-defect" && unit.childSessionID === null && unit.callID && state.decision === "dispatch-admission-rejected") {
|
|
1893
|
+
const history = await messages(state.units.length > 1 ? delegate : context.sessionID);
|
|
1894
|
+
const matching = history.flatMap(message => Array.isArray(message.parts) ? message.parts : []).filter(part => record(part) &&
|
|
1895
|
+
part.type === "tool" && ["task", "subagent"].includes(String(part.tool)) && part.callID === unit.callID && record(part.state) && part.state.status === "error" &&
|
|
1896
|
+
record(part.state.input) && operators.matchesRecordedWorkerTask(state, unit.unit.id, part.state.input) &&
|
|
1897
|
+
taskChildSessionID({ metadata: part.state.metadata, output: typeof part.state.output === "string" ? part.state.output : "" }) === undefined);
|
|
1898
|
+
if (matching.length !== 1 || !await control.hasNoGoalReservation(context.sessionID, taskID, unit.callID))
|
|
1899
|
+
throw new Error("operator-resume-unstarted-proof-missing");
|
|
1900
|
+
unstarted.add(unit.unit.id);
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const child = unit.childSessionID ? await identity(unit.childSessionID) : undefined;
|
|
1904
|
+
if (unit.resultClass !== "process-defect" || !unit.childSessionID || child?.role !== "dog-worker" ||
|
|
1905
|
+
child.parent !== (state.units.length > 1 ? delegate : context.sessionID))
|
|
1906
|
+
throw new Error("operator-resume-unit-not-recoverable");
|
|
1907
|
+
await finishedSession(unit.childSessionID);
|
|
1908
|
+
try {
|
|
1909
|
+
recovered.set(unit.unit.id, await control.recoverUnitEvidence(context.sessionID, { unitID: taskID,
|
|
1910
|
+
childSessionID: unit.childSessionID, manifestPath: unit.manifestPath, manifestHash: unit.hashes[1], goalFingerprint }));
|
|
1911
|
+
}
|
|
1912
|
+
catch (error) {
|
|
1913
|
+
if (!(error instanceof Error) || error.message.split(":", 1)[0] !== "operator-recovery-proof-unavailable-or-stale")
|
|
1914
|
+
throw error;
|
|
1915
|
+
if (!operators.canRetryRepairValidation(state)) {
|
|
1916
|
+
try {
|
|
1917
|
+
const remediation = await operators.requireAcceptanceRemediation(context.sessionID, state.runID);
|
|
1918
|
+
return JSON.stringify(await operatorPacket(remediation));
|
|
1919
|
+
}
|
|
1920
|
+
catch (remediationError) {
|
|
1921
|
+
if (!(remediationError instanceof Error) || remediationError.message !== "operator-process-remediation-not-ready") {
|
|
1922
|
+
throw remediationError;
|
|
1923
|
+
}
|
|
1924
|
+
// A process defect without an authorized Git lifecycle has no committed remediation
|
|
1925
|
+
// baseline. Preserve the failed run and return an explicit replacement route instead
|
|
1926
|
+
// of turning this local routing limitation into a terminal tool exception.
|
|
1927
|
+
const replacement = await operators.processReplacementRequired(context.sessionID, state.runID);
|
|
1928
|
+
return JSON.stringify(processRemediationReplacementPacket(remediationError.message, operators.packet(replacement), cancel, prepare));
|
|
1893
1929
|
}
|
|
1894
|
-
// A process defect without an authorized Git lifecycle has no committed remediation
|
|
1895
|
-
// baseline. Preserve the failed run and return an explicit replacement route instead
|
|
1896
|
-
// of turning this local routing limitation into a terminal tool exception.
|
|
1897
|
-
const replacement = await operators.processReplacementRequired(context.sessionID, state.runID);
|
|
1898
|
-
return JSON.stringify(processRemediationReplacementPacket(remediationError.message, operators.packet(replacement), cancel, prepare));
|
|
1899
1930
|
}
|
|
1931
|
+
const retried = await operators.resumeRepairValidation(context.sessionID, state.runID, source => control.authorizeOperatorContractRepairValidationRetry(context.sessionID, { ...source, declarationFingerprint: goalFingerprint }));
|
|
1932
|
+
await restorePriorAcceptance(context.sessionID, retried);
|
|
1933
|
+
return JSON.stringify({ run_id: retried.runID, acceptance_fingerprint: retried.acceptanceFingerprint,
|
|
1934
|
+
resumed: true, mode: "repair-validation-only", repair_validation_attempt: 2,
|
|
1935
|
+
task: retried.units.length === 1 ? operators.nextWorkerTask(retried) : operators.dispatchTask(retried) });
|
|
1900
1936
|
}
|
|
1901
|
-
const retried = await operators.resumeRepairValidation(context.sessionID, state.runID, source => control.authorizeOperatorContractRepairValidationRetry(context.sessionID, { ...source, declarationFingerprint: goalFingerprint }));
|
|
1902
|
-
await restorePriorAcceptance(context.sessionID, retried);
|
|
1903
|
-
return JSON.stringify({ run_id: retried.runID, acceptance_fingerprint: retried.acceptanceFingerprint,
|
|
1904
|
-
resumed: true, mode: "repair-validation-only", repair_validation_attempt: 2,
|
|
1905
|
-
task: retried.units.length === 1 ? operators.nextWorkerTask(retried) : operators.dispatchTask(retried) });
|
|
1906
1937
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1938
|
+
await control.assertActiveGoal(context.sessionID, goalFingerprint);
|
|
1939
|
+
const resumed = replay ? state : await operators.resume(context.sessionID, state.runID, recovered, unstarted, recoveredDirect);
|
|
1940
|
+
if (recoveredDirect.size)
|
|
1941
|
+
await missions.update(context.sessionID, mission => {
|
|
1942
|
+
if (mission.runID !== resumed.runID || ["completed", "cancelled"].includes(mission.phase))
|
|
1943
|
+
throw new Error("operator-resume-direct-mission-changed");
|
|
1944
|
+
for (const unit of resumed.units.filter(unit => recoveredDirect.has(unit.unit.id))) {
|
|
1945
|
+
const attempt = mission.attempts?.find(attempt => attempt.callID === unit.callID);
|
|
1946
|
+
if (attempt && !attempt.registrationRecovery) {
|
|
1947
|
+
attempt.registrationRecovery = { at: unit.directExecution.registrationRecovery.at, previousStatus: attempt.status,
|
|
1948
|
+
previousResultClass: attempt.resultClass, previousFailure: attempt.failure };
|
|
1949
|
+
attempt.status = "succeeded";
|
|
1950
|
+
attempt.resultClass = "acceptance";
|
|
1951
|
+
delete attempt.failure;
|
|
1952
|
+
}
|
|
1953
|
+
if ([...mission.progress].reverse().find(item => item.unit === `${resumed.runID}/${unit.unit.id}`)?.status !== "succeeded") {
|
|
1954
|
+
mission.progress.push({ unit: `${resumed.runID}/${unit.unit.id}`, title: unit.unit.title, status: "succeeded", at: new Date().toISOString() });
|
|
1955
|
+
}
|
|
1956
|
+
const correction = mission.corrections?.find(item => item.runID === resumed.runID && item.author === unit.directExecution?.actor);
|
|
1957
|
+
if (correction)
|
|
1958
|
+
correction.status = "ready";
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
await restorePriorAcceptance(context.sessionID, resumed);
|
|
1962
|
+
if (recoveredDirect.size)
|
|
1963
|
+
return JSON.stringify({
|
|
1964
|
+
...compactMissionEvidence(missionPacket(await missions.required(context.sessionID), resumed), resumed, context.sessionID),
|
|
1965
|
+
recovered_direct_units: [...recoveredDirect],
|
|
1966
|
+
next_action: "The same direct unit's finish registration is reconciled from currently fresh native checks, without a new unit or check rerun. Retain the original erroneous receipt and cumulative spend. " +
|
|
1967
|
+
(resumed.phase === "awaiting-decision" ? `The recorded boundary decision remains pending: ${resumed.decision}. No replacement Task is admitted. ` :
|
|
1968
|
+
resumed.phase === "prepared" ? "Remaining planned units are still pending under the existing authorization and cumulative budget. " : "") +
|
|
1969
|
+
"Latest-change Review and individual product acceptance remain pending; this recovery is not acceptance, Go or completion."
|
|
1970
|
+
});
|
|
1971
|
+
if (resumed.phase === "awaiting-acceptance") {
|
|
1972
|
+
return JSON.stringify(operators.packet(resumed));
|
|
1973
|
+
}
|
|
1974
|
+
if (resumed.phase === "awaiting-decision")
|
|
1975
|
+
return JSON.stringify(operators.packet(resumed));
|
|
1976
|
+
const remaining = resumed.units.filter(unit => unit.status === "pending").length;
|
|
1977
|
+
control.enableUnits(context.sessionID, remaining);
|
|
1978
|
+
return JSON.stringify({ run_id: resumed.runID, acceptance_fingerprint: resumed.acceptanceFingerprint,
|
|
1979
|
+
resumed: true, remaining_units: remaining,
|
|
1980
|
+
task: resumed.units.length === 1 ? operators.nextWorkerTask(resumed) : operators.dispatchTask(resumed) });
|
|
1981
|
+
});
|
|
1918
1982
|
} };
|
|
1919
1983
|
tools[beginProposal] = { description: `Root-only: freeze original ordered requirements and grant one bounded read-only proposal investigation to dogs-coordinator. The result contains one exact short Task reference; pass task.subagent_type, task.description, and task.prompt byte-for-byte without appending or paraphrasing. The native parent retains that reference; the host expands it only inside the directly claimed proposal child. ${OPERATOR_INTENT_CONTRACT}`,
|
|
1920
1984
|
args: { intent_json: intentSchema }, execute: async (args, context) => {
|
|
@@ -2140,8 +2204,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
2140
2204
|
next_action: "Correct the actual failure or run the missing/affected checks in this same session, then finish_direct_unit. Preserve successful current evidence; do not dispatch a validation-only Worker." });
|
|
2141
2205
|
// A resumed correction Task settles at its native terminal, not at this
|
|
2142
2206
|
// advisory completion call. Inline/direct execution uses its existing finish path.
|
|
2143
|
-
if (direct.unit.directExecution)
|
|
2144
|
-
await control.finishDirectUnit(root, context.sessionID, (direct.unit.reviewerCorrection?.checks ?? direct.unit.directExecution.checks).filter(check => checks.matched?.some(item => item.callID === check.callID)));
|
|
2207
|
+
if (direct.unit.directExecution) {
|
|
2208
|
+
const settled = await control.finishDirectUnit(root, context.sessionID, (direct.unit.reviewerCorrection?.checks ?? direct.unit.directExecution.checks).filter(check => checks.matched?.some(item => item.callID === check.callID)));
|
|
2209
|
+
const saved = (await operators.required(root)).units.find(unit => unit.callID === direct.unit.callID);
|
|
2210
|
+
if (settled.disposition !== "succeeded" || !settled.evidence.length || saved?.status !== "succeeded" || !saved.evidence.length) {
|
|
2211
|
+
const run = await operators.required(root);
|
|
2212
|
+
return JSON.stringify({ ...compactMissionEvidence(missionPacket(await missions.required(root), run), run, root),
|
|
2213
|
+
direct_settlement: { disposition: settled.disposition, result_class: settled.resultClass, evidence_count: settled.evidence.length },
|
|
2214
|
+
next_action: "Direct unit is not validated: retain its actual settlement, native checks, current source and cumulative spend. Inspect the recorded cancellation/failure or use resume_operator for a diagnosed finish-registration defect; do not repeat passed checks or create a replacement unit just to repair registration. Review and Mission acceptance remain pending." });
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2145
2217
|
if (direct.unit.reviewerCorrection)
|
|
2146
2218
|
return JSON.stringify({ status: "correction-validated",
|
|
2147
2219
|
execution: "same-native-task", validation: direct.unit.unit.validation,
|
|
@@ -203,6 +203,15 @@ export interface RuntimeBridge {
|
|
|
203
203
|
manifestPath: string;
|
|
204
204
|
manifestHash: string;
|
|
205
205
|
goalFingerprint: string;
|
|
206
|
+
direct?: {
|
|
207
|
+
callID: string;
|
|
208
|
+
startedAt: string;
|
|
209
|
+
finishedAt: string;
|
|
210
|
+
checks: readonly ReviewerCorrectionCheck[];
|
|
211
|
+
validation: readonly string[];
|
|
212
|
+
directories?: Readonly<Record<string, string>>;
|
|
213
|
+
registeredAt?: Readonly<Record<string, string>>;
|
|
214
|
+
};
|
|
206
215
|
}): Promise<readonly GoalEvidence[]>;
|
|
207
216
|
completionReadiness(rootSessionID: string): Promise<import("./goal-completion.js").CompletionReadiness>;
|
|
208
217
|
missionWorkerTerminal(rootSessionID: string, terminal: MissionWorkerTerminalRecord, writeScopes: readonly string[]): Promise<{
|
|
@@ -212,7 +221,7 @@ export interface RuntimeBridge {
|
|
|
212
221
|
restoreReviewerCorrectionChild(rootSessionID: string, childSessionID: string, callID: string, taskID: string): Promise<void>;
|
|
213
222
|
activateMissionWorker(rootSessionID: string, childSessionID: string, handoffPath: string, admission: MissionImplementationAdmission): Promise<void>;
|
|
214
223
|
activateDirectUnit(rootSessionID: string, actorSessionID: string, prompt: string, handoffPath: string): Promise<void>;
|
|
215
|
-
finishDirectUnit(rootSessionID: string, actorSessionID: string, checks: readonly ReviewerCorrectionCheck[]): Promise<
|
|
224
|
+
finishDirectUnit(rootSessionID: string, actorSessionID: string, checks: readonly ReviewerCorrectionCheck[]): Promise<SerialDispatchSettlement>;
|
|
216
225
|
/** Settle an unfinished direct unit against its actual actor, never a fabricated child Task. */
|
|
217
226
|
failDirectUnit(rootSessionID: string, actorSessionID: string): Promise<void>;
|
|
218
227
|
releaseDirectUnit(actorSessionID: string): Promise<void>;
|
package/dist/plugin/v2.js
CHANGED
|
@@ -110,11 +110,12 @@ function legacyMessage(value, session, agent) {
|
|
|
110
110
|
return converted === undefined ? [] : [converted];
|
|
111
111
|
}) };
|
|
112
112
|
}
|
|
113
|
-
async function legacyMessages(context, id, paginated) {
|
|
113
|
+
async function legacyMessages(context, id, paginated, notBefore) {
|
|
114
114
|
const list = paginated ?? (context.message ? (input) => context.message.list(input) : undefined);
|
|
115
115
|
const [info, history] = await Promise.all([
|
|
116
116
|
context.session.get({ sessionID: id }).catch(() => undefined),
|
|
117
|
-
list ? nativePages(cursor => list({ sessionID: id, limit: 100,
|
|
117
|
+
list ? nativePages(cursor => list({ sessionID: id, limit: 100,
|
|
118
|
+
...(cursor ? { cursor } : { order: notBefore === undefined ? "asc" : "desc" }) }), notBefore)
|
|
118
119
|
: context.session.context({ sessionID: id }),
|
|
119
120
|
]);
|
|
120
121
|
const agent = record(info) ? string(info.agent) : undefined;
|
|
@@ -123,17 +124,24 @@ async function legacyMessages(context, id, paginated) {
|
|
|
123
124
|
return converted === undefined ? [] : [converted];
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
|
-
async function nativePages(fetch) {
|
|
127
|
+
async function nativePages(fetch, notBefore) {
|
|
127
128
|
const items = [], seen = new Set();
|
|
128
129
|
let cursor;
|
|
129
|
-
for (let page = 0; page < 100; page += 1) {
|
|
130
|
+
for (let page = 0; notBefore !== undefined || page < 100; page += 1) {
|
|
130
131
|
const result = await fetch(cursor);
|
|
131
132
|
if (!record(result) || !Array.isArray(result.data) || !record(result.cursor))
|
|
132
133
|
throw new Error("v2-history-page-invalid");
|
|
133
134
|
items.push(...result.data);
|
|
135
|
+
// Retain the whole boundary page: a message created before admission can
|
|
136
|
+
// contain tools that started after it. The check verifier applies its own
|
|
137
|
+
// exact native-start cutoff. Do not scan decades of unrelated controller
|
|
138
|
+
// history or reject a known unit window at the general full-history cap.
|
|
139
|
+
if (notBefore !== undefined && result.data.some(message => record(message) && record(message.time) &&
|
|
140
|
+
typeof message.time.created === "number" && message.time.created < notBefore))
|
|
141
|
+
return items.reverse();
|
|
134
142
|
const next = string(result.cursor.next);
|
|
135
143
|
if (!next)
|
|
136
|
-
return items;
|
|
144
|
+
return notBefore === undefined ? items : items.reverse();
|
|
137
145
|
if (seen.has(next))
|
|
138
146
|
throw new Error("v2-history-cursor-repeated");
|
|
139
147
|
seen.add(next);
|
|
@@ -160,7 +168,9 @@ function legacyClient(context) {
|
|
|
160
168
|
},
|
|
161
169
|
reviewMessages: async (request) => {
|
|
162
170
|
const id = sessionID(request);
|
|
163
|
-
|
|
171
|
+
const query = record(request) && record(request.query) ? request.query : undefined;
|
|
172
|
+
const requestedStart = typeof query?.notBefore === "string" ? Date.parse(query.notBefore) : undefined;
|
|
173
|
+
return { data: id === undefined ? [] : await legacyMessages(context, id, context.message ? input => context.message.list(input) : reviewList, Number.isFinite(requestedStart) ? requestedStart : undefined) };
|
|
164
174
|
},
|
|
165
175
|
reviewExecution: async (request) => {
|
|
166
176
|
const id = sessionID(request);
|