cool-workflow 0.2.4 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +202 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +124 -13
- package/dist/mcp-server.js +20 -0
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +120 -63
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +195 -23
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +32 -0
- package/dist/shell/fs-atomic.js +93 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/multi-agent-cli.js +5 -1
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/pipeline.js +2 -1
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/run-export.js +52 -4
- package/dist/shell/run-store.js +156 -0
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +104 -10
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +79 -5
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +15 -0
- package/docs/cli-mcp-parity.7.md +25 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +36 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +6 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +39 -9
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +10 -4
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +24 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +4 -2
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +128 -30
- package/scripts/release-gate.js +184 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +41 -9
- package/ui/workbench/app.js +160 -27
- package/ui/workbench/index.html +2 -2
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/dist/shell/commit.js
CHANGED
|
@@ -199,7 +199,13 @@ function commitState(run, input) {
|
|
|
199
199
|
// NOT prematurely move the run off "interpret".
|
|
200
200
|
if (gate.verifierGated)
|
|
201
201
|
run.loopStage = "checkpoint";
|
|
202
|
-
(
|
|
202
|
+
// Only store the commit's own (small, bounded) record. The whole run
|
|
203
|
+
// (all tasks, nodes, workers, feedback, and run.commits itself) used to
|
|
204
|
+
// be embedded here too, but no reader anywhere ever parses a `run` key
|
|
205
|
+
// back out of a commits/<id>.json file — see docs/run-retention-reclamation.7.md.
|
|
206
|
+
// Embedding the growing run made total commit-dir bytes grow like N^2
|
|
207
|
+
// in the number of commits.
|
|
208
|
+
(0, fs_atomic_1.writeJson)(snapshotPath, { commit });
|
|
203
209
|
run.commits.push(commit);
|
|
204
210
|
return commit;
|
|
205
211
|
}
|
|
@@ -214,7 +220,7 @@ function recordCommitNode(run, commit, options, gate) {
|
|
|
214
220
|
artifacts: [{ id: "snapshot", kind: "json", path: commit.snapshotPath }],
|
|
215
221
|
evidence: commit.evidence || verifierNode.evidence,
|
|
216
222
|
metadata: { ...(options.metadata || {}), reason: options.reason, commitId: commit.id, verifierGated: true, checkpoint: false, verifierNodeId: verifierNode.id, candidateId: gate.candidateId, selectionId: gate.selectionId, selectionNodeId: gate.selectionNodeId },
|
|
217
|
-
}, { persist: false, persistNode: node_store_1.writeRunNode });
|
|
223
|
+
}, { persist: false, persistNode: node_store_1.writeRunNode, pathExists: fs.existsSync });
|
|
218
224
|
if (gate.selectionNodeId && commitResult.outputNodeId)
|
|
219
225
|
linkAdditionalParent(run, gate.selectionNodeId, commitResult.outputNodeId);
|
|
220
226
|
return commitResult.outputNodeId;
|
|
@@ -158,6 +158,75 @@ function dirtySetsFor(state) {
|
|
|
158
158
|
function markBlackboardDirty(state, kind, id) {
|
|
159
159
|
dirtySetsFor(state)[kind].add(id);
|
|
160
160
|
}
|
|
161
|
+
// Dirty-id tracking for state.messages, PARALLEL to the 5-kind tracking
|
|
162
|
+
// above (not folded in: messages share ONE ordered log file, not a
|
|
163
|
+
// recordPath-per-id file). The only push site is postBlackboardMessage
|
|
164
|
+
// (`state.messages.push(message)`), so the dirty ids at persist time are
|
|
165
|
+
// always exactly the trailing ids of state.messages since the last
|
|
166
|
+
// persist. Kept off the serialized state for the same reason as
|
|
167
|
+
// blackboardDirtySets: a WeakMap, never a field on BlackboardState.
|
|
168
|
+
const blackboardMessagesDirty = new WeakMap();
|
|
169
|
+
function dirtyMessageIdsFor(state) {
|
|
170
|
+
let ids = blackboardMessagesDirty.get(state);
|
|
171
|
+
if (!ids) {
|
|
172
|
+
ids = new Set();
|
|
173
|
+
blackboardMessagesDirty.set(state, ids);
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
function markBlackboardMessageDirty(state, id) {
|
|
178
|
+
dirtyMessageIdsFor(state).add(id);
|
|
179
|
+
}
|
|
180
|
+
// Writes messages.jsonl for a persist call, or skips it entirely when
|
|
181
|
+
// nothing changed (the common case: 7 of the 8 persistBlackboardState call
|
|
182
|
+
// sites never touch state.messages).
|
|
183
|
+
//
|
|
184
|
+
// Fast path (the common case for the ONE call site that does change
|
|
185
|
+
// messages, postBlackboardMessage): the dirty ids are exactly the trailing
|
|
186
|
+
// `k` entries of state.messages (the sole push site always appends). Sort
|
|
187
|
+
// only that small batch, check it does not need to interleave with the
|
|
188
|
+
// already-on-disk tail, and APPEND it — no read, no resort, no rewrite of
|
|
189
|
+
// the earlier messages. This turns posting M messages from O(M^2) written
|
|
190
|
+
// bytes into O(M) total.
|
|
191
|
+
//
|
|
192
|
+
// Fallback (a caller-supplied custom --id ties createdAt with an earlier
|
|
193
|
+
// message and sorts BEFORE it, so a plain append would leave the file out
|
|
194
|
+
// of order): resort the whole array and rewrite the whole file, byte-
|
|
195
|
+
// identical to the code path this replaces.
|
|
196
|
+
//
|
|
197
|
+
// Either way state.messages is left fully sorted after this call, exactly
|
|
198
|
+
// as the old unconditional `.sort()` left it — buildBlackboardGraph (which
|
|
199
|
+
// iterates state.messages in raw array order) sees the same order as
|
|
200
|
+
// before.
|
|
201
|
+
function persistBlackboardMessages(run, state) {
|
|
202
|
+
const dirty = dirtyMessageIdsFor(state);
|
|
203
|
+
if (dirty.size === 0)
|
|
204
|
+
return;
|
|
205
|
+
const file = messagesPath(run);
|
|
206
|
+
const total = state.messages.length;
|
|
207
|
+
const k = dirty.size;
|
|
208
|
+
const priorCount = total - k;
|
|
209
|
+
const batch = priorCount >= 0 ? state.messages.slice(priorCount).sort(cb.compareRecords) : [];
|
|
210
|
+
const batchMatchesDirty = batch.length === dirty.size && batch.every((message) => dirty.has(message.id));
|
|
211
|
+
const priorTail = priorCount > 0 ? state.messages[priorCount - 1] : undefined;
|
|
212
|
+
const canAppend = batchMatchesDirty && (!priorTail || cb.compareRecords(priorTail, batch[0]) <= 0);
|
|
213
|
+
if (canAppend) {
|
|
214
|
+
state.messages.splice(priorCount, k, ...batch);
|
|
215
|
+
const lines = batch.map((message) => `${JSON.stringify(message)}\n`).join("");
|
|
216
|
+
const currentBytes = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
217
|
+
// Same torn-tail guard as trust-audit's events.jsonl (shared helper):
|
|
218
|
+
// a crash mid-append can leave the log without its final "\n"; put the
|
|
219
|
+
// new lines on their own clean line rather than merging with a torn
|
|
220
|
+
// tail.
|
|
221
|
+
const leadingNewline = currentBytes > 0 && !(0, fs_atomic_1.logEndsWithNewline)(file, currentBytes) ? "\n" : "";
|
|
222
|
+
(0, fs_atomic_1.durableAppendFileSync)(file, leadingNewline + lines);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Same bytes the old unconditional code path always wrote.
|
|
226
|
+
(0, fs_atomic_1.writeTextDurable)(file, state.messages.sort(cb.compareRecords).map((message) => JSON.stringify(message)).join("\n") + (state.messages.length ? "\n" : ""));
|
|
227
|
+
}
|
|
228
|
+
dirty.clear();
|
|
229
|
+
}
|
|
161
230
|
function linkMultiAgent(run, blackboardId, topicIds, input) {
|
|
162
231
|
const groupId = input.agentGroupId ?? input.groupId;
|
|
163
232
|
const roleId = input.agentRoleId ?? input.roleId;
|
|
@@ -218,7 +287,9 @@ function persistBlackboardState(run) {
|
|
|
218
287
|
decisions: state.decisions.map(cb.indexRow),
|
|
219
288
|
messages: state.messages.map((message) => ({ id: message.id, blackboardId: message.blackboardId, topicId: message.topicId, createdAt: message.createdAt, status: message.status, author: message.author, evidenceRefs: message.linkedEvidenceRefs, artifactRefIds: message.linkedArtifactRefIds })),
|
|
220
289
|
});
|
|
221
|
-
|
|
290
|
+
// messages.jsonl: see persistBlackboardMessages above for the
|
|
291
|
+
// skip-if-unchanged / append-if-safe / full-rewrite-fallback split.
|
|
292
|
+
persistBlackboardMessages(run, state);
|
|
222
293
|
const dirty = dirtySetsFor(state);
|
|
223
294
|
for (const id of dirty.topics) {
|
|
224
295
|
const record = state.topics.find((entry) => entry.id === id);
|
|
@@ -308,6 +379,7 @@ function postBlackboardMessage(run, input) {
|
|
|
308
379
|
requireArtifactRefs(run, input.artifactRefIds || []);
|
|
309
380
|
const message = cb.buildMessage(run.id, board, topic, input, id, now(), trust_policy_io_1.hashText, sourceForActorLocal);
|
|
310
381
|
state.messages.push(message);
|
|
382
|
+
markBlackboardMessageDirty(state, message.id);
|
|
311
383
|
topic.messageIds = cb.unique([...topic.messageIds, message.id]);
|
|
312
384
|
board.messageCount = state.messages.filter((entry) => entry.blackboardId === board.id).length;
|
|
313
385
|
cb.touch(topic, now());
|
package/dist/shell/drive.js
CHANGED
|
@@ -630,7 +630,7 @@ function maybeExpandLoop(run) {
|
|
|
630
630
|
requiresEvidence: t.requiresEvidence,
|
|
631
631
|
sandboxProfileId: t.sandboxProfileId,
|
|
632
632
|
},
|
|
633
|
-
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node) });
|
|
633
|
+
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node), pathExists: fs.existsSync });
|
|
634
634
|
t.stateNodeId = result.outputNodeId;
|
|
635
635
|
}
|
|
636
636
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
@@ -656,17 +656,32 @@ function driveStep(ctx) {
|
|
|
656
656
|
return processSelectedTask(ctx, selected.id);
|
|
657
657
|
}
|
|
658
658
|
/** Dispatch every batch task (sequential — dispatch mutates state), then
|
|
659
|
-
* collect ALL
|
|
660
|
-
* (
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
659
|
+
* collect ALL agent child outcomes in one concurrent window: spawn-style
|
|
660
|
+
* (CLI-binary) agents through the batch delegate child, and endpoint-mode
|
|
661
|
+
* agents through the HTTP batch delegate child — so `--concurrency N` runs N
|
|
662
|
+
* endpoint delegations in parallel instead of one-at-a-time on the serial
|
|
663
|
+
* path. Returns outcomes keyed by task id; a cache-hit, endpoint-configured
|
|
664
|
+
* agent, or sub-workflow task gets no prepared outcome and settles through
|
|
665
|
+
* the serial accept path inside processSelectedTask (a sub-workflow task is
|
|
666
|
+
* never spawn-eligible in the first place — processSelectedTask always
|
|
667
|
+
* takes its own runSubWorkflow branch for it, so building a spawn job here
|
|
668
|
+
* would only be spawned and thrown away unused). Dispatch failures become
|
|
669
|
+
* recorded fail steps up front, exactly what the serial path would emit. */
|
|
666
670
|
function prepareConcurrentOutcomes(ctx, batch) {
|
|
667
671
|
const failSteps = new Map();
|
|
668
672
|
const jobs = [];
|
|
669
673
|
const jobTaskIds = [];
|
|
674
|
+
const endpointJobs = [];
|
|
675
|
+
const endpointTaskIds = [];
|
|
676
|
+
// Set whenever a task's status flips pending -> running (workerId
|
|
677
|
+
// assigned) this round, regardless of whether it later becomes a real
|
|
678
|
+
// spawn job, an endpoint job, a cache hit, or a sub-workflow task. That
|
|
679
|
+
// dispatch is a real state mutation on the round-cached run object that
|
|
680
|
+
// nothing has written to disk yet — the pre-spawn checkpoint below must
|
|
681
|
+
// flush it even when the batch produces zero spawn/endpoint jobs (e.g. an
|
|
682
|
+
// all-sub-workflow batch), or a crash before the round-end flush loses
|
|
683
|
+
// the dispatch.
|
|
684
|
+
let dispatchedThisRound = false;
|
|
670
685
|
for (const taskId of batch) {
|
|
671
686
|
const run = loadRun(ctx);
|
|
672
687
|
const task = run.tasks.find((candidate) => candidate.id === taskId);
|
|
@@ -681,6 +696,7 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
681
696
|
continue;
|
|
682
697
|
}
|
|
683
698
|
workerId = dispatchedTask.workerId;
|
|
699
|
+
dispatchedThisRound = true;
|
|
684
700
|
}
|
|
685
701
|
if (!workerId) {
|
|
686
702
|
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
|
|
@@ -694,7 +710,15 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
694
710
|
const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
|
|
695
711
|
if (cachePath && fs.existsSync(cachePath))
|
|
696
712
|
continue;
|
|
697
|
-
|
|
713
|
+
// A sub-workflow task never goes through the spawn path — processSelectedTask
|
|
714
|
+
// checks task.subWorkflow before it ever looks at a prepared spawn outcome
|
|
715
|
+
// and always takes its own runSubWorkflow branch instead (mirrors that
|
|
716
|
+
// check byte-for-byte). Building a spawn OR endpoint job for it here would
|
|
717
|
+
// just be a real agent child spawned/delegated and thrown away unused.
|
|
718
|
+
const subWorkflow = task.subWorkflow;
|
|
719
|
+
if (subWorkflow)
|
|
720
|
+
continue;
|
|
721
|
+
const request = {
|
|
698
722
|
schemaVersion: 1,
|
|
699
723
|
runId: ctx.runId,
|
|
700
724
|
taskId: task.id,
|
|
@@ -705,7 +729,8 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
705
729
|
label: task.id,
|
|
706
730
|
timeoutMs: ctx.config.timeoutMs,
|
|
707
731
|
delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
|
|
708
|
-
}
|
|
732
|
+
};
|
|
733
|
+
const job = (0, agent_1.prepareAgentSpawn)(request);
|
|
709
734
|
if (job) {
|
|
710
735
|
const sandboxPolicy = manifest.sandboxPolicy;
|
|
711
736
|
if (sandboxPolicy) {
|
|
@@ -713,32 +738,53 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
713
738
|
}
|
|
714
739
|
jobs.push(job);
|
|
715
740
|
jobTaskIds.push(taskId);
|
|
741
|
+
continue;
|
|
716
742
|
}
|
|
743
|
+
// Endpoint-configured agent: no CLI binary to batch-spawn, but its HTTP
|
|
744
|
+
// delegation joins the concurrent window through the endpoint batch child
|
|
745
|
+
// (runEndpointBatchOutcomes) rather than settling one-at-a-time on the
|
|
746
|
+
// serial path. An unconfigured agent (neither command nor endpoint) returns
|
|
747
|
+
// undefined from both prepares here and falls to the serial refusal.
|
|
748
|
+
const endpointJob = (0, agent_1.prepareEndpointJob)(request);
|
|
749
|
+
if (endpointJob) {
|
|
750
|
+
endpointJobs.push(endpointJob);
|
|
751
|
+
endpointTaskIds.push(taskId);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const totalJobs = jobs.length + endpointJobs.length;
|
|
755
|
+
if (totalJobs) {
|
|
756
|
+
emitProgress(`⇉ concurrent round: ${totalJobs} agent${totalJobs > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
717
757
|
}
|
|
718
|
-
if (
|
|
719
|
-
emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
758
|
+
if (totalJobs || dispatchedThisRound) {
|
|
720
759
|
// Every task above that reached "pending" got dispatched (workerId
|
|
721
760
|
// assigned) in the round-cached run object, but nothing durable was
|
|
722
761
|
// written for it — unlike the serial path's own dispatch branch, which
|
|
723
762
|
// always checkpoints immediately. Flush now, BEFORE the batch's long
|
|
724
|
-
//
|
|
725
|
-
// then leaves state.json correctly showing
|
|
726
|
-
// instead of losing the dispatch entirely.
|
|
763
|
+
// settle window opens: a crash mid-spawn OR mid-sub-workflow-recursion
|
|
764
|
+
// (both can run for minutes) then leaves state.json correctly showing
|
|
765
|
+
// these tasks as dispatched, instead of losing the dispatch entirely.
|
|
766
|
+
// Gated on dispatchedThisRound (not just totalJobs) so a batch made
|
|
767
|
+
// entirely of sub-workflow and/or cache-hit tasks — which builds zero
|
|
768
|
+
// spawn/endpoint jobs — still gets this flush before its long-running
|
|
769
|
+
// settle loop starts.
|
|
727
770
|
(0, run_store_1.saveCheckpoint)(loadRun(ctx));
|
|
728
771
|
}
|
|
729
|
-
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
730
772
|
const outcomes = new Map();
|
|
773
|
+
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
731
774
|
jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
|
|
775
|
+
const endpointSettled = (0, agent_1.runEndpointBatchOutcomes)(endpointJobs);
|
|
776
|
+
endpointTaskIds.forEach((taskId, index) => outcomes.set(taskId, endpointSettled[index]));
|
|
732
777
|
return { outcomes, failSteps };
|
|
733
778
|
}
|
|
734
779
|
/** One concurrent round inside one cached in-memory run: dispatches every
|
|
735
780
|
* batch task, spawns all spawn-style agent children in one concurrent
|
|
736
|
-
* window
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
781
|
+
* window (CLI-binary agents through the batch child, endpoint agents
|
|
782
|
+
* through the HTTP batch child), then settles + accepts in DETERMINISTIC
|
|
783
|
+
* batch (task-id) order regardless of wall-clock finish order. At round end
|
|
784
|
+
* it flushes once: commitState(run, "concurrent-round:<n>-tasks") +
|
|
785
|
+
* writeReport + saveCheckpoint. Cache-hit tasks get no prepared outcome and
|
|
786
|
+
* settle through the serial path (still inside this one deferred-persist
|
|
787
|
+
* round). If no step was produced (nothing
|
|
742
788
|
* runnable at round entry — terminal/blocked/token-budget gate) the
|
|
743
789
|
* round degrades to one plain driveStep. Byte-exact to the old build's
|
|
744
790
|
* src/drive.ts's driveConcurrentRound. */
|
|
@@ -1004,28 +1050,35 @@ function finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhau
|
|
|
1004
1050
|
* exact synchronous shape) and fixed only in driveAsync(). */
|
|
1005
1051
|
function drive(runId, cwd, options = {}) {
|
|
1006
1052
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1053
|
+
// Resolve the run dir before the mutex: a missing run throws "Run not found"
|
|
1054
|
+
// here (never a drive.lock side effect on a nonexistent run), via a stat only
|
|
1055
|
+
// so the per-round read accounting is unchanged. Mirrors withRunStateLock's
|
|
1056
|
+
// own probe-before-lock.
|
|
1057
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1058
|
+
return (0, run_store_1.withDriveLock)(runDir, runId, () => {
|
|
1059
|
+
const steps = [];
|
|
1060
|
+
const run0 = loadRun(ctx);
|
|
1061
|
+
const plannedWorkers = run0.tasks.length;
|
|
1062
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1063
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1064
|
+
let exhaustedMaxIterations = !options.once;
|
|
1065
|
+
const stopSignal = createStopSignalController(runId);
|
|
1066
|
+
stopSignal.install();
|
|
1067
|
+
try {
|
|
1068
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1069
|
+
if (stopSignal.getInterruptedBy())
|
|
1070
|
+
break;
|
|
1071
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1072
|
+
exhaustedMaxIterations = false;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1022
1075
|
}
|
|
1023
1076
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1077
|
+
finally {
|
|
1078
|
+
stopSignal.remove();
|
|
1079
|
+
}
|
|
1080
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1081
|
+
});
|
|
1029
1082
|
}
|
|
1030
1083
|
/** Same contract and same DriveResult as drive() -- but actually
|
|
1031
1084
|
* interruptible. After every round it awaits yieldToEventLoop(), a real
|
|
@@ -1053,29 +1106,33 @@ function drive(runId, cwd, options = {}) {
|
|
|
1053
1106
|
* future cycle if it proves needed in practice. */
|
|
1054
1107
|
async function driveAsync(runId, cwd, options = {}) {
|
|
1055
1108
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1109
|
+
// Resolve the run dir before the mutex — see drive() above.
|
|
1110
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1111
|
+
return (0, run_store_1.withDriveLockAsync)(runDir, runId, async () => {
|
|
1112
|
+
const steps = [];
|
|
1113
|
+
const run0 = loadRun(ctx);
|
|
1114
|
+
const plannedWorkers = run0.tasks.length;
|
|
1115
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1116
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1117
|
+
let exhaustedMaxIterations = !options.once;
|
|
1118
|
+
const stopSignal = createStopSignalController(runId);
|
|
1119
|
+
stopSignal.install();
|
|
1120
|
+
try {
|
|
1121
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1122
|
+
if (stopSignal.getInterruptedBy())
|
|
1123
|
+
break;
|
|
1124
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1125
|
+
exhaustedMaxIterations = false;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
await yieldToEventLoop();
|
|
1071
1129
|
}
|
|
1072
|
-
await yieldToEventLoop();
|
|
1073
1130
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1131
|
+
finally {
|
|
1132
|
+
stopSignal.remove();
|
|
1133
|
+
}
|
|
1134
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1135
|
+
});
|
|
1079
1136
|
}
|
|
1080
1137
|
function drivePreview(runId, cwd, args = {}) {
|
|
1081
1138
|
const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
|
|
@@ -290,6 +290,12 @@ function resolveFeedback(run, feedbackId, result) {
|
|
|
290
290
|
function runPipelineStage(run, stageId, inputNodeId, options = {}, runnerOptions = {}) {
|
|
291
291
|
return (0, runner_1.runPipelineStage)(run, stageId, inputNodeId, options, {
|
|
292
292
|
...runnerOptions,
|
|
293
|
+
// The core gate's `pathExists` defaults to `() => true` (a pure core/
|
|
294
|
+
// module never reads the filesystem). This is the shell seam, so default
|
|
295
|
+
// it to the real `fs.existsSync` — that is what makes the contract's
|
|
296
|
+
// `requireReadablePaths` / `missing-artifact-path` gate live. A caller may
|
|
297
|
+
// still override it (e.g. a replay against a captured path set).
|
|
298
|
+
pathExists: runnerOptions.pathExists || fs.existsSync,
|
|
293
299
|
// Keep the caller-named failure node id (the old build honored outputNodeId
|
|
294
300
|
// for the preserved failure node); the raw core auto-mints when unset.
|
|
295
301
|
failureNodeId: runnerOptions.failureNodeId || options.outputNodeId,
|