@sema-agent/core 1.451.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.
Files changed (57) hide show
  1. package/dist/agents/observer.js +3 -0
  2. package/dist/agents/subagent.js +51 -12
  3. package/dist/core/ask-question.js +5 -5
  4. package/dist/core/background-agent-store.d.ts +2 -0
  5. package/dist/core/background-agent-store.js +5 -1
  6. package/dist/core/exec-output-tail.d.ts +18 -1
  7. package/dist/core/exec-output-tail.js +38 -5
  8. package/dist/core/lsp-session.d.ts +1 -0
  9. package/dist/core/lsp-session.js +24 -6
  10. package/dist/core/lsp.d.ts +10 -0
  11. package/dist/core/lsp.js +63 -6
  12. package/dist/core/mailbox-store.d.ts +3 -2
  13. package/dist/core/mailbox-store.js +19 -4
  14. package/dist/core/memory.js +1 -1
  15. package/dist/core/runner/prepare-task.js +10 -1
  16. package/dist/core/runner/runtask.js +1 -1
  17. package/dist/core/session-reconcile.js +5 -2
  18. package/dist/core/task-notification.d.ts +1 -0
  19. package/dist/core/task-registry.d.ts +15 -9
  20. package/dist/core/task-registry.js +290 -53
  21. package/dist/core/tool-result-store.js +2 -2
  22. package/dist/core/tools.d.ts +5 -0
  23. package/dist/core/tools.js +3 -0
  24. package/dist/core/types.d.ts +1 -0
  25. package/dist/core/workflow-journal-store.d.ts +2 -0
  26. package/dist/core/workflow-journal-store.js +14 -0
  27. package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
  28. package/dist/engine/execution-env/node-execution-env.js +130 -20
  29. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
  30. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  31. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  32. package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  36. package/dist/orchestration/run-workflow-tool.js +29 -6
  37. package/dist/orchestration/workflow.d.ts +9 -0
  38. package/dist/orchestration/workflow.js +80 -5
  39. package/dist/stores/cc/mailbox-store.js +6 -1
  40. package/dist/stores/file/background-agent-store.js +3 -2
  41. package/dist/stores/file/mailbox-store.d.ts +1 -1
  42. package/dist/stores/file/mailbox-store.js +9 -7
  43. package/dist/tools/fs/encoding.d.ts +5 -0
  44. package/dist/tools/fs/encoding.js +6 -0
  45. package/dist/tools/fs/index.js +184 -120
  46. package/dist/tools/fs/notebook.d.ts +43 -0
  47. package/dist/tools/fs/notebook.js +141 -0
  48. package/dist/tools/fs/repo-map.js +2 -2
  49. package/dist/tools/fs/search.js +141 -12
  50. package/dist/tools/gitea-issue.js +4 -2
  51. package/dist/tools/monitor.js +12 -8
  52. package/dist/tools/scheduler-tools.js +16 -16
  53. package/dist/tools/task-list.js +34 -12
  54. package/dist/tools/web.d.ts +2 -0
  55. package/dist/tools/web.js +105 -19
  56. package/dist/tools/worktree.js +14 -14
  57. package/package.json +1 -1
@@ -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 ?? "host process interrupted (stale running row reaped)";
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 = lines.length;
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[b.messages.length - 1];
164
- if (seen === undefined || seen.sentAt >= now - maxAgeMs)
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[b.messages.length - 1];
171
- if (newest === undefined || newest.sentAt >= now - maxAgeMs)
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
+ }