sortie-dogs 0.13.17 → 0.13.19
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 +12 -7
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/cli/codex-mission.js +2 -0
- package/dist/core/goal-bound.d.ts +10 -0
- package/dist/core/goal-bound.js +20 -7
- package/dist/core/initialize.js +4 -1
- package/dist/core/operator-mission.d.ts +11 -1
- package/dist/core/operator-mission.js +23 -1
- package/dist/core/operator-runtime.d.ts +10 -1
- package/dist/core/operator-runtime.js +34 -7
- package/dist/core/run-flight-ledger.js +8 -0
- package/dist/core/validation-budget.js +3 -1
- package/dist/core/work-quantity-observations.d.ts +13 -0
- package/dist/core/work-quantity-observations.js +91 -0
- package/dist/core/work-quantity.d.ts +94 -0
- package/dist/core/work-quantity.js +463 -0
- package/dist/plugin/index.js +103 -7
- package/dist/plugin/mission-progress.d.ts +1 -1
- package/dist/plugin/mission-progress.js +17 -6
- package/dist/plugin/operator-status-output.js +1 -1
- package/dist/plugin/profiled.js +232 -101
- package/dist/plugin/runtime-bridge.d.ts +10 -1
- package/dist/plugin/v2.js +33 -13
- 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.19](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.13.19)**
|
|
23
|
+
· [Release notes](docs/release-v0.13.19.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.19
|
|
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.19
|
|
90
90
|
npx --no-install sortie-dogs init .
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -125,6 +125,11 @@ Discovery: User → Operator → Coordinator → Worker → Validation → Revi
|
|
|
125
125
|
The default Mission profile is serial. Background responsiveness and status observations do not
|
|
126
126
|
add parallel writers, replay unknown commands or imply completion. More agents are not the goal.
|
|
127
127
|
|
|
128
|
+
Whole-request workload appears as `全体作業量 n/100(概算)`, using ordinary plans and native
|
|
129
|
+
work observations, not budget consumption or fixed stage shares. Calculation adds no model
|
|
130
|
+
request or verification; formal acceptance still owns `100/100`.
|
|
131
|
+
[Workload estimates, optional weights and runtime cost](docs/work-quantity-progress.md).
|
|
132
|
+
|
|
128
133
|
For **run-once / result-collection** requests, a terminal failure can be the requested result:
|
|
129
134
|
validate and report it without an unauthorized repair/rerun loop. Acceptance preserves the real
|
|
130
135
|
operation exit and `execution-failed` outcome; it never turns a failed benchmark into a successful
|
|
@@ -188,7 +193,7 @@ known expense. Official local evaluation and leaderboard acceptance are separate
|
|
|
188
193
|
**8/23 (34.8%)**, versus v0.12.25's 7/23; all seven prior resolutions retained, no empty patches or
|
|
189
194
|
official evaluation errors. [Fixed conditions, stopped-run accounting and costs](docs/benchmarks/swebench-lite-history.md#v0131-dev23-2026-09-30).
|
|
190
195
|
|
|
191
|
-
All scores belong to their fixed candidates, **not v0.13.
|
|
196
|
+
All scores belong to their fixed candidates, **not v0.13.19**. SWE-bench remains an optional
|
|
192
197
|
measurement, not a release gate. Different budgets, models and conditions are not controlled comparisons.
|
|
193
198
|
|
|
194
199
|
## Configuration
|
|
@@ -211,7 +216,7 @@ Install the desired package version and rerun **your host's** initializer:
|
|
|
211
216
|
|
|
212
217
|
An OpenCode config-local bridge may resolve a separate package; npm-global update alone does not
|
|
213
218
|
update it. [Global/update instructions](docs/configuration.md#global-availability).
|
|
214
|
-
The current Mission asset marker is `0.13.
|
|
219
|
+
The current Mission asset marker is `0.13.19-workload-progress-v1`; the unchanged Codex skill marker is
|
|
215
220
|
`0.13.11-codex-skill-v2`. Installed markers alone do not prove a running host reloaded the new version.
|
|
216
221
|
|
|
217
222
|
There is no supported uninstall command. Remove the dependency and only known Sortie-owned paths;
|
|
@@ -227,7 +232,7 @@ follow the [manual removal guide](docs/uninstall.md), never delete the entire `.
|
|
|
227
232
|
[SWE-bench history](docs/benchmarks/swebench-lite-history.md) · [Historical local case study](docs/benchmark-reference.md).
|
|
228
233
|
- **Development:** [Testing](docs/testing.md) · [CLI testing](docs/cli-testing.md) ·
|
|
229
234
|
[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) ·
|
|
235
|
+
- **Releases:** [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) ·
|
|
231
236
|
[v0.13.9](docs/release-v0.13.9.md) · [All GitHub releases](https://github.com/zufall-upon/Sortie-dogs/releases).
|
|
232
237
|
|
|
233
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.19-workload-progress-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.19-workload-progress-v1";
|
|
7
7
|
export const CODEX_SKILL_ASSET_VERSION = "0.13.11-codex-skill-v2";
|
|
@@ -118,6 +118,8 @@ export function codexMissionProgress(event) {
|
|
|
118
118
|
command: bounded(args.command), workdir: bounded(args.workdir), cwd: bounded(metadata.cwd), reason: bounded(args.reason),
|
|
119
119
|
agent: bounded(args.subagent_type), description: bounded(args.description), child_session_id: bounded(metadata.sessionId),
|
|
120
120
|
outcome: bounded(receipt.status), exit: metadata.exit, executor: metadata.executor,
|
|
121
|
+
...(record(receipt.work_quantity ?? metadata.work_quantity) ? { work_quantity: receipt.work_quantity ?? metadata.work_quantity,
|
|
122
|
+
work_quantity_display: bounded(receipt.work_quantity_display ?? metadata.work_quantity_display) } : {}),
|
|
121
123
|
next_action: bounded(receipt.next_action),
|
|
122
124
|
summary: bounded(args.summary ?? receipt.summary),
|
|
123
125
|
detail: bounded(receipt.reason ?? receipt.error ?? (item.success === false ? text : undefined)) };
|
|
@@ -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
|
}
|
package/dist/core/initialize.js
CHANGED
|
@@ -391,7 +391,10 @@ async function v010Config(root, global) {
|
|
|
391
391
|
const update = (path, value) => { content = applyEdits(content, modify(content, path, value, options)); };
|
|
392
392
|
const wrapper = `${prefix}plugins/sortie-dogs/index.js`;
|
|
393
393
|
const wrapperSource = await readFile(resolve(root, wrapper), "utf8").catch(() => "");
|
|
394
|
-
|
|
394
|
+
// Both the package re-export and the release fixture's factory wrapper are
|
|
395
|
+
// automatically discovered by V2. Adding the package would load the same ID twice.
|
|
396
|
+
const localV2Wrapper = /^\s*export\s*\{\s*default\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*$/u.test(wrapperSource) ||
|
|
397
|
+
/^\s*import\s*\{\s*createSortieDogsV2Plugin\s*\}\s*from\s*["']sortie-dogs\/server["'];?\s*import\s*\{\s*SortieDogsPlugin\s*\}\s*from\s*["']sortie-dogs\/plugin["'];?\s*export\s+default\s+createSortieDogsV2Plugin\s*\(\s*SortieDogsPlugin\s*\);?\s*$/u.test(wrapperSource);
|
|
395
398
|
const registered = plugins?.some(item => {
|
|
396
399
|
const name = typeof item === "string" ? item : item !== null && typeof item === "object" ? item.package : undefined;
|
|
397
400
|
return typeof name === "string" && /^sortie-dogs(?:@[^/]+)?$/u.test(name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type OperatorPlan, type OperatorState, type OperatorTask, type OperatorRuntime } from "./operator-runtime.js";
|
|
2
2
|
import { type RuntimeProfile } from "./runtime-profile.js";
|
|
3
3
|
import { type MissionContinuity } from "./mission-context.js";
|
|
4
|
+
import { type WorkQuantityState } from "./work-quantity.js";
|
|
4
5
|
export { missionValidationCommand } from "./mission-validation-command.js";
|
|
5
6
|
export declare const MISSION_REFERENCE = "SORTIE_MISSION_REF ";
|
|
6
7
|
export declare const MISSION_REVIEW_REFERENCE = "SORTIE_MISSION_REVIEW_REF ";
|
|
@@ -76,6 +77,13 @@ export interface MissionAttempt {
|
|
|
76
77
|
code: string;
|
|
77
78
|
};
|
|
78
79
|
resultClass?: string;
|
|
80
|
+
/** Retained erroneous finish projection; the original goal receipt remains append-only. */
|
|
81
|
+
registrationRecovery?: {
|
|
82
|
+
at: string;
|
|
83
|
+
previousStatus: MissionAttempt["status"];
|
|
84
|
+
previousResultClass?: string;
|
|
85
|
+
previousFailure?: MissionAttempt["failure"];
|
|
86
|
+
};
|
|
79
87
|
selectedModel?: string;
|
|
80
88
|
}
|
|
81
89
|
export interface MissionTerminalRescue {
|
|
@@ -249,6 +257,8 @@ export interface OperatorMission {
|
|
|
249
257
|
status: string;
|
|
250
258
|
at: string;
|
|
251
259
|
}[];
|
|
260
|
+
/** Display-only request workload; excluded from controls, prompts and acceptance. */
|
|
261
|
+
workQuantity?: WorkQuantityState;
|
|
252
262
|
submission: {
|
|
253
263
|
status: "ready" | "needs-decision" | "blocked";
|
|
254
264
|
summary: string;
|
|
@@ -380,7 +390,7 @@ export declare function missionPlan(mission: OperatorMission, raw: unknown, proj
|
|
|
380
390
|
/** Compact provenance, not a new acceptance verdict or a substitute for original-request comparison. */
|
|
381
391
|
export declare function missionAcceptanceSummary(mission: OperatorMission, run: OperatorState | undefined, operators: OperatorRuntime, observe?: (validation: readonly string[], child: string | null, notBefore?: number) => Promise<unknown>): Promise<Record<string, unknown>>;
|
|
382
392
|
export declare function missionReviewIndependent(mission: OperatorMission, child: string | undefined): boolean;
|
|
383
|
-
export declare function missionPacket(mission: OperatorMission, run?: OperatorState): Record<string, unknown>;
|
|
393
|
+
export declare function missionPacket(mission: OperatorMission, run?: OperatorState, includeWorkQuantity?: boolean): Record<string, unknown>;
|
|
384
394
|
/** Models forward a short capability, never recopy the host's evidence hashes and source packet. */
|
|
385
395
|
export declare function missionReviewTask(mission: OperatorMission): OperatorTask;
|
|
386
396
|
/** Optional implementation notes supplement host-observed source/checks; prose is not a coverage gate. */
|
|
@@ -7,6 +7,8 @@ import { profileAgent } from "./runtime-profile.js";
|
|
|
7
7
|
import { normalizeCommand } from "../plugin/gate.js";
|
|
8
8
|
import { missionValidationCommand } from "./mission-validation-command.js";
|
|
9
9
|
import { retainMissionContext, missionContextView } from "./mission-context.js";
|
|
10
|
+
import { planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
11
|
+
import { applyPendingWork, rememberWorkMission, rememberedWorkRun } from "./work-quantity-observations.js";
|
|
10
12
|
export { missionValidationCommand } from "./mission-validation-command.js";
|
|
11
13
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
12
14
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -299,11 +301,22 @@ export class OperatorMissionRuntime {
|
|
|
299
301
|
return matches.length === 1 ? matches[0] : undefined;
|
|
300
302
|
}
|
|
301
303
|
async save(file, value) {
|
|
304
|
+
if (record(value) && value.version === "0.12" && typeof value.root === "string") {
|
|
305
|
+
const mission = value;
|
|
306
|
+
const run = rememberedWorkRun(this.projectRoot, mission.root);
|
|
307
|
+
const current = run?.runID === mission.runID ? run : undefined;
|
|
308
|
+
if (!mission.workQuantity || current && mission.workQuantity.planRunID !== current.runID) {
|
|
309
|
+
mission.workQuantity = planWorkQuantity(mission, current?.units.map(unit => unit.unit), current, mission.workQuantity, undefined, this.projectRoot);
|
|
310
|
+
}
|
|
311
|
+
mission.workQuantity = syncWorkQuantity(applyPendingWork(this.projectRoot, mission.root, mission.workQuantity), mission, current);
|
|
312
|
+
}
|
|
302
313
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
303
314
|
await mkdir(join(this.projectRoot, this.profile.stateDirectory, "missions"), { recursive: true });
|
|
304
315
|
try {
|
|
305
316
|
await writeFile(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
|
|
306
317
|
await rename(temporary, file);
|
|
318
|
+
if (record(value) && value.version === "0.12" && file === this.file(String(value.root)))
|
|
319
|
+
rememberWorkMission(this.projectRoot, value);
|
|
307
320
|
}
|
|
308
321
|
finally {
|
|
309
322
|
await rm(temporary, { force: true });
|
|
@@ -326,6 +339,8 @@ export class OperatorMissionRuntime {
|
|
|
326
339
|
const state = await this.loadMission(root);
|
|
327
340
|
if (state && (state.version !== "0.12" || state.root !== root))
|
|
328
341
|
throw new Error("mission-state-invalid");
|
|
342
|
+
if (state)
|
|
343
|
+
rememberWorkMission(this.projectRoot, state);
|
|
329
344
|
return state;
|
|
330
345
|
}
|
|
331
346
|
/** Serialize host recovery claims across processes; a stale lock never authorizes takeover. */
|
|
@@ -441,6 +456,7 @@ export class OperatorMissionRuntime {
|
|
|
441
456
|
throw new Error("mission-requirements-preserved: keep the existing ordered requirements and append user additions");
|
|
442
457
|
}
|
|
443
458
|
previous.requirements = requirements.map((text, index) => ({ id: `R${index + 1}`, text }));
|
|
459
|
+
previous.workQuantity = planWorkQuantity(previous, [], undefined, previous.workQuantity);
|
|
444
460
|
// Only an explicit Mission action adopts the latest real turn; chat capture alone never does.
|
|
445
461
|
if (!previous.requests.some(item => item.id === request.id))
|
|
446
462
|
previous.requests.push(request);
|
|
@@ -468,6 +484,10 @@ export class OperatorMissionRuntime {
|
|
|
468
484
|
...((previous === undefined || previous.phase === "cancelled") &&
|
|
469
485
|
(replaceRequirements || previous?.runID === null || previous === undefined || request.id !== previous.requests[0]?.id) && predecessor
|
|
470
486
|
? { supersededRunID: predecessor } : {}) };
|
|
487
|
+
if (previous?.phase === "cancelled" && !replaceRequirements &&
|
|
488
|
+
previous.requirements.every((requirement, index) => state.requirements[index]?.text === requirement.text)) {
|
|
489
|
+
state.workQuantity = planWorkQuantity(state, [], undefined, previous.workQuantity);
|
|
490
|
+
}
|
|
471
491
|
await this.save(this.file(root), state);
|
|
472
492
|
return state;
|
|
473
493
|
});
|
|
@@ -762,7 +782,7 @@ export function missionReviewIndependent(mission, child) {
|
|
|
762
782
|
return child !== undefined && !mission.sourceAuthors?.includes(child) &&
|
|
763
783
|
!(mission.corrections ?? []).some(correction => correction.author === child);
|
|
764
784
|
}
|
|
765
|
-
export function missionPacket(mission, run) {
|
|
785
|
+
export function missionPacket(mission, run, includeWorkQuantity = true) {
|
|
766
786
|
const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
|
|
767
787
|
if (predecessor)
|
|
768
788
|
run = undefined;
|
|
@@ -770,7 +790,9 @@ export function missionPacket(mission, run) {
|
|
|
770
790
|
const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
|
|
771
791
|
const operationStatus = missionExecutionStatus(mission);
|
|
772
792
|
const operationComplete = missionExecutionComplete(mission);
|
|
793
|
+
const workload = syncWorkQuantity(structuredClone(mission.workQuantity ?? planWorkQuantity(mission, run?.runID === mission.runID ? run.units.map(unit => unit.unit) : [], run)), mission, run);
|
|
773
794
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
795
|
+
...(includeWorkQuantity ? { work_quantity: projectWorkQuantity(workload, mission.phase === "completed", mission.phase === "cancelled") } : {}),
|
|
774
796
|
...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
|
|
775
797
|
completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
|
|
776
798
|
note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
|
|
@@ -2,6 +2,7 @@ import { type RuntimeProfile } from "./runtime-profile.js";
|
|
|
2
2
|
import type { GoalEvidence, GoalTerminalReceipt } from "./goal-bound.js";
|
|
3
3
|
import type { SerialDispatchSettlement } from "../plugin/runtime-bridge.js";
|
|
4
4
|
import { type OperatorContractRepairFileIdentity } from "./operator-contract-repair.js";
|
|
5
|
+
import { type WorkQuantityState, type WorkQuantityProgress } from "./work-quantity.js";
|
|
5
6
|
export declare const OPERATOR_LIMITS: Readonly<{
|
|
6
7
|
units: 32;
|
|
7
8
|
planBytes: number;
|
|
@@ -131,6 +132,11 @@ interface UnitState {
|
|
|
131
132
|
actor: string;
|
|
132
133
|
startedAt: string;
|
|
133
134
|
finishedAt?: string;
|
|
135
|
+
registrationRecovery?: {
|
|
136
|
+
at: string;
|
|
137
|
+
previousStatus: "failed" | "cancelled";
|
|
138
|
+
previousResultClass: string | null;
|
|
139
|
+
};
|
|
134
140
|
checks: import("../plugin/runtime-bridge.js").ReviewerCorrectionCheck[];
|
|
135
141
|
validationCwd?: Readonly<Record<string, string>>;
|
|
136
142
|
validationRegisteredAt?: Readonly<Record<string, string>>;
|
|
@@ -200,6 +206,8 @@ export interface OperatorAcceptanceAnchor {
|
|
|
200
206
|
readonly handoffHash: string;
|
|
201
207
|
}
|
|
202
208
|
export interface OperatorState {
|
|
209
|
+
/** Display metadata only, never part of the immutable execution plan. */
|
|
210
|
+
workQuantity?: WorkQuantityState;
|
|
203
211
|
readonly schema_version: "0.1";
|
|
204
212
|
readonly profile: string;
|
|
205
213
|
readonly rootSessionID: string;
|
|
@@ -249,6 +257,7 @@ export interface OperatorState {
|
|
|
249
257
|
receipt: GoalTerminalReceipt | null;
|
|
250
258
|
}
|
|
251
259
|
export interface OperatorProgress {
|
|
260
|
+
readonly work_quantity?: WorkQuantityProgress;
|
|
252
261
|
readonly profile: string;
|
|
253
262
|
readonly view: "progress";
|
|
254
263
|
readonly run_id: string;
|
|
@@ -430,7 +439,7 @@ export declare class OperatorRuntime {
|
|
|
430
439
|
private repairValidationRetryBinding;
|
|
431
440
|
canRetryRepairValidation(state: OperatorState): boolean;
|
|
432
441
|
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>;
|
|
442
|
+
resume(root: string, runID: string, recovered: ReadonlyMap<string, readonly GoalEvidence[]>, unstarted?: ReadonlySet<string>, recoveredDirect?: ReadonlySet<string>): Promise<OperatorState>;
|
|
434
443
|
requireAcceptanceRemediation(root: string, runID: string): Promise<OperatorState>;
|
|
435
444
|
draftStatus(root: string): Promise<unknown | undefined>;
|
|
436
445
|
completionGoalFingerprint(state: OperatorState): Promise<string>;
|
|
@@ -15,6 +15,8 @@ import { ensureGitManagedStateExcluded } from "./git-managed-state.js";
|
|
|
15
15
|
import { operatorContractRepairFingerprint } from "./operator-contract-repair.js";
|
|
16
16
|
import { classifyUnitResult } from "./unit-result-classification.js";
|
|
17
17
|
import { SOURCE_REVIEW_PHASES, SOURCE_REVIEW_RISK_TAGS } from "./consultation.js";
|
|
18
|
+
import { planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
19
|
+
import { applyPendingWork, rememberWorkRun } from "./work-quantity-observations.js";
|
|
18
20
|
export const OPERATOR_LIMITS = Object.freeze({ units: 32, planBytes: 256 * 1024, packetBytes: 24 * 1024,
|
|
19
21
|
remediationReserve: 32 });
|
|
20
22
|
export class OperatorContractError extends Error {
|
|
@@ -319,8 +321,10 @@ export class OperatorRuntime {
|
|
|
319
321
|
async read(root) {
|
|
320
322
|
await this.writes.get(root);
|
|
321
323
|
const cached = this.states.get(root);
|
|
322
|
-
if (cached)
|
|
324
|
+
if (cached) {
|
|
325
|
+
rememberWorkRun(this.projectRoot, cached);
|
|
323
326
|
return structuredClone(cached);
|
|
327
|
+
}
|
|
324
328
|
let source;
|
|
325
329
|
try {
|
|
326
330
|
source = await readFile(this.file(root), "utf8");
|
|
@@ -330,9 +334,13 @@ export class OperatorRuntime {
|
|
|
330
334
|
return undefined;
|
|
331
335
|
throw error;
|
|
332
336
|
}
|
|
333
|
-
if (Buffer.byteLength(source) > OPERATOR_LIMITS.planBytes * 4)
|
|
334
|
-
throw new Error("operator-state-too-large");
|
|
335
337
|
const parsed = JSON.parse(source);
|
|
338
|
+
// Display metadata must not turn an otherwise valid execution state into a refusal.
|
|
339
|
+
if (Buffer.byteLength(source) > OPERATOR_LIMITS.planBytes * 4) {
|
|
340
|
+
const { workQuantity: _display, ...execution } = parsed;
|
|
341
|
+
if (Buffer.byteLength(JSON.stringify(execution)) > OPERATOR_LIMITS.planBytes * 4)
|
|
342
|
+
throw new Error("operator-state-too-large");
|
|
343
|
+
}
|
|
336
344
|
const repairGeneration = parsed.repairGeneration === undefined ? 0 : parsed.repairGeneration;
|
|
337
345
|
const state = { ...parsed,
|
|
338
346
|
repairResidualPaths: parsed.repairResidualPaths === undefined ? [] : parsed.repairResidualPaths,
|
|
@@ -382,9 +390,15 @@ export class OperatorRuntime {
|
|
|
382
390
|
throw new Error("operator-state-invalid");
|
|
383
391
|
}
|
|
384
392
|
this.states.set(root, state);
|
|
393
|
+
rememberWorkRun(this.projectRoot, state);
|
|
385
394
|
return structuredClone(state);
|
|
386
395
|
}
|
|
387
396
|
async save(state) {
|
|
397
|
+
if (!state.workQuantity || state.workQuantity.planRunID !== state.runID)
|
|
398
|
+
state.workQuantity = planWorkQuantity({
|
|
399
|
+
id: state.workQuantity?.scopeID ?? `operator:${state.runID}`, requirements: state.acceptance.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "running", submission: null
|
|
400
|
+
}, state.units.map(unit => unit.unit), state, state.workQuantity, undefined, this.projectRoot);
|
|
401
|
+
syncWorkQuantity(applyPendingWork(this.projectRoot, state.rootSessionID, state.workQuantity), {}, state);
|
|
388
402
|
state.sequence++;
|
|
389
403
|
const serialized = JSON.stringify(state);
|
|
390
404
|
const previous = this.writes.get(state.rootSessionID) ?? Promise.resolve();
|
|
@@ -405,6 +419,7 @@ export class OperatorRuntime {
|
|
|
405
419
|
try {
|
|
406
420
|
await next;
|
|
407
421
|
this.states.set(state.rootSessionID, structuredClone(state));
|
|
422
|
+
rememberWorkRun(this.projectRoot, state);
|
|
408
423
|
}
|
|
409
424
|
catch (error) {
|
|
410
425
|
state.sequence--;
|
|
@@ -1139,6 +1154,7 @@ export class OperatorRuntime {
|
|
|
1139
1154
|
? { ...structuredClone(previous.gitLifecycle), committedHead: null, commitProvenance: null }
|
|
1140
1155
|
: await this.createGitLifecycle(plan, remediationHead);
|
|
1141
1156
|
const state = { schema_version: "0.1", profile: this.profile.id, rootSessionID: root, runID, planHash: validatedPlanHash,
|
|
1157
|
+
...(parent?.workQuantity ? { workQuantity: structuredClone(parent.workQuantity) } : {}),
|
|
1142
1158
|
acceptance: plan.acceptance, acceptanceProof: plan.acceptance_proof, acceptanceFingerprint, sourceRefs: plan.source_refs, createdAt: new Date().toISOString(),
|
|
1143
1159
|
parentRunID: parent?.runID ?? null, ...(superseding ? { supersededRunID: previous.runID } : {}), priorAcceptedUnits,
|
|
1144
1160
|
remediationParent: remediationTaskID && remediationHead && parent?.gitLifecycle ? {
|
|
@@ -2091,16 +2107,17 @@ export class OperatorRuntime {
|
|
|
2091
2107
|
return state;
|
|
2092
2108
|
});
|
|
2093
2109
|
}
|
|
2094
|
-
resume(root, runID, recovered, unstarted = new Set()) {
|
|
2110
|
+
resume(root, runID, recovered, unstarted = new Set(), recoveredDirect = new Set()) {
|
|
2095
2111
|
return this.serial(root, async () => {
|
|
2096
2112
|
const state = await this.required(root);
|
|
2097
2113
|
if (state.contractRepair !== null)
|
|
2098
2114
|
throw new Error("operator-resume-contract-repair-required");
|
|
2099
|
-
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2115
|
+
if (state.runID !== runID || state.phase !== "awaiting-decision" || state.units.some(unit => unit.status === "running" ||
|
|
2116
|
+
unit.status === "cancelled" && !recoveredDirect.has(unit.unit.id))) {
|
|
2100
2117
|
throw new Error("operator-resume-not-ready");
|
|
2101
2118
|
}
|
|
2102
2119
|
for (const unit of state.units) {
|
|
2103
|
-
if (unit.status !== "failed")
|
|
2120
|
+
if (unit.status !== "failed" && unit.status !== "cancelled")
|
|
2104
2121
|
continue;
|
|
2105
2122
|
if (unstarted.has(unit.unit.id)) {
|
|
2106
2123
|
if (unit.resultClass !== "process-defect" || unit.childSessionID !== null || state.decision !== "dispatch-admission-rejected")
|
|
@@ -2111,10 +2128,18 @@ export class OperatorRuntime {
|
|
|
2111
2128
|
continue;
|
|
2112
2129
|
}
|
|
2113
2130
|
const evidence = recovered.get(unit.unit.id);
|
|
2131
|
+
const direct = recoveredDirect.has(unit.unit.id) && unit.directExecution?.actor === unit.childSessionID &&
|
|
2132
|
+
!!unit.directExecution.finishedAt && !!unit.callID && !!evidence?.length;
|
|
2133
|
+
await this.verifyControls(unit);
|
|
2114
2134
|
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)))) {
|
|
2135
|
+
if (unit.resultClass !== "process-defect" && !direct || !evidence?.length || !unit.unit.acceptance_indices.every(index => state.acceptanceProof[index].some(id => ids.has(id)))) {
|
|
2116
2136
|
throw new Error("operator-resume-proof-incomplete");
|
|
2117
2137
|
}
|
|
2138
|
+
if (unit.status === "cancelled" && !direct)
|
|
2139
|
+
throw new Error("operator-resume-proof-incomplete");
|
|
2140
|
+
if (direct)
|
|
2141
|
+
unit.directExecution.registrationRecovery = { at: new Date().toISOString(),
|
|
2142
|
+
previousStatus: unit.status, previousResultClass: unit.resultClass };
|
|
2118
2143
|
unit.status = "succeeded";
|
|
2119
2144
|
unit.resultClass = "acceptance";
|
|
2120
2145
|
unit.evidence = evidence;
|
|
@@ -2210,6 +2235,7 @@ export class OperatorRuntime {
|
|
|
2210
2235
|
const authoritativeAction = nextAction !== undefined ? nextAction
|
|
2211
2236
|
: this.continuationNextAction(state);
|
|
2212
2237
|
return { profile: state.profile, view: "progress", run_id: state.runID, stage: state.phase, decision: state.decision,
|
|
2238
|
+
...(state.workQuantity ? { work_quantity: projectWorkQuantity(state.workQuantity, state.phase === "completed", state.phase === "cancelled") } : {}),
|
|
2213
2239
|
current_unit: current === undefined ? null : { id: current.unit.id, title: current.unit.title, status: current.status },
|
|
2214
2240
|
completed_units: state.units.filter(unit => unit.status === "succeeded").length, total_units: state.units.length,
|
|
2215
2241
|
budget_remaining_units: budgetRemainingUnits, next_action: typeof authoritativeAction === "string" ? authoritativeAction : null };
|
|
@@ -2233,6 +2259,7 @@ export class OperatorRuntime {
|
|
|
2233
2259
|
: `call ${this.profile.toolPrefix}cancel_operator, then call ${this.profile.toolPrefix}prepare_operator with an approved replacement plan for the same goal and exact acceptance; start from git_lifecycle.committed_head, keep writes within git_lifecycle.approved_write_union, retain consumed budget, and do not call resume_operator or edit the committed candidate${carriedAction}`;
|
|
2234
2260
|
const awaitingAcceptanceNextAction = `assess the independent-review requirement and obtain review when required, copying review_dispatch_contract.required_prompt_lines into the reviewer Task prompt with real values; if review PASSes (or policy records an allowed skip), call ${this.profile.toolPrefix}complete_operator with this run_id and acceptance_fingerprint; if review has blocking findings wholly inside the existing exact acceptance, the union of git_lifecycle.approved_write_union and git_lifecycle.remediation_reserve, and remaining cumulative budget, do not complete or ask for user approval: call ${this.profile.toolPrefix}cancel_operator with reason review-blocking, then call ${this.profile.toolPrefix}prepare_operator for one same-goal replacement from git_lifecycle.committed_head targeting only those findings; copy this packet's acceptance array verbatim into the replacement plan without paraphrase, deletion, addition, or reordering, preserve accepted-criteria lineage, and run final canonical validation/review; if the replacement is refused for write scope, report replacement_constraints.blocked_write_paths to the user as the exact paths needing approval and stop, then after the user approves resend the same replacement with git_lifecycle.remediation_scope_expansion set to exactly those paths; if acceptance or budget must increase, stop for the user decision`;
|
|
2235
2261
|
const packet = { profile: state.profile, run_id: state.runID, root_session_id: state.rootSessionID, generation: state.generation,
|
|
2262
|
+
...(state.workQuantity ? { work_quantity: projectWorkQuantity(state.workQuantity, state.phase === "completed", state.phase === "cancelled") } : {}),
|
|
2236
2263
|
sequence: state.sequence, status: state.phase, repair_generation: state.repairGeneration,
|
|
2237
2264
|
acceptance: state.acceptance, acceptance_fingerprint: state.acceptanceFingerprint,
|
|
2238
2265
|
next_task_ref: current?.status === "pending" ? (state.units.length > 1
|
|
@@ -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") &&
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Bounded display observations from already delivered native events. No I/O or authority. */
|
|
2
|
+
import type { OperatorMission } from "./operator-mission.js";
|
|
3
|
+
import type { OperatorState } from "./operator-runtime.js";
|
|
4
|
+
import { type WorkQuantityEvent, type WorkQuantityState, type WorkQuantityProgress } from "./work-quantity.js";
|
|
5
|
+
export declare function rememberWorkMission(directory: string, mission: OperatorMission): void;
|
|
6
|
+
export declare function rememberWorkRun(directory: string, run: OperatorState): void;
|
|
7
|
+
export declare function rememberedWorkRun(directory: string, root: string): OperatorState | undefined;
|
|
8
|
+
export declare function workRootForActor(directory: string, actor: string): string | undefined;
|
|
9
|
+
/** Every event is folded at most once per saved projection; repeated edits are not completed work. */
|
|
10
|
+
export declare function observeActorWork(directory: string, actor: string, event: WorkQuantityEvent): string | undefined;
|
|
11
|
+
export declare function applyPendingWork(directory: string, root: string, state: WorkQuantityState): WorkQuantityState;
|
|
12
|
+
export declare function missionWorkState(directory: string, mission: OperatorMission, run?: OperatorState): WorkQuantityState;
|
|
13
|
+
export declare function cachedWorkProgress(directory: string, actor: string): WorkQuantityProgress | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { observeWorkQuantity, planWorkQuantity, projectWorkQuantity, syncWorkQuantity } from "./work-quantity.js";
|
|
2
|
+
const views = new Map();
|
|
3
|
+
const owners = new Map();
|
|
4
|
+
const identity = (directory, root) => `${directory.replaceAll("\\", "/").replace(/\/$/u, "")}\0${root}`;
|
|
5
|
+
function view(directory, root) {
|
|
6
|
+
const id = identity(directory, root);
|
|
7
|
+
let value = views.get(id);
|
|
8
|
+
if (!value) {
|
|
9
|
+
value = { sequence: 0, events: new Map() };
|
|
10
|
+
views.set(id, value);
|
|
11
|
+
}
|
|
12
|
+
owners.set(id, root);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function rememberWorkMission(directory, mission) {
|
|
16
|
+
const value = view(directory, mission.root);
|
|
17
|
+
if (value.mission?.id !== mission.id && value.mission?.workQuantity?.scopeID !== mission.workQuantity?.scopeID)
|
|
18
|
+
value.events.clear();
|
|
19
|
+
value.mission = mission;
|
|
20
|
+
value.sequence = Math.max(value.sequence, mission.workQuantity?.observationSequence ?? 0);
|
|
21
|
+
delete value.progress;
|
|
22
|
+
if (mission.coordinator)
|
|
23
|
+
owners.set(identity(directory, mission.coordinator), mission.root);
|
|
24
|
+
if (mission.review?.child)
|
|
25
|
+
owners.set(identity(directory, mission.review.child), mission.root);
|
|
26
|
+
for (const attempt of mission.attempts ?? [])
|
|
27
|
+
if (attempt.childSessionID)
|
|
28
|
+
owners.set(identity(directory, attempt.childSessionID), mission.root);
|
|
29
|
+
}
|
|
30
|
+
export function rememberWorkRun(directory, run) {
|
|
31
|
+
const value = view(directory, run.rootSessionID);
|
|
32
|
+
value.run = run;
|
|
33
|
+
value.sequence = Math.max(value.sequence, run.workQuantity?.observationSequence ?? 0);
|
|
34
|
+
delete value.progress;
|
|
35
|
+
if (run.operatorSessionID)
|
|
36
|
+
owners.set(identity(directory, run.operatorSessionID), run.rootSessionID);
|
|
37
|
+
for (const unit of run.units)
|
|
38
|
+
if (unit.childSessionID)
|
|
39
|
+
owners.set(identity(directory, unit.childSessionID), run.rootSessionID);
|
|
40
|
+
}
|
|
41
|
+
export function rememberedWorkRun(directory, root) {
|
|
42
|
+
return views.get(identity(directory, root))?.run;
|
|
43
|
+
}
|
|
44
|
+
export function workRootForActor(directory, actor) { return owners.get(identity(directory, actor)); }
|
|
45
|
+
/** Every event is folded at most once per saved projection; repeated edits are not completed work. */
|
|
46
|
+
export function observeActorWork(directory, actor, event) {
|
|
47
|
+
const root = owners.get(identity(directory, actor));
|
|
48
|
+
if (!root)
|
|
49
|
+
return undefined;
|
|
50
|
+
const value = view(directory, root);
|
|
51
|
+
if (value.mission && ["completed", "cancelled"].includes(value.mission.phase) || !value.mission &&
|
|
52
|
+
value.run && ["completed", "cancelled"].includes(value.run.phase))
|
|
53
|
+
return undefined;
|
|
54
|
+
const run = value.run;
|
|
55
|
+
const unit = run?.units.find(unit => unit.status === "running" && unit.childSessionID === actor);
|
|
56
|
+
const observed = { ...event, ...(unit ? { unit: `${run.runID}/${unit.unit.id}` } : {}) };
|
|
57
|
+
const id = JSON.stringify([event.kind, event.paths, event.command, event.commands, event.checkpoints, observed.unit]);
|
|
58
|
+
value.events.delete(id);
|
|
59
|
+
value.events.set(id, { sequence: ++value.sequence, event: observed });
|
|
60
|
+
delete value.progress;
|
|
61
|
+
if (value.events.size > 512)
|
|
62
|
+
value.events.delete(value.events.keys().next().value);
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
export function applyPendingWork(directory, root, state) {
|
|
66
|
+
const value = views.get(identity(directory, root));
|
|
67
|
+
if (!value)
|
|
68
|
+
return state;
|
|
69
|
+
for (const entry of value.events.values())
|
|
70
|
+
if (entry.sequence > (state.observationSequence ?? 0))
|
|
71
|
+
observeWorkQuantity(state, entry.event);
|
|
72
|
+
state.observationSequence = value.sequence;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
export function missionWorkState(directory, mission, run) {
|
|
76
|
+
const state = mission.workQuantity ?? planWorkQuantity(mission, run?.runID === mission.runID ? run.units.map(unit => unit.unit) : [], run, undefined, undefined, directory);
|
|
77
|
+
return syncWorkQuantity(applyPendingWork(directory, mission.root, structuredClone(state)), mission, run);
|
|
78
|
+
}
|
|
79
|
+
export function cachedWorkProgress(directory, actor) {
|
|
80
|
+
const root = owners.get(identity(directory, actor));
|
|
81
|
+
const value = root && views.get(identity(directory, root));
|
|
82
|
+
if (!value)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (value.progress)
|
|
85
|
+
return value.progress;
|
|
86
|
+
if (value.mission)
|
|
87
|
+
return value.progress = projectWorkQuantity(missionWorkState(directory, value.mission, value.run), value.mission.phase === "completed", value.mission.phase === "cancelled");
|
|
88
|
+
if (value.run?.workQuantity)
|
|
89
|
+
return value.progress = projectWorkQuantity(syncWorkQuantity(applyPendingWork(directory, root, structuredClone(value.run.workQuantity)), {}, value.run), value.run.phase === "completed", value.run.phase === "cancelled");
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|