@sema-agent/core 1.450.0 → 1.452.0
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/dist/agents/observer.js +3 -0
- package/dist/agents/subagent.js +51 -12
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/exec-output-tail.d.ts +18 -1
- package/dist/core/exec-output-tail.js +38 -5
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +10 -0
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/memory.js +1 -1
- package/dist/core/runner/prepare-task.js +11 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry.d.ts +15 -9
- package/dist/core/task-registry.js +290 -53
- package/dist/core/tool-result-store.js +2 -2
- package/dist/core/tools.d.ts +5 -0
- package/dist/core/tools.js +3 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
- package/dist/engine/execution-env/node-execution-env.js +130 -20
- package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +2 -0
- package/dist/orchestration/run-workflow-tool.js +35 -6
- package/dist/orchestration/workflow.d.ts +9 -0
- package/dist/orchestration/workflow.js +80 -5
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.js +3 -2
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/index.js +184 -120
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +2 -0
- package/dist/tools/web.js +105 -19
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { defineTool } from "../core/tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
3
|
import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
4
4
|
import { startWorkflow } from "./workflow.js";
|
|
5
5
|
import { buildWorkflowPrimitives } from "./workflow-primitives.js";
|
|
@@ -27,8 +27,19 @@ function completionSummary(name, run) {
|
|
|
27
27
|
const failed = run.agents.filter((a) => a.status === "failed").length;
|
|
28
28
|
const secs = run.endedAt !== undefined ? Math.max(0, Math.round((run.endedAt - run.startedAt) / 1000)) : undefined;
|
|
29
29
|
const elapsed = secs === undefined ? "" : secs >= 60 ? ` in ${Math.floor(secs / 60)}m${secs % 60}s` : ` in ${secs}s`;
|
|
30
|
+
const resumeClause = run.resume === undefined
|
|
31
|
+
? ""
|
|
32
|
+
: run.resume.journalEntries === 0
|
|
33
|
+
? "; resume found NO journal entries — nothing replayed, the whole script re-ran live"
|
|
34
|
+
: `; resume: replayed ${run.resume.replayed} of ${run.resume.journalEntries} journaled results`;
|
|
30
35
|
return (`workflow${cleanName ? ` "${cleanName}"` : ""} completed${elapsed} — agents ${done}/${run.agents.length} done` +
|
|
31
|
-
`${failed > 0 ? ` (${failed} failed)` : ""}; full result + per-agent rows via TaskOutput("${run.id}")`);
|
|
36
|
+
`${failed > 0 ? ` (${failed} failed)` : ""}${resumeClause}; full result + per-agent rows via TaskOutput("${run.id}")`);
|
|
37
|
+
}
|
|
38
|
+
function completionIdFromRejection(err) {
|
|
39
|
+
if (err === null || (typeof err !== "object" && typeof err !== "function"))
|
|
40
|
+
return undefined;
|
|
41
|
+
const run = err.workflowRun;
|
|
42
|
+
return run?.completionId;
|
|
32
43
|
}
|
|
33
44
|
function workflowUsageBlock(run) {
|
|
34
45
|
const agents = run.agents;
|
|
@@ -38,19 +49,26 @@ function workflowUsageBlock(run) {
|
|
|
38
49
|
agents_done: agents.filter((a) => a.status === "completed").length,
|
|
39
50
|
agents_error: agents.filter((a) => a.status === "failed").length,
|
|
40
51
|
agents_empty_result: agents.filter((a) => a.status === "completed" && (a.output === undefined || a.output === "")).length,
|
|
52
|
+
agents_replayed: agents.filter((a) => a.replayed === true).length,
|
|
53
|
+
...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
|
|
54
|
+
...(run.resume !== undefined ? { resume_journal_entries: run.resume.journalEntries } : {}),
|
|
41
55
|
subagent_tokens: run.stats.tokens + run.stats.nested.tokens,
|
|
42
56
|
tool_uses: agents.reduce((n, a) => n + (a.toolCalls ?? 0), 0),
|
|
43
57
|
...(run.endedAt !== undefined ? { duration_ms: run.endedAt - run.startedAt } : {}),
|
|
44
58
|
};
|
|
45
59
|
}
|
|
46
|
-
function workflowDiagnostics(runId, journalRef) {
|
|
60
|
+
function workflowDiagnostics(runId, journalRef, resumeMiss) {
|
|
47
61
|
const journalTeach = journalRef === undefined
|
|
48
62
|
? ""
|
|
49
63
|
: journalRef.includes("://")
|
|
50
64
|
? ` Full journal: ${journalRef}.`
|
|
51
65
|
: ` Full journal: ${journalRef} (a deployment data path — if Read is denied there, use the TaskOutput route above instead).`;
|
|
66
|
+
const resumeMissTeach = resumeMiss
|
|
67
|
+
? ` NOTE: this was a resume, but the prior run's journal was not usable under this scope — verify the runId and that this deployment wires a journalStore; the results above are from a full live re-run.`
|
|
68
|
+
: "";
|
|
52
69
|
return (`Per-agent rows (status / error / elapsed): TaskOutput({ task_id: "${runId}" }) — read them before assuming an empty or unexpected result.` +
|
|
53
70
|
journalTeach +
|
|
71
|
+
resumeMissTeach +
|
|
54
72
|
` To continue after editing the script: re-invoke with resumeFromRunId: "${runId}" (completed agents replay from the journal; only new/changed calls run).`);
|
|
55
73
|
}
|
|
56
74
|
const SCRIPT_ERROR_CODES = new Set([
|
|
@@ -151,7 +169,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
151
169
|
const builtinsEnabled = d.builtinWorkflows !== false;
|
|
152
170
|
const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore, builtinsEnabled));
|
|
153
171
|
const sizeGuidelineSection = workflowSizeGuidelineSection(d.sizeGuideline ?? lim.sizeGuideline);
|
|
154
|
-
const structuredError = (message) => (
|
|
172
|
+
const structuredError = (message) => errorResult(JSON.stringify({ error: message }));
|
|
155
173
|
return defineTool({
|
|
156
174
|
name: RUN_WORKFLOW_TOOL_NAME,
|
|
157
175
|
aliases: ["RunWorkflow", "run_workflow"],
|
|
@@ -460,6 +478,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
460
478
|
...(notification.result !== undefined ? { result: redactHostLeaks(notification.result) } : {}),
|
|
461
479
|
...(notification.usage !== undefined ? { usage: notification.usage } : {}),
|
|
462
480
|
...(notification.diagnostics !== undefined ? { diagnostics: redactHostLeaks(notification.diagnostics) } : {}),
|
|
481
|
+
...(notification.completionId !== undefined ? { completionId: notification.completionId } : {}),
|
|
463
482
|
}))
|
|
464
483
|
.catch(() => { });
|
|
465
484
|
};
|
|
@@ -472,7 +491,8 @@ export async function createRunWorkflowTool(d) {
|
|
|
472
491
|
summary: completionSummary(meta.name, res.run),
|
|
473
492
|
result: boundedRedactedSummary(res.result, 4000),
|
|
474
493
|
usage: workflowUsageBlock(res.run),
|
|
475
|
-
diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope)),
|
|
494
|
+
diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope), res.run.resume?.journalEntries === 0),
|
|
495
|
+
completionId: res.run.completionId,
|
|
476
496
|
}), (err) => fire({
|
|
477
497
|
task_id: runId,
|
|
478
498
|
task_type: "workflow",
|
|
@@ -480,6 +500,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
480
500
|
status: "failed",
|
|
481
501
|
summary: failureSummary(err, runId),
|
|
482
502
|
diagnostics: workflowDiagnostics(runId, undefined),
|
|
503
|
+
...(completionIdFromRejection(err) !== undefined ? { completionId: completionIdFromRejection(err) } : {}),
|
|
483
504
|
}))
|
|
484
505
|
.catch(() => { });
|
|
485
506
|
}
|
|
@@ -504,7 +525,8 @@ export async function createRunWorkflowTool(d) {
|
|
|
504
525
|
summary: completionSummary(meta.name, res.run),
|
|
505
526
|
result: boundedRedactedSummary(res.result, 4000),
|
|
506
527
|
usage: workflowUsageBlock(res.run),
|
|
507
|
-
diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope)),
|
|
528
|
+
diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope), res.run.resume?.journalEntries === 0),
|
|
529
|
+
completionId: res.run.completionId,
|
|
508
530
|
}), (err) => fire({
|
|
509
531
|
task_id: runId,
|
|
510
532
|
task_type: "workflow",
|
|
@@ -512,6 +534,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
512
534
|
status: "failed",
|
|
513
535
|
summary: failureSummary(err, runId),
|
|
514
536
|
diagnostics: workflowDiagnostics(runId, undefined),
|
|
537
|
+
...(completionIdFromRejection(err) !== undefined ? { completionId: completionIdFromRejection(err) } : {}),
|
|
515
538
|
}))
|
|
516
539
|
.catch(() => { });
|
|
517
540
|
}
|
|
@@ -524,10 +547,16 @@ export async function createRunWorkflowTool(d) {
|
|
|
524
547
|
...(persistedScriptPath !== undefined ? { scriptPath: persistedScriptPath } : {}),
|
|
525
548
|
note: (() => {
|
|
526
549
|
const pollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}" })` : undefined;
|
|
550
|
+
const blockingPollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}", block: true })` : undefined;
|
|
527
551
|
const journalRef = d.journalStore?.locator?.(runId, taskScope ?? d.scope ?? "");
|
|
528
552
|
const carries = pollExpr
|
|
529
553
|
? ` A terminal ${pollExpr} reply carries its result AND per-agent rows — read them${journalRef !== undefined ? ` (full journal: ${journalRef})` : ""} before assuming an empty or unexpected result.`
|
|
530
554
|
: "";
|
|
555
|
+
if (d.oneShot === true) {
|
|
556
|
+
return blockingPollExpr
|
|
557
|
+
? `This is a ONE-SHOT submission (e.g. headless \`-p\`) — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: ${blockingPollExpr}. If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.${carries}`
|
|
558
|
+
: "This is a one-shot submission (e.g. headless `-p`) and no poll path is available — the workflow's outcome may not be recoverable after this turn ends.";
|
|
559
|
+
}
|
|
531
560
|
if (d.notifier) {
|
|
532
561
|
return `The workflow runs asynchronously. You will be NOTIFIED when it completes (the notification carries the result). End your turn and wait — do not poll unless the user asks for progress.${carries}`;
|
|
533
562
|
}
|
|
@@ -93,6 +93,15 @@ export interface WorkflowRun {
|
|
|
93
93
|
error?: string;
|
|
94
94
|
result?: string;
|
|
95
95
|
resultFull?: string;
|
|
96
|
+
completionId?: string;
|
|
97
|
+
resume?: {
|
|
98
|
+
fromRunId: string;
|
|
99
|
+
journalEntries: number;
|
|
100
|
+
replayed: number;
|
|
101
|
+
divergedAtOrdinal?: number;
|
|
102
|
+
divergedReason?: string;
|
|
103
|
+
};
|
|
104
|
+
journalSkips?: number;
|
|
96
105
|
}
|
|
97
106
|
export type WorkflowEvent = {
|
|
98
107
|
type: "run_start";
|
|
@@ -2,10 +2,11 @@ import { resolveModelDisplayLabel } from "../core/roles.js";
|
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { availableParallelism } from "node:os";
|
|
5
|
+
import { uuidv7 } from "../internal/harness.js";
|
|
5
6
|
import { builtinAgentDefinitions } from "../agents/builtin-agents.js";
|
|
6
7
|
import { GENERAL_PURPOSE_SUBAGENT_TYPE } from "../agents/subagent.js";
|
|
7
8
|
import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
|
|
8
|
-
import { callKeyOrdinal } from "../core/workflow-journal-store.js";
|
|
9
|
+
import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
|
|
9
10
|
import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
|
|
10
11
|
import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
|
|
11
12
|
import { boundInputHashOf } from "../core/canonical-json.js";
|
|
@@ -551,13 +552,39 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
551
552
|
});
|
|
552
553
|
};
|
|
553
554
|
let journalTail = Promise.resolve();
|
|
554
|
-
const journalAppend = async (callKey, result) => {
|
|
555
|
+
const journalAppend = async (callKey, result, label) => {
|
|
555
556
|
const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
|
|
556
557
|
if (!journalStore) {
|
|
557
558
|
if (dbg)
|
|
558
559
|
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} SKIP (no journalStore on this run)`);
|
|
559
560
|
return;
|
|
560
561
|
}
|
|
562
|
+
let serialized;
|
|
563
|
+
try {
|
|
564
|
+
serialized = JSON.stringify(result);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
serialized = undefined;
|
|
568
|
+
}
|
|
569
|
+
if (serialized !== undefined && oversizeJournalResult(serialized)) {
|
|
570
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
571
|
+
if (!finalized)
|
|
572
|
+
run.journalSkips = (run.journalSkips ?? 0) + 1;
|
|
573
|
+
emitRunLog(`resume-journal: agent #${callKeyOrdinal(callKey)}${label !== undefined ? ` "${label.slice(0, 80)}"` : ""} result is ${bytes} bytes, ` +
|
|
574
|
+
`over the ${MAX_JOURNAL_RESULT_BYTES}-byte per-entry cap — NOT cached. A resume from this run re-runs this agent and everything after it live.`);
|
|
575
|
+
const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(result, bytes) });
|
|
576
|
+
journalTail = journalTail.then(() => tombstone).catch(() => undefined);
|
|
577
|
+
try {
|
|
578
|
+
await tombstone;
|
|
579
|
+
if (dbg)
|
|
580
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} OVERSIZE-TOMBSTONE (${bytes} bytes)`);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
if (dbg)
|
|
584
|
+
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${err instanceof Error ? err.message : String(err)}`);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
561
588
|
const p = journalStore.append(runId, scope, { callKey, result });
|
|
562
589
|
journalTail = journalTail.then(() => p).catch(() => undefined);
|
|
563
590
|
try {
|
|
@@ -623,6 +650,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
623
650
|
: message;
|
|
624
651
|
emit({ type: "log", runId, message: capped, ts: now() });
|
|
625
652
|
};
|
|
653
|
+
let divergenceNoted = false;
|
|
654
|
+
const noteDivergence = (ordinal, reason) => {
|
|
655
|
+
if (opts.resumeFromRunId === undefined || divergenceNoted)
|
|
656
|
+
return;
|
|
657
|
+
divergenceNoted = true;
|
|
658
|
+
if (run.resume) {
|
|
659
|
+
run.resume.divergedAtOrdinal = ordinal;
|
|
660
|
+
run.resume.divergedReason = reason;
|
|
661
|
+
}
|
|
662
|
+
emitRunLog(`resume: replay stopped at agent #${ordinal} (${reason}) — this call and every later call run live.`);
|
|
663
|
+
};
|
|
626
664
|
const settleAgentError = (rec, result, label, rawOutput) => {
|
|
627
665
|
if (result.status !== "completed") {
|
|
628
666
|
if (result.errorCode !== undefined)
|
|
@@ -695,6 +733,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
695
733
|
...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}),
|
|
696
734
|
output: cachedOutput,
|
|
697
735
|
};
|
|
736
|
+
if (run.resume)
|
|
737
|
+
run.resume.replayed += 1;
|
|
698
738
|
run.agents.push(replayRec);
|
|
699
739
|
if (phaseInstance)
|
|
700
740
|
agentPhaseOf.set(replayRec, phaseInstance);
|
|
@@ -710,6 +750,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
710
750
|
}
|
|
711
751
|
return r;
|
|
712
752
|
}
|
|
753
|
+
noteDivergence(run.agents.length, cached === undefined
|
|
754
|
+
? run.resume?.journalEntries === 0
|
|
755
|
+
? "the journal loaded no entries"
|
|
756
|
+
: run.agents.length >= replayByOrdinal.length
|
|
757
|
+
? "a new call past the journaled prefix — the prior run ended before this point (the intended extend-the-script resume shape)"
|
|
758
|
+
: "no journal entry at this ordinal (never recorded, e.g. an oversize result under a pre-tombstone engine, or dropped)"
|
|
759
|
+
: cached.callKey !== callKey
|
|
760
|
+
? "the call key changed — the script or its args differ here"
|
|
761
|
+
: cached.result.errorCode === JOURNAL_OVERSIZE_ERROR_CODE
|
|
762
|
+
? "the prior result exceeded the journal size cap and was never cached"
|
|
763
|
+
: `the journaled result was ${cached.result.status}, not completed`);
|
|
713
764
|
diverged = true;
|
|
714
765
|
}
|
|
715
766
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
@@ -963,7 +1014,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
963
1014
|
status: "failed",
|
|
964
1015
|
result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
|
|
965
1016
|
stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
|
|
966
|
-
}).catch(() => undefined);
|
|
1017
|
+
}, label).catch(() => undefined);
|
|
967
1018
|
bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
|
|
968
1019
|
}
|
|
969
1020
|
throw err;
|
|
@@ -992,7 +1043,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
992
1043
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
|
|
993
1044
|
void persist("update");
|
|
994
1045
|
bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
|
|
995
|
-
await journalAppend(callKey, result);
|
|
1046
|
+
await journalAppend(callKey, result, label);
|
|
996
1047
|
if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
|
|
997
1048
|
throw new WorkflowAgentSchemaError(label, result);
|
|
998
1049
|
}
|
|
@@ -1026,6 +1077,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1026
1077
|
const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
|
|
1027
1078
|
const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
|
|
1028
1079
|
const model = workflowModelLabel(specForIdentity);
|
|
1080
|
+
noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
|
|
1029
1081
|
diverged = true;
|
|
1030
1082
|
const { tail: activityTail, onActivity } = makeActivityCapture(callKey, label, groupId);
|
|
1031
1083
|
const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
|
|
@@ -1189,7 +1241,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1189
1241
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
|
|
1190
1242
|
void persist("update");
|
|
1191
1243
|
bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
|
|
1192
|
-
await journalAppend(callKey, result).catch(() => undefined);
|
|
1244
|
+
await journalAppend(callKey, result, label).catch(() => undefined);
|
|
1193
1245
|
if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
|
|
1194
1246
|
throw new WorkflowAgentSchemaError(label, result);
|
|
1195
1247
|
}
|
|
@@ -1239,6 +1291,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1239
1291
|
const fanOutOpts = trailing !== undefined && typeof trailing !== "function" ? trailing : undefined;
|
|
1240
1292
|
const stages = (fanOutOpts !== undefined ? stagesAndOpts.slice(0, -1) : stagesAndOpts);
|
|
1241
1293
|
const errorsOut = fanOutOpts?.errors;
|
|
1294
|
+
noteDivergence(run.agents.length, "ctx.pipeline ordinals are latency-dependent, so its calls always run live on a resume");
|
|
1242
1295
|
diverged = true;
|
|
1243
1296
|
const settled = await Promise.all(items.map((item, index) => stages
|
|
1244
1297
|
.reduce((acc, stage) => acc.then((prev) => stage(prev, item, index)), Promise.resolve(item))
|
|
@@ -1370,6 +1423,19 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1370
1423
|
const entries = await journalStore.load(opts.resumeFromRunId, scope);
|
|
1371
1424
|
for (const e of entries)
|
|
1372
1425
|
replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
|
|
1426
|
+
run.resume = {
|
|
1427
|
+
fromRunId: opts.resumeFromRunId.replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 64),
|
|
1428
|
+
journalEntries: entries.length,
|
|
1429
|
+
replayed: 0,
|
|
1430
|
+
};
|
|
1431
|
+
if (entries.length === 0) {
|
|
1432
|
+
emitRunLog("resume: the prior run's journal yielded NO entries under this run's scope — nothing will replay; the entire script runs live. " +
|
|
1433
|
+
"The prior run id may be unknown or pruned, recorded under a different scope, or its journal unreadable.");
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
emitRunLog(`resume: loaded ${entries.length} journaled result${entries.length === 1 ? "" : "s"} — the longest unchanged prefix replays; the first changed/new call and everything after run live.`);
|
|
1437
|
+
}
|
|
1438
|
+
void persist("update");
|
|
1373
1439
|
}
|
|
1374
1440
|
return fn(ctx);
|
|
1375
1441
|
};
|
|
@@ -1416,6 +1482,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1416
1482
|
if (run.result !== fullResult)
|
|
1417
1483
|
run.resultFull = fullResult;
|
|
1418
1484
|
run.status = "completed";
|
|
1485
|
+
run.completionId ??= uuidv7();
|
|
1419
1486
|
run.endedAt = now();
|
|
1420
1487
|
closeAbandonedAgents(run.endedAt);
|
|
1421
1488
|
restampPhaseFailures();
|
|
@@ -1431,6 +1498,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1431
1498
|
settleActiveUsageBeats();
|
|
1432
1499
|
closeOpenMarker("failed");
|
|
1433
1500
|
run.status = "failed";
|
|
1501
|
+
run.completionId ??= uuidv7();
|
|
1434
1502
|
run.endedAt = now();
|
|
1435
1503
|
run.error = err instanceof Error ? err.message : String(err);
|
|
1436
1504
|
closeAbandonedAgents(run.endedAt);
|
|
@@ -1440,6 +1508,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1440
1508
|
run.agentFailures = failedRunFailures;
|
|
1441
1509
|
emit({ type: "run_end", runId, status: "failed", ...(failedRunFailures > 0 ? { agentFailures: failedRunFailures } : {}), ts: run.endedAt });
|
|
1442
1510
|
await persist("update");
|
|
1511
|
+
if (err !== null && (typeof err === "object" || typeof err === "function")) {
|
|
1512
|
+
try {
|
|
1513
|
+
Object.defineProperty(err, "workflowRun", { value: run, enumerable: false, configurable: true });
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1443
1518
|
throw err;
|
|
1444
1519
|
}
|
|
1445
1520
|
finally {
|
|
@@ -89,9 +89,12 @@ export function createCcFileMailboxStore(opts) {
|
|
|
89
89
|
leases.set(key, { owner, expiresAt: t + ttlMs, maxSeq });
|
|
90
90
|
return Promise.resolve({ maxSeq, messages: pending });
|
|
91
91
|
},
|
|
92
|
-
ack: (scope, handle, upToSeq) => {
|
|
92
|
+
ack: (scope, handle, owner, upToSeq) => {
|
|
93
93
|
requireDefaultScope(scope);
|
|
94
94
|
const path = inboxPath(handle);
|
|
95
|
+
const held = leases.get(path);
|
|
96
|
+
if (held === undefined || held.owner !== owner)
|
|
97
|
+
return Promise.resolve();
|
|
95
98
|
if (!existsSync(path))
|
|
96
99
|
return Promise.resolve();
|
|
97
100
|
return withCcLock(path, () => {
|
|
@@ -105,6 +108,8 @@ export function createCcFileMailboxStore(opts) {
|
|
|
105
108
|
}
|
|
106
109
|
if (changed)
|
|
107
110
|
saveBox(path, box);
|
|
111
|
+
if (held.maxSeq <= upToSeq)
|
|
112
|
+
leases.delete(path);
|
|
108
113
|
});
|
|
109
114
|
},
|
|
110
115
|
releaseLease: (scope, handle, owner) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { BackgroundAgentStoreError, } from "../../core/background-agent-store.js";
|
|
2
|
+
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "../../core/background-agent-store.js";
|
|
3
3
|
import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords } from "./fs-atomic.js";
|
|
4
4
|
const sharedAgentDirs = new Map();
|
|
5
5
|
export class FileBackgroundAgentStore {
|
|
@@ -205,7 +205,8 @@ export class FileBackgroundAgentStore {
|
|
|
205
205
|
const next = structuredClone(live);
|
|
206
206
|
next.status = "failed";
|
|
207
207
|
next.stoppedBy = "system";
|
|
208
|
-
next.summary = next.summary ??
|
|
208
|
+
next.summary = next.summary ?? STALE_RUNNING_REAP_ATTRIBUTION;
|
|
209
|
+
next.error = next.error ?? STALE_RUNNING_REAP_ATTRIBUTION;
|
|
209
210
|
next.settledAt = now;
|
|
210
211
|
next.updatedAt = now;
|
|
211
212
|
next.rev = live.rev + 1;
|
|
@@ -21,7 +21,7 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
21
21
|
sentAt: number;
|
|
22
22
|
}): Promise<number>;
|
|
23
23
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
24
|
-
ack(scope: string, handle: string, upToSeq: number): Promise<void>;
|
|
24
|
+
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
|
|
25
25
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
26
26
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
27
27
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, resolve, sep } from "node:path";
|
|
2
2
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
-
import {} from "../../core/mailbox-store.js";
|
|
3
|
+
import { newestSentAt, } from "../../core/mailbox-store.js";
|
|
4
4
|
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
|
|
5
5
|
function realpathSyncSafe(p) {
|
|
6
6
|
try {
|
|
@@ -86,7 +86,7 @@ export class FileMailboxStore {
|
|
|
86
86
|
lines.push(JSON.stringify({ t: "lease", ...b.lease }));
|
|
87
87
|
b.log.closeForSwap();
|
|
88
88
|
atomicWriteFile(this.tmpDir, path, lines.length ? `${lines.join("\n")}\n` : "");
|
|
89
|
-
b.events =
|
|
89
|
+
b.events = 0;
|
|
90
90
|
}
|
|
91
91
|
async append(scope, handle, msg) {
|
|
92
92
|
if (scope === undefined || scope === "")
|
|
@@ -111,9 +111,11 @@ export class FileMailboxStore {
|
|
|
111
111
|
return { messages: b.messages.map((m) => ({ ...m })), maxSeq };
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
|
-
async ack(scope, handle, upToSeq) {
|
|
114
|
+
async ack(scope, handle, owner, upToSeq) {
|
|
115
115
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
116
116
|
const b = this.load(scope, handle);
|
|
117
|
+
if (b.lease === undefined || b.lease.owner !== owner)
|
|
118
|
+
return;
|
|
117
119
|
this.commit(b, this.boxPath(scope, handle), { t: "ack", upToSeq });
|
|
118
120
|
});
|
|
119
121
|
}
|
|
@@ -160,15 +162,15 @@ export class FileMailboxStore {
|
|
|
160
162
|
for (const [key, b] of [...sharedBoxes]) {
|
|
161
163
|
if (!b.path.startsWith(prefix))
|
|
162
164
|
continue;
|
|
163
|
-
const seen = b.messages
|
|
164
|
-
if (seen === undefined || seen
|
|
165
|
+
const seen = newestSentAt(b.messages);
|
|
166
|
+
if (seen === undefined || seen >= now - maxAgeMs)
|
|
165
167
|
continue;
|
|
166
168
|
const swept = await withPathLock(key, () => {
|
|
167
169
|
const live = sharedBoxes.get(key);
|
|
168
170
|
if (live === undefined || live !== b)
|
|
169
171
|
return false;
|
|
170
|
-
const newest = b.messages
|
|
171
|
-
if (newest === undefined || newest
|
|
172
|
+
const newest = newestSentAt(b.messages);
|
|
173
|
+
if (newest === undefined || newest >= now - maxAgeMs)
|
|
172
174
|
return false;
|
|
173
175
|
b.messages = [];
|
|
174
176
|
delete b.lease;
|
|
@@ -13,3 +13,8 @@ export declare function detectFileEncoding(bytes: Uint8Array): DetectedFileEncod
|
|
|
13
13
|
export declare function decodeTextBytes(bytes: Uint8Array): DecodedTextFile;
|
|
14
14
|
export declare function encodeTextForFile(text: string, encoding: DetectedFileEncoding, endings: DetectedLineEndings | "preserve"): string | Uint8Array;
|
|
15
15
|
export declare function normalizeEditText(s: string): string;
|
|
16
|
+
export declare function splitLeadingBom(text: string): {
|
|
17
|
+
hadBom: boolean;
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function normalizeFileText(s: string): string;
|
|
@@ -41,3 +41,9 @@ export function encodeTextForFile(text, encoding, endings) {
|
|
|
41
41
|
export function normalizeEditText(s) {
|
|
42
42
|
return s.replaceAll("\r\n", "\n");
|
|
43
43
|
}
|
|
44
|
+
export function splitLeadingBom(text) {
|
|
45
|
+
return text.charCodeAt(0) === 0xfeff ? { hadBom: true, text: text.slice(1) } : { hadBom: false, text };
|
|
46
|
+
}
|
|
47
|
+
export function normalizeFileText(s) {
|
|
48
|
+
return normalizeEditText(splitLeadingBom(s).text);
|
|
49
|
+
}
|