@wrongstack/core 0.155.0 → 0.236.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 (81) hide show
  1. package/dist/{agent-bridge-BbZU5TPN.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-Bsueu0J2.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
  3. package/dist/{brain-CS_B0vIE.d.ts → brain-sCZ3lCjq.d.ts} +26 -2
  4. package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
  5. package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
  6. package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
  7. package/dist/coordination/index.d.ts +70 -13
  8. package/dist/coordination/index.js +1983 -145
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -26
  11. package/dist/defaults/index.js +1105 -289
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +45 -16
  14. package/dist/execution/index.js +224 -53
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +86 -0
  17. package/dist/execution/prompt-enhancer.js +125 -0
  18. package/dist/execution/prompt-enhancer.js.map +1 -0
  19. package/dist/extension/index.d.ts +6 -6
  20. package/dist/extension/index.js +3 -1
  21. package/dist/extension/index.js.map +1 -1
  22. package/dist/{goal-preamble-CbV8pXLD.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
  23. package/dist/{index-CI1hRfPt.d.ts → index-BlMqh5GO.d.ts} +8 -8
  24. package/dist/{index-B5wz-GXm.d.ts → index-C2eSNPsB.d.ts} +7 -5
  25. package/dist/index.d.ts +438 -128
  26. package/dist/index.js +4974 -836
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -7
  29. package/dist/infrastructure/index.js +61 -13
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +7 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
  35. package/dist/logger-DmmQhf4P.d.ts +65 -0
  36. package/dist/{mcp-servers-CPERR2De.d.ts → mcp-servers-DFbirBv6.d.ts} +3 -3
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +89 -9
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-BSKSFNhv.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
  42. package/dist/{null-fleet-bus-CGOez8Le.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
  45. package/dist/{parallel-eternal-engine-CYoTKjsz.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
  46. package/dist/{path-resolver-DuhlmPil.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
  47. package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
  48. package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
  49. package/dist/pipeline-DsmlwTXu.d.ts +493 -0
  50. package/dist/{plan-templates-DbH7lg-t.d.ts → plan-templates-DPABrDvy.d.ts} +18 -7
  51. package/dist/{provider-runner-Cocq0O9E.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
  52. package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +8 -8
  54. package/dist/sdd/index.js +215 -79
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-w8MbUe2Q.d.ts → secret-vault-CeVNiy_f.d.ts} +3 -2
  57. package/dist/security/index.d.ts +5 -4
  58. package/dist/security/index.js +155 -13
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
  62. package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
  63. package/dist/skills/index.js +171 -21
  64. package/dist/skills/index.js.map +1 -1
  65. package/dist/storage/index.d.ts +150 -12
  66. package/dist/storage/index.js +1041 -214
  67. package/dist/storage/index.js.map +1 -1
  68. package/dist/types/index.d.ts +67 -20
  69. package/dist/types/index.js +557 -52
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/expect-defined.js +3 -1
  72. package/dist/utils/expect-defined.js.map +1 -1
  73. package/dist/utils/index.d.ts +16 -4
  74. package/dist/utils/index.js +40 -14
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
  77. package/package.json +7 -3
  78. package/skills/chimera/SKILL.md +105 -0
  79. package/skills/research-web/SKILL.md +342 -0
  80. package/dist/logger-B9J5puGM.d.ts +0 -32
  81. package/dist/pipeline-BG7UgbDc.d.ts +0 -239
@@ -3,7 +3,7 @@ import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash }
3
3
  import * as fsp from 'fs/promises';
4
4
  import * as path11 from 'path';
5
5
  import { isAbsolute, resolve } from 'path';
6
- import * as fs4 from 'fs';
6
+ import * as fs from 'fs';
7
7
  import * as os from 'os';
8
8
  import { hostname } from 'os';
9
9
  import { execFile } from 'child_process';
@@ -204,21 +204,27 @@ var COLORS = {
204
204
  trace: color.dim
205
205
  };
206
206
  var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
207
+ var LOG_FORMATS = /* @__PURE__ */ new Set(["pretty", "json"]);
207
208
  var DefaultLogger = class _DefaultLogger {
209
+ /** How many file writes between rotation size checks (statSync is not free). */
210
+ static ROTATE_CHECK_EVERY = 100;
208
211
  level;
209
212
  file;
210
213
  bindings;
211
- pretty;
214
+ format;
212
215
  stderr;
216
+ maxFileBytes;
217
+ writesSinceRotateCheck = 0;
213
218
  constructor(opts = {}) {
214
219
  this.level = opts.level ?? parseLogLevel(process.env.WRONGSTACK_LOG_LEVEL);
215
220
  this.file = opts.file;
216
221
  this.bindings = opts.bindings ?? {};
217
- this.pretty = opts.pretty ?? true;
222
+ this.format = opts.format ?? parseLogFormat(process.env.WRONGSTACK_LOG_FORMAT);
218
223
  this.stderr = opts.stderr !== false;
224
+ this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
219
225
  if (this.file) {
220
226
  try {
221
- fs4.mkdirSync(path11.dirname(this.file), { recursive: true });
227
+ fs.mkdirSync(path11.dirname(this.file), { recursive: true });
222
228
  } catch {
223
229
  }
224
230
  }
@@ -242,11 +248,30 @@ var DefaultLogger = class _DefaultLogger {
242
248
  return new _DefaultLogger({
243
249
  level: this.level,
244
250
  file: this.file,
245
- pretty: this.pretty,
251
+ format: this.format,
246
252
  stderr: this.stderr,
253
+ maxFileBytes: this.maxFileBytes,
247
254
  bindings: { ...this.bindings, ...bindings }
248
255
  });
249
256
  }
257
+ /**
258
+ * Size-based rotation: when the file outgrows `maxFileBytes`, rename it to
259
+ * `<file>.1` (dropping the previous `.1`) so the live file restarts empty.
260
+ * Checked on the first write and every ROTATE_CHECK_EVERY writes after.
261
+ * Best-effort: a rename can fail on Windows while another process holds
262
+ * the file — the next check retries. Multiple processes appending to the
263
+ * same log all run this check; whoever crosses the threshold first wins.
264
+ */
265
+ maybeRotate(file) {
266
+ if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
267
+ try {
268
+ const st = fs.statSync(file);
269
+ if (st.size < this.maxFileBytes) return;
270
+ fs.rmSync(`${file}.1`, { force: true });
271
+ fs.renameSync(file, `${file}.1`);
272
+ } catch {
273
+ }
274
+ }
250
275
  log(level, msg, ctx) {
251
276
  const r = LEVEL_RANK[level];
252
277
  const allowed = LEVEL_RANK[this.level];
@@ -258,13 +283,17 @@ var DefaultLogger = class _DefaultLogger {
258
283
  }
259
284
  if (this.file) {
260
285
  try {
261
- fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
286
+ this.maybeRotate(this.file);
287
+ fs.appendFileSync(this.file, `${JSON.stringify(entry)}
262
288
  `);
263
289
  } catch {
264
290
  }
265
291
  }
266
292
  if (!this.stderr) return;
267
- if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
293
+ if (this.format === "json") {
294
+ writeErr(`${JSON.stringify(entry)}
295
+ `);
296
+ } else {
268
297
  const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
269
298
  if (ctx !== void 0) {
270
299
  writeErr(`${head} ${formatCtx(ctx)}
@@ -279,6 +308,9 @@ var DefaultLogger = class _DefaultLogger {
279
308
  function parseLogLevel(raw) {
280
309
  return raw && LOG_LEVELS.has(raw) ? raw : "info";
281
310
  }
311
+ function parseLogFormat(raw) {
312
+ return raw && LOG_FORMATS.has(raw) ? raw : "pretty";
313
+ }
282
314
  function formatCtx(ctx) {
283
315
  if (ctx instanceof Error) return color.dim(ctx.message);
284
316
  if (typeof ctx === "string") return color.dim(ctx);
@@ -292,7 +324,9 @@ function formatCtx(ctx) {
292
324
  // src/utils/expect-defined.ts
293
325
  function expectDefined(value, label) {
294
326
  if (value === null || value === void 0) {
295
- throw new Error("Expected value to be defined");
327
+ const err = new Error("Expected value to be defined");
328
+ err.name = "ExpectDefinedError";
329
+ throw err;
296
330
  }
297
331
  return value;
298
332
  }
@@ -452,7 +486,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
452
486
  onClose: (s) => this.appendToIndex(s)
453
487
  });
454
488
  } catch (err) {
455
- await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
489
+ await handle.close().catch((e) => console.warn(JSON.stringify({
490
+ level: "warn",
491
+ event: "session_store.handle_close_failed",
492
+ message: e instanceof Error ? e.message : String(e),
493
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
494
+ })));
456
495
  throw err;
457
496
  }
458
497
  }
@@ -479,11 +518,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
479
518
  provider: data.metadata.provider
480
519
  },
481
520
  this.events,
482
- { resumed: true, dir: this.dir, filePath: file, secretScrubber: this.secretScrubber, onClose: (s) => this.appendToIndex(s) }
521
+ {
522
+ resumed: true,
523
+ // Shard directory (sessions/<date>/) — must match create() so the
524
+ // .summary.json sidecar lands next to the JSONL instead of the
525
+ // sessions root (where summaryFor() would never find it).
526
+ dir: path11.dirname(file),
527
+ filePath: file,
528
+ secretScrubber: this.secretScrubber,
529
+ onClose: (s) => this.appendToIndex(s)
530
+ }
483
531
  );
484
532
  return { writer, data };
485
533
  } catch (err) {
486
- await handle.close().catch((e) => console.warn(`[session-store] handle.close() failed: ${e}`));
534
+ await handle.close().catch((e) => console.warn(JSON.stringify({
535
+ level: "warn",
536
+ event: "session_store.handle_close_failed",
537
+ message: e instanceof Error ? e.message : String(e),
538
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
539
+ })));
487
540
  throw err;
488
541
  }
489
542
  }
@@ -503,7 +556,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
503
556
  }
504
557
  const meta = this.metaFromEvents(id, events);
505
558
  const { messages, usage } = this.replay(events, id);
506
- return { metadata: meta, events, messages, usage };
559
+ const toolCallEnds = extractToolCallEnds(events);
560
+ return { metadata: meta, events, messages, usage, toolCallEnds };
507
561
  }
508
562
  async list(limit = 20) {
509
563
  try {
@@ -659,10 +713,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
659
713
  const stat6 = await fsp.stat(full);
660
714
  const summary = await this.summarize(id, stat6.mtime.toISOString());
661
715
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
662
- console.warn(
663
- `[session-store] Failed to write manifest for "${id}":`,
664
- err instanceof Error ? err.message : String(err)
665
- );
716
+ console.warn(JSON.stringify({
717
+ level: "warn",
718
+ event: "session_store.manifest_write_failed",
719
+ sessionId: id,
720
+ message: err instanceof Error ? err.message : String(err),
721
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
722
+ }));
666
723
  });
667
724
  return summary;
668
725
  }
@@ -670,17 +727,48 @@ var DefaultSessionStore = class _DefaultSessionStore {
670
727
  /**
671
728
  * Delete a session and all associated files: JSONL, summary, plan/todos
672
729
  * sidecars, and the session directory (fleet.json, shared/, subagents/).
730
+ *
731
+ * Individual file deletions are best-effort (logged as structured warnings),
732
+ * but a tombstone is always written so readIndex() filters this session out.
733
+ * If the session directory itself can't be removed, the error is surfaced
734
+ * to the caller so prune() can report it.
673
735
  */
674
736
  async deleteSession(id) {
675
- await fsp.unlink(this.sessionPath(id, ".jsonl")).catch((err) => console.warn(`[session-store] delete .jsonl failed: ${err}`));
676
- await fsp.unlink(this.sessionPath(id, ".summary.json")).catch((err) => console.warn(`[session-store] delete .summary.json failed: ${err}`));
737
+ const jsonlPath = this.sessionPath(id, ".jsonl");
738
+ const summaryPath = this.sessionPath(id, ".summary.json");
677
739
  const shardDir = path11.dirname(path11.join(this.dir, id));
678
740
  const base = path11.basename(id);
679
- for (const ext of [".plan.json", ".todos.json"]) {
680
- await fsp.unlink(path11.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
681
- }
682
741
  const sessDir = path11.join(shardDir, base);
683
- await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => console.warn(`[session-store] delete session dir failed: ${err}`));
742
+ const deletions = [
743
+ fsp.unlink(jsonlPath),
744
+ fsp.unlink(summaryPath),
745
+ fsp.unlink(path11.join(shardDir, `${base}.plan.json`)),
746
+ fsp.unlink(path11.join(shardDir, `${base}.todos.json`))
747
+ ];
748
+ const results = await Promise.allSettled(deletions);
749
+ for (const r of results) {
750
+ if (r.status === "rejected") {
751
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
752
+ if (r.reason?.code !== "ENOENT") {
753
+ console.warn(JSON.stringify({
754
+ level: "warn",
755
+ event: "session_store.delete_failed",
756
+ sessionId: id,
757
+ message: msg,
758
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
759
+ }));
760
+ }
761
+ }
762
+ }
763
+ await fsp.rm(sessDir, { recursive: true, force: true }).catch((err) => {
764
+ console.warn(JSON.stringify({
765
+ level: "warn",
766
+ event: "session_store.rmdir_failed",
767
+ sessionId: id,
768
+ message: err instanceof Error ? err.message : String(err),
769
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
770
+ }));
771
+ });
684
772
  await this.writeTombstone(id);
685
773
  }
686
774
  async delete(id) {
@@ -696,24 +784,33 @@ var DefaultSessionStore = class _DefaultSessionStore {
696
784
  activeSessionId = active.sessionId ?? null;
697
785
  } catch {
698
786
  }
787
+ const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
788
+ const pruneFile = async (dir, name, prefix) => {
789
+ const jsonlPath = path11.join(dir, name);
790
+ try {
791
+ const stat6 = await fsp.stat(jsonlPath);
792
+ if (stat6.mtimeMs >= cutoff) return;
793
+ } catch {
794
+ return;
795
+ }
796
+ const base = name.replace(/\.jsonl$/, "");
797
+ const id = prefix ? `${prefix}/${base}` : base;
798
+ if (activeSessionId && id === activeSessionId) return;
799
+ await this.deleteSession(id);
800
+ deleted++;
801
+ };
699
802
  const entries = await fsp.readdir(this.dir, { withFileTypes: true }).catch(() => []);
700
803
  for (const entry of entries) {
804
+ if (entry.isFile()) {
805
+ if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
806
+ continue;
807
+ }
701
808
  if (!entry.isDirectory()) continue;
702
809
  const dateDir = path11.join(this.dir, entry.name);
703
810
  const files = await fsp.readdir(dateDir, { withFileTypes: true }).catch(() => []);
704
811
  for (const file of files) {
705
- if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
706
- const jsonlPath = path11.join(dateDir, file.name);
707
- try {
708
- const stat6 = await fsp.stat(jsonlPath);
709
- if (stat6.mtimeMs >= cutoff) continue;
710
- } catch {
711
- continue;
712
- }
713
- const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
714
- if (activeSessionId && id === activeSessionId) continue;
715
- await this.deleteSession(id);
716
- deleted++;
812
+ if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
813
+ await pruneFile(dateDir, file.name, entry.name);
717
814
  }
718
815
  }
719
816
  if (deleted > 0) {
@@ -802,7 +899,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
802
899
  }
803
900
  metaFromEvents(id, events) {
804
901
  const start = events.find((e) => e.type === "session_start");
805
- const end = events.find((e) => e.type === "session_end");
902
+ const end = events.findLast((e) => e.type === "session_end");
806
903
  return {
807
904
  id,
808
905
  startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
@@ -819,9 +916,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
819
916
  for (const e of events) {
820
917
  if (e.type === "user_input") {
821
918
  openToolUses.clear();
822
- messages.push({ role: "user", content: e.content });
919
+ messages.push({ role: "user", content: e.content, ts: e.ts });
823
920
  } else if (e.type === "llm_response") {
824
- messages.push({ role: "assistant", content: e.content });
921
+ messages.push({ role: "assistant", content: e.content, ts: e.ts });
825
922
  for (const b of e.content) {
826
923
  if (b.type === "tool_use") openToolUses.add(b.id);
827
924
  }
@@ -840,25 +937,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
840
937
  continue;
841
938
  }
842
939
  openToolUses.delete(e.id);
843
- const content = [
844
- {
845
- type: "tool_result",
846
- tool_use_id: e.id,
847
- content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
848
- is_error: e.isError
849
- }
850
- ];
940
+ const resultBlock = {
941
+ type: "tool_result",
942
+ tool_use_id: e.id,
943
+ content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
944
+ is_error: e.isError
945
+ };
851
946
  const last = messages[messages.length - 1];
852
- if (last && last.role === "user") {
853
- if (Array.isArray(last.content)) {
854
- last.content.push(...content);
855
- } else if (typeof last.content === "string") {
856
- last.content = [{ type: "text", text: last.content }, ...content];
857
- } else {
858
- messages.push({ role: "user", content });
859
- }
947
+ const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
948
+ if (lastIsToolResultUser && Array.isArray(last.content)) {
949
+ last.content.push(resultBlock);
860
950
  } else {
861
- messages.push({ role: "user", content });
951
+ messages.push({ role: "user", content: [resultBlock], ts: e.ts });
862
952
  }
863
953
  }
864
954
  }
@@ -878,7 +968,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
878
968
  return { messages: repaired.messages, usage };
879
969
  }
880
970
  };
881
- var FileSessionWriter = class {
971
+ function extractToolCallEnds(events) {
972
+ const result = [];
973
+ for (const e of events) {
974
+ if (e.type === "tool_call_end") {
975
+ result.push({
976
+ name: e.name,
977
+ id: e.id,
978
+ durationMs: e.durationMs,
979
+ ok: e.ok ?? false,
980
+ outputBytes: e.outputBytes,
981
+ outputTokens: e.outputTokens,
982
+ outputLines: e.outputLines
983
+ });
984
+ }
985
+ }
986
+ return result;
987
+ }
988
+ var FileSessionWriter = class _FileSessionWriter {
882
989
  constructor(id, handle, startedAt, meta, events, opts = {}) {
883
990
  this.id = id;
884
991
  this.handle = handle;
@@ -905,7 +1012,7 @@ var FileSessionWriter = class {
905
1012
  meta;
906
1013
  events;
907
1014
  closed = false;
908
- closing = false;
1015
+ closePromise = null;
909
1016
  manifestFile;
910
1017
  summary;
911
1018
  tokenIn = 0;
@@ -914,12 +1021,51 @@ var FileSessionWriter = class {
914
1021
  get transcriptPath() {
915
1022
  return this.filePath || void 0;
916
1023
  }
917
- initDone = false;
1024
+ /**
1025
+ * Lazy session_start/session_resumed init, shared by all appenders.
1026
+ * A single promise (not a boolean) so a second append racing the first
1027
+ * can't push its event into the buffer BEFORE the first append's event —
1028
+ * every appender awaits the same init and resumes in FIFO call order.
1029
+ */
1030
+ initPromise = null;
1031
+ ensureInit() {
1032
+ if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
1033
+ return this.initPromise;
1034
+ }
918
1035
  resumed;
919
1036
  appendFailCount = 0;
920
1037
  lastAppendWarnAt = 0;
921
1038
  secretScrubber;
922
1039
  onCloseCb;
1040
+ // ── Write buffer — batches events to reduce per-event disk I/O ─────────
1041
+ //
1042
+ // Every append() pushes the scrubbed event into an in-memory buffer instead
1043
+ // of calling handle.appendFile() synchronously. The buffer flushes to disk
1044
+ // when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
1045
+ // This cuts the number of disk writes by ~95% without changing the on-disk
1046
+ // format — the JSONL is still one JSON object per line.
1047
+ writeBuffer = [];
1048
+ flushTimer = null;
1049
+ static FLUSH_INTERVAL_MS = 500;
1050
+ static FLUSH_SIZE = 50;
1051
+ // ── Write serialization ─────────────────────────────────────────────────
1052
+ //
1053
+ // All disk writes are funneled through a FIFO promise chain. Without it,
1054
+ // a timer-driven flush racing an explicit flush()/close() issues two
1055
+ // concurrent appendFile() calls on the shared O_APPEND handle — the kernel
1056
+ // may complete them out of order (chronology breaks) or, for large
1057
+ // batches, interleave partial writes (torn JSONL lines). The chain keeps
1058
+ // exactly one write in flight; failures don't break the chain.
1059
+ writeChain = Promise.resolve();
1060
+ /** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
1061
+ enqueueWrite(data) {
1062
+ const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
1063
+ this.writeChain = write.then(
1064
+ () => void 0,
1065
+ () => void 0
1066
+ );
1067
+ return write;
1068
+ }
923
1069
  // ── Enriched summary tracking ──────────────────────────────────────────
924
1070
  iterationCount = 0;
925
1071
  toolCallCount = 0;
@@ -969,31 +1115,91 @@ var FileSessionWriter = class {
969
1115
  })}
970
1116
  `;
971
1117
  try {
972
- if (this.filePath) {
973
- await fsp.writeFile(this.filePath, record, { flag: "a", mode: 384 });
974
- }
1118
+ await this.enqueueWrite(record);
975
1119
  } catch {
976
1120
  }
977
1121
  }
978
1122
  async append(event) {
979
1123
  if (this.closed) return;
980
- if (!this.initDone) {
981
- this.initDone = true;
982
- await this.writeSessionStartLazy();
983
- }
1124
+ await this.ensureInit();
984
1125
  const scrubbed = this.scrubEvent(event);
985
1126
  this.observeForSummary(scrubbed);
1127
+ this.writeBuffer.push(scrubbed);
1128
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
1129
+ if (this.flushTimer) {
1130
+ clearTimeout(this.flushTimer);
1131
+ this.flushTimer = null;
1132
+ }
1133
+ await this.flushBuffer();
1134
+ } else {
1135
+ this.scheduleFlush();
1136
+ }
1137
+ }
1138
+ async appendBatch(events) {
1139
+ if (this.closed || events.length === 0) return;
1140
+ await this.ensureInit();
1141
+ for (const event of events) {
1142
+ const scrubbed = this.scrubEvent(event);
1143
+ this.observeForSummary(scrubbed);
1144
+ this.writeBuffer.push(scrubbed);
1145
+ }
1146
+ if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
1147
+ if (this.flushTimer) {
1148
+ clearTimeout(this.flushTimer);
1149
+ this.flushTimer = null;
1150
+ }
1151
+ await this.flushBuffer();
1152
+ } else {
1153
+ this.scheduleFlush();
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Flush buffered events to disk immediately. Critical events
1158
+ * (user_input, llm_response) call this so they survive SIGKILL/crash
1159
+ * instead of sitting in the in-memory buffer for up to 500ms.
1160
+ *
1161
+ * Idempotent — cancels any pending timer and writes whatever has
1162
+ * accumulated in the buffer. Safe to call even when the buffer
1163
+ * is empty (no-op).
1164
+ */
1165
+ async flush() {
1166
+ if (this.flushTimer) {
1167
+ clearTimeout(this.flushTimer);
1168
+ this.flushTimer = null;
1169
+ }
1170
+ await this.flushBuffer();
1171
+ }
1172
+ /** Schedule a deferred flush. No-op if a timer is already pending. */
1173
+ scheduleFlush() {
1174
+ if (this.flushTimer) return;
1175
+ this.flushTimer = setTimeout(() => {
1176
+ this.flushTimer = null;
1177
+ this.flushBuffer().catch(() => {
1178
+ });
1179
+ }, _FileSessionWriter.FLUSH_INTERVAL_MS);
1180
+ }
1181
+ /**
1182
+ * Flush all buffered events to disk as a single appendFile call.
1183
+ * Errors use the same throttled-warning pattern the old per-event
1184
+ * append path used — one warning every 5s with a suppressed count.
1185
+ * On failure the buffer is cleared (events are best-effort, same as
1186
+ * the old per-event path where a failed write was silently dropped).
1187
+ */
1188
+ async flushBuffer() {
1189
+ if (this.writeBuffer.length === 0) return;
1190
+ const eventCount = this.writeBuffer.length;
1191
+ const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
1192
+ this.writeBuffer = [];
986
1193
  try {
987
- await this.handle.appendFile(`${JSON.stringify(scrubbed)}
988
- `, "utf8");
1194
+ await this.enqueueWrite(batch);
989
1195
  } catch (err) {
990
- this.appendFailCount++;
1196
+ this.appendFailCount += eventCount;
991
1197
  const now = Date.now();
992
1198
  if (now - this.lastAppendWarnAt > 5e3) {
993
1199
  const suppressed = this.appendFailCount - 1;
994
1200
  const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
995
1201
  console.warn(
996
- "[session] append failed:",
1202
+ "[session] flush failed:",
997
1203
  err instanceof Error ? err.message : String(err),
998
1204
  tail
999
1205
  );
@@ -1003,6 +1209,11 @@ var FileSessionWriter = class {
1003
1209
  }
1004
1210
  }
1005
1211
  observeForSummary(event) {
1212
+ if (event.type === "llm_response") {
1213
+ for (const block of event.content) {
1214
+ if (block.type === "tool_use") this.openToolUses.add(block.id);
1215
+ }
1216
+ }
1006
1217
  if (event.type === "tool_use") {
1007
1218
  this.openToolUses.add(event.id);
1008
1219
  } else if (event.type === "tool_call_start") {
@@ -1036,9 +1247,18 @@ var FileSessionWriter = class {
1036
1247
  }
1037
1248
  }
1038
1249
  async close() {
1039
- if (this.closing) return;
1040
- this.closing = true;
1250
+ if (this.closePromise) return this.closePromise;
1251
+ this.closePromise = this.doClose();
1252
+ return this.closePromise;
1253
+ }
1254
+ async doClose() {
1041
1255
  this.closed = true;
1256
+ if (this.flushTimer) {
1257
+ clearTimeout(this.flushTimer);
1258
+ this.flushTimer = null;
1259
+ }
1260
+ await this.flushBuffer();
1261
+ await this.writeChain;
1042
1262
  this.summary = {
1043
1263
  ...this.summary,
1044
1264
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1094,6 +1314,12 @@ var FileSessionWriter = class {
1094
1314
  }
1095
1315
  async truncateToCheckpoint(targetPromptIndex) {
1096
1316
  if (!this.filePath) return 0;
1317
+ if (this.flushTimer) {
1318
+ clearTimeout(this.flushTimer);
1319
+ this.flushTimer = null;
1320
+ }
1321
+ await this.flushBuffer();
1322
+ await this.writeChain;
1097
1323
  const raw = await fsp.readFile(this.filePath, "utf8");
1098
1324
  const lines = raw.split("\n");
1099
1325
  const kept = [];
@@ -1156,6 +1382,12 @@ var FileSessionWriter = class {
1156
1382
  }
1157
1383
  async clearSession() {
1158
1384
  if (!this.filePath) return;
1385
+ if (this.flushTimer) {
1386
+ clearTimeout(this.flushTimer);
1387
+ this.flushTimer = null;
1388
+ }
1389
+ this.writeBuffer = [];
1390
+ await this.writeChain;
1159
1391
  const record = `${JSON.stringify({
1160
1392
  type: "session_start",
1161
1393
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1225,7 +1457,13 @@ var QueueStore = class {
1225
1457
  } catch (err) {
1226
1458
  const code = err.code;
1227
1459
  if (code === "ENOENT") return [];
1228
- console.warn(`[QueueStore] failed to read queue file "${this.file}":`, err instanceof Error ? err.message : String(err));
1460
+ console.warn(JSON.stringify({
1461
+ level: "warn",
1462
+ event: "queue_store.read_failed",
1463
+ path: this.file,
1464
+ message: err instanceof Error ? err.message : String(err),
1465
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1466
+ }));
1229
1467
  return [];
1230
1468
  }
1231
1469
  let parsed;
@@ -1247,7 +1485,13 @@ var QueueStore = class {
1247
1485
  } catch (err) {
1248
1486
  const code = err.code;
1249
1487
  if (code === "ENOENT") return;
1250
- console.warn(`QueueStore.clear() failed for ${this.file}: ${err.message}`);
1488
+ console.warn(JSON.stringify({
1489
+ level: "warn",
1490
+ event: "queue_store.clear_failed",
1491
+ path: this.file,
1492
+ message: err.message,
1493
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1494
+ }));
1251
1495
  }
1252
1496
  }
1253
1497
  };
@@ -1670,10 +1914,9 @@ var DefaultMemoryStore = class {
1670
1914
  }
1671
1915
  async runSerialized(scope, work) {
1672
1916
  const prior = this.writeChain.get(scope) ?? Promise.resolve();
1673
- prior.catch((err) => {
1917
+ const next = prior.catch((err) => {
1674
1918
  this.writeErrors.set(scope, err);
1675
- });
1676
- const next = prior.catch(() => void 0).then(work);
1919
+ }).then(() => work());
1677
1920
  this.writeChain.set(scope, next);
1678
1921
  try {
1679
1922
  return await next;
@@ -1921,6 +2164,158 @@ function labelOf(scope) {
1921
2164
  }
1922
2165
  }
1923
2166
 
2167
+ // src/types/errors.ts
2168
+ var ERROR_CODES = {
2169
+ // Provider
2170
+ PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
2171
+ PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
2172
+ PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
2173
+ PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
2174
+ PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
2175
+ PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
2176
+ PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
2177
+ // Tool
2178
+ TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
2179
+ TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
2180
+ TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
2181
+ TOOL_TIMEOUT: "TOOL_TIMEOUT",
2182
+ TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
2183
+ // Config
2184
+ CONFIG_INVALID: "CONFIG_INVALID",
2185
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
2186
+ CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
2187
+ CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
2188
+ // Plugin
2189
+ PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
2190
+ PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
2191
+ PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
2192
+ // Agent
2193
+ AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
2194
+ AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
2195
+ AGENT_ABORTED: "AGENT_ABORTED",
2196
+ AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
2197
+ // Session
2198
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
2199
+ SESSION_CORRUPTED: "SESSION_CORRUPTED",
2200
+ SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
2201
+ // Container / Registry
2202
+ CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
2203
+ CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
2204
+ CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
2205
+ REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
2206
+ REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
2207
+ REGISTRY_INVALID: "REGISTRY_INVALID",
2208
+ // File system
2209
+ FS_READ_FAILED: "FS_READ_FAILED",
2210
+ FS_WRITE_FAILED: "FS_WRITE_FAILED",
2211
+ FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
2212
+ FS_DELETE_FAILED: "FS_DELETE_FAILED",
2213
+ FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
2214
+ // SDD (Spec-Driven Development)
2215
+ SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
2216
+ SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
2217
+ SDD_INVALID_STATE: "SDD_INVALID_STATE",
2218
+ SDD_NOT_READY: "SDD_NOT_READY",
2219
+ // General
2220
+ VALIDATION_ERROR: "VALIDATION_ERROR",
2221
+ UNKNOWN: "UNKNOWN"
2222
+ };
2223
+ var WrongStackError = class extends Error {
2224
+ code;
2225
+ subsystem;
2226
+ severity;
2227
+ recoverable;
2228
+ context;
2229
+ constructor(opts) {
2230
+ super(opts.message, { cause: opts.cause });
2231
+ this.name = "WrongStackError";
2232
+ this.code = opts.code;
2233
+ this.subsystem = opts.subsystem;
2234
+ this.severity = opts.severity ?? "error";
2235
+ this.recoverable = opts.recoverable ?? false;
2236
+ this.context = opts.context;
2237
+ }
2238
+ /**
2239
+ * Render a one-line user-facing description.
2240
+ * Subclasses should override for domain-specific formatting.
2241
+ */
2242
+ describe() {
2243
+ const ctx = this.context ? ` ${formatContext(this.context)}` : "";
2244
+ return `${this.code}: ${this.message}${ctx}`;
2245
+ }
2246
+ };
2247
+ function formatContext(ctx) {
2248
+ const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
2249
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
2250
+ }
2251
+ var ConfigError = class extends WrongStackError {
2252
+ constructor(opts) {
2253
+ super({
2254
+ message: opts.message,
2255
+ code: opts.code,
2256
+ subsystem: "config",
2257
+ severity: "fatal",
2258
+ recoverable: false,
2259
+ context: opts.context,
2260
+ cause: opts.cause
2261
+ });
2262
+ this.name = "ConfigError";
2263
+ }
2264
+ };
2265
+ var AgentError = class extends WrongStackError {
2266
+ constructor(opts) {
2267
+ super({
2268
+ message: opts.message,
2269
+ code: opts.code,
2270
+ subsystem: "agent",
2271
+ severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
2272
+ recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
2273
+ context: opts.context,
2274
+ cause: opts.cause
2275
+ });
2276
+ this.name = "AgentError";
2277
+ }
2278
+ };
2279
+ function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
2280
+ if (err instanceof WrongStackError) return err;
2281
+ const message = err instanceof Error ? err.message : String(err);
2282
+ return new AgentError({
2283
+ message,
2284
+ code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
2285
+ cause: err
2286
+ });
2287
+ }
2288
+ var SddError = class extends WrongStackError {
2289
+ constructor(opts) {
2290
+ super({
2291
+ message: opts.message,
2292
+ code: opts.code,
2293
+ subsystem: "sdd",
2294
+ severity: opts.code === ERROR_CODES.SDD_PARSE_FAILED ? "warning" : "error",
2295
+ recoverable: opts.code === ERROR_CODES.SDD_NOT_READY,
2296
+ context: opts.context,
2297
+ cause: opts.cause
2298
+ });
2299
+ this.name = "SddError";
2300
+ }
2301
+ };
2302
+ var FsError = class extends WrongStackError {
2303
+ path;
2304
+ constructor(opts) {
2305
+ super({
2306
+ message: opts.message,
2307
+ code: opts.code,
2308
+ subsystem: "fs",
2309
+ severity: "error",
2310
+ recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
2311
+ context: { path: opts.path, ...opts.context },
2312
+ cause: opts.cause
2313
+ });
2314
+ this.name = "FsError";
2315
+ this.path = opts.path;
2316
+ }
2317
+ };
2318
+
1924
2319
  // src/storage/config-store.ts
1925
2320
  function stripEphemeralFields(cfg) {
1926
2321
  const env = cfg._envSource;
@@ -1952,7 +2347,11 @@ var DefaultConfigStore = class {
1952
2347
  const scrubbed = stripEphemeralFields(partial);
1953
2348
  const next = deepFreeze(structuredClone({ ...this.current, ...scrubbed }));
1954
2349
  if (next.version !== 1) {
1955
- throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
2350
+ throw new ConfigError({
2351
+ message: `ConfigStore.update: version must remain 1, got ${String(next.version)}`,
2352
+ code: ERROR_CODES.CONFIG_INVALID,
2353
+ context: { field: "version", actual: next.version }
2354
+ });
1956
2355
  }
1957
2356
  const prev = this.current;
1958
2357
  this.current = next;
@@ -1960,7 +2359,12 @@ var DefaultConfigStore = class {
1960
2359
  try {
1961
2360
  w(next, prev);
1962
2361
  } catch (err) {
1963
- console.error("[config-store] watcher threw:", err);
2362
+ console.error(JSON.stringify({
2363
+ level: "error",
2364
+ event: "config_store.watcher_threw",
2365
+ message: err instanceof Error ? err.message : String(err),
2366
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2367
+ }));
1964
2368
  }
1965
2369
  }
1966
2370
  return next;
@@ -2053,6 +2457,26 @@ var KEY_BYTES = 32;
2053
2457
  var IV_BYTES = 12;
2054
2458
  var TAG_BYTES = 16;
2055
2459
  var ALGO = "aes-256-gcm";
2460
+ var KEY_FILE_MODE = 384;
2461
+ function checkKeyFilePermissions(keyFile) {
2462
+ if (process.platform === "win32") return;
2463
+ try {
2464
+ const stat6 = fs.statSync(keyFile);
2465
+ const actualMode = stat6.mode & 511;
2466
+ if (actualMode !== KEY_FILE_MODE) {
2467
+ console.warn(JSON.stringify({
2468
+ level: "warn",
2469
+ event: "vault.key_file_wrong_permissions",
2470
+ message: `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`,
2471
+ keyFile,
2472
+ expectedMode: KEY_FILE_MODE,
2473
+ actualMode,
2474
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2475
+ }));
2476
+ }
2477
+ } catch {
2478
+ }
2479
+ }
2056
2480
  var DefaultSecretVault = class {
2057
2481
  keyFile;
2058
2482
  key;
@@ -2076,14 +2500,26 @@ var DefaultSecretVault = class {
2076
2500
  const rest = value.slice(ENCRYPTED_PREFIX.length);
2077
2501
  const parts = rest.split(":");
2078
2502
  if (parts.length !== 3) {
2079
- throw new Error("SecretVault: malformed encrypted value");
2503
+ throw new ConfigError({
2504
+ message: "SecretVault: malformed encrypted value",
2505
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
2506
+ context: { field: "encrypted_value" }
2507
+ });
2080
2508
  }
2081
2509
  const [ivB64, tagB64, ctB64] = parts;
2082
2510
  const iv = Buffer.from(ivB64, "base64");
2083
2511
  const tag = Buffer.from(tagB64, "base64");
2084
2512
  const ct = Buffer.from(ctB64, "base64");
2085
- if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
2086
- if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
2513
+ if (iv.length !== IV_BYTES) throw new ConfigError({
2514
+ message: "SecretVault: bad IV length",
2515
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
2516
+ context: { expected: IV_BYTES, actual: iv.length }
2517
+ });
2518
+ if (tag.length !== TAG_BYTES) throw new ConfigError({
2519
+ message: "SecretVault: bad tag length",
2520
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
2521
+ context: { expected: TAG_BYTES, actual: tag.length }
2522
+ });
2087
2523
  const key = this.loadOrCreateKey();
2088
2524
  const decipher = createDecipheriv(ALGO, key, iv);
2089
2525
  decipher.setAuthTag(tag);
@@ -2093,30 +2529,36 @@ var DefaultSecretVault = class {
2093
2529
  loadOrCreateKey() {
2094
2530
  if (this.key) return this.key;
2095
2531
  try {
2096
- const buf = fs4.readFileSync(this.keyFile);
2532
+ const buf = fs.readFileSync(this.keyFile);
2097
2533
  if (buf.length !== KEY_BYTES) {
2098
- throw new Error(
2099
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
2100
- );
2534
+ throw new ConfigError({
2535
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
2536
+ code: ERROR_CODES.CONFIG_INVALID,
2537
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
2538
+ });
2101
2539
  }
2102
2540
  this.key = buf;
2541
+ checkKeyFilePermissions(this.keyFile);
2103
2542
  return this.key;
2104
2543
  } catch (err) {
2105
2544
  if (err.code !== "ENOENT") throw err;
2106
2545
  }
2107
- fs4.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
2546
+ fs.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
2108
2547
  const key = randomBytes(KEY_BYTES);
2109
2548
  try {
2110
- fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2549
+ fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2111
2550
  } catch (err) {
2112
2551
  if (err.code !== "EEXIST") throw err;
2113
- const buf = fs4.readFileSync(this.keyFile);
2552
+ const buf = fs.readFileSync(this.keyFile);
2114
2553
  if (buf.length !== KEY_BYTES) {
2115
- throw new Error(
2116
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
2117
- );
2554
+ throw new ConfigError({
2555
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
2556
+ code: ERROR_CODES.CONFIG_INVALID,
2557
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
2558
+ });
2118
2559
  }
2119
2560
  this.key = buf;
2561
+ checkKeyFilePermissions(this.keyFile);
2120
2562
  return this.key;
2121
2563
  }
2122
2564
  this.key = key;
@@ -2177,7 +2619,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
2177
2619
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2178
2620
  await restrictFilePermissions(configPath);
2179
2621
  }
2180
- async function migratePlaintextSecrets(configPath, vault) {
2622
+ async function migratePlaintextSecrets(configPath, vault, logger) {
2181
2623
  let raw;
2182
2624
  try {
2183
2625
  raw = await fsp.readFile(configPath, "utf8");
@@ -2194,11 +2636,14 @@ async function migratePlaintextSecrets(configPath, vault) {
2194
2636
  const migrated = walkCount(parsed, vault, counter);
2195
2637
  if (counter.n === 0) return { migrated: 0, file: configPath };
2196
2638
  await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
2197
- await restrictFilePermissions(configPath);
2639
+ await restrictFilePermissions(
2640
+ configPath,
2641
+ logger ? { warn: (msg) => logger.warn(msg) } : void 0
2642
+ );
2198
2643
  return { migrated: counter.n, file: configPath };
2199
2644
  }
2200
2645
  async function restrictFilePermissions(filePath, opts) {
2201
- const warn = ((msg) => console.warn(msg));
2646
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
2202
2647
  if (process.platform === "win32") {
2203
2648
  try {
2204
2649
  const { execFile: execFile2 } = await import('child_process');
@@ -2480,7 +2925,13 @@ var DefaultConfigLoader = class {
2480
2925
  cfg = deepMerge2(cfg, patch);
2481
2926
  }
2482
2927
  } catch (err) {
2483
- console.warn(`Config source "${src.name}" failed`, err);
2928
+ console.warn(JSON.stringify({
2929
+ level: "warn",
2930
+ event: "config.source_load_failed",
2931
+ source: src.name,
2932
+ message: err instanceof Error ? err.message : String(err),
2933
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2934
+ }));
2484
2935
  }
2485
2936
  }
2486
2937
  if (opts.cliFlags) {
@@ -2543,7 +2994,12 @@ var DefaultConfigLoader = class {
2543
2994
  return parsed.value;
2544
2995
  } catch (err) {
2545
2996
  if (err.code === "ENOENT") return null;
2546
- console.warn("[config] Failed to load sync config:", err);
2997
+ console.warn(JSON.stringify({
2998
+ level: "warn",
2999
+ event: "config.sync_load_failed",
3000
+ message: err instanceof Error ? err.message : String(err),
3001
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3002
+ }));
2547
3003
  return null;
2548
3004
  }
2549
3005
  }
@@ -2553,33 +3009,63 @@ var DefaultConfigLoader = class {
2553
3009
  raw = await fsp.readFile(file, "utf8");
2554
3010
  } catch (err) {
2555
3011
  if (err.code !== "ENOENT") {
2556
- console.warn(`[config] Failed to read "${file}":`, err);
3012
+ console.warn(JSON.stringify({
3013
+ level: "warn",
3014
+ event: "config.read_failed",
3015
+ path: file,
3016
+ message: err instanceof Error ? err.message : String(err),
3017
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3018
+ }));
2557
3019
  }
2558
3020
  return {};
2559
3021
  }
2560
3022
  const parsed = safeParse(raw);
2561
3023
  if (!parsed.ok || !parsed.value) {
2562
- console.warn(
2563
- `[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
2564
- );
3024
+ console.warn(JSON.stringify({
3025
+ level: "warn",
3026
+ event: "config.parse_failed",
3027
+ path: file,
3028
+ message: "invalid JSON \u2014 falling back to defaults for this layer",
3029
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3030
+ }));
2565
3031
  return {};
2566
3032
  }
2567
3033
  return parsed.value;
2568
3034
  }
2569
3035
  validateBehavior(cfg) {
2570
- if (cfg.version === void 0) throw new Error("Config: missing version field");
2571
- if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
3036
+ if (cfg.version === void 0) throw new ConfigError({
3037
+ message: "Config: missing version field",
3038
+ code: ERROR_CODES.CONFIG_INVALID,
3039
+ context: { field: "version" }
3040
+ });
3041
+ if (cfg.version !== 1) throw new ConfigError({
3042
+ message: `Config: unsupported version ${cfg.version}`,
3043
+ code: ERROR_CODES.CONFIG_INVALID,
3044
+ context: { field: "version", actual: cfg.version }
3045
+ });
2572
3046
  const c = cfg.context;
2573
- if (!c) throw new Error("Config: missing context section");
3047
+ if (!c) throw new ConfigError({
3048
+ message: "Config: missing context section",
3049
+ code: ERROR_CODES.CONFIG_INVALID,
3050
+ context: { field: "context" }
3051
+ });
2574
3052
  const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
2575
3053
  for (const f of fields) {
2576
3054
  const v = c[f];
2577
3055
  if (typeof v !== "number" || !Number.isFinite(v)) {
2578
- throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
3056
+ throw new ConfigError({
3057
+ message: `Config: context.${String(f)} must be a finite number (got ${typeof v})`,
3058
+ code: ERROR_CODES.CONFIG_INVALID,
3059
+ context: { field: `context.${String(f)}`, actualType: typeof v }
3060
+ });
2579
3061
  }
2580
3062
  }
2581
3063
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
2582
- throw new Error("Config: context thresholds must satisfy warn < soft < hard");
3064
+ throw new ConfigError({
3065
+ message: "Config: context thresholds must satisfy warn < soft < hard",
3066
+ code: ERROR_CODES.CONFIG_INVALID,
3067
+ context: { warn: c.warnThreshold, soft: c.softThreshold, hard: c.hardThreshold }
3068
+ });
2583
3069
  }
2584
3070
  if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
2585
3071
  const known = listContextWindowModes().map((m) => m.id).join(", ");
@@ -2591,12 +3077,18 @@ var DefaultConfigLoader = class {
2591
3077
  }
2592
3078
  validateIdentity(cfg) {
2593
3079
  if (!cfg.provider) {
2594
- throw new Error(
2595
- "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
2596
- );
3080
+ throw new ConfigError({
3081
+ message: "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER.",
3082
+ code: ERROR_CODES.CONFIG_INVALID,
3083
+ context: { field: "provider" }
3084
+ });
2597
3085
  }
2598
3086
  if (!cfg.model) {
2599
- throw new Error("Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.");
3087
+ throw new ConfigError({
3088
+ message: "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.",
3089
+ code: ERROR_CODES.CONFIG_INVALID,
3090
+ context: { field: "model" }
3091
+ });
2600
3092
  }
2601
3093
  }
2602
3094
  };
@@ -2697,7 +3189,10 @@ var RecoveryLock = class {
2697
3189
  if (this.sessionStore) {
2698
3190
  try {
2699
3191
  const data = await this.sessionStore.load(lock.sessionId);
2700
- const closed = data.events.some((e) => e.type === "session_end");
3192
+ const lastEnd = data.events.findLastIndex((e) => e.type === "session_end");
3193
+ const closed = lastEnd >= 0 && !data.events.slice(lastEnd + 1).some(
3194
+ (e) => e.type === "user_input" || e.type === "llm_response" || e.type === "in_flight_start"
3195
+ );
2701
3196
  if (closed) return null;
2702
3197
  messageCount = data.messages.length;
2703
3198
  } catch {
@@ -3242,6 +3737,7 @@ function isAllowed(type, level) {
3242
3737
  }
3243
3738
  function createSessionEventBridge(writer, level = "standard", options = {}) {
3244
3739
  const normalizedLevel = level ?? "standard";
3740
+ const resolveWriter = typeof writer === "function" ? writer : () => writer;
3245
3741
  const progressCounters = /* @__PURE__ */ new Map();
3246
3742
  const toolProgressConfig = options.sampling?.toolProgress ?? {};
3247
3743
  const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
@@ -3266,13 +3762,26 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
3266
3762
  return isAllowed(type, normalizedLevel);
3267
3763
  },
3268
3764
  async append(event) {
3269
- if (!writer) return;
3765
+ const target = resolveWriter();
3766
+ if (!target) return;
3270
3767
  if (!isAllowed(event.type, normalizedLevel)) return;
3271
3768
  if (!shouldSample(event)) return;
3272
3769
  try {
3273
- await writer.append(event);
3770
+ await target.append(event);
3274
3771
  } catch (err) {
3275
3772
  }
3773
+ },
3774
+ async appendBatch(events) {
3775
+ const target = resolveWriter();
3776
+ if (!target || events.length === 0) return;
3777
+ const allowed = events.filter(
3778
+ (e) => isAllowed(e.type, normalizedLevel) && shouldSample(e)
3779
+ );
3780
+ if (allowed.length === 0) return;
3781
+ try {
3782
+ await target.appendBatch(allowed);
3783
+ } catch {
3784
+ }
3276
3785
  }
3277
3786
  };
3278
3787
  }
@@ -3326,10 +3835,12 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
3326
3835
  try {
3327
3836
  await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
3328
3837
  } catch (err) {
3329
- console.warn(
3330
- "[todos-checkpoint] save failed:",
3331
- err instanceof Error ? err.message : String(err)
3332
- );
3838
+ console.warn(JSON.stringify({
3839
+ level: "warn",
3840
+ event: "todos_checkpoint.save_failed",
3841
+ message: err instanceof Error ? err.message : String(err),
3842
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3843
+ }));
3333
3844
  }
3334
3845
  }
3335
3846
  function attachTodosCheckpoint(state, filePath, sessionId) {
@@ -3339,7 +3850,13 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
3339
3850
  const enqueueWrite = (todos) => {
3340
3851
  writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
3341
3852
  const msg = err instanceof Error ? err.message : String(err);
3342
- console.error(`[TodosCheckpoint] save failed for session ${sessionId}: ${msg}`);
3853
+ console.error(JSON.stringify({
3854
+ level: "error",
3855
+ event: "todos_checkpoint.write_chain_failed",
3856
+ sessionId,
3857
+ message: msg,
3858
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3859
+ }));
3343
3860
  });
3344
3861
  return writeChain;
3345
3862
  };
@@ -3478,14 +3995,16 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
3478
3995
  id: `todo_${Date.now()}_plan`,
3479
3996
  content: item.title,
3480
3997
  status: "in_progress",
3481
- activeForm: item.title
3998
+ activeForm: item.title,
3999
+ promotedFromPlan: item.id
3482
4000
  });
3483
4001
  if (subtasks && subtasks.length > 0) {
3484
4002
  for (const st of subtasks) {
3485
4003
  todos.push({
3486
4004
  id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
3487
4005
  content: st,
3488
- status: "pending"
4006
+ status: "pending",
4007
+ promotedFromPlan: item.id
3489
4008
  });
3490
4009
  }
3491
4010
  }
@@ -4390,91 +4909,6 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
4390
4909
  }
4391
4910
  };
4392
4911
 
4393
- // src/types/errors.ts
4394
- var ERROR_CODES = {
4395
- // Provider
4396
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
4397
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
4398
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
4399
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
4400
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
4401
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
4402
- // Agent
4403
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
4404
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
4405
- AGENT_ABORTED: "AGENT_ABORTED",
4406
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
4407
- // File system
4408
- FS_READ_FAILED: "FS_READ_FAILED",
4409
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
4410
- var WrongStackError = class extends Error {
4411
- code;
4412
- subsystem;
4413
- severity;
4414
- recoverable;
4415
- context;
4416
- constructor(opts) {
4417
- super(opts.message, { cause: opts.cause });
4418
- this.name = "WrongStackError";
4419
- this.code = opts.code;
4420
- this.subsystem = opts.subsystem;
4421
- this.severity = opts.severity ?? "error";
4422
- this.recoverable = opts.recoverable ?? false;
4423
- this.context = opts.context;
4424
- }
4425
- /**
4426
- * Render a one-line user-facing description.
4427
- * Subclasses should override for domain-specific formatting.
4428
- */
4429
- describe() {
4430
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
4431
- return `${this.code}: ${this.message}${ctx}`;
4432
- }
4433
- };
4434
- function formatContext(ctx) {
4435
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
4436
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
4437
- }
4438
- var AgentError = class extends WrongStackError {
4439
- constructor(opts) {
4440
- super({
4441
- message: opts.message,
4442
- code: opts.code,
4443
- subsystem: "agent",
4444
- severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
4445
- recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
4446
- context: opts.context,
4447
- cause: opts.cause
4448
- });
4449
- this.name = "AgentError";
4450
- }
4451
- };
4452
- function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
4453
- if (err instanceof WrongStackError) return err;
4454
- const message = err instanceof Error ? err.message : String(err);
4455
- return new AgentError({
4456
- message,
4457
- code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
4458
- cause: err
4459
- });
4460
- }
4461
- var FsError = class extends WrongStackError {
4462
- path;
4463
- constructor(opts) {
4464
- super({
4465
- message: opts.message,
4466
- code: opts.code,
4467
- subsystem: "fs",
4468
- severity: "error",
4469
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
4470
- context: { path: opts.path, ...opts.context },
4471
- cause: opts.cause
4472
- });
4473
- this.name = "FsError";
4474
- this.path = opts.path;
4475
- }
4476
- };
4477
-
4478
4912
  // src/types/provider.ts
4479
4913
  var ProviderError = class extends WrongStackError {
4480
4914
  status;
@@ -5030,8 +5464,9 @@ function handleMessageStop(state, ev) {
5030
5464
  state.stopReason = ev.stopReason ?? "end_turn";
5031
5465
  state.usage = ev.usage ?? { input: 0, output: 0 };
5032
5466
  }
5033
- async function streamProviderToResponse(provider, req, signal, ctx, events) {
5467
+ async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
5034
5468
  const state = createStreamingState(req.model);
5469
+ logger.debug("Stream started", { providerId: provider.id, model: req.model });
5035
5470
  const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
5036
5471
  try {
5037
5472
  for (; ; ) {
@@ -5086,20 +5521,42 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
5086
5521
  case "message_stop":
5087
5522
  handleMessageStop(state, ev);
5088
5523
  break;
5089
- default:
5524
+ default: {
5525
+ const unknownEv = ev;
5526
+ logger.warn(`Stream received unknown event type: "${String(unknownEv.type)}"`, {
5527
+ providerId: provider.id,
5528
+ model: req.model,
5529
+ eventType: String(unknownEv.type)
5530
+ });
5090
5531
  break;
5532
+ }
5091
5533
  }
5092
5534
  } catch (handlerErr) {
5535
+ const errMsg = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
5536
+ const evAny = ev;
5537
+ logger.warn(`Stream handler error for event type "${String(evAny.type)}": ${errMsg}`, {
5538
+ providerId: provider.id,
5539
+ model: req.model,
5540
+ eventType: String(evAny.type),
5541
+ errorMessage: errMsg
5542
+ });
5093
5543
  events.emit("provider.stream_error", {
5094
5544
  ctx,
5095
- eventType: ev.type,
5096
- msg: handlerErr instanceof Error ? handlerErr.message : String(handlerErr)
5545
+ eventType: String(evAny.type),
5546
+ msg: errMsg
5097
5547
  });
5098
5548
  }
5099
5549
  }
5100
5550
  } catch (err) {
5101
5551
  if (signal.aborted) {
5102
5552
  state.stopReason = "end_turn";
5553
+ logger.debug("Stream aborted \u2014 returning partial state", {
5554
+ providerId: provider.id,
5555
+ model: req.model,
5556
+ textBlockCount: state.textBuffers.length,
5557
+ toolBlockCount: state.tools.size,
5558
+ thinkingBlockCount: state.thinking.length
5559
+ });
5103
5560
  return buildResponse(state);
5104
5561
  }
5105
5562
  throw err;
@@ -5121,10 +5578,29 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
5121
5578
  } catch {
5122
5579
  }
5123
5580
  }
5581
+ logger.debug("Stream completed", {
5582
+ providerId: provider.id,
5583
+ model: req.model,
5584
+ stopReason: state.stopReason,
5585
+ textBlockCount: state.textBuffers.length,
5586
+ toolBlockCount: state.tools.size,
5587
+ thinkingBlockCount: state.thinking.length,
5588
+ usageInput: state.usage.input,
5589
+ usageOutput: state.usage.output
5590
+ });
5124
5591
  return buildResponse(state);
5125
5592
  }
5126
5593
 
5127
5594
  // src/core/provider-runner.ts
5595
+ function providerLogCtx(p, r) {
5596
+ return {
5597
+ providerId: p.id,
5598
+ model: r.model,
5599
+ streaming: p.capabilities.streaming,
5600
+ msgCount: r.messages.length,
5601
+ toolCount: r.tools?.length ?? 0
5602
+ };
5603
+ }
5128
5604
  async function runProviderWithRetry(opts) {
5129
5605
  const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
5130
5606
  let attempt = 0;
@@ -5135,12 +5611,22 @@ async function runProviderWithRetry(opts) {
5135
5611
  "provider.streaming": provider.capabilities.streaming,
5136
5612
  "provider.attempt": attempt
5137
5613
  });
5614
+ logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
5138
5615
  try {
5139
- const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
5616
+ const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events, logger) : await provider.complete(request, { signal });
5140
5617
  span?.setAttribute("provider.stopReason", res.stopReason);
5141
5618
  span?.setAttribute("provider.usage_in", res.usage.input);
5142
5619
  span?.setAttribute("provider.usage_out", res.usage.output);
5143
5620
  span?.end();
5621
+ logger.info("Provider call succeeded", {
5622
+ ...providerLogCtx(provider, request),
5623
+ stopReason: res.stopReason,
5624
+ usageInput: res.usage.input,
5625
+ usageOutput: res.usage.output,
5626
+ cacheRead: res.usage.cacheRead,
5627
+ cacheWrite: res.usage.cacheWrite,
5628
+ attempts: attempt + 1
5629
+ });
5144
5630
  return res;
5145
5631
  } catch (err) {
5146
5632
  if (err instanceof Error) span?.recordError(err);
@@ -5159,11 +5645,27 @@ async function runProviderWithRetry(opts) {
5159
5645
  retryable: false
5160
5646
  });
5161
5647
  }
5162
- throw err;
5648
+ logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
5649
+ ...providerLogCtx(provider, request),
5650
+ attempts: attempt + 1,
5651
+ errorDescription: description,
5652
+ status: isProviderErr ? err.status : void 0,
5653
+ errorName: err instanceof Error ? err.name : void 0,
5654
+ errorStack: err instanceof Error ? err.stack?.split("\n").slice(0, 3).join("\n") : void 0
5655
+ });
5656
+ throw toWrongStackError(err);
5163
5657
  }
5164
5658
  const delay = Math.round(retry.delayMs(attempt));
5165
5659
  const attemptNum = attempt + 1;
5166
- logger.warn(`Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`);
5660
+ const maxAttempts = retry.maxAttempts(isProviderErr ? err : errAsErr);
5661
+ logger.warn(`Provider retry ${attemptNum}/${maxAttempts} in ${delay}ms \u2014 ${description}`, {
5662
+ ...providerLogCtx(provider, request),
5663
+ attempt: attemptNum,
5664
+ maxAttempts,
5665
+ delayMs: delay,
5666
+ errorDescription: description,
5667
+ status: isProviderErr ? err.status : void 0
5668
+ });
5167
5669
  if (isProviderErr) {
5168
5670
  events.emit("provider.retry", {
5169
5671
  providerId: err.providerId,
@@ -5250,22 +5752,31 @@ function estimateToolResultTokens(content) {
5250
5752
  function estimateTextTokens(text) {
5251
5753
  return RoughTokenEstimate(text);
5252
5754
  }
5755
+ function computeMessageTokens(msg) {
5756
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
5757
+ let total = 0;
5758
+ for (const b of msg.content) {
5759
+ if (b.type === "text") total += estimateTextTokens(b.text);
5760
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
5761
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
5762
+ else total += RoughTokenEstimate(JSON.stringify(b));
5763
+ }
5764
+ return total;
5765
+ }
5253
5766
  function estimateMessageTokens(messages) {
5254
5767
  let total = 0;
5255
5768
  for (const m of messages) {
5256
- if (typeof m.content === "string") {
5257
- total += estimateTextTokens(m.content);
5258
- } else {
5259
- for (const b of m.content) {
5260
- if (b.type === "text") total += estimateTextTokens(b.text);
5261
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
5262
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
5263
- }
5769
+ if (typeof m._estTokens === "number" && m._estTokens > 0) {
5770
+ total += m._estTokens;
5771
+ continue;
5264
5772
  }
5773
+ total += computeMessageTokens(m);
5265
5774
  }
5266
5775
  return total;
5267
5776
  }
5268
5777
  function estimateToolDefTokens(tool) {
5778
+ const cached = tool._estDefTokens;
5779
+ if (typeof cached === "number" && cached > 0) return cached;
5269
5780
  return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
5270
5781
  }
5271
5782
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
@@ -5275,6 +5786,11 @@ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = C
5275
5786
  } else if (Array.isArray(messages)) {
5276
5787
  for (const m of messages) {
5277
5788
  if (typeof m === "object" && m !== null && "content" in m) {
5789
+ const cached = m._estTokens;
5790
+ if (typeof cached === "number" && cached > 0) {
5791
+ messagesTokens += cached;
5792
+ continue;
5793
+ }
5278
5794
  const content = m.content;
5279
5795
  if (typeof content === "string") {
5280
5796
  messagesTokens += RoughTokenEstimate(content);
@@ -5367,6 +5883,18 @@ function findPreserveStart(messages, preserveK) {
5367
5883
  }
5368
5884
  function eliseOldToolResults(messages, opts) {
5369
5885
  const preserveStart = findPreserveStart(messages, opts.preserveK);
5886
+ let hasOversized = false;
5887
+ for (let i = 0; i < preserveStart && !hasOversized; i++) {
5888
+ const msg = messages[i];
5889
+ if (!msg || !Array.isArray(msg.content)) continue;
5890
+ for (const b of msg.content) {
5891
+ if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
5892
+ hasOversized = true;
5893
+ break;
5894
+ }
5895
+ }
5896
+ }
5897
+ if (!hasOversized) return { messages, saved: 0, changed: false };
5370
5898
  let saved = 0;
5371
5899
  let changed = false;
5372
5900
  const next = new Array(messages.length);
@@ -6260,6 +6788,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6260
6788
  static NOOP_RETRY_DELTA_TOKENS = 2e3;
6261
6789
  /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
6262
6790
  lastNoopAttempt = null;
6791
+ /**
6792
+ * Cached token estimate from the last handler() invocation. When the
6793
+ * message count and tool count haven't changed since the last estimate
6794
+ * (autonomous idle loops), we skip the expensive O(n) token estimation
6795
+ * and reuse this value. Reset to -1 when the context changes.
6796
+ */
6797
+ _cachedTokens = -1;
6798
+ _cachedMsgCount = -1;
6799
+ _cachedToolCount = -1;
6263
6800
  /**
6264
6801
  * @param compactor Compactor to use for compaction.
6265
6802
  * @param maxContext Provider's max context window in tokens.
@@ -6295,12 +6832,24 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
6295
6832
  }
6296
6833
  handler() {
6297
6834
  return async (ctx, next) => {
6298
- const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokensCalibrated(
6299
- ctx.messages,
6300
- ctx.systemPrompt,
6301
- ctx.tools ?? [],
6302
- `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
6303
- ).total;
6835
+ const msgCount = ctx.messages.length;
6836
+ const toolCount = (ctx.tools ?? []).length;
6837
+ let tokens;
6838
+ if (this._estimator) {
6839
+ tokens = this._estimator(ctx);
6840
+ } else if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && this._cachedTokens >= 0) {
6841
+ tokens = this._cachedTokens;
6842
+ } else {
6843
+ tokens = estimateRequestTokensCalibrated(
6844
+ ctx.messages,
6845
+ ctx.systemPrompt,
6846
+ ctx.tools ?? [],
6847
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
6848
+ ).total;
6849
+ this._cachedTokens = tokens;
6850
+ this._cachedMsgCount = msgCount;
6851
+ this._cachedToolCount = toolCount;
6852
+ }
6304
6853
  const load = tokens / this._maxContext;
6305
6854
  const policy = this.policyProvider?.(ctx);
6306
6855
  const thresholds = policy?.thresholds ?? {
@@ -6537,7 +7086,7 @@ function createToolOutputSerializer(opts = {}) {
6537
7086
  }
6538
7087
 
6539
7088
  // src/execution/tool-executor.ts
6540
- var ToolExecutor = class {
7089
+ var ToolExecutor = class _ToolExecutor {
6541
7090
  constructor(registry, opts) {
6542
7091
  this.registry = registry;
6543
7092
  this.opts = opts;
@@ -6549,6 +7098,10 @@ var ToolExecutor = class {
6549
7098
  }
6550
7099
  registry;
6551
7100
  opts;
7101
+ /** Minimum gap between coalesced `partial_output` tool.progress emits. */
7102
+ static PROGRESS_EMIT_INTERVAL_MS = 100;
7103
+ /** Max chars of accumulated stream text carried per coalesced emit. */
7104
+ static PROGRESS_TAIL_CHARS = 16384;
6552
7105
  serializer;
6553
7106
  iterationTimeoutMs;
6554
7107
  maxToolTimeoutMs;
@@ -6594,9 +7147,6 @@ Please call the tool again with arguments that match its inputSchema. You can us
6594
7147
  return { result, tool, durationMs: Date.now() - start };
6595
7148
  }
6596
7149
  const toolDangerousCaps = getDangerousCapabilities(tool);
6597
- if (toolDangerousCaps.length > 0) {
6598
- if (this.opts.events) ;
6599
- }
6600
7150
  if (hasMalformedArguments(use.input)) {
6601
7151
  const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
6602
7152
  budget = this.decrementBudget(result, budget);
@@ -6834,17 +7384,48 @@ ${post.additionalContext}` };
6834
7384
  throw new Error(`Tool "${tool.name}" does not support streaming execution`);
6835
7385
  }
6836
7386
  const stream = tool.executeStream(input, ctx, { signal });
6837
- for await (const ev of stream) {
6838
- if (ev.type === "final") {
6839
- finalOutput = ev.output;
6840
- sawFinal = true;
6841
- break;
6842
- }
7387
+ const iter = stream[Symbol.asyncIterator]();
7388
+ let progressTail = "";
7389
+ let lastProgressEmitAt = 0;
7390
+ const emitProgress = (ev) => {
6843
7391
  this.opts.events?.emit("tool.progress", {
6844
7392
  name: tool.name,
6845
7393
  id: toolUseId ?? "<unknown>",
6846
7394
  event: ev
6847
7395
  });
7396
+ };
7397
+ const flushProgressTail = (force) => {
7398
+ if (progressTail.length === 0) return;
7399
+ const now = Date.now();
7400
+ if (!force && now - lastProgressEmitAt < _ToolExecutor.PROGRESS_EMIT_INTERVAL_MS) return;
7401
+ const text = progressTail;
7402
+ progressTail = "";
7403
+ lastProgressEmitAt = now;
7404
+ emitProgress({ type: "partial_output", text });
7405
+ };
7406
+ try {
7407
+ while (true) {
7408
+ const { done, value: ev } = await iter.next();
7409
+ if (done) break;
7410
+ if (ev.type === "final") {
7411
+ finalOutput = ev.output;
7412
+ sawFinal = true;
7413
+ break;
7414
+ }
7415
+ if (ev.type === "partial_output" && typeof ev.text === "string") {
7416
+ progressTail += ev.text;
7417
+ if (progressTail.length > _ToolExecutor.PROGRESS_TAIL_CHARS) {
7418
+ progressTail = progressTail.slice(-_ToolExecutor.PROGRESS_TAIL_CHARS);
7419
+ }
7420
+ flushProgressTail(false);
7421
+ continue;
7422
+ }
7423
+ flushProgressTail(true);
7424
+ emitProgress(ev);
7425
+ }
7426
+ flushProgressTail(true);
7427
+ } finally {
7428
+ await iter.return?.(void 0);
6848
7429
  }
6849
7430
  if (!sawFinal) {
6850
7431
  throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
@@ -6955,9 +7536,11 @@ function extractMalformedRaw(input) {
6955
7536
 
6956
7537
  // src/utils/assert-never.ts
6957
7538
  function assertNever(x, message) {
6958
- throw new Error(
7539
+ const err = new Error(
6959
7540
  `Unhandled case: ${JSON.stringify(x)}`
6960
7541
  );
7542
+ err.name = "AssertNeverError";
7543
+ throw err;
6961
7544
  }
6962
7545
 
6963
7546
  // src/execution/autonomous-runner.ts
@@ -6968,7 +7551,13 @@ var DoneConditionChecker = class {
6968
7551
  const result = compileUserRegex(condition.pattern, "");
6969
7552
  this.compiledRegex = result.ok ? result.regex : null;
6970
7553
  if (!result.ok) {
6971
- console.warn(`[DoneConditionChecker] Invalid regex pattern "${condition.pattern}": ${result.reason}`);
7554
+ console.warn(JSON.stringify({
7555
+ level: "warn",
7556
+ event: "autonomous.done_condition_invalid_regex",
7557
+ pattern: condition.pattern,
7558
+ reason: result.reason,
7559
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7560
+ }));
6972
7561
  }
6973
7562
  } else {
6974
7563
  this.compiledRegex = null;
@@ -7144,9 +7733,13 @@ function projectSlug(absRoot) {
7144
7733
  function slugify(name) {
7145
7734
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
7146
7735
  }
7736
+ function wstackGlobalRoot() {
7737
+ const fromEnv = process.env["WRONGSTACK_HOME"];
7738
+ if (fromEnv && fromEnv.trim().length > 0) return path11.resolve(fromEnv);
7739
+ return path11.join(os.homedir(), ".wrongstack");
7740
+ }
7147
7741
  function resolveWstackPaths(opts) {
7148
- const home = opts.userHome ?? os.homedir();
7149
- const globalRoot = opts.globalRoot ?? path11.join(home, ".wrongstack");
7742
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path11.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
7150
7743
  const hash = projectHash(opts.projectRoot);
7151
7744
  const slug = projectSlug(opts.projectRoot);
7152
7745
  const projectDir = path11.join(globalRoot, "projects", slug);
@@ -7203,12 +7796,24 @@ async function loadGoal(filePath) {
7203
7796
  try {
7204
7797
  const parsed = JSON.parse(raw);
7205
7798
  if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
7206
- console.warn(`[goal-store] Corrupt goal.json at ${filePath} \u2014 invalid schema. Consider deleting it and re-creating.`);
7799
+ console.warn(JSON.stringify({
7800
+ level: "warn",
7801
+ event: "goal_store.invalid_schema",
7802
+ path: filePath,
7803
+ message: "invalid schema \u2014 consider deleting and re-creating",
7804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7805
+ }));
7207
7806
  return null;
7208
7807
  }
7209
7808
  return parsed;
7210
7809
  } catch {
7211
- console.warn(`[goal-store] Corrupt goal.json at ${filePath} \u2014 JSON parse failed. Consider deleting it and re-creating.`);
7810
+ console.warn(JSON.stringify({
7811
+ level: "warn",
7812
+ event: "goal_store.parse_failed",
7813
+ path: filePath,
7814
+ message: "JSON parse failed \u2014 consider deleting and re-creating",
7815
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7816
+ }));
7212
7817
  return null;
7213
7818
  }
7214
7819
  }
@@ -7322,7 +7927,14 @@ var EternalAutonomyEngine = class {
7322
7927
  stop() {
7323
7928
  this.stopRequested = true;
7324
7929
  this.currentCtrl?.abort();
7325
- void this.persistEngineState("stopped").catch(() => {
7930
+ void this.persistEngineState("stopped").catch((err) => {
7931
+ console.error(JSON.stringify({
7932
+ level: "error",
7933
+ event: "engine.persist_state_failed",
7934
+ message: err instanceof Error ? err.message : String(err),
7935
+ context: { expectedState: "stopped" },
7936
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7937
+ }));
7326
7938
  });
7327
7939
  this.state = "stopped";
7328
7940
  }
@@ -8438,21 +9050,40 @@ function makeAgentSubagentRunner(opts) {
8438
9050
  if (budgetError) throw budgetError;
8439
9051
  }
8440
9052
  if (result.status === "failed") {
8441
- throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
9053
+ throw result.error instanceof AgentError ? result.error : new AgentError({
9054
+ message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
9055
+ code: ERROR_CODES.AGENT_RUN_FAILED,
9056
+ cause: result.error
9057
+ });
8442
9058
  }
8443
9059
  if (result.status === "aborted") {
8444
- throw new Error("agent aborted");
9060
+ throw new AgentError({
9061
+ message: "agent aborted",
9062
+ code: ERROR_CODES.AGENT_ABORTED
9063
+ });
8445
9064
  }
8446
9065
  if (result.status === "max_iterations") {
8447
- throw new Error("agent exhausted iteration limit");
9066
+ throw new AgentError({
9067
+ message: "agent exhausted iteration limit",
9068
+ code: ERROR_CODES.AGENT_ITERATION_LIMIT,
9069
+ recoverable: true
9070
+ });
8448
9071
  }
8449
9072
  const usage = ctx.budget.usage();
8450
9073
  const finalText = (result.finalText ?? "").trim();
8451
9074
  if (finalText.length === 0 && usage.toolCalls === 0) {
8452
- throw new Error("empty response");
9075
+ throw new AgentError({
9076
+ message: "empty response \u2014 agent produced no text and no tool calls",
9077
+ code: ERROR_CODES.AGENT_RUN_FAILED,
9078
+ context: { iterations: result.iterations }
9079
+ });
8453
9080
  }
8454
9081
  if (finalText.length === 0 && lastToolFailed !== null) {
8455
- throw new Error(`tool failed: ${lastToolFailed}`);
9082
+ throw new AgentError({
9083
+ message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
9084
+ code: ERROR_CODES.AGENT_RUN_FAILED,
9085
+ context: { tool: lastToolFailed, iterations: result.iterations }
9086
+ });
8456
9087
  }
8457
9088
  return {
8458
9089
  result: result.finalText,
@@ -8484,11 +9115,11 @@ var HEAVY_BUDGET = {
8484
9115
  };
8485
9116
  var TOOLS = {
8486
9117
  /** Pure read/inspect — safe for analysis and review agents. */
8487
- read: ["read", "grep", "glob", "search", "tree"],
9118
+ read: ["read", "grep", "glob", "search", "tree", "mailbox"],
8488
9119
  /** Read + structured inspection (logs, diffs, json, dependency audit). */
8489
- inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit"],
9120
+ inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
8490
9121
  /** Read + edit (no shell). For agents that write code/docs but don't run it. */
8491
- write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch"],
9122
+ write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
8492
9123
  /** Full build loop: edit + run (lint/format/typecheck/test/bash). */
8493
9124
  build: [
8494
9125
  "read",
@@ -8505,16 +9136,17 @@ var TOOLS = {
8505
9136
  "lint",
8506
9137
  "format",
8507
9138
  "typecheck",
8508
- "test"
9139
+ "test",
9140
+ "mailbox"
8509
9141
  ],
8510
9142
  /** Version control. */
8511
9143
  vcs: ["read", "grep", "glob", "git", "diff"],
8512
9144
  /** Dependency management + CVE audit. */
8513
- deps: ["read", "grep", "glob", "install", "outdated", "audit", "json"],
9145
+ deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
8514
9146
  /** Documentation authoring. */
8515
- docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document"],
9147
+ docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
8516
9148
  /** Web research. */
8517
- research: ["read", "grep", "glob", "search", "fetch"]
9149
+ research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
8518
9150
  };
8519
9151
 
8520
9152
  // src/coordination/agents/phase1-discovery.ts
@@ -10927,7 +11559,7 @@ Working rules:
10927
11559
  id: "tech-stack",
10928
11560
  name: "Tech Stack Validator",
10929
11561
  role: "tech-stack",
10930
- tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
11562
+ tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
10931
11563
  prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
10932
11564
  before any package, library, or framework choice is committed.
10933
11565
 
@@ -10935,6 +11567,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
10935
11567
  You are the "this isn't code, this is 10-year-old technology" agent. Intervene
10936
11568
  hard when the LLM hallucinates a version number or suggests dead tech.
10937
11569
 
11570
+ ## Before you begin
11571
+
11572
+ Check the inter-agent mailbox for pending tasks. Other agents or the file-watcher
11573
+ may have left assign messages with dependency files to audit:
11574
+ - mailbox action=check
11575
+
11576
+ If you find an assign message, use the specified file path and packages.
11577
+ When done, post results back:
11578
+ - mailbox action=send to=<sender> type=result subject="Tech stack audit results" body="..."
11579
+
10938
11580
  ## Critical rules
10939
11581
 
10940
11582
  1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
@@ -10993,11 +11635,11 @@ When APPROVED:
10993
11635
  **Install**: pnpm add <name>@^<major>.<minor>.0`
10994
11636
  },
10995
11637
  budget: {
10996
- timeoutMs: 6e4,
10997
- maxIterations: 5,
10998
- maxToolCalls: 20,
10999
- maxTokens: 4e4,
11000
- maxCostUsd: 0.1
11638
+ timeoutMs: 12e4,
11639
+ maxIterations: 10,
11640
+ maxToolCalls: 40,
11641
+ maxTokens: 6e4,
11642
+ maxCostUsd: 0.25
11001
11643
  },
11002
11644
  capability: {
11003
11645
  phase: "meta",
@@ -11198,6 +11840,9 @@ Do not add prose, markdown, or code fences.`;
11198
11840
 
11199
11841
  // src/coordination/coordinator/error-classifier.ts
11200
11842
  function classifySubagentError(err, hints = {}) {
11843
+ if (err instanceof AgentError && err.cause) {
11844
+ return classifySubagentError(err.cause, hints);
11845
+ }
11201
11846
  const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
11202
11847
  if (err instanceof ProviderError) {
11203
11848
  const baseMessage2 = err.describe();
@@ -11230,7 +11875,7 @@ function classifySubagentError(err, hints = {}) {
11230
11875
  if (/agent exhausted iteration limit$/i.test(baseMessage)) {
11231
11876
  return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
11232
11877
  }
11233
- if (/empty response$/i.test(baseMessage)) {
11878
+ if (/empty response/i.test(baseMessage)) {
11234
11879
  return { kind: "empty_response", message: baseMessage, retryable: false, cause };
11235
11880
  }
11236
11881
  if (/^tool failed: /i.test(baseMessage)) {
@@ -12394,7 +13039,14 @@ var ParallelEternalEngine = class {
12394
13039
  }
12395
13040
  stop() {
12396
13041
  this.stopRequested = true;
12397
- void this.persistState("stopped").catch(() => {
13042
+ void this.persistState("stopped").catch((err) => {
13043
+ console.error(JSON.stringify({
13044
+ level: "error",
13045
+ event: "engine.persist_state_failed",
13046
+ message: err instanceof Error ? err.message : String(err),
13047
+ context: { expectedState: "stopped" },
13048
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13049
+ }));
12398
13050
  });
12399
13051
  this.state = "stopped";
12400
13052
  }
@@ -12994,24 +13646,36 @@ var InMemoryAgentBridge = class {
12994
13646
  return () => this.subscriptions.delete(handler);
12995
13647
  }
12996
13648
  async request(msg, timeoutMs) {
12997
- if (this.stopped) throw new Error("Bridge is stopped");
13649
+ if (this.stopped) throw new AgentError({
13650
+ message: "Bridge is stopped",
13651
+ code: ERROR_CODES.AGENT_ABORTED
13652
+ });
12998
13653
  const timeout = timeoutMs ?? this.timeoutMs;
12999
13654
  const correlationId = msg.id;
13000
13655
  if (this.inflightGuards.has(correlationId)) {
13001
- throw new Error(
13002
- `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
13003
- );
13656
+ throw new AgentError({
13657
+ message: `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`,
13658
+ code: ERROR_CODES.AGENT_RUN_FAILED,
13659
+ context: { correlationId }
13660
+ });
13004
13661
  }
13005
13662
  this.inflightGuards.add(correlationId);
13006
13663
  return new Promise((resolve5, reject) => {
13007
13664
  const timer = setTimeout(() => {
13008
13665
  this.inflightGuards.delete(correlationId);
13009
13666
  this.pendingRequests.delete(correlationId);
13010
- reject(new Error(`Request ${correlationId} timed out after ${timeout}ms`));
13667
+ reject(new AgentError({
13668
+ message: `Request ${correlationId} timed out after ${timeout}ms`,
13669
+ code: ERROR_CODES.AGENT_RUN_FAILED,
13670
+ context: { correlationId, timeoutMs: timeout }
13671
+ }));
13011
13672
  }, timeout);
13012
13673
  if (!this.inflightGuards.has(correlationId)) {
13013
13674
  clearTimeout(timer);
13014
- reject(new Error("Bridge stopped"));
13675
+ reject(new AgentError({
13676
+ message: "Bridge stopped",
13677
+ code: ERROR_CODES.AGENT_ABORTED
13678
+ }));
13015
13679
  return;
13016
13680
  }
13017
13681
  this.pendingRequests.set(correlationId, {
@@ -13032,7 +13696,10 @@ var InMemoryAgentBridge = class {
13032
13696
  this.stopped = true;
13033
13697
  for (const [, p] of this.pendingRequests) {
13034
13698
  clearTimeout(p.timer);
13035
- p.reject(new Error("Bridge stopped"));
13699
+ p.reject(new AgentError({
13700
+ message: "Bridge stopped",
13701
+ code: ERROR_CODES.AGENT_ABORTED
13702
+ }));
13036
13703
  }
13037
13704
  this.pendingRequests.clear();
13038
13705
  this.inflightGuards.clear();
@@ -13803,7 +14470,23 @@ Bridge contract:
13803
14470
  subagents' context. Those are not yours to read.
13804
14471
  - Your final task output is what the Director sees. Be concise,
13805
14472
  structured, and self-contained \u2014 assume the Director will paste your
13806
- output into its own context.`;
14473
+ output into its own context.
14474
+
14475
+ Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` tools):
14476
+ - You are part of a project-wide fleet that may span other terminals and
14477
+ WebUIs. Your mailbox identity is \`<your-name>@<session-tag>\` (unique
14478
+ per session); mail addressed to you, to your bare name, or broadcast
14479
+ to \`*\` is injected into your conversation automatically before each
14480
+ step \u2014 read it once, it is marked read.
14481
+ - Broadcast milestones: when you complete a significant piece of work,
14482
+ \`mail_send to="*"\` a one-line summary so parallel agents don't collide
14483
+ with or duplicate it.
14484
+ - Hand off matching work: if another online agent's role fits a follow-up
14485
+ better (e.g. a reviewer while you just wrote code), \`mail_send\` it to
14486
+ their exact id instead of doing everything yourself. Discover ids with
14487
+ \`mailbox action=online\`.
14488
+ - Answer your mail: reply to the sender's exact \`from\` id. When done with
14489
+ an assigned task, post a \`result\` back to whoever assigned it.`;
13807
14490
  function composeDirectorPrompt(parts = {}) {
13808
14491
  const sections = [];
13809
14492
  const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
@@ -16853,6 +17536,77 @@ Remember: your job is to make the user a better developer, not just to complete
16853
17536
  tags: ["teaching", "mentor", "learning"],
16854
17537
  toolPreferences: ["read", "edit", "explain"],
16855
17538
  suggestedSkills: ["prompt-engineering", "skill-creator", "node-modern", "typescript-strict"]
17539
+ },
17540
+ {
17541
+ id: "research-web",
17542
+ name: "Research Web",
17543
+ description: "Current-data research \u2014 search web, verify, inject findings into context",
17544
+ prompt: `## Research Web Mode
17545
+
17546
+ You are in research mode. Your role: find, verify, and incorporate
17547
+ current web data. Your training data is stale \u2014 every factual claim
17548
+ about version numbers, API surfaces, package status, or ecosystem
17549
+ changes must be verified against live sources.
17550
+
17551
+ ### When to research
17552
+ - The user asks "is this still the case?", "what's current?", "latest version?"
17553
+ - You're about to claim a version number, deprecation, or API change
17554
+ - You're comparing tools, packages, or approaches released in the last 12 months
17555
+ - You realize your knowledge may be >6 months old on a fast-moving topic
17556
+
17557
+ ### Research methodology
17558
+ 1. **Search first, fetch selectively.** Use web_search with 5-8 results for
17559
+ broad queries. Then web_fetch the 1-2 most authoritative results for detail.
17560
+ Don't fetch every result \u2014 you'll burn tokens on noise.
17561
+ 2. **Cross-reference.** One source is a data point. Two sources that agree
17562
+ is a signal. Three is confirmation. Flag single-source claims as tentative.
17563
+ 3. **Cite sources.** Every factual claim from web data must include where it
17564
+ came from: domain name, and date if visible on the page.
17565
+ 4. **Know when to stop.** 2-3 searches + 1-2 fetches is usually sufficient.
17566
+ If you're on your 5th search without a clear answer, pause and tell the user
17567
+ what you've found and what's still unclear \u2014 let them decide to dig deeper.
17568
+ 5. **Inject findings for reuse.** After gathering current data, use
17569
+ context_manager with add_note to inject a structured "Research Findings"
17570
+ block into the conversation. Future turns see this and don't re-search.
17571
+
17572
+ ### Self-injection pattern
17573
+ When you discover current data mid-research, inject it so subsequent turns
17574
+ benefit without re-searching:
17575
+
17576
+ web_search("Next.js middleware breaking changes 2025")
17577
+ \u2192 Surfaced: Next.js 15.2 changed middleware runtime from edge to node
17578
+ web_fetch("https://nextjs.org/docs/messages/middleware-upgrade-guide")
17579
+ \u2192 Confirmed: middleware now runs on Node.js runtime by default
17580
+ context_manager: add_note(
17581
+ "## Research: Next.js middleware
17582
+ - Next.js 15.2: middleware defaults to Node.js runtime (was edge)
17583
+ - Breaking: edge-only APIs (crypto.subtle, WebSocket) no longer available
17584
+ - Migration: use node:* equivalents or set runtime: 'edge' explicitly
17585
+ - Source: nextjs.org/docs/messages/middleware-upgrade-guide"
17586
+ )
17587
+
17588
+ The add_note persists in conversation \u2014 you won't re-search on the next turn.
17589
+
17590
+ ### Anti-patterns
17591
+ - Don't research things already in the conversation context (including
17592
+ earlier add_note blocks you injected)
17593
+ - Don't treat a single web search result as ground truth \u2014 cross-reference
17594
+ - Don't inject raw JSON or search result dumps via add_note \u2014 summarize
17595
+ - Don't research while the user is waiting for a quick code edit \u2014 toggle
17596
+ research-web mode only during analysis/discussion phases
17597
+ - Don't research-loop: 5+ searches on one topic \u2192 stop and ask the user
17598
+
17599
+ ### Exiting research mode
17600
+ When the user no longer needs current-data research, suggest switching back
17601
+ to the previous mode. You stay in research mode until explicitly told to
17602
+ switch \u2014 but don't force web searches on every turn. The methodology rules
17603
+ above already gate when to actually search.
17604
+
17605
+ When you're done with research: suggest the user run \`/mode default\` or
17606
+ their previous mode.`,
17607
+ tags: ["research", "web", "current-data", "up-to-date"],
17608
+ toolPreferences: ["web_search", "web_fetch", "search", "fetch", "context_manager"],
17609
+ suggestedSkills: ["research-web", "tech-stack", "node-modern", "security-scanner", "react-modern"]
16856
17610
  }
16857
17611
  ];
16858
17612
 
@@ -17478,7 +18232,10 @@ var TaskTracker = class {
17478
18232
  return this.graph;
17479
18233
  }
17480
18234
  addNode(node) {
17481
- if (!this.graph) throw new Error("No graph loaded");
18235
+ if (!this.graph) throw new SddError({
18236
+ message: "No graph loaded",
18237
+ code: ERROR_CODES.SDD_INVALID_STATE
18238
+ });
17482
18239
  const now = Date.now();
17483
18240
  const newNode = {
17484
18241
  ...node,
@@ -17496,7 +18253,10 @@ var TaskTracker = class {
17496
18253
  return newNode;
17497
18254
  }
17498
18255
  addEdge(from, to, type = "depends_on") {
17499
- if (!this.graph) throw new Error("No graph loaded");
18256
+ if (!this.graph) throw new SddError({
18257
+ message: "No graph loaded",
18258
+ code: ERROR_CODES.SDD_INVALID_STATE
18259
+ });
17500
18260
  this.graph.edges.push({
17501
18261
  id: crypto.randomUUID(),
17502
18262
  from,
@@ -17507,9 +18267,16 @@ var TaskTracker = class {
17507
18267
  this.persist();
17508
18268
  }
17509
18269
  updateNodeStatus(id, status, reason) {
17510
- if (!this.graph) throw new Error("No graph loaded");
18270
+ if (!this.graph) throw new SddError({
18271
+ message: "No graph loaded",
18272
+ code: ERROR_CODES.SDD_INVALID_STATE
18273
+ });
17511
18274
  const node = this.graph.nodes.get(id);
17512
- if (!node) throw new Error(`Node ${id} not found`);
18275
+ if (!node) throw new SddError({
18276
+ message: `Node ${id} not found`,
18277
+ code: ERROR_CODES.SDD_NOT_READY,
18278
+ context: { nodeId: id }
18279
+ });
17513
18280
  const from = node.status;
17514
18281
  const now = Date.now();
17515
18282
  node.status = status;
@@ -17532,9 +18299,16 @@ var TaskTracker = class {
17532
18299
  this.persist();
17533
18300
  }
17534
18301
  updateNode(id, patch) {
17535
- if (!this.graph) throw new Error("No graph loaded");
18302
+ if (!this.graph) throw new SddError({
18303
+ message: "No graph loaded",
18304
+ code: ERROR_CODES.SDD_INVALID_STATE
18305
+ });
17536
18306
  const node = this.graph.nodes.get(id);
17537
- if (!node) throw new Error(`Node ${id} not found`);
18307
+ if (!node) throw new SddError({
18308
+ message: `Node ${id} not found`,
18309
+ code: ERROR_CODES.SDD_NOT_READY,
18310
+ context: { nodeId: id }
18311
+ });
17538
18312
  if (patch.title !== void 0) node.title = patch.title;
17539
18313
  if (patch.description !== void 0) node.description = patch.description;
17540
18314
  if (patch.priority !== void 0) node.priority = patch.priority;
@@ -17653,7 +18427,12 @@ var TaskTracker = class {
17653
18427
  persist() {
17654
18428
  if (!this.graph) return;
17655
18429
  this.opts.store.saveGraph(this.graph).catch((err) => {
17656
- this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
18430
+ this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(JSON.stringify({
18431
+ level: "warn",
18432
+ event: "task_tracker.save_graph_failed",
18433
+ message: err instanceof Error ? err.message : String(err),
18434
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
18435
+ }));
17657
18436
  });
17658
18437
  }
17659
18438
  };
@@ -17706,12 +18485,14 @@ var TaskFlow = class {
17706
18485
  const analysis = parser.analyze(this.spec);
17707
18486
  this.emit("spec.analyzed", { analysis });
17708
18487
  if (analysis.completeness < 50) {
17709
- this.emit("error", {
17710
- phase: "analyzing",
17711
- error: new Error(`Spec completeness too low: ${analysis.completeness}%`)
18488
+ const err = new SddError({
18489
+ message: `Spec completeness too low: ${analysis.completeness}%`,
18490
+ code: ERROR_CODES.SDD_VALIDATION_FAILED,
18491
+ context: { completeness: analysis.completeness }
17712
18492
  });
18493
+ this.emit("error", { phase: "analyzing", error: err });
17713
18494
  this.setPhase("failed");
17714
- throw new Error("Spec too incomplete");
18495
+ throw err;
17715
18496
  }
17716
18497
  this.setPhase("generating");
17717
18498
  const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
@@ -17719,7 +18500,11 @@ var TaskFlow = class {
17719
18500
  return this.graph;
17720
18501
  }
17721
18502
  async execute(ctx) {
17722
- if (!this.graph) throw new Error("No graph loaded. Call fromSpec first.");
18503
+ if (!this.graph) throw new SddError({
18504
+ message: "No graph loaded. Call fromSpec first.",
18505
+ code: ERROR_CODES.SDD_INVALID_STATE,
18506
+ context: { phase: this.phase }
18507
+ });
17723
18508
  this.setPhase("executing");
17724
18509
  this.stopped = false;
17725
18510
  const pendingTasks = this.getExecutableTasks();
@@ -17759,7 +18544,11 @@ var TaskFlow = class {
17759
18544
  }
17760
18545
  async reviewTask(taskId, approved, comment) {
17761
18546
  const task = this.opts.tracker.getNode(taskId);
17762
- if (!task) throw new Error(`Task ${taskId} not found`);
18547
+ if (!task) throw new SddError({
18548
+ message: `Task ${taskId} not found`,
18549
+ code: ERROR_CODES.SDD_NOT_READY,
18550
+ context: { taskId }
18551
+ });
17763
18552
  if (approved) {
17764
18553
  this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
17765
18554
  this.emit("task.completed", { taskId });
@@ -18403,7 +19192,11 @@ var AISpecBuilder = class {
18403
19192
  switch (this.session.phase) {
18404
19193
  case "questioning":
18405
19194
  if (!this.session.spec) {
18406
- throw new Error("Cannot approve: no spec generated yet.");
19195
+ throw new SddError({
19196
+ message: "Cannot approve: no spec generated yet.",
19197
+ code: ERROR_CODES.SDD_INVALID_STATE,
19198
+ context: { phase: "questioning", sessionId: this.session.id }
19199
+ });
18407
19200
  }
18408
19201
  this.session.phase = "spec_review";
18409
19202
  break;
@@ -18461,7 +19254,11 @@ var AISpecBuilder = class {
18461
19254
  */
18462
19255
  async saveSpec() {
18463
19256
  if (!this.session.spec) {
18464
- throw new Error("No spec to save.");
19257
+ throw new SddError({
19258
+ message: "No spec to save.",
19259
+ code: ERROR_CODES.SDD_NOT_READY,
19260
+ context: { sessionId: this.session.id }
19261
+ });
18465
19262
  }
18466
19263
  await this.store.save(this.session.spec);
18467
19264
  return this.session.spec;
@@ -18476,17 +19273,30 @@ var AISpecBuilder = class {
18476
19273
  try {
18477
19274
  parsed = JSON.parse(jsonStr);
18478
19275
  } catch (e) {
18479
- throw new Error(`Invalid JSON for spec: ${e instanceof Error ? e.message : "parse error"}`);
19276
+ throw new SddError({
19277
+ message: "Invalid JSON for spec",
19278
+ code: ERROR_CODES.SDD_PARSE_FAILED,
19279
+ cause: e,
19280
+ context: { detail: e instanceof Error ? e.message : "parse error" }
19281
+ });
18480
19282
  }
18481
19283
  if (!parsed || typeof parsed !== "object") {
18482
- throw new Error("Spec JSON must be an object.");
19284
+ throw new SddError({
19285
+ message: "Spec JSON must be an object",
19286
+ code: ERROR_CODES.SDD_VALIDATION_FAILED,
19287
+ context: { actualType: typeof parsed }
19288
+ });
18483
19289
  }
18484
19290
  const raw = parsed;
18485
19291
  const now = Date.now();
18486
19292
  const title = String(raw.title ?? this.session.title ?? "Untitled");
18487
19293
  const overview = String(raw.overview ?? "");
18488
19294
  if (!overview || overview === "undefined") {
18489
- throw new Error("Spec must have an overview.");
19295
+ throw new SddError({
19296
+ message: "Spec must have an overview",
19297
+ code: ERROR_CODES.SDD_VALIDATION_FAILED,
19298
+ context: { field: "overview", title }
19299
+ });
18490
19300
  }
18491
19301
  const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
18492
19302
  const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
@@ -19554,7 +20364,10 @@ var SddParallelRun = class {
19554
20364
  "\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
19555
20365
  "\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
19556
20366
  ].join("\n");
19557
- if (!this.coordinator) throw new Error("SDD parallel runner requires a coordinator");
20367
+ if (!this.coordinator) throw new SddError({
20368
+ message: "SDD parallel runner requires a coordinator",
20369
+ code: ERROR_CODES.SDD_INVALID_STATE
20370
+ });
19558
20371
  const coordinator = this.coordinator;
19559
20372
  const spawns = subagentIds.map(
19560
20373
  (subagentId) => coordinator.spawn({
@@ -19566,7 +20379,10 @@ var SddParallelRun = class {
19566
20379
  );
19567
20380
  const spawnResults = await Promise.all(spawns);
19568
20381
  if (!spawnResults.every((r) => Boolean(r.subagentId))) {
19569
- throw new Error("One or more subagent spawns failed");
20382
+ throw new SddError({
20383
+ message: "One or more subagent spawns failed",
20384
+ code: ERROR_CODES.SDD_INVALID_STATE
20385
+ });
19570
20386
  }
19571
20387
  const assignPromises = tasks.map((task, i) => {
19572
20388
  const spec = {