sortie-dogs 0.9.3 → 0.9.5
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 +1 -1
- package/dist/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +5 -0
- package/dist/core/goal-bound.js +8 -1
- package/dist/core/goal-report.d.ts +18 -0
- package/dist/core/goal-report.js +21 -0
- package/dist/core/run-flight-ledger.d.ts +5 -0
- package/dist/core/run-flight-ledger.js +17 -1
- package/dist/plugin/fast-lane.d.ts +12 -0
- package/dist/plugin/fast-lane.js +26 -0
- package/dist/plugin/index.js +117 -55
- package/dist/plugin/run-metrics.d.ts +10 -2
- package/dist/plugin/run-metrics.js +61 -11
- package/dist/plugin/sortie-career.d.ts +55 -0
- package/dist/plugin/sortie-career.js +144 -0
- package/dist/plugin/sortie-debrief.d.ts +54 -0
- package/dist/plugin/sortie-debrief.js +251 -0
- package/dist/runtime-assets.d.ts +18 -12
- package/dist/runtime-assets.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
24
24
|
|
|
25
25
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
|
|
26
26
|
|
|
27
|
-
Release: [v0.9.
|
|
27
|
+
Release: [v0.9.5](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.5)
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
package/dist/asset-version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export declare const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
|
|
6
6
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
|
-
export const RUNTIME_ASSET_VERSION = "0.3.
|
|
5
|
+
export const RUNTIME_ASSET_VERSION = "0.3.81-mission-debrief-v1";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ValidationOutcome } from "./validation-budget.js";
|
|
2
|
+
import type { GoalReport } from "./goal-report.js";
|
|
2
3
|
export declare const GOAL_BOUND_SCHEMA_VERSION: "0.1";
|
|
3
4
|
export declare const GOAL_BOUND_METADATA_KEY: "sortie-dogs.goal-bound/v1";
|
|
4
5
|
export type GoalDeliveryMode = "planning-only" | "mvp-first" | "repair-first" | "controlled-change";
|
|
@@ -177,6 +178,10 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
177
178
|
readonly kind: "goal.terminal";
|
|
178
179
|
readonly goal_id: string;
|
|
179
180
|
readonly receipt: GoalTerminalReceipt;
|
|
181
|
+
}) | (GoalEventBase & {
|
|
182
|
+
readonly kind: "goal.reported";
|
|
183
|
+
readonly goal_id: string;
|
|
184
|
+
readonly report: GoalReport;
|
|
180
185
|
});
|
|
181
186
|
export interface GoalFlightEventRecord {
|
|
182
187
|
readonly sequence: number;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -111,6 +111,10 @@ export function reduceGoalFlight(records) {
|
|
|
111
111
|
requireState(record.sequence === index + 1 && record.previous_hash === previous && HASH.test(record.event_hash), "invalid", "Goal ledger chain is malformed.");
|
|
112
112
|
previous = record.event_hash;
|
|
113
113
|
const event = record.event;
|
|
114
|
+
// Optional presentation records never participate in execution replay. Writers validate them;
|
|
115
|
+
// older/newer telemetry definitions must not prevent an existing goal from resuming.
|
|
116
|
+
if (event.kind === "goal.reported")
|
|
117
|
+
continue;
|
|
114
118
|
requireState(instant(event.at), "invalid", "Goal event timestamp is invalid.");
|
|
115
119
|
if (event.kind === "goal.accepted") {
|
|
116
120
|
requireState(state.goal_id === null || state.phase === "terminal", "transition", "An active goal already owns this root.");
|
|
@@ -134,13 +138,16 @@ export function reduceGoalFlight(records) {
|
|
|
134
138
|
else if (event.kind === "goal.revised") {
|
|
135
139
|
requireState(state.phase !== "terminal" && event.revision === state.revision + 1 && event.scope_epoch === state.scope_epoch + 1 &&
|
|
136
140
|
HASH.test(event.acceptance_fingerprint) && event.budget.max_units >= state.consumed_units &&
|
|
141
|
+
event.budget.max_units >= state.validation_budget.consumed &&
|
|
137
142
|
validAcceptanceContract(event.acceptance_contract), "transition", "Scope revision is stale or resets consumed budget.");
|
|
138
143
|
state = { ...state, revision: event.revision, scope_epoch: event.scope_epoch,
|
|
139
144
|
acceptance_fingerprint: event.acceptance_fingerprint, latest_user_message_id: event.origin_user_message_id,
|
|
140
145
|
selected_agent: event.selected_agent, delivery: event.delivery, budget: event.budget,
|
|
146
|
+
validation_budget: { ...state.validation_budget,
|
|
147
|
+
limit: state.validation_budget.limit === null ? null : event.budget.max_units },
|
|
141
148
|
acceptance_contract: event.acceptance_contract,
|
|
142
149
|
session_ids: addUnique(state.session_ids, event.session_id), phase: "active", stop_reason: null,
|
|
143
|
-
tickets: [] };
|
|
150
|
+
receipt: null, tickets: [] };
|
|
144
151
|
}
|
|
145
152
|
else if (event.kind === "ticket.issued") {
|
|
146
153
|
requireState(state.phase === "active" && event.revision === state.revision && event.scope_epoch === state.scope_epoch &&
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Optional, bounded telemetry retained with the existing terminal goal ledger, never an authority input. */
|
|
2
|
+
export interface GoalReport {
|
|
3
|
+
readonly definition: "pre-terminal-host-tokens/v1";
|
|
4
|
+
readonly terminal_key: string;
|
|
5
|
+
readonly tokens: number | null;
|
|
6
|
+
readonly models: readonly {
|
|
7
|
+
readonly model: string;
|
|
8
|
+
readonly tokens: number;
|
|
9
|
+
}[] | null;
|
|
10
|
+
readonly first_pass_eligible: boolean;
|
|
11
|
+
readonly traits: readonly ("pack-tactics" | "recovery" | "clean-sweep")[];
|
|
12
|
+
readonly overlap?: {
|
|
13
|
+
readonly definition: "worker-span-union/v1";
|
|
14
|
+
readonly worker_ms: number;
|
|
15
|
+
readonly wall_ms: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function validGoalReport(value: unknown): value is GoalReport;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function validGoalReport(value) {
|
|
2
|
+
if (value === null || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const item = value;
|
|
5
|
+
const amount = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
6
|
+
const duration = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
7
|
+
const overlap = item.overlap;
|
|
8
|
+
return Object.keys(item).every((key) => ["definition", "terminal_key", "tokens", "models", "first_pass_eligible", "traits", "overlap"].includes(key)) &&
|
|
9
|
+
item.definition === "pre-terminal-host-tokens/v1" && /^sha256:[a-f0-9]{64}$/u.test(item.terminal_key) &&
|
|
10
|
+
(item.tokens === null || amount(item.tokens)) && typeof item.first_pass_eligible === "boolean" &&
|
|
11
|
+
Array.isArray(item.traits) && item.traits.length <= 3 && new Set(item.traits).size === item.traits.length &&
|
|
12
|
+
item.traits.every((trait) => ["pack-tactics", "recovery", "clean-sweep"].includes(trait)) &&
|
|
13
|
+
(overlap === undefined || overlap !== null && typeof overlap === "object" &&
|
|
14
|
+
Object.keys(overlap).every((key) => ["definition", "worker_ms", "wall_ms"].includes(key)) && overlap.definition === "worker-span-union/v1" &&
|
|
15
|
+
duration(overlap.worker_ms) && duration(overlap.wall_ms) && overlap.worker_ms >= overlap.wall_ms) &&
|
|
16
|
+
(item.models === null || Array.isArray(item.models) && item.models.length <= 128 &&
|
|
17
|
+
new Set(item.models.map((entry) => entry?.model)).size === item.models.length &&
|
|
18
|
+
item.models.every((entry) => entry !== null && typeof entry === "object" && Object.keys(entry).every((key) => key === "model" || key === "tokens") &&
|
|
19
|
+
typeof entry.model === "string" && entry.model.length > 0 && entry.model.length <= 512 && amount(entry.tokens)) &&
|
|
20
|
+
item.tokens !== null && item.models.reduce((sum, entry) => sum + entry.tokens, 0) === item.tokens);
|
|
21
|
+
}
|
|
@@ -366,6 +366,11 @@ export declare class RunFlightLedger {
|
|
|
366
366
|
static open(filePath: string, evidence: RunFlightEvidenceAccess): Promise<RunFlightLedger>;
|
|
367
367
|
/** Root goal checkpoints share this owner and lock/write discipline without inventing a Git run. */
|
|
368
368
|
static openGoal(filePath: string): Promise<RunFlightLedger>;
|
|
369
|
+
/** Read-only Career inventory: validates one existing goal ledger without opening another store. */
|
|
370
|
+
static readGoalFile(filePath: string): Promise<{
|
|
371
|
+
readonly records: readonly GoalFlightEventRecord[];
|
|
372
|
+
readonly state: GoalFlightState;
|
|
373
|
+
}>;
|
|
369
374
|
read(): Promise<{
|
|
370
375
|
readonly records: readonly RunFlightEventRecord[];
|
|
371
376
|
readonly state: RunFlightState;
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { LUNA_FABRIC_MAX_ACTIVE } from "./luna-fabric-scheduler.js";
|
|
5
5
|
import { normalizeRelativePath } from "./path.js";
|
|
6
6
|
import { CHILD_TERMINAL_EVIDENCE_FIELDS, isChildTerminalIdentity, reconcileChildTerminal, sameChildTerminalIdentity } from "./child-terminal-reconciliation.js";
|
|
7
|
-
import { GOAL_BOUND_SCHEMA_VERSION, GoalBoundError, reduceGoalFlight } from "./goal-bound.js";
|
|
7
|
+
import { GOAL_BOUND_SCHEMA_VERSION, GoalBoundError, reduceGoalFlight, goalFingerprint } from "./goal-bound.js";
|
|
8
8
|
export const RUN_FLIGHT_LEDGER_SCHEMA_VERSION = "0.1";
|
|
9
9
|
export const MAX_RUN_FLIGHT_EVENTS = 2048;
|
|
10
10
|
export const MAX_RUN_FLIGHT_LEDGER_BYTES = 1024 * 1024;
|
|
@@ -657,6 +657,10 @@ export class RunFlightLedger {
|
|
|
657
657
|
await ledger.readGoal();
|
|
658
658
|
return ledger;
|
|
659
659
|
}
|
|
660
|
+
/** Read-only Career inventory: validates one existing goal ledger without opening another store. */
|
|
661
|
+
static async readGoalFile(filePath) {
|
|
662
|
+
return new RunFlightLedger(filePath, undefined, true).readGoal();
|
|
663
|
+
}
|
|
660
664
|
async read() {
|
|
661
665
|
if (this.#goalMode)
|
|
662
666
|
throw new RunFlightLedgerError("invalid", "Goal ledger requires readGoal().");
|
|
@@ -820,6 +824,18 @@ export class RunFlightLedger {
|
|
|
820
824
|
throw new RunFlightLedgerError("conflict", "Ledger lock remained busy.");
|
|
821
825
|
try {
|
|
822
826
|
const records = await this.#readGoalRecords();
|
|
827
|
+
if (event.kind === "goal.reported" && records.some(({ event: stored }) => stored.kind === "goal.reported" &&
|
|
828
|
+
stored.goal_id === event.goal_id && stored.report?.terminal_key === event.report.terminal_key)) {
|
|
829
|
+
return reduceGoalFlight(records);
|
|
830
|
+
}
|
|
831
|
+
if (event.kind === "goal.reported") {
|
|
832
|
+
const { validGoalReport } = await import("./goal-report.js");
|
|
833
|
+
const state = reduceGoalFlight(records);
|
|
834
|
+
if (state.receipt === null || event.goal_id !== state.goal_id || !validGoalReport(event.report) ||
|
|
835
|
+
event.report.terminal_key !== goalFingerprint(state.receipt)) {
|
|
836
|
+
throw new RunFlightLedgerError("invalid", "Report is not bound to the current terminal receipt.");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
823
839
|
const sequence = records.length + 1;
|
|
824
840
|
const previousHash = records.at(-1)?.event_hash ?? null;
|
|
825
841
|
const record = { sequence, previous_hash: previousHash,
|
|
@@ -4,6 +4,15 @@ export declare class FastLaneDeniedError extends Error {
|
|
|
4
4
|
readonly code: FastLaneDenialCode;
|
|
5
5
|
constructor(code: FastLaneDenialCode);
|
|
6
6
|
}
|
|
7
|
+
export interface WorkerAccounting {
|
|
8
|
+
readonly totalWorkerDispatches: number;
|
|
9
|
+
readonly workerDispatches: number;
|
|
10
|
+
readonly workerInFlight: boolean;
|
|
11
|
+
readonly workerResumeUsed: boolean;
|
|
12
|
+
readonly workerResumeSessionID?: string;
|
|
13
|
+
readonly workerResumeTaskID?: string;
|
|
14
|
+
readonly workerTaskID?: string;
|
|
15
|
+
}
|
|
7
16
|
export interface FastLaneToolOptions {
|
|
8
17
|
readonly readonlyDiagnosisAuthorized?: boolean;
|
|
9
18
|
readonly consultationFallbackAuthorized?: boolean;
|
|
@@ -25,5 +34,8 @@ export declare class FastLaneController {
|
|
|
25
34
|
continuationQueued(sessionID: string): void;
|
|
26
35
|
authorizeRecoverableWorkerResume(sessionID: string, taskID: string, childSessionID: string): boolean;
|
|
27
36
|
workerCompleted(sessionID: string): void;
|
|
37
|
+
/** Snapshot worker accounting so a later denial in the same dispatch cannot leak the slot. */
|
|
38
|
+
snapshotWorkerAccounting(sessionID: string): WorkerAccounting | undefined;
|
|
39
|
+
restoreWorkerAccounting(sessionID: string, snapshot: WorkerAccounting | undefined): void;
|
|
28
40
|
beforeTool(sessionID: string, tool: string, args: unknown, options?: FastLaneToolOptions): string | undefined;
|
|
29
41
|
}
|
package/dist/plugin/fast-lane.js
CHANGED
|
@@ -199,6 +199,32 @@ export class FastLaneController {
|
|
|
199
199
|
if (state !== undefined)
|
|
200
200
|
state.workerInFlight = false;
|
|
201
201
|
}
|
|
202
|
+
/** Snapshot worker accounting so a later denial in the same dispatch cannot leak the slot. */
|
|
203
|
+
snapshotWorkerAccounting(sessionID) {
|
|
204
|
+
const state = this.sessions.get(sessionID);
|
|
205
|
+
if (state === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
return { totalWorkerDispatches: state.totalWorkerDispatches, workerDispatches: state.workerDispatches,
|
|
208
|
+
workerInFlight: state.workerInFlight, workerResumeUsed: state.workerResumeUsed,
|
|
209
|
+
workerResumeSessionID: state.workerResumeSessionID, workerResumeTaskID: state.workerResumeTaskID,
|
|
210
|
+
workerTaskID: state.workerTaskID };
|
|
211
|
+
}
|
|
212
|
+
restoreWorkerAccounting(sessionID, snapshot) {
|
|
213
|
+
const state = this.sessions.get(sessionID);
|
|
214
|
+
if (state === undefined || snapshot === undefined)
|
|
215
|
+
return;
|
|
216
|
+
state.totalWorkerDispatches = snapshot.totalWorkerDispatches;
|
|
217
|
+
state.workerDispatches = snapshot.workerDispatches;
|
|
218
|
+
state.workerInFlight = snapshot.workerInFlight;
|
|
219
|
+
state.workerResumeUsed = snapshot.workerResumeUsed;
|
|
220
|
+
for (const [key, value] of Object.entries({ workerResumeSessionID: snapshot.workerResumeSessionID,
|
|
221
|
+
workerResumeTaskID: snapshot.workerResumeTaskID, workerTaskID: snapshot.workerTaskID })) {
|
|
222
|
+
if (value === undefined)
|
|
223
|
+
delete state[key];
|
|
224
|
+
else
|
|
225
|
+
state[key] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
202
228
|
beforeTool(sessionID, tool, args, options = {}) {
|
|
203
229
|
const state = this.sessions.get(sessionID);
|
|
204
230
|
if (state === undefined) {
|
package/dist/plugin/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import { TerminalRescueRuntime } from "../core/terminal-rescue-runtime.js";
|
|
|
45
45
|
import { OpenCodeTerminalRescueHost } from "./terminal-rescue-host.js";
|
|
46
46
|
import { AdaptiveRemediationRuntime, AdaptiveRunFlightLineage } from "../core/adaptive-remediation-runtime.js";
|
|
47
47
|
import { DEFAULT_ADAPTIVE_REMEDIATION_MODEL, GitAdaptiveRemediationHost, OpenCodeAdaptiveRemediationProvider } from "./adaptive-remediation-host.js";
|
|
48
|
-
import { collectRunMetrics, createSortieResult, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
48
|
+
import { collectRunMetrics, createSortieResult, createGoalReport, insertRunMetrics, insertSortieResult, replaceDoneTerminalStatus, replaceTerminalStatus, sanitizeTerminalReport, terminalRunOutcome } from "./run-metrics.js";
|
|
49
49
|
const INPUT_LIMITS = { config: 64 * 1024, manifest: 512 * 1024, handoff: 2 * 1024 * 1024, parallel: 512 * 1024 };
|
|
50
50
|
const INSPECTION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
51
51
|
const ACTIVE_SESSION_CACHE = { maximum: 256, ttlMilliseconds: 30 * 60 * 1000 };
|
|
@@ -1127,15 +1127,18 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1127
1127
|
const parallelAcceptanceContinuity = new Map();
|
|
1128
1128
|
const goalRootSessions = new Map();
|
|
1129
1129
|
const goalLedgers = new Map();
|
|
1130
|
+
const goalLedgerFiles = new Map();
|
|
1131
|
+
const goalLedgerDirectories = new Set();
|
|
1130
1132
|
const goalReservations = new Map();
|
|
1131
1133
|
const hostGoalExecutions = new Map();
|
|
1134
|
+
const goalValidationDefects = new Set();
|
|
1132
1135
|
const goalDeclarationAuthority = new Map();
|
|
1133
1136
|
const pendingRealGoalTurns = new Map();
|
|
1134
1137
|
const pendingGoalRecoveries = new Map();
|
|
1135
1138
|
const scheduledGoalRecoveries = new Map();
|
|
1136
1139
|
const globalConfig = await readOptionalGlobalConfig();
|
|
1137
1140
|
const sessionOperationMetrics = new Map();
|
|
1138
|
-
function appLogInfo(message, sessionID, extra) {
|
|
1141
|
+
function appLogInfo(message, sessionID, extra, level = "info") {
|
|
1139
1142
|
const app = input.client?.app;
|
|
1140
1143
|
const log = app?.log;
|
|
1141
1144
|
if (log === undefined)
|
|
@@ -1144,7 +1147,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1144
1147
|
const result = log.call(app, {
|
|
1145
1148
|
body: {
|
|
1146
1149
|
service: "sortie-dogs",
|
|
1147
|
-
level
|
|
1150
|
+
level,
|
|
1148
1151
|
message,
|
|
1149
1152
|
extra: { sessionID: sessionID.slice(0, 128), ...extra },
|
|
1150
1153
|
},
|
|
@@ -1242,15 +1245,22 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1242
1245
|
const key = createHash("sha256").update(root).digest("hex");
|
|
1243
1246
|
const projectRoot = resolve(input.worktree ?? input.directory);
|
|
1244
1247
|
const legacyPath = join(projectRoot, ".sortie-dogs", "run-flight", `${key}.json`);
|
|
1248
|
+
goalLedgerDirectories.add(dirname(legacyPath));
|
|
1245
1249
|
const opened = (async () => {
|
|
1250
|
+
const legacyExists = await stat(legacyPath).then((value) => value.isFile()).catch(() => false);
|
|
1251
|
+
const leaseRoot = legacyExists ? await durableScopeRoot(projectRoot).catch(() => undefined) : await durableScopeRoot(projectRoot);
|
|
1252
|
+
if (leaseRoot !== undefined)
|
|
1253
|
+
goalLedgerDirectories.add(join(dirname(leaseRoot), "run-flight"));
|
|
1246
1254
|
// Existing roots remain on their original owner so an in-flight pre-upgrade goal is not forked.
|
|
1247
|
-
if (
|
|
1255
|
+
if (legacyExists) {
|
|
1256
|
+
goalLedgerFiles.set(root, legacyPath);
|
|
1248
1257
|
return await RunFlightLedger.openGoal(legacyPath);
|
|
1249
1258
|
}
|
|
1250
|
-
const leaseRoot = await durableScopeRoot(projectRoot);
|
|
1251
1259
|
const filePath = leaseRoot === undefined
|
|
1252
1260
|
? legacyPath
|
|
1253
1261
|
: join(dirname(leaseRoot), "run-flight", `${key}.json`);
|
|
1262
|
+
goalLedgerFiles.set(root, filePath);
|
|
1263
|
+
goalLedgerDirectories.add(dirname(filePath));
|
|
1254
1264
|
return await RunFlightLedger.openGoal(filePath);
|
|
1255
1265
|
})();
|
|
1256
1266
|
goalLedgers.set(root, opened);
|
|
@@ -1362,8 +1372,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1362
1372
|
const unitID = authorization.taskID;
|
|
1363
1373
|
const unitBound = unitID !== undefined && goal.outstanding_reservations.some((reservation) => reservation.unit_id === unitID && reservation.session_id === identity.parentID);
|
|
1364
1374
|
const criteria = goal.acceptance_contract?.criteria.filter((criterion) => criterion.validation_command === rawCommand && criterion.expected_outcome === "pass") ?? [];
|
|
1375
|
+
const denyValidation = (reason) => {
|
|
1376
|
+
goalValidationDefects.add(toolInput.sessionID);
|
|
1377
|
+
return new Error(`SORTIE_VALIDATION_BUDGET_DENIED: ${reason}`);
|
|
1378
|
+
};
|
|
1365
1379
|
if (!unitBound)
|
|
1366
|
-
throw
|
|
1380
|
+
throw denyValidation("requirement-unbound");
|
|
1367
1381
|
// Exact generation and formatting checks may support acceptance without proving a criterion.
|
|
1368
1382
|
if (criteria.length === 0)
|
|
1369
1383
|
return;
|
|
@@ -1377,10 +1391,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1377
1391
|
reservation = await ledger.reserveValidation(request, goal.budget?.max_units ?? 1);
|
|
1378
1392
|
}
|
|
1379
1393
|
catch (error) {
|
|
1380
|
-
throw
|
|
1394
|
+
throw denyValidation(`authority-unavailable:${error instanceof Error ? error.name : "unknown"}`);
|
|
1381
1395
|
}
|
|
1382
1396
|
if (reservation.decision !== "ALLOW" || reservation.reservation_id === null) {
|
|
1383
|
-
throw
|
|
1397
|
+
throw denyValidation(reservation.reason);
|
|
1384
1398
|
}
|
|
1385
1399
|
validation = { ledger, request, reservation: reservation.reservation_id };
|
|
1386
1400
|
}
|
|
@@ -1615,11 +1629,12 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1615
1629
|
: "external_dependency";
|
|
1616
1630
|
receipt = await terminalGoal(sessionID, reason, "stopped").catch(() => undefined);
|
|
1617
1631
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1632
|
+
const snapshot = receipt === undefined ? undefined : await goalLedger(sessionID).then((ledger) => ledger.readGoal()).catch(() => undefined);
|
|
1633
|
+
if (snapshot !== undefined)
|
|
1634
|
+
goal = snapshot.state;
|
|
1620
1635
|
if (receipt !== undefined)
|
|
1621
1636
|
rootAcceptanceContinuity.delete(sessionID);
|
|
1622
|
-
return { outcome, goal, receipt, delivery };
|
|
1637
|
+
return { outcome, goal, receipt, delivery, records: snapshot?.records };
|
|
1623
1638
|
}
|
|
1624
1639
|
function goalDeclarationContract(prompt) {
|
|
1625
1640
|
const lines = prompt.split(/\r?\n/u);
|
|
@@ -1768,27 +1783,29 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1768
1783
|
throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: validated.defects });
|
|
1769
1784
|
}
|
|
1770
1785
|
const declaration = validated.declaration;
|
|
1786
|
+
const units = Number(handoffValue(entries, ["goal_budget_units"]));
|
|
1787
|
+
const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
|
|
1788
|
+
? units : state.budget?.max_units ?? 32;
|
|
1789
|
+
const declaredTime = Number(handoffValue(entries, ["goal_budget_time_ms"]));
|
|
1790
|
+
const declaredCost = Number(handoffValue(entries, ["goal_budget_cost_usd"]));
|
|
1791
|
+
const timeBudget = Number.isFinite(declaredTime) && declaredTime > 0 ? declaredTime : state.budget?.time_ms ?? null;
|
|
1792
|
+
const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
|
|
1793
|
+
const budgetChanged = maxUnits !== state.budget?.max_units || timeBudget !== state.budget?.time_ms ||
|
|
1794
|
+
costBudget !== state.budget?.cost_usd;
|
|
1771
1795
|
const authority = goalDeclarationAuthority.get(sessionID);
|
|
1772
1796
|
if (authority === undefined || authority !== state.latest_user_message_id) {
|
|
1773
|
-
if (declaration.fingerprint !== state.acceptance_fingerprint) {
|
|
1797
|
+
if (declaration.fingerprint !== state.acceptance_fingerprint || budgetChanged) {
|
|
1774
1798
|
throw new HandoffDeniedError("contract-invalid", "<goal-declaration>", { defects: [
|
|
1775
1799
|
contractDefect("contract", "/goal_acceptance_fingerprint", "goal_revision_unauthorized"),
|
|
1776
1800
|
] });
|
|
1777
1801
|
}
|
|
1778
1802
|
return state;
|
|
1779
1803
|
}
|
|
1780
|
-
if (declaration.fingerprint === state.acceptance_fingerprint &&
|
|
1804
|
+
if (!budgetChanged && declaration.fingerprint === state.acceptance_fingerprint &&
|
|
1781
1805
|
goalFingerprint(declaration.contract) === goalFingerprint(state.acceptance_contract)) {
|
|
1782
1806
|
goalDeclarationAuthority.delete(sessionID);
|
|
1783
1807
|
return state;
|
|
1784
1808
|
}
|
|
1785
|
-
const units = Number(handoffValue(entries, ["goal_budget_units"]));
|
|
1786
|
-
const maxUnits = Number.isSafeInteger(units) && units >= state.consumed_units && units > 0
|
|
1787
|
-
? units : state.budget?.max_units ?? 32;
|
|
1788
|
-
const declaredTime = Number(handoffValue(entries, ["goal_budget_time_ms"]));
|
|
1789
|
-
const declaredCost = Number(handoffValue(entries, ["goal_budget_cost_usd"]));
|
|
1790
|
-
const timeBudget = Number.isFinite(declaredTime) && declaredTime > 0 ? declaredTime : state.budget?.time_ms ?? null;
|
|
1791
|
-
const costBudget = Number.isFinite(declaredCost) && declaredCost > 0 ? declaredCost : state.budget?.cost_usd ?? null;
|
|
1792
1809
|
state = await ledger.appendGoal({ kind: "goal.revised", at: new Date().toISOString(), goal_id: state.goal_id,
|
|
1793
1810
|
revision: state.revision + 1, scope_epoch: state.scope_epoch + 1,
|
|
1794
1811
|
acceptance_fingerprint: declaration.fingerprint, origin_user_message_id: state.latest_user_message_id,
|
|
@@ -1904,7 +1921,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1904
1921
|
output.status === "cancel" || output.status === "cancelled";
|
|
1905
1922
|
const hostBindingDefect = childSessionID !== undefined && [...(bindingDenials.get(reservation.root)?.values() ?? [])]
|
|
1906
1923
|
.some((candidateDenials) => [...candidateDenials.values()].includes(childSessionID));
|
|
1907
|
-
const
|
|
1924
|
+
const failedAcceptanceExecution = [...hostGoalExecutions.values()].some((execution) => execution.root === reservation.root && execution.sessionID === childSessionID &&
|
|
1925
|
+
execution.endedAt !== undefined && Date.parse(execution.startedAt) >= reservation.started - 1000 &&
|
|
1926
|
+
execution.outcome === "fail");
|
|
1927
|
+
const processDefect = !failedAcceptanceExecution && (childSessionID === undefined || hostBindingDefect ||
|
|
1928
|
+
goalValidationDefects.has(childSessionID));
|
|
1908
1929
|
const resultClass = progress ? "acceptance" : interrupted ? "interrupted" : processDefect ? "process-defect" : "acceptance";
|
|
1909
1930
|
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(),
|
|
1910
1931
|
reservation_id: reservation.reservationID, receipt_id: goalFingerprint({ call_id: callID, output: outputText.slice(0, 2048) }),
|
|
@@ -1913,6 +1934,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1913
1934
|
progress_fingerprint: progress ? goalFingerprint(acceptedEvidence) : null,
|
|
1914
1935
|
evidence: acceptedEvidence, elapsed_ms: Math.max(0, Date.now() - reservation.started), cost_usd: null });
|
|
1915
1936
|
if (childSessionID !== undefined) {
|
|
1937
|
+
goalValidationDefects.delete(childSessionID);
|
|
1916
1938
|
for (const [executionCallID, execution] of hostGoalExecutions) {
|
|
1917
1939
|
if (execution.root === reservation.root && execution.sessionID === childSessionID)
|
|
1918
1940
|
hostGoalExecutions.delete(executionCallID);
|
|
@@ -4118,7 +4140,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4118
4140
|
const remedies = {
|
|
4119
4141
|
"session-inactive": {
|
|
4120
4142
|
recoverable: true,
|
|
4121
|
-
remedy: "Freshly redispatch
|
|
4143
|
+
remedy: "Freshly redispatch through dog-coordinator's admitted dog-worker Task, with prompt text containing role, project_root, source_manifest or operation_manifest, and acceptance or validation fields. A standalone build/fixer Task is not a registered Sortie worker; copying these fields, a bare resume, or a file read cannot activate it. Preserve explicit agent selection and resolve any existing goal stop before using the Sortie workflow; do not repeat the same standalone redispatch or reset its ledger to bypass a stop.",
|
|
4122
4144
|
},
|
|
4123
4145
|
"session-expired": {
|
|
4124
4146
|
recoverable: true,
|
|
@@ -4659,6 +4681,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4659
4681
|
clearSessionLinks(sessionID);
|
|
4660
4682
|
}
|
|
4661
4683
|
function evictSession(sessionID) {
|
|
4684
|
+
goalValidationDefects.delete(sessionID);
|
|
4662
4685
|
activeSessions.delete(sessionID);
|
|
4663
4686
|
sessionOperationMetrics.delete(sessionID);
|
|
4664
4687
|
rootAcceptanceContinuity.delete(sessionID);
|
|
@@ -5354,13 +5377,33 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5354
5377
|
startedAt: terminal.receipt.started_at,
|
|
5355
5378
|
endedAt: terminal.receipt.ended_at,
|
|
5356
5379
|
}).catch(() => undefined));
|
|
5357
|
-
|
|
5380
|
+
let sortieResult = terminal?.receipt === undefined || terminal.goal === undefined
|
|
5358
5381
|
? undefined
|
|
5359
|
-
: createSortieResult(terminal.receipt, terminal.goal, metrics, new Date().toISOString());
|
|
5382
|
+
: createSortieResult(terminal.receipt, terminal.goal, metrics, new Date().toISOString(), terminal.records);
|
|
5383
|
+
if (sortieResult !== undefined && terminal?.receipt !== undefined && terminal.records !== undefined) {
|
|
5384
|
+
try {
|
|
5385
|
+
const ledger = await goalLedger(textInput.sessionID);
|
|
5386
|
+
const report = createGoalReport(sortieResult, terminal.receipt);
|
|
5387
|
+
let records = terminal.records;
|
|
5388
|
+
if (!records.some(({ event }) => event.kind === "goal.reported" && event.report?.terminal_key === report.terminal_key)) {
|
|
5389
|
+
await ledger.appendGoal({ kind: "goal.reported", at: new Date().toISOString(), goal_id: terminal.receipt.goal_id, report });
|
|
5390
|
+
records = (await ledger.readGoal()).records;
|
|
5391
|
+
}
|
|
5392
|
+
const currentPath = goalLedgerFiles.get(goalRoot(textInput.sessionID));
|
|
5393
|
+
if (currentPath !== undefined) {
|
|
5394
|
+
const { collectCareer } = await import("./sortie-career.js");
|
|
5395
|
+
sortieResult = { ...sortieResult, career: await collectCareer([...goalLedgerDirectories], currentPath, records, (path) => RunFlightLedger.readGoalFile(path)) };
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
catch {
|
|
5399
|
+
appLogInfo("run-metrics.career-unavailable", textInput.sessionID, { outcome: runOutcome }, "warn");
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5360
5402
|
if (sortieResult !== undefined)
|
|
5361
5403
|
textOutput.text = insertSortieResult(textOutput.text, sortieResult);
|
|
5362
5404
|
else if (metrics !== undefined && runOutcome === "DONE")
|
|
5363
5405
|
textOutput.text = insertRunMetrics(textOutput.text, metrics);
|
|
5406
|
+
const { debrief: _debriefObservation, ...metricSummary } = metrics ?? {};
|
|
5364
5407
|
appLogInfo("run-metrics.snapshot", textInput.sessionID, {
|
|
5365
5408
|
available: metrics !== undefined,
|
|
5366
5409
|
outcome: runOutcome,
|
|
@@ -5371,7 +5414,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5371
5414
|
resultProof: sortieResult.proof.overall,
|
|
5372
5415
|
accountingPhase: sortieResult.accounting_phase,
|
|
5373
5416
|
}),
|
|
5374
|
-
...
|
|
5417
|
+
...metricSummary,
|
|
5375
5418
|
...operationMetricsSnapshot(textInput.sessionID),
|
|
5376
5419
|
});
|
|
5377
5420
|
}
|
|
@@ -5705,6 +5748,16 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5705
5748
|
*/
|
|
5706
5749
|
"tool.execute.after": async (toolInput, output) => {
|
|
5707
5750
|
await recordHostGoalEnd(toolInput, output);
|
|
5751
|
+
// A completed host question is a new user-interaction boundary, just like chat input.
|
|
5752
|
+
// It authorizes one subsequent typed declaration; it does not itself grant budget or clear a stop.
|
|
5753
|
+
if (toolInput.tool === "question" && toolInput.sessionID !== undefined &&
|
|
5754
|
+
isCoordinatorSession(toolInput.sessionID) && typeof output.output === "string" &&
|
|
5755
|
+
output.output.trim().length > 0 && output.status !== "error" && output.status !== "cancelled") {
|
|
5756
|
+
const goal = await currentGoal(toolInput.sessionID);
|
|
5757
|
+
if (goal.phase !== "terminal" && goal.latest_user_message_id !== null) {
|
|
5758
|
+
goalDeclarationAuthority.set(toolInput.sessionID, goal.latest_user_message_id);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5708
5761
|
diagnosisChildren.get(toolInput.sessionID ?? "")?.tools.delete(toolInput.callID ?? "");
|
|
5709
5762
|
const diagnosisCandidate = toolInput.tool === "task" ? diagnosisCalls.get(toolInput.callID ?? "") : undefined;
|
|
5710
5763
|
const diagnosis = diagnosisCandidate?.context.ownerRoot === toolInput.sessionID ? diagnosisCandidate : undefined;
|
|
@@ -6231,45 +6284,54 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6231
6284
|
// therefore be repaired by a corrected Task call in this same coordinator turn.
|
|
6232
6285
|
await bindGoalDeclaration(toolInput.sessionID, typeof output.args.prompt === "string" ? output.args.prompt : "");
|
|
6233
6286
|
}
|
|
6287
|
+
// Accounting is provisional until this dispatch is fully admitted. A later denial such as a
|
|
6288
|
+
// budget stop must release the serial slot, or no worker could be dispatched or resumed again.
|
|
6289
|
+
const workerAccounting = fastLane.snapshotWorkerAccounting(toolInput.sessionID);
|
|
6234
6290
|
const resumedWorkerSessionID = fastLane.beforeTool(toolInput.sessionID, toolInput.tool, output.args, {
|
|
6235
6291
|
readonlyDiagnosisAuthorized: readonlyDiagnosis,
|
|
6236
6292
|
consultationFallbackAuthorized,
|
|
6237
6293
|
parallelWorkerAlreadyBound,
|
|
6238
6294
|
parallelWorkerAuthorized,
|
|
6239
6295
|
});
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
|
|
6245
|
-
rootAcceptanceContinuity.delete(toolInput.sessionID);
|
|
6246
|
-
rootAcceptanceContinuity.set(toolInput.sessionID, validatedRootAcceptance);
|
|
6247
|
-
while (rootAcceptanceContinuity.size > ACTIVE_SESSION_CACHE.maximum) {
|
|
6248
|
-
rootAcceptanceContinuity.delete(rootAcceptanceContinuity.keys().next().value);
|
|
6296
|
+
try {
|
|
6297
|
+
if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole) &&
|
|
6298
|
+
isRecord(output.args)) {
|
|
6299
|
+
await reserveGoalDispatch(toolInput.sessionID, toolInput.callID, typeof output.args.prompt === "string" ? output.args.prompt : "");
|
|
6249
6300
|
}
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6301
|
+
if (validatedRootAcceptance !== undefined && reservedParallelDescriptor === undefined) {
|
|
6302
|
+
rootAcceptanceContinuity.delete(toolInput.sessionID);
|
|
6303
|
+
rootAcceptanceContinuity.set(toolInput.sessionID, validatedRootAcceptance);
|
|
6304
|
+
while (rootAcceptanceContinuity.size > ACTIVE_SESSION_CACHE.maximum) {
|
|
6305
|
+
rootAcceptanceContinuity.delete(rootAcceptanceContinuity.keys().next().value);
|
|
6306
|
+
}
|
|
6256
6307
|
}
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
bootstrapIdleWarnings.delete(toolInput.sessionID);
|
|
6265
|
-
beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
6266
|
-
if (reservedParallelDescriptor !== undefined) {
|
|
6267
|
-
parallelCalls.set(toolInput.callID, {
|
|
6268
|
-
ownerRoot: toolInput.sessionID,
|
|
6269
|
-
descriptor: reservedParallelDescriptor,
|
|
6270
|
-
completionCallID: toolInput.callID,
|
|
6271
|
-
});
|
|
6308
|
+
if (resumedWorkerSessionID !== undefined) {
|
|
6309
|
+
const recoverableParallel = parallelRecoverableChildren.get(resumedWorkerSessionID);
|
|
6310
|
+
if (recoverableParallel !== undefined) {
|
|
6311
|
+
parallelCalls.set(toolInput.callID, recoverableParallel);
|
|
6312
|
+
parallelRecoverableChildren.delete(resumedWorkerSessionID);
|
|
6313
|
+
}
|
|
6314
|
+
recoverableWorkerChildren.delete(resumedWorkerSessionID);
|
|
6272
6315
|
}
|
|
6316
|
+
if (toolInput.tool === "task" && taskRole !== undefined && IMPLEMENTATION_AGENTS.has(taskRole)) {
|
|
6317
|
+
if (readonlyDiagnosis && isRecord(output.args))
|
|
6318
|
+
await claimDiagnosisTask(toolInput.sessionID, toolInput.callID, output.args, true);
|
|
6319
|
+
bootstrapRequired = false;
|
|
6320
|
+
bootstrapCompleted = true;
|
|
6321
|
+
bootstrapIdleWarnings.delete(toolInput.sessionID);
|
|
6322
|
+
beginCoordinatorTask(toolInput.sessionID, toolInput.callID);
|
|
6323
|
+
if (reservedParallelDescriptor !== undefined) {
|
|
6324
|
+
parallelCalls.set(toolInput.callID, {
|
|
6325
|
+
ownerRoot: toolInput.sessionID,
|
|
6326
|
+
descriptor: reservedParallelDescriptor,
|
|
6327
|
+
completionCallID: toolInput.callID,
|
|
6328
|
+
});
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
catch (error) {
|
|
6333
|
+
fastLane.restoreWorkerAccounting(toolInput.sessionID, workerAccounting);
|
|
6334
|
+
throw error;
|
|
6273
6335
|
}
|
|
6274
6336
|
return;
|
|
6275
6337
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { GoalFlightState, GoalTerminalReceipt } from "../core/goal-bound.js";
|
|
1
|
+
import type { GoalFlightState, GoalTerminalReceipt, GoalFlightEventRecord } from "../core/goal-bound.js";
|
|
2
|
+
import { type Debrief, type DebriefObservation } from "./sortie-debrief.ts";
|
|
3
|
+
import type { GoalReport } from "../core/goal-report.ts";
|
|
4
|
+
import { type SortieCareer } from "./sortie-career.ts";
|
|
2
5
|
export interface RunMetricsClient {
|
|
3
6
|
readonly session?: {
|
|
4
7
|
readonly get?: (request: {
|
|
@@ -44,6 +47,8 @@ export interface RunMetrics {
|
|
|
44
47
|
readonly sessions: number | undefined;
|
|
45
48
|
readonly cacheRatio: number | undefined;
|
|
46
49
|
readonly roles: Readonly<Record<string, RunRoleMetrics>> | undefined;
|
|
50
|
+
/** Optional for compatibility with older host snapshots. Collected in the existing history pass. */
|
|
51
|
+
readonly debrief?: DebriefObservation;
|
|
47
52
|
}
|
|
48
53
|
export interface RunRoleMetrics {
|
|
49
54
|
readonly tokens: number;
|
|
@@ -72,6 +77,8 @@ export interface SortieResult {
|
|
|
72
77
|
readonly result_id: readonly [goalID: string, terminalRevision: number];
|
|
73
78
|
readonly accounting_phase: "pre-terminal";
|
|
74
79
|
readonly as_of: string;
|
|
80
|
+
readonly debrief?: Debrief;
|
|
81
|
+
readonly career?: SortieCareer;
|
|
75
82
|
readonly mission: {
|
|
76
83
|
readonly status: "COMPLETED" | "INTERRUPTED" | "EXTERNAL_BLOCKER" | "USER_DECISION";
|
|
77
84
|
readonly stop_reason: GoalTerminalReceipt["stop_reason"];
|
|
@@ -102,7 +109,7 @@ export interface SortieResult {
|
|
|
102
109
|
}
|
|
103
110
|
type SortieGoalSnapshot = Pick<GoalFlightState, "acceptance_contract" | "consumed_time_ms" | "satisfied_criteria">;
|
|
104
111
|
/** Builds a pure terminal snapshot from the durable goal receipt and already-observed host metrics. */
|
|
105
|
-
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string): SortieResult;
|
|
112
|
+
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string, records?: readonly GoalFlightEventRecord[]): SortieResult;
|
|
106
113
|
export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
|
|
107
114
|
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
|
|
108
115
|
export declare function formatSortieResult(result: SortieResult): string;
|
|
@@ -114,4 +121,5 @@ export declare function replaceDoneTerminalStatus(text: string, replacement: str
|
|
|
114
121
|
export declare function sanitizeTerminalReport(text: string): string;
|
|
115
122
|
export declare function insertRunMetrics(text: string, metrics: RunMetrics): string;
|
|
116
123
|
export declare function insertSortieResult(text: string, result: SortieResult): string;
|
|
124
|
+
export declare function createGoalReport(result: SortieResult, receipt: GoalTerminalReceipt): GoalReport;
|
|
117
125
|
export {};
|