@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.8

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 (46) hide show
  1. package/README.md +124 -37
  2. package/package.json +8 -8
  3. package/src/agent-file-toggle.ts +6 -4
  4. package/src/agent-manager.ts +131 -28
  5. package/src/agent-runner.ts +155 -62
  6. package/src/cross-extension-rpc.ts +11 -36
  7. package/src/custom-agents.ts +9 -5
  8. package/src/default-agents.ts +2 -4
  9. package/src/enabled-models.ts +8 -7
  10. package/src/enterprise-local.ts +5 -5
  11. package/src/home-paths.ts +27 -0
  12. package/src/index.ts +191 -90
  13. package/src/invocation-config.ts +3 -16
  14. package/src/memory.ts +9 -9
  15. package/src/mention-clone.ts +9 -5
  16. package/src/model-resolver.ts +41 -11
  17. package/src/model-scope.ts +2 -1
  18. package/src/model-selection.ts +159 -0
  19. package/src/model-tiers.ts +171 -0
  20. package/src/nested-tools.ts +22 -26
  21. package/src/output-file.ts +23 -1
  22. package/src/package-resources.ts +25 -10
  23. package/src/process-budget.ts +6 -3
  24. package/src/quota-admission.ts +21 -9
  25. package/src/resource-loader.ts +51 -0
  26. package/src/schedule.ts +36 -18
  27. package/src/settings.ts +42 -12
  28. package/src/skill-loader.ts +8 -7
  29. package/src/stale-ctx.ts +16 -0
  30. package/src/types.ts +27 -0
  31. package/src/ui/agent-row.ts +13 -3
  32. package/src/ui/agent-widget.ts +15 -4
  33. package/src/ui/conversation-viewer.ts +6 -3
  34. package/src/ui/fallback-flag.ts +90 -0
  35. package/src/ui/fleet-list.ts +363 -53
  36. package/src/ui/fleet-state.ts +154 -0
  37. package/src/ui/notification-view.ts +18 -2
  38. package/src/ui/workflow-card.ts +5 -1
  39. package/src/ui/workflow-dialog.ts +10 -0
  40. package/src/workflow/host.ts +43 -45
  41. package/src/workflow/journal.ts +13 -1
  42. package/src/workflow/progress.ts +4 -0
  43. package/src/workflow/runtime.ts +25 -8
  44. package/src/workflow/saved.ts +19 -17
  45. package/src/workflow/task.ts +10 -0
  46. package/src/workflow/worker-source.ts +156 -38
@@ -39,7 +39,7 @@
39
39
 
40
40
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
41
41
  import { isAbsolute, join } from "node:path";
42
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
42
+ import { resolveAgentDir } from "../home-paths.js";
43
43
  import { isSymlink, isUnsafeName, safeReadFile } from "../memory.js";
44
44
  import { isPackageResourceExcluded, packageWorkflowDirs, packageWorkflowFiles } from "../package-resources.js";
45
45
  import { hasMetaDeclaration } from "./meta.js";
@@ -61,12 +61,12 @@ const WORKFLOW_EXTENSION = ".js";
61
61
  * reaches this without an options bag travelling through `resolveWorkflowScript`,
62
62
  * `resolveWorkflowSource` and `readSavedWorkflow` purely to be consulted once.
63
63
  */
64
- export function savedWorkflowRoots(cwd: string): string[] {
64
+ export function savedWorkflowRoots(cwd: string, homeDir?: string): string[] {
65
65
  return [
66
66
  join(cwd, ".pi", "workflows"),
67
67
  join(cwd, ".agents", "workflows"),
68
- join(getAgentDir(), "workflows"),
69
- ...packageWorkflowDirs(cwd),
68
+ join(resolveAgentDir(homeDir), "workflows"),
69
+ ...packageWorkflowDirs(cwd, { homeDir }),
70
70
  ];
71
71
  }
72
72
 
@@ -81,7 +81,7 @@ export type SavedWorkflow =
81
81
  * names that do exist — a model that guessed the name can correct itself from
82
82
  * the error instead of spending a turn asking.
83
83
  */
84
- export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
84
+ export function readSavedWorkflow(name: string, cwd: string, homeDir?: string): SavedWorkflow {
85
85
  const trimmed = name.trim();
86
86
  if (isUnsafeName(trimmed)) {
87
87
  return {
@@ -92,13 +92,13 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
92
92
  };
93
93
  }
94
94
 
95
- const roots = savedWorkflowRoots(cwd);
95
+ const roots = savedWorkflowRoots(cwd, homeDir);
96
96
  for (const root of roots) {
97
97
  if (isSymlink(root)) continue; // reject a symlinked root entirely, as skill-loader does
98
98
  const path = join(root, `${trimmed}${WORKFLOW_EXTENSION}`);
99
99
  // A `!` entry in the declaring package's manifest. Only ever true for a path
100
100
  // under a package root, so the local roots pay one set lookup for it.
101
- if (isPackageResourceExcluded(path, cwd, "workflows")) continue;
101
+ if (isPackageResourceExcluded(path, cwd, "workflows", { homeDir })) continue;
102
102
  const script = safeReadFile(path);
103
103
  if (script === undefined) continue;
104
104
  // Found the file, so stop looking — a shadowing name that turns out not to
@@ -117,7 +117,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
117
117
  // Last: a package that declared an individual file rather than a directory.
118
118
  // It carries no root to look a name up in, so it is matched by name here
119
119
  // instead — after every root, which keeps package scripts lowest-precedence.
120
- const declared = packageWorkflowFiles(cwd).get(trimmed);
120
+ const declared = packageWorkflowFiles(cwd, { homeDir }).get(trimmed);
121
121
  if (declared !== undefined) {
122
122
  const script = safeReadFile(declared);
123
123
  if (script !== undefined) {
@@ -133,7 +133,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
133
133
  }
134
134
  }
135
135
 
136
- const known = listSavedWorkflows(cwd);
136
+ const known = listSavedWorkflows(cwd, homeDir);
137
137
  return {
138
138
  ok: false,
139
139
  message:
@@ -154,6 +154,7 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
154
154
  export function resolveWorkflowSource(
155
155
  ref: { name?: string; scriptPath?: string },
156
156
  cwd: string,
157
+ homeDir?: string,
157
158
  ): SavedWorkflow {
158
159
  const path = ref.scriptPath?.trim();
159
160
  if (path !== undefined && path !== "") {
@@ -168,14 +169,14 @@ export function resolveWorkflowSource(
168
169
  }
169
170
  }
170
171
  const name = ref.name?.trim();
171
- if (name !== undefined && name !== "") return readSavedWorkflow(name, cwd);
172
+ if (name !== undefined && name !== "") return readSavedWorkflow(name, cwd, homeDir);
172
173
  return { ok: false, message: "A workflow reference needs a `name` or a `scriptPath`." };
173
174
  }
174
175
 
175
176
  /** Every saved workflow name, de-duplicated across roots and sorted. */
176
- export function listSavedWorkflows(cwd: string): string[] {
177
+ export function listSavedWorkflows(cwd: string, homeDir?: string): string[] {
177
178
  const names = new Set<string>();
178
- for (const root of savedWorkflowRoots(cwd)) {
179
+ for (const root of savedWorkflowRoots(cwd, homeDir)) {
179
180
  if (!existsSync(root) || isSymlink(root)) continue;
180
181
  let entries: string[];
181
182
  try {
@@ -188,11 +189,11 @@ export function listSavedWorkflows(cwd: string): string[] {
188
189
  const name = entry.slice(0, -WORKFLOW_EXTENSION.length);
189
190
  if (isUnsafeName(name)) continue;
190
191
  const path = join(root, entry);
191
- if (isPackageResourceExcluded(path, cwd, "workflows")) continue;
192
+ if (isPackageResourceExcluded(path, cwd, "workflows", { homeDir })) continue;
192
193
  if (isWorkflowFile(path)) names.add(name);
193
194
  }
194
195
  }
195
- for (const [name, path] of packageWorkflowFiles(cwd)) {
196
+ for (const [name, path] of packageWorkflowFiles(cwd, { homeDir })) {
196
197
  if (isUnsafeName(name)) continue;
197
198
  if (isWorkflowFile(path)) names.add(name);
198
199
  }
@@ -228,10 +229,11 @@ function isWorkflowFile(path: string): boolean {
228
229
  export function resolveWorkflowScript(
229
230
  params: { script?: string; scriptPath?: string; name?: string },
230
231
  cwd: string,
232
+ homeDir?: string,
231
233
  ): { ok: true; script: string; scriptPath?: string } | { ok: false; message: string } {
232
234
  const path = params.scriptPath?.trim();
233
235
  if (path !== undefined && path !== "") {
234
- const resolved = resolveWorkflowSource({ scriptPath: path }, cwd);
236
+ const resolved = resolveWorkflowSource({ scriptPath: path }, cwd, homeDir);
235
237
  return resolved.ok ? { ok: true, script: resolved.script, scriptPath: resolved.path } : resolved;
236
238
  }
237
239
  const script = params.script;
@@ -244,11 +246,11 @@ export function resolveWorkflowScript(
244
246
  // reference means.
245
247
  const name = params.name?.trim();
246
248
  if (name !== undefined && name !== "") {
247
- const saved = resolveWorkflowSource({ name }, cwd);
249
+ const saved = resolveWorkflowSource({ name }, cwd, homeDir);
248
250
  return saved.ok ? { ok: true, script: saved.script, scriptPath: saved.path } : saved;
249
251
  }
250
252
 
251
- const known = listSavedWorkflows(cwd);
253
+ const known = listSavedWorkflows(cwd, homeDir);
252
254
  return {
253
255
  ok: false,
254
256
  message:
@@ -86,6 +86,8 @@ export interface WorkflowTask {
86
86
  /** The script's return value, once the run produced one. */
87
87
  value?: unknown;
88
88
  error?: string;
89
+ /** Full authored stack for the result; the compact status keeps only error. */
90
+ errorStack?: string;
89
91
  }
90
92
 
91
93
  export function createWorkflowTask(init: {
@@ -205,6 +207,7 @@ export function completeWorkflowTask(task: WorkflowTask, result: WorkflowRunResu
205
207
  task.replayedCount = result.replayedCount;
206
208
  task.value = result.value;
207
209
  task.error = result.error;
210
+ task.errorStack = result.errorStack;
208
211
  task.endTime = Date.now();
209
212
  }
210
213
 
@@ -222,6 +225,13 @@ export function failWorkflowTask(task: WorkflowTask, error: string): void {
222
225
 
223
226
  /** The run's outcome as text, for the notification and the LLM-facing result. */
224
227
  export function workflowResultText(task: WorkflowTask): string {
228
+ if (task.errorStack !== undefined) {
229
+ const stack = task.errorStack.slice(0, 16384);
230
+ const firstLine = stack.split("\n", 1)[0];
231
+ const message = task.error;
232
+ if (message === undefined || message === "" || firstLine === message || firstLine.endsWith(`: ${message}`)) return stack;
233
+ return `${message}\n${stack}`;
234
+ }
225
235
  if (task.error !== undefined) return task.error;
226
236
  if (task.value === undefined) return "No output.";
227
237
  if (typeof task.value === "string") return task.value;
@@ -26,10 +26,11 @@
26
26
  *
27
27
  * That said: our injected globals are themselves host closures, so
28
28
  * `agent.constructor` is still the host `Function`. The hygiene shrinks the
29
- * surface; it does not close the hole. **`codeGeneration: { strings: false }` is
30
- * the load-bearing defense** — it makes `Function("…")` and `eval("…")` throw
31
- * `EvalError`, so a captured host `Function` cannot compile anything. Treat this
32
- * as a determinism boundary, not a security boundary against a hostile script.
29
+ * surface; it does not close the hole. `codeGeneration: { strings: false }`
30
+ * makes realm-local `Function` and `eval` throw `EvalError`, but does not stop a
31
+ * captured host `Function`. The stack formatter stays realm-native rather than
32
+ * adding another host callback. Treat this as a determinism boundary, not a
33
+ * security boundary against a hostile script.
33
34
  *
34
35
  * ## Why determinism is a prelude and not a stub
35
36
  *
@@ -48,9 +49,8 @@
48
49
  /**
49
50
  * Runs inside the realm, ahead of the script body, on a single line.
50
51
  *
51
- * One line matters: the body is compiled at `\n` + line 1, and the host passes
52
- * `lineOffset: -1` so reported line numbers match the file the author wrote. Any
53
- * newline in here shifts every stack frame in every workflow script.
52
+ * The shared compiler puts this and the wrapper on the body's first line.
53
+ * No lineOffset is needed (Bun ignores it); only first-line columns need mapping.
54
54
  */
55
55
  const DETERMINISM_PRELUDE =
56
56
  "const Date = (function () {" +
@@ -96,13 +96,13 @@ const pendingCalls = new Map();
96
96
  */
97
97
  let spentOutput = 0;
98
98
 
99
- function callHost(method, payload) {
99
+ function callHost(method, payload, onModelNote) {
100
100
  // Drain first, so the phase() that named this agent reaches the host ahead of
101
101
  // the agent entry rather than a tick behind it.
102
102
  flushProgress();
103
103
  return new Promise(function (resolve, reject) {
104
104
  const callId = nextCallId++;
105
- pendingCalls.set(callId, { resolve: resolve, reject: reject });
105
+ pendingCalls.set(callId, { resolve: resolve, reject: reject, onModelNote: onModelNote });
106
106
  port.postMessage({ type: "call", callId: callId, method: method, payload: payload });
107
107
  });
108
108
  }
@@ -114,6 +114,7 @@ port.on("message", function (message) {
114
114
  if (!waiter) return;
115
115
  pendingCalls.delete(message.callId);
116
116
  if (message.ok) {
117
+ if (message.modelNote !== undefined && waiter.onModelNote) waiter.onModelNote(message.modelNote);
117
118
  waiter.resolve(message.value);
118
119
  return;
119
120
  }
@@ -523,6 +524,8 @@ async function agentIn(scope, prompt, opts) {
523
524
  const phaseIndex = phaseName !== undefined ? definePhaseIn(scope, phaseName) : scope.ambientPhaseIndex;
524
525
  const phaseTitle = phaseName !== undefined ? scopedTitle(scope, phaseName) : scope.ambientPhaseTitle;
525
526
 
527
+ // Set when the host ran the child on another model than configured.
528
+ let modelNote;
526
529
  const result = await callHost("agent", {
527
530
  prompt: text,
528
531
  label: label,
@@ -535,20 +538,60 @@ async function agentIn(scope, prompt, opts) {
535
538
  resume: resume,
536
539
  effort: effort,
537
540
  schema: schema,
538
- });
539
- if (result === undefined || result === null) return null;
541
+ }, function (note) { modelNote = note; });
542
+ if (result === undefined || result === null) {
543
+ // A failed child is a null; its note would otherwise be lost with it.
544
+ if (modelNote !== undefined) logIn(scope, modelNote);
545
+ return null;
546
+ }
540
547
  if (schema === undefined) return result;
541
548
  // Parsed with the realm's own JSON.parse so the script gets an object whose
542
549
  // prototype is its own — \`x instanceof Object\` and \`x.list instanceof Array\`
543
550
  // both hold, and it survives assertBoundary if the script returns it.
544
551
  try {
545
- return realmParse(result);
552
+ const parsed = realmParse(result);
553
+ // A schema'd payload must stay exactly what the schema describes, so the
554
+ // note cannot lead it as text. It goes to the run log, and onto a plain
555
+ // object payload as a sibling \`_modelNote\` the script can read.
556
+ if (modelNote !== undefined) {
557
+ logIn(scope, modelNote);
558
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && canCarryModelNote(schema) && !Object.prototype.hasOwnProperty.call(parsed, "_modelNote")) parsed._modelNote = modelNote;
559
+ }
560
+ return parsed;
546
561
  } catch (error) {
547
562
  logIn(scope, "agent(): the host returned a structured result that is not JSON");
548
563
  return null;
549
564
  }
550
565
  }
551
566
 
567
+ /**
568
+ * Whether the host may add a sibling \`_modelNote\` to a schema'd payload.
569
+ *
570
+ * Deliberately conservative: only a PLAIN object-with-properties schema, using
571
+ * nothing but the keywords below, qualifies. Anything that could declare the
572
+ * field somewhere we do not look (allOf/anyOf/oneOf/$ref/patternProperties/
573
+ * if-then-else/dependent schemas/a schema-valued additionalProperties) or forbid
574
+ * it (additionalProperties: false) never gets an injected field, and the note
575
+ * goes to the run log instead.
576
+ */
577
+ var PLAIN_SCHEMA_KEYS = ["$schema", "type", "properties", "required", "description", "title"];
578
+ function canCarryModelNote(schema) {
579
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
580
+ if (schema.type !== "object") return false;
581
+ const props = schema.properties;
582
+ if (!props || typeof props !== "object" || Array.isArray(props)) return false;
583
+ if (Object.prototype.hasOwnProperty.call(props, "_modelNote")) return false;
584
+ const keys = Object.keys(schema);
585
+ for (let i = 0; i < keys.length; i++) {
586
+ if (PLAIN_SCHEMA_KEYS.indexOf(keys[i]) === -1) {
587
+ // \`additionalProperties: true\` changes nothing, so it stays plain.
588
+ if (keys[i] === "additionalProperties" && schema.additionalProperties === true) continue;
589
+ return false;
590
+ }
591
+ }
592
+ return true;
593
+ }
594
+
552
595
  /**
553
596
  * A barrier: every thunk starts now, and nothing past the await runs until all
554
597
  * of them have settled. A thunk that throws resolves to null rather than
@@ -675,19 +718,7 @@ async function workflowIn(scope, nameOrRef, args) {
675
718
  child.ambientPhaseIndex = definePhaseIn(child, undefined);
676
719
  child.ambientPhaseTitle = scopedTitle(child, undefined);
677
720
 
678
- let run;
679
- try {
680
- const compiled = new vm.Script(
681
- // \`meta\` is deliberately not a parameter: the body still opens with its
682
- // own \`const meta = { ... }\` (extractMeta strips only the \`export\`), so a
683
- // parameter of that name would collide with it.
684
- "(async (agent, phase, log, workflow, console, args) => {" + PRELUDE + "\\n" + loaded.body + "\\n})",
685
- { filename: "workflow:" + loaded.name + ".js", lineOffset: -1 }
686
- );
687
- run = compiled.runInContext(realmContext);
688
- } catch (error) {
689
- throw new Error('workflow("' + label + '"): ' + describe(error));
690
- }
721
+ const run = compileWorkflow(loaded.body, "workflow:" + loaded.name + ".js");
691
722
 
692
723
  const value = await run(child.agent, child.phase, child.log, child.workflow, child.console, args);
693
724
  checkBoundary(value, 'the result of workflow("' + label + '")');
@@ -721,6 +752,73 @@ function makeBudget() {
721
752
  };
722
753
  }
723
754
 
755
+ /* ------------------------------------------------------------------ *
756
+ * Authored source locations
757
+ * ------------------------------------------------------------------ */
758
+
759
+ // Neither lineOffset nor columnOffset is implemented by Bun 1.3/1.4. Keep
760
+ // authored lines intact and map only the prefix on line 1. Both entry points
761
+ // compile the same function; calling it outside the VM adds no synthetic frame.
762
+ const WORKFLOW_PREFIX = "(async (agent, phase, log, workflow, console, args) => {" + PRELUDE;
763
+ let registerWorkflowFile = null;
764
+
765
+ // Compiled from source in the realm, not installed as a host callback. Only
766
+ // primitive prefix length and filenames cross into its private realm-native Set.
767
+ function installAuthoredStacks(prefixLength) {
768
+ const workflowFiles = new Set();
769
+ function authoredFrame(frame) {
770
+ const text = String(frame);
771
+ const file = frame.getFileName();
772
+ if (!workflowFiles.has(file) || frame.getLineNumber() !== 1) return text;
773
+ const location = file + ":1:";
774
+ const at = text.lastIndexOf(location);
775
+ if (at === -1) return text;
776
+ const start = at + location.length;
777
+ // Bun 1.3's getColumnNumber() is zero-based, but CallSite.toString()
778
+ // is one-based on all supported engines.
779
+ const column = Number.parseInt(text.slice(start), 10);
780
+ if (column <= prefixLength) return text;
781
+ const digits = String(column).length;
782
+ return text.slice(0, start) + (column - prefixLength) + text.slice(start + digits);
783
+ }
784
+ Error.prepareStackTrace = function (error, frames) {
785
+ return Error.prototype.toString.call(error) + frames.map(function (frame) {
786
+ return "\\n at " + authoredFrame(frame);
787
+ }).join("");
788
+ };
789
+ return function (filename) { workflowFiles.add(filename); };
790
+ }
791
+
792
+ function compileWorkflow(body, filename) {
793
+ registerWorkflowFile(filename);
794
+ try {
795
+ return new vm.Script(WORKFLOW_PREFIX + body + "\\n})", { filename: filename }).runInContext(realmContext);
796
+ } catch (error) {
797
+ // Parser errors are created outside the realm. Retain the actual error,
798
+ // but remove the generated prefix from a first-line source/caret excerpt.
799
+ if (error && typeof error.stack === "string") {
800
+ if (error.stack.startsWith(filename + ":1\\n")) {
801
+ const lines = error.stack.split("\\n");
802
+ if (lines[1]?.startsWith(WORKFLOW_PREFIX)) {
803
+ lines[1] = lines[1].slice(WORKFLOW_PREFIX.length);
804
+ if (lines[2] !== undefined) lines[2] = lines[2].slice(WORKFLOW_PREFIX.length);
805
+ error.stack = lines.join("\\n");
806
+ }
807
+ } else if (error.sourceURL === filename && error.stack.startsWith("[native code]:0\\n")) {
808
+ // Bun 1.3 defers parsing until runInContext and loses the source
809
+ // excerpt/column. It still supplies the authored line; don't invent
810
+ // a caret for a column the engine did not report.
811
+ const line = error.line;
812
+ const sourceLines = body.split(/\\r\\n|[\\n\\r\\u2028\\u2029]/);
813
+ if (Number.isInteger(line) && line > 0 && line <= sourceLines.length) {
814
+ error.stack = filename + ":" + line + "\\n" + sourceLines[line - 1] + "\\n" + error.stack.slice(error.stack.indexOf("\\n") + 1);
815
+ }
816
+ }
817
+ }
818
+ throw error;
819
+ }
820
+ }
821
+
724
822
  /* ------------------------------------------------------------------ *
725
823
  * Run
726
824
  * ------------------------------------------------------------------ */
@@ -754,14 +852,15 @@ async function main() {
754
852
  sandbox.meta = realmParse(workerData.metaJson);
755
853
  sandbox.args = workerData.argsJson === undefined ? undefined : realmParse(workerData.argsJson);
756
854
 
757
- const script = new vm.Script("(async () => {" + PRELUDE + "\\n" + workerData.body + "\\n})()", {
758
- filename: "workflow.js",
759
- // The wrapper adds exactly one line above the body; undo it so a thrown
760
- // error points at the line the author wrote.
761
- lineOffset: -1,
762
- });
763
-
764
- const value = await script.runInContext(context);
855
+ // Structured CallSites retain constructor-expression columns on both V8 and
856
+ // JSC. Formatting changes no Error constructor, prototype, identity or cause.
857
+ registerWorkflowFile = vm.runInContext(
858
+ "(" + installAuthoredStacks.toString() + ")(" + WORKFLOW_PREFIX.length + ")",
859
+ context,
860
+ { filename: "workflow-stack.js" }
861
+ );
862
+ const run = compileWorkflow(workerData.body, "workflow.js");
863
+ const value = await run(rootScope.agent, rootScope.phase, rootScope.log, rootScope.workflow, rootScope.console, sandbox.args);
765
864
  checkBoundary(value, "the workflow result");
766
865
  flushProgress();
767
866
  port.postMessage({
@@ -770,12 +869,31 @@ async function main() {
770
869
  });
771
870
  }
772
871
 
872
+ /** Serialize diagnostics only: keep realm errors untouched and bound cause traversal/output. */
873
+ function failureDetails(error) {
874
+ const limit = 16384;
875
+ let message = "Workflow threw an unreadable error.";
876
+ let stack = "";
877
+ const seen = new Set();
878
+ try {
879
+ message = String(error && error.message ? error.message : error).slice(0, 4096);
880
+ let current = error;
881
+ for (let depth = 0; depth < 8 && stack.length < limit; depth++) {
882
+ if (seen.has(current)) break;
883
+ seen.add(current);
884
+ const text = current && typeof current.stack === "string" ? current.stack : String(current);
885
+ stack += (depth === 0 ? "" : "\\nCaused by: ") + text.slice(0, limit - stack.length);
886
+ if (!current || current.cause === undefined) break;
887
+ current = current.cause;
888
+ }
889
+ } catch {
890
+ // A diagnostic getter must not prevent the failed run from settling.
891
+ }
892
+ return { type: "error", message: message, stack: stack.slice(0, limit) || undefined };
893
+ }
894
+
773
895
  main().catch(function (error) {
774
896
  flushProgress();
775
- port.postMessage({
776
- type: "error",
777
- message: error && error.message ? String(error.message) : String(error),
778
- stack: error && error.stack ? String(error.stack) : undefined,
779
- });
897
+ port.postMessage(failureDetails(error));
780
898
  });
781
899
  `;