sortie-dogs 0.10.22 → 0.10.23
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.
|
@@ -5,7 +5,7 @@ export declare const DEFAULT_OPERATOR_PROPOSAL_BUDGET: Readonly<{
|
|
|
5
5
|
max_submissions: 9;
|
|
6
6
|
}>;
|
|
7
7
|
export declare const OPERATOR_PROPOSAL_BUDGET_CAPS: Readonly<{
|
|
8
|
-
max_reads:
|
|
8
|
+
max_reads: 228;
|
|
9
9
|
max_submissions: 24;
|
|
10
10
|
}>;
|
|
11
11
|
export declare const OPERATOR_PROPOSAL_REVISION_CONTRACT: string;
|
|
@@ -94,6 +94,24 @@ export interface OperatorProposalState {
|
|
|
94
94
|
/** Preparation may publish execution controls before approval commits; never revise that pinned plan. */
|
|
95
95
|
approval_started?: boolean;
|
|
96
96
|
root_revisions?: OperatorProposalRevision[];
|
|
97
|
+
/** Root-authorized cumulative increases; the submitted plan and its identity do not change. */
|
|
98
|
+
budget_revisions?: readonly {
|
|
99
|
+
readonly at: string;
|
|
100
|
+
readonly rationale: string;
|
|
101
|
+
readonly from: OperatorIntent["proposal_budget"];
|
|
102
|
+
readonly to: OperatorIntent["proposal_budget"];
|
|
103
|
+
readonly proposal_id: string;
|
|
104
|
+
readonly content_hash: string;
|
|
105
|
+
readonly goal_binding: OperatorProposalGoalBinding;
|
|
106
|
+
}[];
|
|
107
|
+
/** Submitted contracts displaced by a newly evidenced scope gap, never erased or approved. */
|
|
108
|
+
submitted_history?: readonly {
|
|
109
|
+
readonly archive_hash: string;
|
|
110
|
+
readonly proposal_id: string;
|
|
111
|
+
readonly revision: number;
|
|
112
|
+
readonly content_hash: string;
|
|
113
|
+
readonly missing_write_paths: readonly string[];
|
|
114
|
+
}[];
|
|
97
115
|
/** Immutable archive identities of earlier approved contracts on this root. */
|
|
98
116
|
approved_history?: readonly {
|
|
99
117
|
readonly archive_hash: string;
|
|
@@ -162,8 +180,13 @@ export declare class OperatorProposalRuntime {
|
|
|
162
180
|
private resolveTask;
|
|
163
181
|
begin(root: string, raw: unknown): Promise<OperatorProposalState>;
|
|
164
182
|
private beginUnlocked;
|
|
183
|
+
private submittedArchiveFile;
|
|
184
|
+
/** Increase a submitted proposal's cumulative allowance without replacing its goal, plan or investigation. */
|
|
185
|
+
extendSubmittedBudget(root: string, actor: string, raw: unknown, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
|
|
186
|
+
/** Reopen only a newly diagnosed scope gap in a submitted plan, preserving its exact archived identity. */
|
|
187
|
+
reopenSubmittedScope(root: string, actor: string, raw: unknown, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
|
|
165
188
|
/** Explicit root-only transition after an approved contract's execution lane has ended. */
|
|
166
|
-
reviseApproved(root: string, actor: string, raw: unknown, readRun: () => Promise<OperatorState | undefined>, currentGoal: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
|
|
189
|
+
reviseApproved(root: string, actor: string, raw: unknown, readRun: () => Promise<OperatorState | undefined>, currentGoal: OperatorProposalGoalBinding, proveApprovedRunAncestry?: (oldGoalID: string, parentRunID: string) => Promise<boolean>): Promise<OperatorProposalState>;
|
|
167
190
|
bindGoal(root: string, binding: OperatorProposalGoalBinding): Promise<OperatorProposalState>;
|
|
168
191
|
private bindGoalUnlocked;
|
|
169
192
|
admit(root: string, callID: string, args: unknown, reserveBudget?: () => Promise<void>): Promise<OperatorTask>;
|
|
@@ -9,7 +9,7 @@ const text = (value) => typeof value === "string" && value.trim().length > 0 &&
|
|
|
9
9
|
const ORIGINAL_REQUEST_MAX_CHARACTERS = 128 * 1024;
|
|
10
10
|
const ORIGINAL_REQUEST_MAX_BYTES = 128 * 1024;
|
|
11
11
|
export const DEFAULT_OPERATOR_PROPOSAL_BUDGET = Object.freeze({ max_reads: 45, max_submissions: 9 });
|
|
12
|
-
export const OPERATOR_PROPOSAL_BUDGET_CAPS = Object.freeze({ max_reads:
|
|
12
|
+
export const OPERATOR_PROPOSAL_BUDGET_CAPS = Object.freeze({ max_reads: 228, max_submissions: 24 });
|
|
13
13
|
export const OPERATOR_PROPOSAL_REVISION_CONTRACT = "revision_json must encode exactly {proposal_id:string,revision:positive integer,content_hash:string,rationale:nonblank single-line string,patches:array}. " +
|
|
14
14
|
"Pin the current submitted identity from operator_status. patches contains 1..32 {op,path,value} objects. " +
|
|
15
15
|
"replace is allowed only at /coverage, /existing_surface, /uncovered, /negative_handling, /read_scope, /plan/units, /plan/acceptance_proof, " +
|
|
@@ -558,8 +558,102 @@ export class OperatorProposalRuntime {
|
|
|
558
558
|
await this.save(state);
|
|
559
559
|
return state;
|
|
560
560
|
}
|
|
561
|
+
submittedArchiveFile(root, intentID, proposalHash) {
|
|
562
|
+
return join(this.projectRoot, this.profile.stateDirectory, "operator-proposals", `${hash(root)}.${intentID}.${proposalHash}.submitted.json`);
|
|
563
|
+
}
|
|
564
|
+
/** Increase a submitted proposal's cumulative allowance without replacing its goal, plan or investigation. */
|
|
565
|
+
async extendSubmittedBudget(root, actor, raw, currentGoal) {
|
|
566
|
+
if (actor !== root)
|
|
567
|
+
throw new Error("operator-proposal-budget-revision-root-required");
|
|
568
|
+
return this.serial(root, async () => {
|
|
569
|
+
const state = await this.requiredUnlocked(root);
|
|
570
|
+
if (state.phase !== "submitted" || !state.proposal || !state.goal_binding || state.approval_started) {
|
|
571
|
+
throw new Error("operator-proposal-budget-revision-not-submitted");
|
|
572
|
+
}
|
|
573
|
+
if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "proposal_budget"]) ||
|
|
574
|
+
!text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
|
|
575
|
+
!text(raw.rationale) || raw.rationale.length > 2000 || !record(raw.proposal_budget) ||
|
|
576
|
+
!exactSet(raw.proposal_budget, ["max_reads", "max_submissions"])) {
|
|
577
|
+
throw new Error("operator-proposal-budget-revision-invalid");
|
|
578
|
+
}
|
|
579
|
+
if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision ||
|
|
580
|
+
raw.content_hash !== state.proposal_hash)
|
|
581
|
+
throw new Error("operator-proposal-budget-revision-identity-mismatch");
|
|
582
|
+
if (JSON.stringify(currentGoal) !== JSON.stringify(state.goal_binding)) {
|
|
583
|
+
throw new Error("operator-proposal-budget-revision-goal-mismatch");
|
|
584
|
+
}
|
|
585
|
+
const before = state.intent.proposal_budget;
|
|
586
|
+
const budget = raw.proposal_budget;
|
|
587
|
+
const reads = budget.max_reads, submissions = budget.max_submissions;
|
|
588
|
+
if (!Number.isSafeInteger(reads) || !Number.isSafeInteger(submissions) ||
|
|
589
|
+
reads > OPERATOR_PROPOSAL_BUDGET_CAPS.max_reads || submissions > OPERATOR_PROPOSAL_BUDGET_CAPS.max_submissions ||
|
|
590
|
+
reads < before.max_reads || submissions < before.max_submissions ||
|
|
591
|
+
(reads === before.max_reads && submissions === before.max_submissions) ||
|
|
592
|
+
reads <= state.read_count || submissions <= state.submission_count) {
|
|
593
|
+
throw new Error("operator-proposal-budget-revision-range-invalid");
|
|
594
|
+
}
|
|
595
|
+
const next = { max_reads: reads, max_submissions: submissions };
|
|
596
|
+
const intent = { ...state.intent, proposal_budget: next };
|
|
597
|
+
// The intent hash protects the stored budget; the original intent ID and submitted proposal identity stay pinned.
|
|
598
|
+
const revised = { ...state, intent, intent_hash: hash(JSON.stringify(intent)),
|
|
599
|
+
budget_revisions: [...(state.budget_revisions ?? []), { at: new Date().toISOString(), rationale: raw.rationale,
|
|
600
|
+
from: { ...before }, to: next, proposal_id: state.proposal_id, content_hash: state.proposal_hash,
|
|
601
|
+
goal_binding: structuredClone(state.goal_binding) }] };
|
|
602
|
+
await this.save(revised);
|
|
603
|
+
return revised;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/** Reopen only a newly diagnosed scope gap in a submitted plan, preserving its exact archived identity. */
|
|
607
|
+
async reopenSubmittedScope(root, actor, raw, currentGoal) {
|
|
608
|
+
if (actor !== root)
|
|
609
|
+
throw new Error("operator-proposal-scope-reopen-root-required");
|
|
610
|
+
return this.serial(root, async () => {
|
|
611
|
+
const state = await this.requiredUnlocked(root);
|
|
612
|
+
if (state.phase !== "submitted" || !state.proposal || !state.goal_binding || state.approval_started) {
|
|
613
|
+
throw new Error("operator-proposal-scope-reopen-not-submitted");
|
|
614
|
+
}
|
|
615
|
+
if (!record(raw) || !exactSet(raw, ["proposal_id", "revision", "content_hash", "rationale", "missing_write_paths"]) ||
|
|
616
|
+
!text(raw.proposal_id) || !Number.isSafeInteger(raw.revision) || !text(raw.content_hash) ||
|
|
617
|
+
!text(raw.rationale) || raw.rationale.length > 2000 || !Array.isArray(raw.missing_write_paths) ||
|
|
618
|
+
raw.missing_write_paths.length < 1 || raw.missing_write_paths.length > 16 ||
|
|
619
|
+
!raw.missing_write_paths.every(normalizedRelativePath) ||
|
|
620
|
+
new Set(raw.missing_write_paths).size !== raw.missing_write_paths.length) {
|
|
621
|
+
throw new Error("operator-proposal-scope-reopen-invalid");
|
|
622
|
+
}
|
|
623
|
+
if (raw.proposal_id !== state.proposal_id || raw.revision !== state.proposal_revision ||
|
|
624
|
+
raw.content_hash !== state.proposal_hash)
|
|
625
|
+
throw new Error("operator-proposal-scope-reopen-identity-mismatch");
|
|
626
|
+
if (JSON.stringify(currentGoal) !== JSON.stringify(state.goal_binding)) {
|
|
627
|
+
throw new Error("operator-proposal-scope-reopen-goal-mismatch");
|
|
628
|
+
}
|
|
629
|
+
this.assertBudgetAvailable(state);
|
|
630
|
+
const oldWrites = state.proposal.write_scope;
|
|
631
|
+
const missing = raw.missing_write_paths;
|
|
632
|
+
if (missing.some(path => oldWrites.some(scope => path === scope || path.startsWith(`${scope}/`)) ||
|
|
633
|
+
!state.intent.allow_read.some(scope => path === scope || path.startsWith(`${scope}/`)))) {
|
|
634
|
+
throw new Error("operator-proposal-scope-reopen-path-invalid");
|
|
635
|
+
}
|
|
636
|
+
const archive = JSON.stringify({ submitted_proposal: state, missing_write_paths: missing, rationale: raw.rationale });
|
|
637
|
+
const archivePath = this.submittedArchiveFile(root, state.intent_id, state.proposal_hash);
|
|
638
|
+
try {
|
|
639
|
+
await writeFile(archivePath, archive, { flag: "wx", mode: 0o600 });
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
if (error.code !== "EEXIST" || hash(await readFile(archivePath, "utf8")) !== hash(archive))
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
const reopened = { ...state, phase: "investigating", created_at: new Date().toISOString(),
|
|
646
|
+
prior_spend: { reads: state.read_count, submissions: state.submission_count },
|
|
647
|
+
proposal_call_id: null, proposal_session_id: null, read_paths: [], proposal_id: null, proposal_revision: null,
|
|
648
|
+
proposal_hash: null, proposal: null, approval_rationale: null,
|
|
649
|
+
submitted_history: [...(state.submitted_history ?? []), { archive_hash: hash(archive), proposal_id: state.proposal_id,
|
|
650
|
+
revision: state.proposal_revision, content_hash: state.proposal_hash, missing_write_paths: missing }] };
|
|
651
|
+
await this.save(reopened);
|
|
652
|
+
return reopened;
|
|
653
|
+
});
|
|
654
|
+
}
|
|
561
655
|
/** Explicit root-only transition after an approved contract's execution lane has ended. */
|
|
562
|
-
async reviseApproved(root, actor, raw, readRun, currentGoal) {
|
|
656
|
+
async reviseApproved(root, actor, raw, readRun, currentGoal, proveApprovedRunAncestry) {
|
|
563
657
|
if (actor !== root)
|
|
564
658
|
throw new Error("operator-proposal-approved-revision-root-required");
|
|
565
659
|
return this.serial(root, async () => {
|
|
@@ -577,10 +671,15 @@ export class OperatorProposalRuntime {
|
|
|
577
671
|
throw new Error("operator-proposal-approved-revision-identity-mismatch");
|
|
578
672
|
const run = await readRun();
|
|
579
673
|
if (!run || !["cancelled", "completed"].includes(run.phase) || run.rootSessionID !== root ||
|
|
580
|
-
run.runID !== raw.operator_run_id ||
|
|
674
|
+
run.runID !== raw.operator_run_id ||
|
|
581
675
|
JSON.stringify(run.acceptance) !== JSON.stringify(previous.proposal.plan.acceptance)) {
|
|
582
676
|
throw new Error("operator-proposal-approved-revision-terminal-run-required");
|
|
583
677
|
}
|
|
678
|
+
const originalPlan = run.planHash === hash(JSON.stringify(previous.proposal.plan));
|
|
679
|
+
if (!originalPlan && (!run.parentRunID || !proveApprovedRunAncestry ||
|
|
680
|
+
!await proveApprovedRunAncestry(previous.goal_binding.goal_id, run.parentRunID))) {
|
|
681
|
+
throw new Error("operator-proposal-approved-revision-terminal-run-required");
|
|
682
|
+
}
|
|
584
683
|
if (!text(currentGoal.goal_id) || !Number.isSafeInteger(currentGoal.revision) || currentGoal.revision < 1 ||
|
|
585
684
|
!Number.isSafeInteger(currentGoal.scope_epoch) || currentGoal.scope_epoch < 1 ||
|
|
586
685
|
!/^sha256:[a-f0-9]{64}$/u.test(currentGoal.acceptance_fingerprint)) {
|
|
@@ -592,8 +691,7 @@ export class OperatorProposalRuntime {
|
|
|
592
691
|
const intent = parseIntent(raw.intent), intentHash = hash(JSON.stringify(intent));
|
|
593
692
|
if (intentHash === previous.intent_hash)
|
|
594
693
|
throw new Error("operator-proposal-approved-revision-intent-unchanged");
|
|
595
|
-
if (
|
|
596
|
-
JSON.stringify(intent.requirements.slice(0, previous.intent.requirements.length)) !== JSON.stringify(previous.intent.requirements)) {
|
|
694
|
+
if (JSON.stringify(intent.requirements.slice(0, previous.intent.requirements.length)) !== JSON.stringify(previous.intent.requirements)) {
|
|
597
695
|
throw new Error("operator-proposal-approved-revision-acceptance-continuity-required");
|
|
598
696
|
}
|
|
599
697
|
const remainingReads = intent.proposal_budget.max_reads - previous.read_count;
|
|
@@ -608,7 +706,8 @@ export class OperatorProposalRuntime {
|
|
|
608
706
|
}
|
|
609
707
|
const archive = JSON.stringify({ approved_proposal: previous, terminal_run: {
|
|
610
708
|
run_id: run.runID, plan_hash: run.planHash, acceptance: run.acceptance, acceptance_fingerprint: run.acceptanceFingerprint,
|
|
611
|
-
phase: run.phase, decision: run.decision, receipt: run.receipt, generation: run.generation
|
|
709
|
+
phase: run.phase, decision: run.decision, receipt: run.receipt, generation: run.generation,
|
|
710
|
+
parent_run_id: originalPlan ? null : run.parentRunID
|
|
612
711
|
} });
|
|
613
712
|
const archivePath = this.approvedArchiveFile(root, previous.intent_id, previous.proposal_hash, run.runID);
|
|
614
713
|
try {
|
|
@@ -930,10 +1029,13 @@ export class OperatorProposalRuntime {
|
|
|
930
1029
|
packet(state) {
|
|
931
1030
|
return { status: state.phase, intent_id: state.intent_id, intent_hash: state.intent_hash,
|
|
932
1031
|
proposal_id: state.proposal_id, revision: state.proposal_revision, content_hash: state.proposal_hash, reads: state.read_count,
|
|
933
|
-
|
|
1032
|
+
max_reads: state.intent.proposal_budget.max_reads, remaining_reads: state.intent.proposal_budget.max_reads - state.read_count,
|
|
1033
|
+
submissions: state.submission_count, max_submissions: state.intent.proposal_budget.max_submissions,
|
|
934
1034
|
remaining_submissions: state.intent.proposal_budget.max_submissions - state.submission_count,
|
|
935
1035
|
task_admitted: state.proposal_call_id !== null, uncovered: state.proposal?.uncovered ?? [], proposal: state.proposal,
|
|
936
|
-
approval_rationale: state.approval_rationale, root_revisions: state.root_revisions ?? [],
|
|
1036
|
+
approval_rationale: state.approval_rationale, root_revisions: state.root_revisions ?? [],
|
|
1037
|
+
budget_revisions: state.budget_revisions ?? [], submitted_history: state.submitted_history ?? [],
|
|
1038
|
+
approved_history: state.approved_history ?? [],
|
|
937
1039
|
revision_available: state.phase === "submitted" && !state.approval_started && state.submission_count < state.intent.proposal_budget.max_submissions };
|
|
938
1040
|
}
|
|
939
1041
|
}
|
package/dist/plugin/index.js
CHANGED
|
@@ -1617,13 +1617,63 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1617
1617
|
!isRecord(part.state) || !["completed", "error"].includes(String(part.state.status)) ||
|
|
1618
1618
|
!isRecord(part.state.input) || typeof part.state.input.prompt !== "string")
|
|
1619
1619
|
continue;
|
|
1620
|
-
const proposal = reservation.unit_id.startsWith("proposal:");
|
|
1621
1620
|
const role = String(part.state.input.subagent_type);
|
|
1622
|
-
|
|
1621
|
+
const prompt = part.state.input.prompt;
|
|
1622
|
+
const proposal = reservation.unit_id.startsWith("proposal:");
|
|
1623
|
+
let unitID;
|
|
1624
|
+
// The profiled client maps the external V2 `dogs-coordinator` to this canonical role.
|
|
1625
|
+
if (role === "dog-operator" &&
|
|
1626
|
+
(prompt.startsWith("SORTIE_OPERATOR_DELEGATE_REF ") ||
|
|
1627
|
+
prompt.startsWith("SORTIE_OPERATOR_PROPOSAL_TASK_REF "))) {
|
|
1628
|
+
// Native V2 stores the short Task handle, not the expanded child prompt. The
|
|
1629
|
+
// handle's owner and unit must agree with the durable reservation and call ID.
|
|
1630
|
+
const prefix = prompt.startsWith("SORTIE_OPERATOR_DELEGATE_REF ")
|
|
1631
|
+
? "SORTIE_OPERATOR_DELEGATE_REF " : "SORTIE_OPERATOR_PROPOSAL_TASK_REF ";
|
|
1632
|
+
let ref;
|
|
1633
|
+
try {
|
|
1634
|
+
ref = JSON.parse(prompt.slice(prefix.length));
|
|
1635
|
+
}
|
|
1636
|
+
catch {
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
if (!isRecord(ref) || ref.r !== root || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
|
|
1640
|
+
(prefix.startsWith("SORTIE_OPERATOR_DELEGATE") && !/^[a-f0-9]{64}$/u.test(String(ref.p))))
|
|
1641
|
+
continue;
|
|
1642
|
+
if (prefix.startsWith("SORTIE_OPERATOR_DELEGATE")) {
|
|
1643
|
+
if (ref.k !== "delegate" || typeof ref.n !== "string" ||
|
|
1644
|
+
!Number.isSafeInteger(ref.g) || ref.g < 1 ||
|
|
1645
|
+
!reservation.unit_id.startsWith(`${ref.n}-`) ||
|
|
1646
|
+
!/^[1-9][0-9]*$/u.test(reservation.unit_id.slice(ref.n.length + 1)))
|
|
1647
|
+
continue;
|
|
1648
|
+
unitID = reservation.unit_id;
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
if (ref.k !== "proposal" || typeof ref.i !== "string" ||
|
|
1652
|
+
reservation.unit_id !== `proposal:${ref.i}`)
|
|
1653
|
+
continue;
|
|
1654
|
+
unitID = reservation.unit_id;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
else if (["dog-worker", "dog-luna-worker"].includes(role)) {
|
|
1658
|
+
if (prompt.startsWith("SORTIE_OPERATOR_TASK_REF ")) {
|
|
1659
|
+
let ref;
|
|
1660
|
+
try {
|
|
1661
|
+
ref = JSON.parse(prompt.slice("SORTIE_OPERATOR_TASK_REF ".length));
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (!isRecord(ref) || ref.r !== root || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
|
|
1667
|
+
!/^[a-f0-9]{64}$/u.test(String(ref.p)) || typeof ref.t !== "string")
|
|
1668
|
+
continue;
|
|
1669
|
+
unitID = ref.t;
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
unitID = handoffValue(handoffEntries(prompt), ["task_id"]) ?? part.callID;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (unitID === undefined || (proposal && role !== "dog-operator"))
|
|
1623
1676
|
continue;
|
|
1624
|
-
const unitID = proposal
|
|
1625
|
-
? reservation.unit_id
|
|
1626
|
-
: handoffValue(handoffEntries(part.state.input.prompt), ["task_id"]) ?? part.callID;
|
|
1627
1677
|
if (unitID !== reservation.unit_id || goalFingerprint({ goal_id: state.goal_id, unit_id: unitID,
|
|
1628
1678
|
call_id: part.callID }) !== reservation.reservation_id)
|
|
1629
1679
|
continue;
|
|
@@ -4028,12 +4078,13 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
4028
4078
|
async function serializeChatTransition(sessionID, operation) {
|
|
4029
4079
|
const previous = chatTransitions.get(sessionID) ?? Promise.resolve();
|
|
4030
4080
|
const current = previous.catch(() => undefined).then(operation);
|
|
4031
|
-
|
|
4081
|
+
const tail = current.then(() => undefined, () => undefined);
|
|
4082
|
+
chatTransitions.set(sessionID, tail);
|
|
4032
4083
|
try {
|
|
4033
|
-
await current;
|
|
4084
|
+
return await current;
|
|
4034
4085
|
}
|
|
4035
4086
|
finally {
|
|
4036
|
-
if (chatTransitions.get(sessionID) ===
|
|
4087
|
+
if (chatTransitions.get(sessionID) === tail)
|
|
4037
4088
|
chatTransitions.delete(sessionID);
|
|
4038
4089
|
}
|
|
4039
4090
|
}
|
|
@@ -7338,11 +7389,132 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
7338
7389
|
assertActiveGoal: async (root, fingerprint) => {
|
|
7339
7390
|
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7340
7391
|
throw new Error("operator-coordinator-required");
|
|
7392
|
+
await recoverCompletedGoalReservations(root);
|
|
7341
7393
|
const goal = await currentGoal(root);
|
|
7342
7394
|
if (!goal || goal.phase !== "active" || goal.receipt !== null || goal.acceptance_fingerprint !== fingerprint || goal.outstanding_reservations.length) {
|
|
7343
7395
|
throw new Error("operator-resume-goal-identity-mismatch");
|
|
7344
7396
|
}
|
|
7345
7397
|
},
|
|
7398
|
+
reconcileAbortedOperatorOrphan: root => serializeChatTransition(root, async () => {
|
|
7399
|
+
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7400
|
+
throw new Error("operator-coordinator-required");
|
|
7401
|
+
const ledger = await goalLedger(root), initial = (await ledger.readGoal()).state;
|
|
7402
|
+
if (initial.phase !== "active" || initial.goal_id === null || initial.receipt !== null) {
|
|
7403
|
+
return { status: "unproven", reason: "active-goal-required" };
|
|
7404
|
+
}
|
|
7405
|
+
if (initial.outstanding_reservations.length === 0)
|
|
7406
|
+
return { status: "no-orphan" };
|
|
7407
|
+
if (initial.outstanding_reservations.length !== 1)
|
|
7408
|
+
return { status: "unproven", reason: "multiple-reservations" };
|
|
7409
|
+
const reservation = initial.outstanding_reservations[0];
|
|
7410
|
+
const unitID = reservation.unit_id, runID = unitID.replace(/-[1-9][0-9]*$/u, "");
|
|
7411
|
+
if (reservation.session_id !== root || runID === unitID || !/^operator-[a-f0-9-]+$/u.test(runID) ||
|
|
7412
|
+
input.client?.session?.children === undefined || input.client.session.messages === undefined ||
|
|
7413
|
+
input.client.session.abort === undefined) {
|
|
7414
|
+
return { status: "unproven", reservation_id: reservation.reservation_id, reason: "owned-native-lineage-unavailable" };
|
|
7415
|
+
}
|
|
7416
|
+
const sessionAPI = input.client.session;
|
|
7417
|
+
const messages = async (id) => {
|
|
7418
|
+
const response = await sessionAPI.messages({ path: { id }, query: { directory: input.directory } });
|
|
7419
|
+
return isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
|
|
7420
|
+
};
|
|
7421
|
+
const children = async (id) => {
|
|
7422
|
+
const response = await sessionAPI.children({ path: { id }, query: { directory: input.directory } });
|
|
7423
|
+
return isRecord(response) && Array.isArray(response.data) ? response.data.filter(isRecord) : [];
|
|
7424
|
+
};
|
|
7425
|
+
const nativeTaskParts = (history) => history.flatMap(message => isRecord(message.info) && message.info.role === "assistant" && Array.isArray(message.parts)
|
|
7426
|
+
? message.parts.filter(part => isRecord(part) && part.type === "tool" && part.tool === "task") : []);
|
|
7427
|
+
const refFrom = (part, prefix) => {
|
|
7428
|
+
const state = isRecord(part.state) ? part.state : undefined;
|
|
7429
|
+
const toolInput = isRecord(state?.input) ? state.input : undefined;
|
|
7430
|
+
if (typeof toolInput?.prompt !== "string" || !toolInput.prompt.startsWith(prefix))
|
|
7431
|
+
return undefined;
|
|
7432
|
+
try {
|
|
7433
|
+
const ref = JSON.parse(toolInput.prompt.slice(prefix.length));
|
|
7434
|
+
return isRecord(ref) ? ref : undefined;
|
|
7435
|
+
}
|
|
7436
|
+
catch {
|
|
7437
|
+
return undefined;
|
|
7438
|
+
}
|
|
7439
|
+
};
|
|
7440
|
+
const rootParts = nativeTaskParts(await messages(root));
|
|
7441
|
+
const parentTasks = rootParts.filter(part => {
|
|
7442
|
+
const state = isRecord(part.state) ? part.state : undefined;
|
|
7443
|
+
const ref = refFrom(part, "SORTIE_OPERATOR_DELEGATE_REF ");
|
|
7444
|
+
return state?.status === "error" && isRecord(state.error) && state.error.type === "aborted" &&
|
|
7445
|
+
ref?.k === "delegate" && ref.r === root && ref.n === runID &&
|
|
7446
|
+
/^[a-f0-9]{64}$/u.test(String(ref.p)) && /^[a-f0-9]{64}$/u.test(String(ref.h)) &&
|
|
7447
|
+
Number.isSafeInteger(ref.g) && ref.g > 0 &&
|
|
7448
|
+
isRecord(state.time) && typeof state.time.end === "number";
|
|
7449
|
+
});
|
|
7450
|
+
if (parentTasks.length !== 1)
|
|
7451
|
+
return { status: "unproven", reservation_id: reservation.reservation_id, reason: "terminal-parent-unproven" };
|
|
7452
|
+
const parentEnd = parentTasks[0].state.time.end;
|
|
7453
|
+
const parentRef = refFrom(parentTasks[0], "SORTIE_OPERATOR_DELEGATE_REF ");
|
|
7454
|
+
const candidates = [];
|
|
7455
|
+
for (const delegate of await children(root)) {
|
|
7456
|
+
if (typeof delegate.id !== "string" || delegate.parentID !== root || delegate.agent !== "dog-operator")
|
|
7457
|
+
continue;
|
|
7458
|
+
const parts = nativeTaskParts(await messages(delegate.id));
|
|
7459
|
+
for (const part of parts) {
|
|
7460
|
+
const ref = refFrom(part, "SORTIE_OPERATOR_TASK_REF ");
|
|
7461
|
+
if (ref?.r !== root || ref.n !== runID || ref.t !== unitID || typeof part.callID !== "string" ||
|
|
7462
|
+
ref.p !== parentRef.p || ref.g !== parentRef.g || !/^[a-f0-9]{64}$/u.test(String(ref.h)) ||
|
|
7463
|
+
goalFingerprint({ goal_id: initial.goal_id, unit_id: unitID, call_id: part.callID }) !== reservation.reservation_id ||
|
|
7464
|
+
!isRecord(part.state) || part.state.status !== "running")
|
|
7465
|
+
continue;
|
|
7466
|
+
for (const worker of await children(delegate.id)) {
|
|
7467
|
+
if (typeof worker.id !== "string" || worker.parentID !== delegate.id ||
|
|
7468
|
+
!["dog-worker", "dog-luna-worker"].includes(String(worker.agent)))
|
|
7469
|
+
continue;
|
|
7470
|
+
const history = await messages(worker.id);
|
|
7471
|
+
const claimed = history.some(message => isRecord(message.info) && message.info.role === "user" &&
|
|
7472
|
+
Array.isArray(message.parts) && message.parts.some(textPart => isRecord(textPart) &&
|
|
7473
|
+
textPart.type === "text" && typeof textPart.text === "string" &&
|
|
7474
|
+
handoffValue(handoffEntries(textPart.text), ["task_id"]) === unitID));
|
|
7475
|
+
const lastActivity = Math.max(0, ...history.flatMap(message => isRecord(message.info) && isRecord(message.info.time) &&
|
|
7476
|
+
typeof message.info.time.created === "number" ? [message.info.time.created] : []));
|
|
7477
|
+
if (claimed && lastActivity > 0 && lastActivity <= parentEnd) {
|
|
7478
|
+
candidates.push({ delegate: delegate.id, worker: worker.id, callID: part.callID });
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
}
|
|
7483
|
+
if (candidates.length !== 1)
|
|
7484
|
+
return { status: "unproven", reservation_id: reservation.reservation_id, reason: "stale-child-lineage-unproven" };
|
|
7485
|
+
const candidate = candidates[0];
|
|
7486
|
+
for (const id of [candidate.worker, candidate.delegate]) {
|
|
7487
|
+
const acknowledgement = await sessionAPI.abort({ path: { id }, query: { directory: input.directory } });
|
|
7488
|
+
const response = isRecord(acknowledgement) && isRecord(acknowledgement.data) ? acknowledgement.data : acknowledgement;
|
|
7489
|
+
if (!isRecord(response) || typeof response.interrupted !== "boolean") {
|
|
7490
|
+
return { status: "unproven", reservation_id: reservation.reservation_id, reason: "native-interrupt-unconfirmed" };
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
const current = (await ledger.readGoal()).state;
|
|
7494
|
+
if (current.phase !== "active" || current.receipt !== null ||
|
|
7495
|
+
current.goal_id !== initial.goal_id || current.revision !== initial.revision ||
|
|
7496
|
+
current.scope_epoch !== initial.scope_epoch || current.acceptance_fingerprint !== initial.acceptance_fingerprint ||
|
|
7497
|
+
current.outstanding_reservations.length !== 1 ||
|
|
7498
|
+
current.outstanding_reservations[0].reservation_id !== reservation.reservation_id) {
|
|
7499
|
+
return { status: "unproven", reservation_id: reservation.reservation_id, reason: "goal-changed-during-interrupt" };
|
|
7500
|
+
}
|
|
7501
|
+
await ledger.appendGoal({ kind: "unit.settled", at: new Date().toISOString(), reservation_id: reservation.reservation_id,
|
|
7502
|
+
receipt_id: goalFingerprint({ orphan_interrupted: candidate, reservation_id: reservation.reservation_id }),
|
|
7503
|
+
goal_id: initial.goal_id, unit_id: unitID, disposition: "cancelled", result_class: "interrupted",
|
|
7504
|
+
progress_fingerprint: null, evidence: [], elapsed_ms: null, cost_usd: null });
|
|
7505
|
+
return { status: "settled", reservation_id: reservation.reservation_id, unit_id: unitID };
|
|
7506
|
+
}),
|
|
7507
|
+
proveApprovedRunAncestry: async (root, oldGoalID, parentRunID) => {
|
|
7508
|
+
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7509
|
+
throw new Error("operator-coordinator-required");
|
|
7510
|
+
const { records } = await (await goalLedger(root)).readGoal();
|
|
7511
|
+
if (!records.some(({ event }) => event.kind === "goal.accepted" && event.goal_id === oldGoalID))
|
|
7512
|
+
return false;
|
|
7513
|
+
return records.some(({ event }) => event.kind === "dispatch.reserved" && event.goal_id === oldGoalID &&
|
|
7514
|
+
event.session_id === root && event.unit_id.startsWith(`${parentRunID}-`) &&
|
|
7515
|
+
records.some(({ event: settled }) => settled.kind === "unit.settled" && settled.goal_id === oldGoalID &&
|
|
7516
|
+
settled.unit_id === event.unit_id && settled.reservation_id === event.reservation_id));
|
|
7517
|
+
},
|
|
7346
7518
|
retainOperatorContractRepairWorker: async (root, taskID, childSessionID) => {
|
|
7347
7519
|
if (!isCoordinatorSession(root) && !await recoverCoordinatorRoot(root))
|
|
7348
7520
|
throw new Error("operator-coordinator-required");
|
package/dist/plugin/profiled.js
CHANGED
|
@@ -405,9 +405,27 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
405
405
|
const revisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
406
406
|
? stringSchema.describe(OPERATOR_PROPOSAL_REVISION_CONTRACT)
|
|
407
407
|
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: OPERATOR_PROPOSAL_REVISION_CONTRACT };
|
|
408
|
+
const budgetRevisionContract = "Root-only: increase the cumulative read/submission limits of this exact unapproved submitted proposal. " +
|
|
409
|
+
"revision_json must encode exactly {proposal_id,revision,content_hash,rationale,proposal_budget:{max_reads,max_submissions}}. " +
|
|
410
|
+
"Pin the submitted identity from operator_status and supply user-authorized cumulative limits, strictly above retained spend and no lower than old limits. " +
|
|
411
|
+
"The current goal binding must still match. This changes neither proposal identity, ordered requirements, plan, goal, read evidence nor spent counters; " +
|
|
412
|
+
"it grants no new investigation Task, approval, execution or acceptance. Recompare the proposal before separately revising or approving it.";
|
|
413
|
+
const budgetRevisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
414
|
+
? stringSchema.describe(budgetRevisionContract)
|
|
415
|
+
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: budgetRevisionContract };
|
|
416
|
+
const scopeReopenContract = "Root-only: after observing a newly missing implementation write path, reopen this exact unapproved submitted proposal " +
|
|
417
|
+
"for a bounded new investigation without cancellation or budget reset. revision_json must encode exactly " +
|
|
418
|
+
"{proposal_id,revision,content_hash,rationale,missing_write_paths:string[]}. Pin its current identity; every path must be an exact " +
|
|
419
|
+
"normalized repository-relative path absent from the submitted write scope and within the frozen intent.allow_read. " +
|
|
420
|
+
"The submitted contract is archived, ordered requirements, goal binding, intent, original source refs and cumulative spend are retained; " +
|
|
421
|
+
"old child and read paths grant no authority to the new child. This consumes a new goal dispatch unit only when its returned Task is admitted. " +
|
|
422
|
+
"Do not use this to retry a known field defect correctable by revise_operator_proposal or to claim any old acceptance.";
|
|
423
|
+
const scopeReopenSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
424
|
+
? stringSchema.describe(scopeReopenContract)
|
|
425
|
+
: { ...(record(stringSchema) ? stringSchema : { type: "string" }), description: scopeReopenContract };
|
|
408
426
|
const approvedRevisionContract = "revision_json must encode exactly {proposal_id:string,revision:positive integer,content_hash:string,operator_run_id:string,rationale:nonblank single-line string,intent:object}. " +
|
|
409
427
|
"The intent has the exact begin_operator_proposal intent_json shape and a cumulative proposal_budget with room above all retained reads/submissions. " +
|
|
410
|
-
"Pin the approved proposal identity and the cancelled/completed run ID from operator_status.
|
|
428
|
+
"Pin the approved proposal identity and the cancelled/completed run ID from operator_status. The new requirements must begin with every old ordered requirement byte-for-byte even if the active goal ID has changed. Only a root acting for a newly authorized active goal may revise; active/prepared runs, stale identity or unchanged goal binding are denied. " +
|
|
411
429
|
"Old acceptance, terminal status and proposal accounting remain durable. This returns an investigation Task, not implementation or acceptance.";
|
|
412
430
|
const approvedRevisionSchema = record(stringSchema) && typeof stringSchema.describe === "function"
|
|
413
431
|
? stringSchema.describe(approvedRevisionContract)
|
|
@@ -438,6 +456,9 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
438
456
|
const submitProposal = profileTool(profile, "sortie_submit_operator_proposal");
|
|
439
457
|
const approveProposal = profileTool(profile, "sortie_approve_operator_proposal");
|
|
440
458
|
const reviseProposal = profileTool(profile, "sortie_revise_operator_proposal");
|
|
459
|
+
const extendProposalBudget = profileTool(profile, "sortie_extend_operator_proposal_budget");
|
|
460
|
+
const reopenProposalScope = profileTool(profile, "sortie_reopen_operator_proposal_scope");
|
|
461
|
+
const reconcileOrphan = profileTool(profile, "sortie_reconcile_orphaned_operator_dispatch");
|
|
441
462
|
const reviseApprovedIntent = profileTool(profile, "sortie_revise_approved_operator_intent");
|
|
442
463
|
async function stop(root, reason, retireRoot = true) {
|
|
443
464
|
if (retireRoot)
|
|
@@ -575,13 +596,16 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
575
596
|
? proposal.proposal_call_id !== null
|
|
576
597
|
? "proposal Task is already admitted; do not redispatch it or call operator_next. Continue submission repairs only in the same active claimed child. If that child terminated or was interrupted without submission, an explicit root decision to retry may call cancel_operator with reason=plain to release this grant. Cancellation stops owned children and durably preserves cumulative reads/submissions and goal spend. Pass the returned retry_intent unchanged to begin_operator_proposal, then dispatch only its new exact Task within the remaining budget. Never reuse the terminated child, reset spend, or replace the goal or ordered requirements. Exhaustion requires an explicit cumulative budget revision, not a reset."
|
|
577
598
|
: proposal.submission_count >= proposal.intent.proposal_budget.max_submissions
|
|
578
|
-
?
|
|
599
|
+
? `proposal submission budget exhausted; do not call operator_next. For the exact same unapproved submitted proposal and active goal, an explicitly user-authorized cumulative increase can use ${extendProposalBudget}; retain all previous spend and compare the unchanged proposal again. Otherwise report the bounded failure.`
|
|
579
600
|
: "proposal is not submitted; do not call operator_next; complete or repair the bounded proposal submission"
|
|
580
601
|
: proposal?.phase === "submitted"
|
|
581
|
-
? `compare every original requirement with the exact proposal. For a known semantic defect, use ${reviseProposal} with its current identity and bounded field patches, then compare again; do not redispatch the ended child or cancel/reinvestigate the same defect. Approval preparation pins the plan and closes revision. Do not call operator_next before approval prepares a run. Approval authorizes implementation, not product acceptance. Preserve every required live measurement, consultation, root-owned push/global apply and user approval as pending acceptance obligations; canonical commands do not substitute for them. Use the host budget counters, not plan.goal_declaration.goal_budget_units, for remaining capacity; approval validates the retained goal binding and execution budget. A bounded diagnostic run may use cancel_operator after admission without inventing a plan validation command for cancellation.`
|
|
582
|
-
:
|
|
602
|
+
? `compare every original requirement with the exact proposal. For a known semantic defect, use ${reviseProposal} with its current identity and bounded field patches, then compare again; do not redispatch the ended child or cancel/reinvestigate the same defect. If exhausted, an explicitly user-authorized cumulative increase on this same submitted identity uses ${extendProposalBudget}. An observed missing implementation write path inside the frozen allow_read needs separate root-authorized ${reopenProposalScope}; this archives the old submission and returns a new read-only Task while retaining all spend. Approval preparation pins the plan and closes revision. Do not call operator_next before approval prepares a run. Approval authorizes implementation, not product acceptance. Preserve every required live measurement, consultation, root-owned push/global apply and user approval as pending acceptance obligations; canonical commands do not substitute for them. Use the host budget counters, not plan.goal_declaration.goal_budget_units, for remaining capacity; approval validates the retained goal binding and execution budget. A bounded diagnostic run may use cancel_operator after admission without inventing a plan validation command for cancellation.`
|
|
603
|
+
: state?.phase === "cancelled"
|
|
604
|
+
? `approved proposal remains pinned to a cancelled run. Do not call operator_next or reuse its Task. If the currently active goal differs, use ${reconcileOrphan} for one proven aborted native child reservation, then ${reviseApprovedIntent} with the exact cancelled run and retained ordered acceptance for a newly authorized investigation.`
|
|
605
|
+
: "approved proposal has no operator run; do not call operator_next; reconcile the approval or preparation failure";
|
|
583
606
|
return JSON.stringify(state ? { ...await operatorPacket(state), ...(draft ? { pending_draft: draft } : {}),
|
|
584
|
-
...(proposal ? { proposal: proposalIdentity(proposal) } : {})
|
|
607
|
+
...(proposal ? { proposal: proposalIdentity(proposal), proposal_next_action: proposalNextAction } : {}),
|
|
608
|
+
...(state.phase === "cancelled" ? { orphan_recovery_action: `If the active goal has one reservation from an aborted owned V2 Task, use ${reconcileOrphan} only after native lineage and interrupt proof. If a terminal replacement run is still bound to a previous approved goal, use ${reviseApprovedIntent} with its exact identity, preserved ordered requirements and cumulative spend; it starts fresh investigation, not acceptance.` } : {}) }
|
|
585
609
|
: draft ? { ...draft, ...(proposal ? { proposal: proposals.packet(proposal) } : {}) }
|
|
586
610
|
: proposal ? { profile: profile.id, proposal: proposals.packet(proposal),
|
|
587
611
|
goal_binding: proposal.goal_binding, budget: await control.currentBudget(context.sessionID),
|
|
@@ -834,7 +858,7 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
834
858
|
try {
|
|
835
859
|
const binding = await control.proposalGoalBinding(context.sessionID);
|
|
836
860
|
await control.assertActiveGoal(context.sessionID, binding.acceptance_fingerprint);
|
|
837
|
-
const revised = await proposals.reviseApproved(context.sessionID, context.sessionID, request, () => new OperatorRuntime(input.directory, profile).read(context.sessionID), binding);
|
|
861
|
+
const revised = await proposals.reviseApproved(context.sessionID, context.sessionID, request, () => new OperatorRuntime(input.directory, profile).read(context.sessionID), binding, (oldGoalID, parentRunID) => control.proveApprovedRunAncestry(context.sessionID, oldGoalID, parentRunID));
|
|
838
862
|
return JSON.stringify({ ...proposals.packet(revised), task: proposals.referenceTask(revised),
|
|
839
863
|
dispatch_instruction: "Dispatch this new exact Task reference once; do not reuse the prior approved contract or its child." });
|
|
840
864
|
}
|
|
@@ -889,6 +913,57 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
889
913
|
}
|
|
890
914
|
});
|
|
891
915
|
} };
|
|
916
|
+
tools[extendProposalBudget] = { description: budgetRevisionContract,
|
|
917
|
+
args: { revision_json: budgetRevisionSchema }, execute: async (args, context) => {
|
|
918
|
+
await requireRoot(context.sessionID);
|
|
919
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
920
|
+
try {
|
|
921
|
+
const binding = await control.proposalGoalBinding(context.sessionID);
|
|
922
|
+
const revised = await proposals.extendSubmittedBudget(context.sessionID, context.sessionID, JSON.parse(args.revision_json), binding);
|
|
923
|
+
return JSON.stringify({ ...proposals.packet(revised),
|
|
924
|
+
next_action: "Compare all original ordered requirements against this unchanged proposal. Revise diagnosed fields within the remaining submission budget or explicitly approve; no worker was started." });
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
if (error instanceof SyntaxError)
|
|
928
|
+
return JSON.stringify({ status: "invalid-budget-revision", code: "operator-proposal-budget-revision-json-invalid" });
|
|
929
|
+
if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
|
|
930
|
+
throw error;
|
|
931
|
+
return JSON.stringify({ status: "invalid-budget-revision", code: error.message });
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
} };
|
|
935
|
+
tools[reopenProposalScope] = { description: scopeReopenContract,
|
|
936
|
+
args: { revision_json: scopeReopenSchema }, execute: async (args, context) => {
|
|
937
|
+
await requireRoot(context.sessionID);
|
|
938
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
939
|
+
try {
|
|
940
|
+
const binding = await control.proposalGoalBinding(context.sessionID);
|
|
941
|
+
await control.assertActiveGoal(context.sessionID, binding.acceptance_fingerprint);
|
|
942
|
+
const reopened = await proposals.reopenSubmittedScope(context.sessionID, context.sessionID, JSON.parse(args.revision_json), binding);
|
|
943
|
+
return JSON.stringify({ ...proposals.packet(reopened), task: proposals.referenceTask(reopened),
|
|
944
|
+
dispatch_instruction: "Dispatch this new exact read-only Task once; investigate the newly missing surfaces and submit a complete plan. Never reuse the archived child or its read paths." });
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
if (error instanceof OperatorProposalBudgetError)
|
|
948
|
+
return JSON.stringify(error.diagnostic);
|
|
949
|
+
if (error instanceof SyntaxError)
|
|
950
|
+
return JSON.stringify({ status: "invalid-scope-reopen", code: "operator-proposal-scope-reopen-json-invalid" });
|
|
951
|
+
if (!(error instanceof Error) || !error.message.startsWith("operator-proposal-"))
|
|
952
|
+
throw error;
|
|
953
|
+
return JSON.stringify({ status: "invalid-scope-reopen", code: error.message });
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
} };
|
|
957
|
+
tools[reconcileOrphan] = { description: "Root-only: after explicitly cancelling the operator, reconcile one orphaned goal reservation from an aborted native delegate and its still-running historical child Task. The host requires exact goal/call/Task identity, owned parent-child lineage, native timestamps, and acknowledged interruption of both owned sessions. It records interrupted lifecycle spend with no validation evidence or acceptance. If any proof is absent it returns unproven without settling the reservation; never use worker prose as proof.",
|
|
958
|
+
args: {}, execute: async (_args, context) => {
|
|
959
|
+
await requireRoot(context.sessionID);
|
|
960
|
+
return serializeDispatchTransition(context.sessionID, async () => {
|
|
961
|
+
const run = await operators.read(context.sessionID);
|
|
962
|
+
if (run?.phase !== "cancelled")
|
|
963
|
+
return JSON.stringify({ status: "unproven", reason: "cancelled-operator-required" });
|
|
964
|
+
return JSON.stringify(await control.reconcileAbortedOperatorOrphan(context.sessionID));
|
|
965
|
+
});
|
|
966
|
+
} };
|
|
892
967
|
tools[approveProposal] = { description: "Root-only: record semantic comparison of the exact proposal revision/hash, then connect its immutable plan to the existing execution lane. " + OPERATOR_APPROVAL_CONTRACT,
|
|
893
968
|
args: { approval_json: approvalSchema }, execute: async (args, context) => {
|
|
894
969
|
await requireRoot(context.sessionID);
|
|
@@ -928,7 +1003,8 @@ export function createProfiledPlugin(profile, assetVersion) {
|
|
|
928
1003
|
});
|
|
929
1004
|
} };
|
|
930
1005
|
const ownTools = new Set([prepare, repair, next, status, cancel, complete, resume, resolveContractRepair,
|
|
931
|
-
beginProposal, submitProposal, approveProposal, reviseProposal,
|
|
1006
|
+
beginProposal, submitProposal, approveProposal, reviseProposal, extendProposalBudget, reopenProposalScope,
|
|
1007
|
+
reconcileOrphan, reviseApprovedIntent]);
|
|
932
1008
|
const protocolMap = CANONICAL_AGENT_ROLES.map(role => `${role}=${profileAgent(profile, role)}`).join(", ");
|
|
933
1009
|
function rememberRendered(key, text) {
|
|
934
1010
|
renderedParts.set(key, goalFingerprint(text));
|
|
@@ -62,6 +62,13 @@ export interface RuntimeBridge {
|
|
|
62
62
|
assertProposalExecutionBudget(rootSessionID: string, binding: OperatorProposalGoalBinding, executionUnits: number): Promise<void>;
|
|
63
63
|
hasNoGoalReservation(rootSessionID: string, unitID: string, callID: string): Promise<boolean>;
|
|
64
64
|
assertActiveGoal(rootSessionID: string, goalFingerprint: string): Promise<void>;
|
|
65
|
+
reconcileAbortedOperatorOrphan(rootSessionID: string): Promise<{
|
|
66
|
+
readonly status: "settled" | "no-orphan" | "unproven";
|
|
67
|
+
readonly reservation_id?: string;
|
|
68
|
+
readonly unit_id?: string;
|
|
69
|
+
readonly reason?: string;
|
|
70
|
+
}>;
|
|
71
|
+
proveApprovedRunAncestry(rootSessionID: string, oldGoalID: string, parentRunID: string): Promise<boolean>;
|
|
65
72
|
retainOperatorContractRepairWorker(rootSessionID: string, taskID: string, childSessionID: string): Promise<void>;
|
|
66
73
|
assertOperatorContractRepairValidationAvailable(rootSessionID: string, taskID: string, childSessionID: string): Promise<void>;
|
|
67
74
|
remediationScopeExpansionAuthority(rootSessionID: string): Promise<string | undefined>;
|