@osovv/vv-opencode 1.4.2 → 1.4.3-rc.1
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/CHANGELOG.md +20 -0
- package/README.md +20 -6
- package/dist/lib/orchestration.js +12 -3
- package/dist/lib/orchestration.js.map +1 -1
- package/dist/lib/spec-lint.d.ts +2 -64
- package/dist/lib/spec-lint.js +47 -45
- package/dist/lib/spec-lint.js.map +1 -1
- package/dist/lib/workflow-contract.d.ts +277 -0
- package/dist/lib/workflow-contract.js +636 -0
- package/dist/lib/workflow-contract.js.map +1 -0
- package/dist/plugins/system-context-injection/index.js +15 -7
- package/dist/plugins/system-context-injection/index.js.map +1 -1
- package/dist/plugins/workflow/authority.d.ts +99 -0
- package/dist/plugins/workflow/authority.js +435 -0
- package/dist/plugins/workflow/authority.js.map +1 -0
- package/dist/plugins/workflow/checkpoint-io.d.ts +10 -1
- package/dist/plugins/workflow/checkpoint-io.js +22 -3
- package/dist/plugins/workflow/checkpoint-io.js.map +1 -1
- package/dist/plugins/workflow/checkpoints.d.ts +58 -1
- package/dist/plugins/workflow/checkpoints.js +362 -34
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +150 -2
- package/dist/plugins/workflow/delegated.js +551 -15
- package/dist/plugins/workflow/delegated.js.map +1 -1
- package/dist/plugins/workflow/execution.d.ts +361 -0
- package/dist/plugins/workflow/execution.js +1807 -0
- package/dist/plugins/workflow/execution.js.map +1 -0
- package/dist/plugins/workflow/index.js +539 -80
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/persistence.d.ts +18 -4
- package/dist/plugins/workflow/persistence.js +439 -42
- package/dist/plugins/workflow/persistence.js.map +1 -1
- package/dist/plugins/workflow/repair.d.ts +2 -0
- package/dist/plugins/workflow/repair.js +16 -8
- package/dist/plugins/workflow/repair.js.map +1 -1
- package/dist/plugins/workflow/snapshots.js +3 -3
- package/dist/plugins/workflow/snapshots.js.map +1 -1
- package/dist/plugins/workflow/state.d.ts +13 -0
- package/dist/plugins/workflow/state.js +37 -7
- package/dist/plugins/workflow/state.js.map +1 -1
- package/dist/plugins/workflow/system-instruction.md +15 -1
- package/dist/plugins/workflow/tooling.d.ts +81 -6
- package/dist/plugins/workflow/tooling.js +956 -23
- package/dist/plugins/workflow/tooling.js.map +1 -1
- package/dist/plugins/workflow/transactions.d.ts +54 -0
- package/dist/plugins/workflow/transactions.js +147 -0
- package/dist/plugins/workflow/transactions.js.map +1 -0
- package/package.json +1 -1
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/vv-code-reviewer.md +1 -0
- package/templates/agents/vv-controller.md +26 -13
- package/templates/agents/vv-implementer.md +1 -0
- package/templates/agents/vv-spec-reviewer.md +1 -0
- package/templates/skills/vv-execute/SKILL.md +6 -5
- package/templates/skills/vv-plan/SKILL.md +1 -1
- package/templates/skills/vv-review/SKILL.md +1 -0
- package/templates/skills/vv-spec/SKILL.md +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/index.ts
|
|
2
|
-
// VERSION: 0.
|
|
2
|
+
// VERSION: 0.7.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
|
-
// PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including delegated control tools, host-call-bound attempts, and checkpoint reviewer linkage.
|
|
5
|
-
// SCOPE: work_item_open/list/close registration, delegated-only work_item_decide and work_checkpoint registration with root-session authorization, tracked launch validation with delegated barriers and overlapping-write gates, live host-call bindings that convert supported foreground vv-implementer task launches into failed delegated attempts on confirmed host-terminal errors, result normalization and bounded same-session continuation with explicit hard-stop suppression, callID-bound delegated attempt results
|
|
4
|
+
// PURPOSE: Register workflow tools and enforcement while injecting only startup-profile-compatible vv-controller guidance, including delegated control tools with bounded recovery, host-call-bound attempts, terminal report-rejection settlement, and checkpoint reviewer linkage.
|
|
5
|
+
// SCOPE: work_item_open/list/close registration, delegated-only work_item_decide and work_checkpoint registration with root-session authorization and an SDK-backed read-only authorization-message lookup for user-authorized recovery, tracked launch validation with delegated barriers and overlapping-write gates, live host-call bindings that convert supported foreground vv-implementer task launches into failed delegated attempts on confirmed host-terminal errors, result normalization and bounded same-session continuation with explicit hard-stop suppression, callID-bound delegated attempt results, terminal settlement of protocol-invalid reports as report_rejected attempts through staged persistence, checkpoint reviewer bookkeeping, round aggregation with bounded excerpts, implementation round limits, durably committed recovery with synchronous persist and rollback so launch permissions appear only after a durable write, checked persistence, and profile-selected chat.message guidance.
|
|
6
6
|
// DEPENDS: [@opencode-ai/plugin, src/lib/config-layers.ts, src/lib/orchestration.ts, src/lib/plugin-toggle-config.ts, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/checkpoints.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/persistence.ts, src/plugins/workflow/protocol.ts, src/plugins/workflow/repair.ts, src/plugins/workflow/state.ts, src/plugins/workflow/tooling.ts, src/plugins/workflow/transitions.ts]
|
|
7
7
|
// LINKS: M-PLUGIN-WORKFLOW, M-ORCHESTRATION-PROFILES, M-WORKFLOW-PROTOCOL, M-WORKFLOW-REPAIR, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-TOOLING, M-WORKFLOW-PERSISTENCE, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, V-M-PLUGIN-WORKFLOW
|
|
8
8
|
// ROLE: RUNTIME
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
|
-
// WorkflowPlugin - Registers workflow work-item tools, delegated control tools under the delegated profile, tracked task protocol enforcement with callID-bound delegated attempts, checkpoint linkage,
|
|
13
|
+
// WorkflowPlugin - Registers workflow work-item tools, delegated control tools under the delegated profile, tracked task protocol enforcement with callID-bound delegated attempts, bounded recovery with durable persist-and-rollback commits, terminal report-rejection settlement, checkpoint linkage, live host-call failure bindings, and primary-session workflow guidance injection.
|
|
14
14
|
// END_MODULE_MAP
|
|
15
15
|
//
|
|
16
16
|
// START_CHANGE_SUMMARY
|
|
17
|
-
// LAST_CHANGE: [
|
|
17
|
+
// LAST_CHANGE: [C-WORKFLOW-BOUNDED-RECOVERY-R1 - Added decision/checkpoint recover wiring with an SDK-backed authorization lookup and durable persist-then-rollback commits, settled terminal protocol-invalid delegated reports as report_rejected attempts, and calibrated the delegated instruction for bounded recovery and native plan registration.]
|
|
18
18
|
// END_CHANGE_SUMMARY
|
|
19
19
|
import { tool } from "@opencode-ai/plugin";
|
|
20
|
-
import { attemptTrackedResultRepair,
|
|
20
|
+
import { attemptTrackedResultRepair, detectExplicitHardStopStatus, isTrackedResultRepairEligible, unwrapResumableTaskResult, } from "./repair.js";
|
|
21
21
|
import { parseResultBlock, parseWorkItemHeader, TRACKED_SUBAGENT_NAMES, } from "./protocol.js";
|
|
22
|
-
import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, getReviewRound, getWorkItem, revertReviewerLaunch, } from "./state.js";
|
|
23
|
-
import { applyDelegatedLaunchFailure, applyDelegatedResult, beginDelegatedLaunch, revertInFlightDelegatedLaunches, } from "./delegated.js";
|
|
22
|
+
import { applyTrackedResult, createRecordLookupKey, createWorkflowResultExcerpt, beginTrackedLaunch, createWorkItemStore, createWorkItemStoreView, getReviewRound, getWorkItem, revertReviewerLaunch, } from "./state.js";
|
|
23
|
+
import { applyDelegatedLaunchFailure, applyDelegatedReportRejection, applyDelegatedResult, beginDelegatedLaunch, revertInFlightDelegatedLaunches, summarizeDelegatedProgress, } from "./delegated.js";
|
|
24
24
|
import { checkpointBarrierUnsatisfied, findOverlappingInFlightReview, recordCheckpointReviewerLaunch, recordCheckpointReviewerResult, } from "./checkpoints.js";
|
|
25
25
|
import { getAllowedNextAgents, getAttemptedImplementationRound, getReviewerRoleForAgent, shouldBlockRound, } from "./transitions.js";
|
|
26
26
|
import { createWorkItemCloseTool, createWorkItemDecideTool, createWorkItemListTool, createWorkItemOpenTool, createWorkCheckpointTool, } from "./tooling.js";
|
|
@@ -30,6 +30,8 @@ import { resolveOrchestrationPolicy, } from "../../lib/orchestration.js";
|
|
|
30
30
|
import { isVvocPluginEnabled } from "../../lib/plugin-toggle-config.js";
|
|
31
31
|
import { deleteWorkflowSessionDir, hydrateWorkflowStateChecked, snapshotWorkflowStateChecked, } from "./persistence.js";
|
|
32
32
|
import { loadApprovedDelegatedPlan } from "./checkpoint-io.js";
|
|
33
|
+
import { isTaskLaunchableInStore } from "./execution.js";
|
|
34
|
+
import { runWorkflowTransaction, WorkflowTransactionQueue } from "./transactions.js";
|
|
33
35
|
const z = tool.schema;
|
|
34
36
|
const TRACKED_SUBAGENT_SET = new Set(TRACKED_SUBAGENT_NAMES);
|
|
35
37
|
const WORK_ITEM_MISSING_MARKER = "__VVOC" + "_SECRET_BEARER_TOKEN_a6f582092f05__";
|
|
@@ -71,10 +73,31 @@ work_item_decide with the attempt number to accept or request changes. DONE_WITH
|
|
|
71
73
|
an explicit concerns disposition. Controller retries are bounded to one correction attempt before
|
|
72
74
|
explicit recovery; BLOCKED and NEEDS_CONTEXT remain hard stops.
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
Bounded recovery: when a delegated task stops (BLOCKED or NEEDS_CONTEXT) or exhausts its two
|
|
77
|
+
ordinary attempts without acceptance, diagnose it yourself and call work_item_decide with decision
|
|
78
|
+
"recover", the terminal attempt number, a diagnosis, the changed condition or approach, the
|
|
79
|
+
required verification references, and a stable recoveryId. A recovery resumes the same work item
|
|
80
|
+
and its ordinary budget; when a further attempt is necessary it grants exactly one. The single
|
|
81
|
+
autonomous grant per target is consumed on first use. After that, a further unit requires either a
|
|
82
|
+
fresh root-user message referenced by userMessageId — validated for role, session identity, and
|
|
83
|
+
timing, never wording — or a recorded advance authority referenced by runId and authorityId whose
|
|
84
|
+
finite shared reserve still has a unit. Recovery never accepts a result, replaces a reviewer, or
|
|
85
|
+
changes declared scope. A rejected report (a worker execution that finished with a protocol-invalid
|
|
86
|
+
result) is recorded with bounded diagnostics and has the same recovery path; it never becomes DONE.
|
|
87
|
+
|
|
88
|
+
Requirements may come from the supported approved native spec/plan package under .vvoc/specs/, a
|
|
89
|
+
provided plan reference, or the current conversation. Register a native package once with
|
|
90
|
+
work_checkpoint register (planPath); start and verify declared checkpoints only after their
|
|
91
|
+
prerequisite tasks are accepted. A provided-plan or conversation-scoped run is registered once with
|
|
92
|
+
work_item_open carrying an execution descriptor (executionKey, source, goal, boundary) and its
|
|
93
|
+
first task batch; later tasks append with the same runId plus amendmentId and rationale, and generic
|
|
94
|
+
review obligations run through work_checkpoint start/review/complete. A selected source stays
|
|
95
|
+
authoritative: a failed native package is never silently reopened as weaker generic execution, and
|
|
96
|
+
a provided document is never converted or executed as commands. A checkpoint passes only when
|
|
97
|
+
every declared reviewer passes against the pinned snapshot; a closed review-only FAIL report is a
|
|
98
|
+
findings result, not approval. A checkpoint generation that stopped or exhausted its ordinary
|
|
99
|
+
generations recovers through work_checkpoint action "recover" with the same bounded fields and the
|
|
100
|
+
same authority options. Do not write source files yourself:
|
|
78
101
|
delegate implementation edits, including fixes requested by reviewers, through bounded task
|
|
79
102
|
packets, while keeping planning artifacts, acceptance decisions, and verification commands in this
|
|
80
103
|
session.
|
|
@@ -250,7 +273,6 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
250
273
|
return {};
|
|
251
274
|
const resolvedPolicy = resolveOrchestrationPolicy(vvoc.config);
|
|
252
275
|
const workflowSystemInstruction = getWorkflowSystemInstruction(resolvedPolicy);
|
|
253
|
-
const delegatedProfileActive = resolvedPolicy.profile === "delegated";
|
|
254
276
|
const trustedWorkspaceRoot = worktree || directory;
|
|
255
277
|
// START_BLOCK_PERSISTENCE_SETUP
|
|
256
278
|
// Each session (main or subagent) gets its own isolated store.
|
|
@@ -452,6 +474,76 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
452
474
|
})
|
|
453
475
|
.catch(() => undefined);
|
|
454
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Settle a confirmed terminal protocol-invalid delegated report: the domain
|
|
479
|
+
* reducer applies to a staged copy, the staged state persists, and only then
|
|
480
|
+
* the settled record commits to the live store, returning the settled
|
|
481
|
+
* attempt number. A failed write keeps the live in-flight attempt so an
|
|
482
|
+
* unpersisted settlement never frees a launch slot.
|
|
483
|
+
*/
|
|
484
|
+
function commitDelegatedReportRejection(sessionId, workItemId, callId, protocolErrorCode, excerpt, explicitHardStop) {
|
|
485
|
+
const liveStore = stores.get(sessionId);
|
|
486
|
+
if (!liveStore || invalidHydrationSessions.has(sessionId))
|
|
487
|
+
return undefined;
|
|
488
|
+
const liveData = liveStore.getStoreData();
|
|
489
|
+
const stagedStore = createWorkItemStore(liveData);
|
|
490
|
+
const applied = applyDelegatedReportRejection(stagedStore, {
|
|
491
|
+
sessionId,
|
|
492
|
+
workItemId,
|
|
493
|
+
callId,
|
|
494
|
+
protocolErrorCode,
|
|
495
|
+
excerpt,
|
|
496
|
+
...(explicitHardStop ? { explicitHardStop } : {}),
|
|
497
|
+
});
|
|
498
|
+
if (!applied.ok) {
|
|
499
|
+
// Stale, duplicate, or already-transitioned: nothing to settle.
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
const stagedData = stagedStore.getStoreData();
|
|
503
|
+
const persisted = snapshotWorkflowStateChecked(sessionId, stagedData);
|
|
504
|
+
if (!persisted.ok) {
|
|
505
|
+
void client.app
|
|
506
|
+
.log({
|
|
507
|
+
body: {
|
|
508
|
+
service: "workflow",
|
|
509
|
+
level: "error",
|
|
510
|
+
message: "[workflow][reportRejection][BLOCK_REPORT_REJECTION] persistence failed",
|
|
511
|
+
extra: {
|
|
512
|
+
sessionID: sessionId,
|
|
513
|
+
workItemId,
|
|
514
|
+
attempt: applied.attempt,
|
|
515
|
+
error: persisted.error.slice(0, 300),
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
})
|
|
519
|
+
.catch(() => undefined);
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
const lookupKey = createRecordLookupKey(sessionId, workItemId);
|
|
523
|
+
const committed = stagedData.records.get(lookupKey);
|
|
524
|
+
if (!committed)
|
|
525
|
+
return undefined;
|
|
526
|
+
liveData.records.set(lookupKey, committed);
|
|
527
|
+
void client.app
|
|
528
|
+
.log({
|
|
529
|
+
body: {
|
|
530
|
+
service: "workflow",
|
|
531
|
+
level: "warn",
|
|
532
|
+
message: "[workflow][reportRejection][BLOCK_REPORT_REJECTION] terminal report rejected and settled",
|
|
533
|
+
extra: {
|
|
534
|
+
sessionID: sessionId,
|
|
535
|
+
workItemId,
|
|
536
|
+
attempt: applied.attempt,
|
|
537
|
+
protocolErrorCode,
|
|
538
|
+
observedHardStop: explicitHardStop,
|
|
539
|
+
consumedAttempts: applied.consumedAttempts,
|
|
540
|
+
attemptBudget: applied.attemptBudget,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
.catch(() => undefined);
|
|
545
|
+
return applied.attempt;
|
|
546
|
+
}
|
|
455
547
|
function handleDelegatedPartUpdated(properties) {
|
|
456
548
|
const part = properties.part;
|
|
457
549
|
if (!part || typeof part !== "object")
|
|
@@ -570,6 +662,185 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
570
662
|
}
|
|
571
663
|
}
|
|
572
664
|
// END_BLOCK_DELEGATED_AUTHORIZATION
|
|
665
|
+
// START_BLOCK_RECOVERY_SUPPORT
|
|
666
|
+
// Read-only SDK message lookup for user-authorized recovery. Only identity
|
|
667
|
+
// and timing metadata (role, sessionID, id, time.created) are retained;
|
|
668
|
+
// message bodies never enter validation, persistence, or logs. Transport
|
|
669
|
+
// failures throw so the domain reports AUTHORIZATION_LOOKUP_FAILED instead
|
|
670
|
+
// of conflating an unreachable lookup with a nonexistent message.
|
|
671
|
+
const lookupRecoveryUserMessage = async (sessionId, messageId) => {
|
|
672
|
+
const response = await client.session.message({
|
|
673
|
+
path: { id: sessionId, messageID: messageId },
|
|
674
|
+
query: { directory },
|
|
675
|
+
});
|
|
676
|
+
if (response.error || !response.data) {
|
|
677
|
+
const errorName = response.error?.name;
|
|
678
|
+
if (errorName && errorName !== "NotFound") {
|
|
679
|
+
throw new Error(`session message lookup failed: ${errorName}`);
|
|
680
|
+
}
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
const info = response.data.info;
|
|
684
|
+
return {
|
|
685
|
+
role: info.role,
|
|
686
|
+
sessionID: info.sessionID,
|
|
687
|
+
id: info.id,
|
|
688
|
+
timeCreatedMs: typeof info.time?.created === "number" && Number.isFinite(info.time.created)
|
|
689
|
+
? info.time.created
|
|
690
|
+
: undefined,
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
// Advance-authority provenance uses the same pinned SDK message response.
|
|
694
|
+
// Only verified identity/timing/eligibility metadata leaves this lookup; raw
|
|
695
|
+
// user text is never persisted or logged.
|
|
696
|
+
const lookupAuthorityMessage = async (input) => {
|
|
697
|
+
const response = await client.session.message({
|
|
698
|
+
path: { id: input.sessionId, messageID: input.messageId },
|
|
699
|
+
query: { directory },
|
|
700
|
+
});
|
|
701
|
+
if (response.error || !response.data) {
|
|
702
|
+
const errorName = response.error?.name;
|
|
703
|
+
if (errorName && errorName !== "NotFound") {
|
|
704
|
+
throw new Error(`authority message lookup failed: ${errorName}`);
|
|
705
|
+
}
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
const info = response.data.info;
|
|
709
|
+
const parts = Array.isArray(response.data.parts) ? response.data.parts : [];
|
|
710
|
+
const textParts = parts
|
|
711
|
+
.map((part) => part)
|
|
712
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
713
|
+
.map((part) => part.text);
|
|
714
|
+
return {
|
|
715
|
+
messageId: info.id ?? input.messageId,
|
|
716
|
+
sessionId: info.sessionID ?? input.sessionId,
|
|
717
|
+
role: info.role === "user" ? "user" : "assistant",
|
|
718
|
+
createdMs: typeof info.time?.created === "number" && Number.isFinite(info.time.created)
|
|
719
|
+
? info.time.created
|
|
720
|
+
: 0,
|
|
721
|
+
ignored: info.ignored === true,
|
|
722
|
+
syntheticOnly: false,
|
|
723
|
+
textParts,
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
// Per-session serialization for generic mutating tool calls. The staged
|
|
727
|
+
// snapshot persists before the committed state is published, so a failed
|
|
728
|
+
// write never exposes new obligations, authority, or launch permissions.
|
|
729
|
+
const workflowTransactions = new WorkflowTransactionQueue();
|
|
730
|
+
async function commitGenericToolResult(sessionId, run) {
|
|
731
|
+
const liveStore = stores.get(sessionId);
|
|
732
|
+
if (!liveStore) {
|
|
733
|
+
return {
|
|
734
|
+
ok: false,
|
|
735
|
+
errorCode: "SESSION_MISMATCH",
|
|
736
|
+
message: `no live workflow store for session ${sessionId}`,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (invalidHydrationSessions.has(sessionId)) {
|
|
740
|
+
return {
|
|
741
|
+
ok: false,
|
|
742
|
+
errorCode: "INVALID_STATE",
|
|
743
|
+
message: `persisted workflow state for session ${sessionId} is invalid`,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const outcome = await runWorkflowTransaction({
|
|
747
|
+
queue: workflowTransactions,
|
|
748
|
+
sessionId,
|
|
749
|
+
getData: () => liveStore.getStoreData(),
|
|
750
|
+
operation: async (staged) => {
|
|
751
|
+
const result = await run(createWorkItemStoreView(staged));
|
|
752
|
+
if (result.ok !== true) {
|
|
753
|
+
// Validation failed: persist nothing and publish nothing.
|
|
754
|
+
return { result, skipPersist: true };
|
|
755
|
+
}
|
|
756
|
+
return { result };
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
if (!outcome.ok) {
|
|
760
|
+
void client.app
|
|
761
|
+
.log({
|
|
762
|
+
body: {
|
|
763
|
+
service: "workflow",
|
|
764
|
+
level: "error",
|
|
765
|
+
message: "[workflow][generic][BLOCK_GENERIC_COMMIT] persistence failed",
|
|
766
|
+
extra: { sessionID: sessionId, error: outcome.error.slice(0, 300) },
|
|
767
|
+
},
|
|
768
|
+
})
|
|
769
|
+
.catch(() => undefined);
|
|
770
|
+
return {
|
|
771
|
+
ok: false,
|
|
772
|
+
errorCode: "PERSISTENCE_FAILED",
|
|
773
|
+
message: `generic workflow mutation could not be persisted: ${outcome.error}`,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return outcome.result;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Execute one recovery mutation durably on the live store: the domain
|
|
780
|
+
* reducer applies to the live entry (re-prechecking after any authorization
|
|
781
|
+
* await), and the whole live store is then persisted synchronously. If the
|
|
782
|
+
* write fails, the captured prior entry is restored so no unpersisted launch
|
|
783
|
+
* permission is ever exposed — there is no await between mutation, persist,
|
|
784
|
+
* and rollback, so no concurrent actor can observe the intermediate state,
|
|
785
|
+
* and no stale whole-store snapshot can regress concurrent transitions.
|
|
786
|
+
*/
|
|
787
|
+
async function executeCommittedRecovery(sessionId, runRecovery, captureRestore, wasApplied) {
|
|
788
|
+
const liveStore = stores.get(sessionId);
|
|
789
|
+
if (!liveStore || invalidHydrationSessions.has(sessionId)) {
|
|
790
|
+
throw new Error(`CONTROL_DENIED: persisted workflow state for session ${sessionId} is invalid; resolve or remove it before new control mutations.`);
|
|
791
|
+
}
|
|
792
|
+
const restore = captureRestore();
|
|
793
|
+
const result = await runRecovery(liveStore);
|
|
794
|
+
if (!wasApplied(result)) {
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
const persisted = snapshotWorkflowStateChecked(sessionId, liveStore.getStoreData());
|
|
798
|
+
if (!persisted.ok) {
|
|
799
|
+
restore();
|
|
800
|
+
void client.app
|
|
801
|
+
.log({
|
|
802
|
+
body: {
|
|
803
|
+
service: "workflow",
|
|
804
|
+
level: "error",
|
|
805
|
+
message: "[workflow][recovery][BLOCK_RECOVERY_COMMIT] recovery persistence failed",
|
|
806
|
+
extra: { sessionID: sessionId, error: persisted.error.slice(0, 300) },
|
|
807
|
+
},
|
|
808
|
+
})
|
|
809
|
+
.catch(() => undefined);
|
|
810
|
+
throw new Error(`PERSISTENCE_FAILED: recovery applied in memory but could not be persisted and was rolled back: ${persisted.error}`);
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
/** Capture and restore one work-item record entry for recovery rollback. */
|
|
815
|
+
function captureRecordRestore(sessionId, workItemId) {
|
|
816
|
+
const liveStore = stores.get(sessionId);
|
|
817
|
+
if (!liveStore)
|
|
818
|
+
return () => undefined;
|
|
819
|
+
const liveData = liveStore.getStoreData();
|
|
820
|
+
const lookupKey = createRecordLookupKey(sessionId, workItemId);
|
|
821
|
+
const prior = liveData.records.get(lookupKey);
|
|
822
|
+
return () => {
|
|
823
|
+
if (prior) {
|
|
824
|
+
liveData.records.set(lookupKey, prior);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/** Capture and restore one checkpoint entry for recovery rollback. */
|
|
829
|
+
function captureCheckpointRestore(sessionId, runId, checkpointId) {
|
|
830
|
+
const liveStore = stores.get(sessionId);
|
|
831
|
+
if (!liveStore)
|
|
832
|
+
return () => undefined;
|
|
833
|
+
const run = liveStore.getStoreData().planRuns.get(runId);
|
|
834
|
+
if (!run)
|
|
835
|
+
return () => undefined;
|
|
836
|
+
const priorCheckpoint = run.checkpoints.get(checkpointId);
|
|
837
|
+
return () => {
|
|
838
|
+
if (priorCheckpoint) {
|
|
839
|
+
run.checkpoints.set(checkpointId, priorCheckpoint);
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
// END_BLOCK_RECOVERY_SUPPORT
|
|
573
844
|
// START_BLOCK_CHECKPOINT_LINKAGE
|
|
574
845
|
/** Find the in-flight checkpoint generation whose linked review item matches. */
|
|
575
846
|
function findCheckpointByReviewItem(sessionId, reviewWorkItemId) {
|
|
@@ -592,8 +863,13 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
592
863
|
const workItemOpenTool = createWorkItemOpenTool(dummyStore);
|
|
593
864
|
const workItemListTool = createWorkItemListTool(dummyStore);
|
|
594
865
|
const workItemCloseTool = createWorkItemCloseTool(dummyStore);
|
|
595
|
-
const workItemDecideTool = createWorkItemDecideTool(dummyStore
|
|
596
|
-
|
|
866
|
+
const workItemDecideTool = createWorkItemDecideTool(dummyStore, {
|
|
867
|
+
lookupUserMessage: lookupRecoveryUserMessage,
|
|
868
|
+
});
|
|
869
|
+
const workCheckpointTool = createWorkCheckpointTool(dummyStore, {
|
|
870
|
+
lookupUserMessage: lookupRecoveryUserMessage,
|
|
871
|
+
lookupAuthorityMessage,
|
|
872
|
+
});
|
|
597
873
|
return {
|
|
598
874
|
tool: {
|
|
599
875
|
work_item_open: tool({
|
|
@@ -607,10 +883,42 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
607
883
|
writeScope: z.array(z.string()).optional(),
|
|
608
884
|
planRunId: z.string().optional(),
|
|
609
885
|
planTaskId: z.string().optional(),
|
|
886
|
+
taskId: z.string().optional(),
|
|
887
|
+
goal: z.string().optional(),
|
|
888
|
+
acceptanceCriteria: z.array(z.string()).optional(),
|
|
889
|
+
verification: z.array(z.string()).optional(),
|
|
890
|
+
dependsOn: z.array(z.string()).optional(),
|
|
891
|
+
blockedBy: z.array(z.string()).optional(),
|
|
610
892
|
})),
|
|
893
|
+
execution: z
|
|
894
|
+
.object({
|
|
895
|
+
executionKey: z.string(),
|
|
896
|
+
source: z.record(z.string(), z.unknown()),
|
|
897
|
+
goal: z.string(),
|
|
898
|
+
boundary: z.object({
|
|
899
|
+
files: z.array(z.string()),
|
|
900
|
+
directories: z.array(z.string()),
|
|
901
|
+
}),
|
|
902
|
+
checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
903
|
+
})
|
|
904
|
+
.optional(),
|
|
905
|
+
runId: z.string().optional(),
|
|
906
|
+
amendmentId: z.string().optional(),
|
|
907
|
+
rationale: z.string().optional(),
|
|
611
908
|
},
|
|
612
909
|
async execute(args, context) {
|
|
613
910
|
assertWorkflowToolAccess(context.agent, "work_item_open");
|
|
911
|
+
const isGeneric = args.execution !== undefined || args.runId !== undefined;
|
|
912
|
+
if (isGeneric) {
|
|
913
|
+
// Hydrate/validate the session store before the transaction boundary
|
|
914
|
+
// reads it, matching every other tool entry point.
|
|
915
|
+
getOrCreateStore(context.sessionID);
|
|
916
|
+
const result = await commitGenericToolResult(context.sessionID, (view) => workItemOpenTool.execute(args, {
|
|
917
|
+
sessionId: context.sessionID,
|
|
918
|
+
workspaceRoot: trustedWorkspaceRoot,
|
|
919
|
+
}, view));
|
|
920
|
+
return stringifyToolOutput(result);
|
|
921
|
+
}
|
|
614
922
|
const sessionStore = getOrCreateStore(context.sessionID);
|
|
615
923
|
const opened = workItemOpenTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
616
924
|
snapshotSession(context.sessionID);
|
|
@@ -641,73 +949,153 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
641
949
|
return stringifyToolOutput(closed);
|
|
642
950
|
},
|
|
643
951
|
}),
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
952
|
+
// Control tools are registered independent of the startup profile; they
|
|
953
|
+
// remain root-session, agent, workspace, and invalid-hydration gated at
|
|
954
|
+
// execution time.
|
|
955
|
+
...{
|
|
956
|
+
work_item_decide: tool({
|
|
957
|
+
description: workItemDecideTool.description,
|
|
958
|
+
args: {
|
|
959
|
+
workItemId: z.string(),
|
|
960
|
+
attempt: z.number().int().min(1),
|
|
961
|
+
decision: z.enum(["accept", "request_changes", "rework", "recover"]),
|
|
962
|
+
rationale: z.string().optional(),
|
|
963
|
+
evidence: z.array(z.string()).optional(),
|
|
964
|
+
concernsDisposition: z.string().optional(),
|
|
965
|
+
runId: z.string().optional(),
|
|
966
|
+
checkpointId: z.string().optional(),
|
|
967
|
+
diagnosis: z.string().optional(),
|
|
968
|
+
changedCondition: z.string().optional(),
|
|
969
|
+
verification: z.array(z.string()).optional(),
|
|
970
|
+
recoveryId: z.string().optional(),
|
|
971
|
+
userMessageId: z.string().optional(),
|
|
972
|
+
authorityId: z.string().optional(),
|
|
973
|
+
},
|
|
974
|
+
async execute(args, context) {
|
|
975
|
+
// Resolve the store first so invalid persisted state is detected
|
|
976
|
+
// before the authorization check reports it as a control denial.
|
|
977
|
+
const sessionStore = getOrCreateStore(context.sessionID);
|
|
978
|
+
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_item_decide");
|
|
979
|
+
// Recovery is the only decision family that must persist
|
|
980
|
+
// before it exposes new launch permissions. Authority-bearing
|
|
981
|
+
// recovery stages the work-item recovery and the reserve debit in
|
|
982
|
+
// one serialized transaction so they commit or fail together.
|
|
983
|
+
if (args.decision === "recover") {
|
|
984
|
+
if (args.authorityId !== undefined) {
|
|
985
|
+
const staged = await commitGenericToolResult(context.sessionID, (view) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, view));
|
|
986
|
+
return stringifyToolOutput(staged);
|
|
671
987
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
checkpointId: z.string().optional(),
|
|
682
|
-
complete: z.boolean().optional(),
|
|
683
|
-
},
|
|
684
|
-
async execute(args, context) {
|
|
685
|
-
// Resolve the store first so invalid persisted state is detected
|
|
686
|
-
// before the authorization check reports it as a control denial.
|
|
687
|
-
const sessionStore = getOrCreateStore(context.sessionID);
|
|
688
|
-
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
|
|
689
|
-
const toolContext = {
|
|
690
|
-
sessionId: context.sessionID,
|
|
691
|
-
workspaceRoot: trustedWorkspaceRoot,
|
|
692
|
-
loadPlan: async (planPath, workspaceRoot) => {
|
|
693
|
-
const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
|
|
694
|
-
return loaded.ok
|
|
695
|
-
? loaded.plan
|
|
696
|
-
: { loadError: `${loaded.code}: ${loaded.message}` };
|
|
697
|
-
},
|
|
698
|
-
};
|
|
699
|
-
const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
|
|
700
|
-
if (result.ok) {
|
|
701
|
-
const persisted = snapshotSession(context.sessionID);
|
|
702
|
-
if (!persisted.ok) {
|
|
703
|
-
throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
|
|
704
|
-
}
|
|
988
|
+
const workItemId = String(args.workItemId ?? "");
|
|
989
|
+
const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workItemDecideTool.execute(args, { sessionId: context.sessionID }, liveStore), () => captureRecordRestore(context.sessionID, workItemId), (result) => result.ok === true);
|
|
990
|
+
return stringifyToolOutput(recovered);
|
|
991
|
+
}
|
|
992
|
+
const decided = await workItemDecideTool.execute(args, { sessionId: context.sessionID }, sessionStore);
|
|
993
|
+
if (decided.ok) {
|
|
994
|
+
const persisted = snapshotSession(context.sessionID);
|
|
995
|
+
if (!persisted.ok) {
|
|
996
|
+
throw new Error(`PERSISTENCE_FAILED: decision applied in memory but could not be persisted: ${persisted.error}`);
|
|
705
997
|
}
|
|
998
|
+
}
|
|
999
|
+
return stringifyToolOutput(decided);
|
|
1000
|
+
},
|
|
1001
|
+
}),
|
|
1002
|
+
work_checkpoint: tool({
|
|
1003
|
+
description: workCheckpointTool.description,
|
|
1004
|
+
args: {
|
|
1005
|
+
action: z.enum([
|
|
1006
|
+
"register",
|
|
1007
|
+
"start",
|
|
1008
|
+
"verify",
|
|
1009
|
+
"recover",
|
|
1010
|
+
"review",
|
|
1011
|
+
"bind",
|
|
1012
|
+
"complete",
|
|
1013
|
+
"amend",
|
|
1014
|
+
"authorize",
|
|
1015
|
+
"record_approval",
|
|
1016
|
+
"revoke_authority",
|
|
1017
|
+
]),
|
|
1018
|
+
planPath: z.string().optional(),
|
|
1019
|
+
runId: z.string().optional(),
|
|
1020
|
+
checkpointId: z.string().optional(),
|
|
1021
|
+
complete: z.boolean().optional(),
|
|
1022
|
+
diagnosis: z.string().optional(),
|
|
1023
|
+
changedCondition: z.string().optional(),
|
|
1024
|
+
verification: z.array(z.string()).optional(),
|
|
1025
|
+
recoveryId: z.string().optional(),
|
|
1026
|
+
userMessageId: z.string().optional(),
|
|
1027
|
+
checkpoints: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1028
|
+
tasks: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
1029
|
+
amendmentId: z.string().optional(),
|
|
1030
|
+
rationale: z.string().optional(),
|
|
1031
|
+
startFingerprint: z.string().optional(),
|
|
1032
|
+
reviewer: z.string().optional(),
|
|
1033
|
+
authorityId: z.string().optional(),
|
|
1034
|
+
messageId: z.string().optional(),
|
|
1035
|
+
approvalId: z.string().optional(),
|
|
1036
|
+
stage: z.string().optional(),
|
|
1037
|
+
stages: z.array(z.string()).optional(),
|
|
1038
|
+
decisionScope: z.string().optional(),
|
|
1039
|
+
fileBoundary: z.array(z.string()).optional(),
|
|
1040
|
+
reservedStops: z.array(z.string()).optional(),
|
|
1041
|
+
artifactPath: z.string().optional(),
|
|
1042
|
+
artifactSha256: z.string().optional(),
|
|
1043
|
+
revocationId: z.string().optional(),
|
|
1044
|
+
},
|
|
1045
|
+
async execute(args, context) {
|
|
1046
|
+
// Resolve the store first so invalid persisted state is detected
|
|
1047
|
+
// before the authorization check reports it as a control denial.
|
|
1048
|
+
const sessionStore = getOrCreateStore(context.sessionID);
|
|
1049
|
+
await assertPrimaryControllerMutation(context.agent, context.sessionID, { directory: context.directory, worktree: context.worktree }, "work_checkpoint");
|
|
1050
|
+
const toolContext = {
|
|
1051
|
+
sessionId: context.sessionID,
|
|
1052
|
+
workspaceRoot: trustedWorkspaceRoot,
|
|
1053
|
+
loadPlan: async (planPath, workspaceRoot) => {
|
|
1054
|
+
const loaded = await loadApprovedDelegatedPlan({ workspaceRoot, planPath });
|
|
1055
|
+
return loaded.ok ? loaded.plan : { loadError: `${loaded.code}: ${loaded.message}` };
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
// Generic (non-native) executions route through the atomic
|
|
1059
|
+
// transaction boundary: staged persist, then publish.
|
|
1060
|
+
const runIdArg = String(args.runId ?? "");
|
|
1061
|
+
const liveExecution = runIdArg
|
|
1062
|
+
? sessionStore.getStoreData().executions.get(runIdArg)
|
|
1063
|
+
: undefined;
|
|
1064
|
+
const authorityAction = args.action === "authorize" ||
|
|
1065
|
+
args.action === "record_approval" ||
|
|
1066
|
+
args.action === "revoke_authority";
|
|
1067
|
+
const genericAction = args.action !== "register" &&
|
|
1068
|
+
liveExecution !== undefined &&
|
|
1069
|
+
(liveExecution.source.kind !== "native-package" || authorityAction);
|
|
1070
|
+
const nativeAuthorityRecover = args.action === "recover" &&
|
|
1071
|
+
args.authorityId !== undefined &&
|
|
1072
|
+
liveExecution !== undefined;
|
|
1073
|
+
const genericRegister = args.action === "register" && !args.planPath;
|
|
1074
|
+
if (genericAction || genericRegister || nativeAuthorityRecover) {
|
|
1075
|
+
const result = await commitGenericToolResult(context.sessionID, (view) => workCheckpointTool.execute(args, toolContext, view));
|
|
706
1076
|
return stringifyToolOutput(result);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1077
|
+
}
|
|
1078
|
+
// Checkpoint recovery commits durably on the live store and
|
|
1079
|
+
// rolls the checkpoint entry back on write failure, so a
|
|
1080
|
+
// granted generation is never exposed before its state is
|
|
1081
|
+
// durably recorded.
|
|
1082
|
+
if (args.action === "recover") {
|
|
1083
|
+
const runId = String(args.runId ?? "");
|
|
1084
|
+
const checkpointId = String(args.checkpointId ?? "");
|
|
1085
|
+
const recovered = await executeCommittedRecovery(context.sessionID, (liveStore) => workCheckpointTool.execute(args, toolContext, liveStore), () => captureCheckpointRestore(context.sessionID, runId, checkpointId), (result) => result.ok === true);
|
|
1086
|
+
return stringifyToolOutput(recovered);
|
|
1087
|
+
}
|
|
1088
|
+
const result = await workCheckpointTool.execute({ ...args }, toolContext, sessionStore);
|
|
1089
|
+
if (result.ok) {
|
|
1090
|
+
const persisted = snapshotSession(context.sessionID);
|
|
1091
|
+
if (!persisted.ok) {
|
|
1092
|
+
throw new Error(`PERSISTENCE_FAILED: checkpoint change applied in memory but could not be persisted: ${persisted.error}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return stringifyToolOutput(result);
|
|
1096
|
+
},
|
|
1097
|
+
}),
|
|
1098
|
+
},
|
|
711
1099
|
},
|
|
712
1100
|
"tool.execute.before": async (input, output) => {
|
|
713
1101
|
if (input.tool !== "task") {
|
|
@@ -754,6 +1142,41 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
754
1142
|
});
|
|
755
1143
|
throw new Error(`${WORK_ITEM_MISSING_MARKER} LAUNCH_REJECTED_UNKNOWN_WORK_ITEM: no open work item ${header.value} exists in this session. Use work_item_open first or check state with work_item_list.`);
|
|
756
1144
|
}
|
|
1145
|
+
// Generic executions enforce declared task dependencies and checkpoint
|
|
1146
|
+
// barriers at launch, not only at completion.
|
|
1147
|
+
if (subagentType === "vv-implementer") {
|
|
1148
|
+
const data = sessionStore.getStoreData();
|
|
1149
|
+
for (const execution of data.executions.values()) {
|
|
1150
|
+
if (execution.sessionId !== input.sessionID)
|
|
1151
|
+
continue;
|
|
1152
|
+
const boundTask = [...execution.tasks.values()].find((binding) => binding.workItemId === workItem.workItemId);
|
|
1153
|
+
if (!boundTask)
|
|
1154
|
+
continue;
|
|
1155
|
+
const launchable = isTaskLaunchableInStore(data, {
|
|
1156
|
+
sessionId: input.sessionID,
|
|
1157
|
+
runId: execution.runId,
|
|
1158
|
+
taskId: boundTask.taskId,
|
|
1159
|
+
});
|
|
1160
|
+
if (!launchable.ok) {
|
|
1161
|
+
await client.app.log({
|
|
1162
|
+
body: {
|
|
1163
|
+
service: "workflow",
|
|
1164
|
+
level: "warn",
|
|
1165
|
+
message: "[workflow][launchValidation][BLOCK_VALIDATE_LAUNCH] dependency gate",
|
|
1166
|
+
extra: {
|
|
1167
|
+
sessionID: input.sessionID,
|
|
1168
|
+
workItemId: workItem.workItemId,
|
|
1169
|
+
runId: execution.runId,
|
|
1170
|
+
taskId: boundTask.taskId,
|
|
1171
|
+
reason: launchable.reason,
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
throw new Error(`${INVALID_NEXT_AGENT_MARKER} LAUNCH_REJECTED_DEPENDENCY: task ${boundTask.taskId} cannot start yet: ${launchable.message}`);
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
757
1180
|
const allowedNextAgents = getAllowedNextAgents(workItem);
|
|
758
1181
|
const reviewRound = getReviewRound(workItem);
|
|
759
1182
|
const attemptedRound = subagentType === "vv-implementer" ? getAttemptedImplementationRound(workItem) : reviewRound;
|
|
@@ -984,7 +1407,7 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
984
1407
|
expectedWorkItemId: header.value,
|
|
985
1408
|
});
|
|
986
1409
|
if (!parsed.ok) {
|
|
987
|
-
const explicitHardStop =
|
|
1410
|
+
const explicitHardStop = detectExplicitHardStopStatus(unwrapped.normalizedOutput);
|
|
988
1411
|
if (unwrapped.envelope &&
|
|
989
1412
|
isTrackedResultRepairEligible(parsed.error.code) &&
|
|
990
1413
|
!explicitHardStop) {
|
|
@@ -1042,8 +1465,44 @@ export const WorkflowPlugin = async ({ client, directory, worktree }) => {
|
|
|
1042
1465
|
},
|
|
1043
1466
|
});
|
|
1044
1467
|
revertLaunch();
|
|
1468
|
+
// A confirmed terminal delegated execution whose report stays
|
|
1469
|
+
// protocol-invalid settles as a completed execution with a rejected
|
|
1470
|
+
// report: never in flight, never DONE, with the observed substantive
|
|
1471
|
+
// hard stop preserved and a supported recovery path remaining.
|
|
1472
|
+
// Unknown (non-string) or mismatched outputs never reach here.
|
|
1473
|
+
const currentForSettlement = getWorkItem(getOrCreateStore(input.sessionID), input.sessionID, header.value);
|
|
1474
|
+
const settledAttempt = (() => {
|
|
1475
|
+
if (!protocolFailureExcerpt || !currentForSettlement)
|
|
1476
|
+
return undefined;
|
|
1477
|
+
if (currentForSettlement.state === "closed" ||
|
|
1478
|
+
currentForSettlement.mode !== "delegated" ||
|
|
1479
|
+
subagentType !== "vv-implementer") {
|
|
1480
|
+
return undefined;
|
|
1481
|
+
}
|
|
1482
|
+
if (!currentForSettlement.delegated?.attempts.some((attempt) => attempt.status === "in_flight" && attempt.callId === input.callID)) {
|
|
1483
|
+
return undefined;
|
|
1484
|
+
}
|
|
1485
|
+
const observedHardStop = detectExplicitHardStopStatus(unwrapped.normalizedOutput);
|
|
1486
|
+
return commitDelegatedReportRejection(input.sessionID, header.value, input.callID, parsed.error.code, protocolFailureExcerpt, observedHardStop);
|
|
1487
|
+
})();
|
|
1488
|
+
const settlementLines = settledAttempt
|
|
1489
|
+
? [
|
|
1490
|
+
`Report rejected: attempt ${settledAttempt} of ${header.value} settled as report_rejected with bounded diagnostics retained.`,
|
|
1491
|
+
(() => {
|
|
1492
|
+
const settledRecord = getWorkItem(getOrCreateStore(input.sessionID), input.sessionID, header.value);
|
|
1493
|
+
const progress = settledRecord
|
|
1494
|
+
? summarizeDelegatedProgress(settledRecord)
|
|
1495
|
+
: undefined;
|
|
1496
|
+
return `Next action: ${progress?.nextAction ?? "inspect work_item_list"}${progress?.nextAction === "recover" ||
|
|
1497
|
+
progress?.nextAction === "recover_with_user_authorization"
|
|
1498
|
+
? ' through work_item_decide decision "recover"'
|
|
1499
|
+
: ""}.`;
|
|
1500
|
+
})(),
|
|
1501
|
+
]
|
|
1502
|
+
: [];
|
|
1045
1503
|
throw new Error([
|
|
1046
1504
|
`RESULT_PROTOCOL_ERROR: ${parsed.error.message}`,
|
|
1505
|
+
...settlementLines,
|
|
1047
1506
|
formatResultExcerptForError(protocolFailureExcerpt),
|
|
1048
1507
|
].join("\n"));
|
|
1049
1508
|
}
|