@wrongstack/core 0.148.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.
- package/dist/{agent-bridge-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
- package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
- package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
- package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
- package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
- package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
- package/dist/coordination/index.d.ts +70 -13
- package/dist/coordination/index.js +2126 -151
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +27 -27
- package/dist/defaults/index.js +1328 -354
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +367 -59
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +86 -0
- package/dist/execution/prompt-enhancer.js +125 -0
- package/dist/execution/prompt-enhancer.js.map +1 -0
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js +3 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
- package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
- package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
- package/dist/index.d.ts +439 -129
- package/dist/index.js +5206 -905
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +72 -15
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +7 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
- package/dist/logger-DmmQhf4P.d.ts +65 -0
- package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +89 -9
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
- package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
- package/dist/observability/index.d.ts +2 -2
- package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
- package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
- package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
- package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
- package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
- package/dist/pipeline-DsmlwTXu.d.ts +493 -0
- package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
- package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
- package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +358 -85
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
- package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
- package/dist/security/index.d.ts +6 -5
- package/dist/security/index.js +214 -35
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
- package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
- package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
- package/dist/skills/index.js +171 -21
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +151 -13
- package/dist/storage/index.js +1117 -256
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +68 -21
- package/dist/types/index.js +616 -74
- package/dist/types/index.js.map +1 -1
- package/dist/utils/expect-defined.js +3 -1
- package/dist/utils/expect-defined.js.map +1 -1
- package/dist/utils/index.d.ts +80 -4
- package/dist/utils/index.js +100 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
- package/package.json +7 -3
- package/skills/chimera/SKILL.md +105 -0
- package/skills/research-web/SKILL.md +342 -0
- package/dist/logger-B9J5puGM.d.ts +0 -32
- package/dist/pipeline-BG7UgbDc.d.ts +0 -239
package/dist/defaults/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
-
{
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
|
706
|
-
|
|
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.
|
|
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
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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]
|
|
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.
|
|
1040
|
-
this.
|
|
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(
|
|
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(
|
|
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
|
};
|
|
@@ -1502,7 +1746,7 @@ var FileMemoryBackend = class {
|
|
|
1502
1746
|
const line = `
|
|
1503
1747
|
- [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
|
|
1504
1748
|
`;
|
|
1505
|
-
const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `#
|
|
1749
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + line : `# Agent Memory
|
|
1506
1750
|
${line}`;
|
|
1507
1751
|
await atomicWrite(file, next);
|
|
1508
1752
|
}
|
|
@@ -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
|
|
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(
|
|
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;
|
|
@@ -1988,10 +2392,91 @@ var ENCRYPTED_PREFIX = "enc:v1:";
|
|
|
1988
2392
|
|
|
1989
2393
|
// src/security/secret-vault.ts
|
|
1990
2394
|
init_atomic_write();
|
|
2395
|
+
|
|
2396
|
+
// src/utils/deep-merge.ts
|
|
2397
|
+
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
2398
|
+
"__proto__",
|
|
2399
|
+
"constructor",
|
|
2400
|
+
"prototype",
|
|
2401
|
+
"__defineGetter__",
|
|
2402
|
+
"__defineSetter__",
|
|
2403
|
+
"__lookupGetter__",
|
|
2404
|
+
"__lookupSetter__"
|
|
2405
|
+
]);
|
|
2406
|
+
function isPrimitiveArray(a) {
|
|
2407
|
+
return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
|
|
2408
|
+
}
|
|
2409
|
+
function deepMerge(base, patch, options = {}) {
|
|
2410
|
+
const {
|
|
2411
|
+
conflictResolution = "prefer-patch",
|
|
2412
|
+
arrayMode = "replace",
|
|
2413
|
+
protectProto = true,
|
|
2414
|
+
onNonPrimitiveArrayReplace
|
|
2415
|
+
} = options;
|
|
2416
|
+
if (typeof base !== "object" || base === null) {
|
|
2417
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2418
|
+
}
|
|
2419
|
+
if (typeof patch !== "object" || patch === null) {
|
|
2420
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2421
|
+
}
|
|
2422
|
+
if (Array.isArray(base) && Array.isArray(patch)) {
|
|
2423
|
+
if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
|
|
2424
|
+
return [.../* @__PURE__ */ new Set([...base, ...patch])];
|
|
2425
|
+
}
|
|
2426
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2427
|
+
}
|
|
2428
|
+
if (Array.isArray(base) || Array.isArray(patch)) {
|
|
2429
|
+
return conflictResolution === "prefer-patch" ? patch : base;
|
|
2430
|
+
}
|
|
2431
|
+
const baseObj = base;
|
|
2432
|
+
const patchObj = patch;
|
|
2433
|
+
const out = { ...baseObj };
|
|
2434
|
+
for (const [k, v] of Object.entries(patchObj)) {
|
|
2435
|
+
if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
2436
|
+
const existing = out[k];
|
|
2437
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2438
|
+
out[k] = deepMerge(existing, v, options);
|
|
2439
|
+
} else if (Array.isArray(v) && Array.isArray(existing)) {
|
|
2440
|
+
if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
|
|
2441
|
+
onNonPrimitiveArrayReplace(k, existing.length, v.length);
|
|
2442
|
+
}
|
|
2443
|
+
out[k] = deepMerge(existing, v, options);
|
|
2444
|
+
} else if (v !== void 0) {
|
|
2445
|
+
if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
|
|
2446
|
+
const existingLen = Array.isArray(existing) ? existing.length : 0;
|
|
2447
|
+
onNonPrimitiveArrayReplace(k, existingLen, v.length);
|
|
2448
|
+
}
|
|
2449
|
+
out[k] = v;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return out;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// src/security/secret-vault.ts
|
|
1991
2456
|
var KEY_BYTES = 32;
|
|
1992
2457
|
var IV_BYTES = 12;
|
|
1993
2458
|
var TAG_BYTES = 16;
|
|
1994
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
|
+
}
|
|
1995
2480
|
var DefaultSecretVault = class {
|
|
1996
2481
|
keyFile;
|
|
1997
2482
|
key;
|
|
@@ -2015,14 +2500,26 @@ var DefaultSecretVault = class {
|
|
|
2015
2500
|
const rest = value.slice(ENCRYPTED_PREFIX.length);
|
|
2016
2501
|
const parts = rest.split(":");
|
|
2017
2502
|
if (parts.length !== 3) {
|
|
2018
|
-
throw new
|
|
2503
|
+
throw new ConfigError({
|
|
2504
|
+
message: "SecretVault: malformed encrypted value",
|
|
2505
|
+
code: ERROR_CODES.CONFIG_PARSE_FAILED,
|
|
2506
|
+
context: { field: "encrypted_value" }
|
|
2507
|
+
});
|
|
2019
2508
|
}
|
|
2020
2509
|
const [ivB64, tagB64, ctB64] = parts;
|
|
2021
2510
|
const iv = Buffer.from(ivB64, "base64");
|
|
2022
2511
|
const tag = Buffer.from(tagB64, "base64");
|
|
2023
2512
|
const ct = Buffer.from(ctB64, "base64");
|
|
2024
|
-
if (iv.length !== IV_BYTES) throw new
|
|
2025
|
-
|
|
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
|
+
});
|
|
2026
2523
|
const key = this.loadOrCreateKey();
|
|
2027
2524
|
const decipher = createDecipheriv(ALGO, key, iv);
|
|
2028
2525
|
decipher.setAuthTag(tag);
|
|
@@ -2032,30 +2529,36 @@ var DefaultSecretVault = class {
|
|
|
2032
2529
|
loadOrCreateKey() {
|
|
2033
2530
|
if (this.key) return this.key;
|
|
2034
2531
|
try {
|
|
2035
|
-
const buf =
|
|
2532
|
+
const buf = fs.readFileSync(this.keyFile);
|
|
2036
2533
|
if (buf.length !== KEY_BYTES) {
|
|
2037
|
-
throw new
|
|
2038
|
-
`SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key
|
|
2039
|
-
|
|
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
|
+
});
|
|
2040
2539
|
}
|
|
2041
2540
|
this.key = buf;
|
|
2541
|
+
checkKeyFilePermissions(this.keyFile);
|
|
2042
2542
|
return this.key;
|
|
2043
2543
|
} catch (err) {
|
|
2044
2544
|
if (err.code !== "ENOENT") throw err;
|
|
2045
2545
|
}
|
|
2046
|
-
|
|
2546
|
+
fs.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
|
|
2047
2547
|
const key = randomBytes(KEY_BYTES);
|
|
2048
2548
|
try {
|
|
2049
|
-
|
|
2549
|
+
fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
|
|
2050
2550
|
} catch (err) {
|
|
2051
2551
|
if (err.code !== "EEXIST") throw err;
|
|
2052
|
-
const buf =
|
|
2552
|
+
const buf = fs.readFileSync(this.keyFile);
|
|
2053
2553
|
if (buf.length !== KEY_BYTES) {
|
|
2054
|
-
throw new
|
|
2055
|
-
`SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key
|
|
2056
|
-
|
|
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
|
+
});
|
|
2057
2559
|
}
|
|
2058
2560
|
this.key = buf;
|
|
2561
|
+
checkKeyFilePermissions(this.keyFile);
|
|
2059
2562
|
return this.key;
|
|
2060
2563
|
}
|
|
2061
2564
|
this.key = key;
|
|
@@ -2116,7 +2619,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
|
|
|
2116
2619
|
await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2117
2620
|
await restrictFilePermissions(configPath);
|
|
2118
2621
|
}
|
|
2119
|
-
async function migratePlaintextSecrets(configPath, vault) {
|
|
2622
|
+
async function migratePlaintextSecrets(configPath, vault, logger) {
|
|
2120
2623
|
let raw;
|
|
2121
2624
|
try {
|
|
2122
2625
|
raw = await fsp.readFile(configPath, "utf8");
|
|
@@ -2133,11 +2636,14 @@ async function migratePlaintextSecrets(configPath, vault) {
|
|
|
2133
2636
|
const migrated = walkCount(parsed, vault, counter);
|
|
2134
2637
|
if (counter.n === 0) return { migrated: 0, file: configPath };
|
|
2135
2638
|
await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
|
|
2136
|
-
await restrictFilePermissions(
|
|
2639
|
+
await restrictFilePermissions(
|
|
2640
|
+
configPath,
|
|
2641
|
+
logger ? { warn: (msg) => logger.warn(msg) } : void 0
|
|
2642
|
+
);
|
|
2137
2643
|
return { migrated: counter.n, file: configPath };
|
|
2138
2644
|
}
|
|
2139
2645
|
async function restrictFilePermissions(filePath, opts) {
|
|
2140
|
-
const warn = ((msg) => console.warn(msg));
|
|
2646
|
+
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
2141
2647
|
if (process.platform === "win32") {
|
|
2142
2648
|
try {
|
|
2143
2649
|
const { execFile: execFile2 } = await import('child_process');
|
|
@@ -2189,28 +2695,6 @@ function walkCount(node, vault, counter) {
|
|
|
2189
2695
|
}
|
|
2190
2696
|
return out;
|
|
2191
2697
|
}
|
|
2192
|
-
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
2193
|
-
"__proto__",
|
|
2194
|
-
"constructor",
|
|
2195
|
-
"prototype",
|
|
2196
|
-
"__defineGetter__",
|
|
2197
|
-
"__defineSetter__",
|
|
2198
|
-
"__lookupGetter__",
|
|
2199
|
-
"__lookupSetter__"
|
|
2200
|
-
]);
|
|
2201
|
-
function deepMerge(a, b) {
|
|
2202
|
-
const out = { ...a };
|
|
2203
|
-
for (const [k, v] of Object.entries(b)) {
|
|
2204
|
-
if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
2205
|
-
const existing = out[k];
|
|
2206
|
-
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2207
|
-
out[k] = deepMerge(existing, v);
|
|
2208
|
-
} else {
|
|
2209
|
-
out[k] = v;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
return out;
|
|
2213
|
-
}
|
|
2214
2698
|
|
|
2215
2699
|
// src/storage/config-loader.ts
|
|
2216
2700
|
init_atomic_write();
|
|
@@ -2393,43 +2877,16 @@ var defaultIndexing = {
|
|
|
2393
2877
|
watchExternal: true,
|
|
2394
2878
|
debounceMs: 400
|
|
2395
2879
|
};
|
|
2396
|
-
function isPrimitiveArray(a) {
|
|
2397
|
-
return a.every((v) => v === null || typeof v !== "object");
|
|
2398
|
-
}
|
|
2399
|
-
var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set([
|
|
2400
|
-
"__proto__",
|
|
2401
|
-
"constructor",
|
|
2402
|
-
"prototype",
|
|
2403
|
-
"__defineGetter__",
|
|
2404
|
-
"__defineSetter__",
|
|
2405
|
-
"__lookupGetter__",
|
|
2406
|
-
"__lookupSetter__"
|
|
2407
|
-
]);
|
|
2408
2880
|
function deepMerge2(base, patch) {
|
|
2409
|
-
|
|
2410
|
-
if (
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
2417
|
-
out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
|
|
2418
|
-
} else {
|
|
2419
|
-
out[k] = v;
|
|
2420
|
-
if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
|
|
2421
|
-
console.warn(
|
|
2422
|
-
`[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
|
|
2423
|
-
);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
} else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
|
|
2427
|
-
out[k] = deepMerge2(existing, v);
|
|
2428
|
-
} else if (v !== void 0) {
|
|
2429
|
-
out[k] = v;
|
|
2430
|
-
}
|
|
2881
|
+
const opts = { arrayMode: "concat-primitives" };
|
|
2882
|
+
if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
|
|
2883
|
+
opts.onNonPrimitiveArrayReplace = (key, existingLen, patchLen) => {
|
|
2884
|
+
console.warn(
|
|
2885
|
+
`[config] Non-primitive array for "${key}" replaced (global + local config merge). Global entries: ${existingLen}, local entries: ${patchLen}.`
|
|
2886
|
+
);
|
|
2887
|
+
};
|
|
2431
2888
|
}
|
|
2432
|
-
return
|
|
2889
|
+
return deepMerge(base, patch, opts);
|
|
2433
2890
|
}
|
|
2434
2891
|
var DefaultConfigLoader = class {
|
|
2435
2892
|
paths;
|
|
@@ -2468,7 +2925,13 @@ var DefaultConfigLoader = class {
|
|
|
2468
2925
|
cfg = deepMerge2(cfg, patch);
|
|
2469
2926
|
}
|
|
2470
2927
|
} catch (err) {
|
|
2471
|
-
console.warn(
|
|
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
|
+
}));
|
|
2472
2935
|
}
|
|
2473
2936
|
}
|
|
2474
2937
|
if (opts.cliFlags) {
|
|
@@ -2531,7 +2994,12 @@ var DefaultConfigLoader = class {
|
|
|
2531
2994
|
return parsed.value;
|
|
2532
2995
|
} catch (err) {
|
|
2533
2996
|
if (err.code === "ENOENT") return null;
|
|
2534
|
-
console.warn(
|
|
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
|
+
}));
|
|
2535
3003
|
return null;
|
|
2536
3004
|
}
|
|
2537
3005
|
}
|
|
@@ -2541,33 +3009,63 @@ var DefaultConfigLoader = class {
|
|
|
2541
3009
|
raw = await fsp.readFile(file, "utf8");
|
|
2542
3010
|
} catch (err) {
|
|
2543
3011
|
if (err.code !== "ENOENT") {
|
|
2544
|
-
console.warn(
|
|
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
|
+
}));
|
|
2545
3019
|
}
|
|
2546
3020
|
return {};
|
|
2547
3021
|
}
|
|
2548
3022
|
const parsed = safeParse(raw);
|
|
2549
3023
|
if (!parsed.ok || !parsed.value) {
|
|
2550
|
-
console.warn(
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
+
}));
|
|
2553
3031
|
return {};
|
|
2554
3032
|
}
|
|
2555
3033
|
return parsed.value;
|
|
2556
3034
|
}
|
|
2557
3035
|
validateBehavior(cfg) {
|
|
2558
|
-
if (cfg.version === void 0) throw new
|
|
2559
|
-
|
|
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
|
+
});
|
|
2560
3046
|
const c = cfg.context;
|
|
2561
|
-
if (!c) throw new
|
|
3047
|
+
if (!c) throw new ConfigError({
|
|
3048
|
+
message: "Config: missing context section",
|
|
3049
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3050
|
+
context: { field: "context" }
|
|
3051
|
+
});
|
|
2562
3052
|
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
2563
3053
|
for (const f of fields) {
|
|
2564
3054
|
const v = c[f];
|
|
2565
3055
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2566
|
-
throw new
|
|
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
|
+
});
|
|
2567
3061
|
}
|
|
2568
3062
|
}
|
|
2569
3063
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
2570
|
-
throw new
|
|
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
|
+
});
|
|
2571
3069
|
}
|
|
2572
3070
|
if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
|
|
2573
3071
|
const known = listContextWindowModes().map((m) => m.id).join(", ");
|
|
@@ -2579,12 +3077,18 @@ var DefaultConfigLoader = class {
|
|
|
2579
3077
|
}
|
|
2580
3078
|
validateIdentity(cfg) {
|
|
2581
3079
|
if (!cfg.provider) {
|
|
2582
|
-
throw new
|
|
2583
|
-
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
2584
|
-
|
|
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
|
+
});
|
|
2585
3085
|
}
|
|
2586
3086
|
if (!cfg.model) {
|
|
2587
|
-
throw new
|
|
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
|
+
});
|
|
2588
3092
|
}
|
|
2589
3093
|
}
|
|
2590
3094
|
};
|
|
@@ -2685,7 +3189,10 @@ var RecoveryLock = class {
|
|
|
2685
3189
|
if (this.sessionStore) {
|
|
2686
3190
|
try {
|
|
2687
3191
|
const data = await this.sessionStore.load(lock.sessionId);
|
|
2688
|
-
const
|
|
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
|
+
);
|
|
2689
3196
|
if (closed) return null;
|
|
2690
3197
|
messageCount = data.messages.length;
|
|
2691
3198
|
} catch {
|
|
@@ -3230,6 +3737,7 @@ function isAllowed(type, level) {
|
|
|
3230
3737
|
}
|
|
3231
3738
|
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
3232
3739
|
const normalizedLevel = level ?? "standard";
|
|
3740
|
+
const resolveWriter = typeof writer === "function" ? writer : () => writer;
|
|
3233
3741
|
const progressCounters = /* @__PURE__ */ new Map();
|
|
3234
3742
|
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
3235
3743
|
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
@@ -3254,13 +3762,26 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
3254
3762
|
return isAllowed(type, normalizedLevel);
|
|
3255
3763
|
},
|
|
3256
3764
|
async append(event) {
|
|
3257
|
-
|
|
3765
|
+
const target = resolveWriter();
|
|
3766
|
+
if (!target) return;
|
|
3258
3767
|
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
3259
3768
|
if (!shouldSample(event)) return;
|
|
3260
3769
|
try {
|
|
3261
|
-
await
|
|
3770
|
+
await target.append(event);
|
|
3262
3771
|
} catch (err) {
|
|
3263
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
|
+
}
|
|
3264
3785
|
}
|
|
3265
3786
|
};
|
|
3266
3787
|
}
|
|
@@ -3314,10 +3835,12 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
|
3314
3835
|
try {
|
|
3315
3836
|
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
3316
3837
|
} catch (err) {
|
|
3317
|
-
console.warn(
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
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
|
+
}));
|
|
3321
3844
|
}
|
|
3322
3845
|
}
|
|
3323
3846
|
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
@@ -3327,7 +3850,13 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
3327
3850
|
const enqueueWrite = (todos) => {
|
|
3328
3851
|
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
3329
3852
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3330
|
-
console.error(
|
|
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
|
+
}));
|
|
3331
3860
|
});
|
|
3332
3861
|
return writeChain;
|
|
3333
3862
|
};
|
|
@@ -3466,14 +3995,16 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
|
|
|
3466
3995
|
id: `todo_${Date.now()}_plan`,
|
|
3467
3996
|
content: item.title,
|
|
3468
3997
|
status: "in_progress",
|
|
3469
|
-
activeForm: item.title
|
|
3998
|
+
activeForm: item.title,
|
|
3999
|
+
promotedFromPlan: item.id
|
|
3470
4000
|
});
|
|
3471
4001
|
if (subtasks && subtasks.length > 0) {
|
|
3472
4002
|
for (const st of subtasks) {
|
|
3473
4003
|
todos.push({
|
|
3474
4004
|
id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
|
|
3475
4005
|
content: st,
|
|
3476
|
-
status: "pending"
|
|
4006
|
+
status: "pending",
|
|
4007
|
+
promotedFromPlan: item.id
|
|
3477
4008
|
});
|
|
3478
4009
|
}
|
|
3479
4010
|
}
|
|
@@ -4378,91 +4909,6 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
4378
4909
|
}
|
|
4379
4910
|
};
|
|
4380
4911
|
|
|
4381
|
-
// src/types/errors.ts
|
|
4382
|
-
var ERROR_CODES = {
|
|
4383
|
-
// Provider
|
|
4384
|
-
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
4385
|
-
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
4386
|
-
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
4387
|
-
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
4388
|
-
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
4389
|
-
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
4390
|
-
// Agent
|
|
4391
|
-
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
4392
|
-
AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
|
|
4393
|
-
AGENT_ABORTED: "AGENT_ABORTED",
|
|
4394
|
-
AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
|
|
4395
|
-
// File system
|
|
4396
|
-
FS_READ_FAILED: "FS_READ_FAILED",
|
|
4397
|
-
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
|
|
4398
|
-
var WrongStackError = class extends Error {
|
|
4399
|
-
code;
|
|
4400
|
-
subsystem;
|
|
4401
|
-
severity;
|
|
4402
|
-
recoverable;
|
|
4403
|
-
context;
|
|
4404
|
-
constructor(opts) {
|
|
4405
|
-
super(opts.message, { cause: opts.cause });
|
|
4406
|
-
this.name = "WrongStackError";
|
|
4407
|
-
this.code = opts.code;
|
|
4408
|
-
this.subsystem = opts.subsystem;
|
|
4409
|
-
this.severity = opts.severity ?? "error";
|
|
4410
|
-
this.recoverable = opts.recoverable ?? false;
|
|
4411
|
-
this.context = opts.context;
|
|
4412
|
-
}
|
|
4413
|
-
/**
|
|
4414
|
-
* Render a one-line user-facing description.
|
|
4415
|
-
* Subclasses should override for domain-specific formatting.
|
|
4416
|
-
*/
|
|
4417
|
-
describe() {
|
|
4418
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
4419
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
4420
|
-
}
|
|
4421
|
-
};
|
|
4422
|
-
function formatContext(ctx) {
|
|
4423
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
4424
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
4425
|
-
}
|
|
4426
|
-
var AgentError = class extends WrongStackError {
|
|
4427
|
-
constructor(opts) {
|
|
4428
|
-
super({
|
|
4429
|
-
message: opts.message,
|
|
4430
|
-
code: opts.code,
|
|
4431
|
-
subsystem: "agent",
|
|
4432
|
-
severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
|
|
4433
|
-
recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
4434
|
-
context: opts.context,
|
|
4435
|
-
cause: opts.cause
|
|
4436
|
-
});
|
|
4437
|
-
this.name = "AgentError";
|
|
4438
|
-
}
|
|
4439
|
-
};
|
|
4440
|
-
function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
|
|
4441
|
-
if (err instanceof WrongStackError) return err;
|
|
4442
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4443
|
-
return new AgentError({
|
|
4444
|
-
message,
|
|
4445
|
-
code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
|
|
4446
|
-
cause: err
|
|
4447
|
-
});
|
|
4448
|
-
}
|
|
4449
|
-
var FsError = class extends WrongStackError {
|
|
4450
|
-
path;
|
|
4451
|
-
constructor(opts) {
|
|
4452
|
-
super({
|
|
4453
|
-
message: opts.message,
|
|
4454
|
-
code: opts.code,
|
|
4455
|
-
subsystem: "fs",
|
|
4456
|
-
severity: "error",
|
|
4457
|
-
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
4458
|
-
context: { path: opts.path, ...opts.context },
|
|
4459
|
-
cause: opts.cause
|
|
4460
|
-
});
|
|
4461
|
-
this.name = "FsError";
|
|
4462
|
-
this.path = opts.path;
|
|
4463
|
-
}
|
|
4464
|
-
};
|
|
4465
|
-
|
|
4466
4912
|
// src/types/provider.ts
|
|
4467
4913
|
var ProviderError = class extends WrongStackError {
|
|
4468
4914
|
status;
|
|
@@ -5018,8 +5464,9 @@ function handleMessageStop(state, ev) {
|
|
|
5018
5464
|
state.stopReason = ev.stopReason ?? "end_turn";
|
|
5019
5465
|
state.usage = ev.usage ?? { input: 0, output: 0 };
|
|
5020
5466
|
}
|
|
5021
|
-
async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
5467
|
+
async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
|
|
5022
5468
|
const state = createStreamingState(req.model);
|
|
5469
|
+
logger.debug("Stream started", { providerId: provider.id, model: req.model });
|
|
5023
5470
|
const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
|
|
5024
5471
|
try {
|
|
5025
5472
|
for (; ; ) {
|
|
@@ -5074,20 +5521,42 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
5074
5521
|
case "message_stop":
|
|
5075
5522
|
handleMessageStop(state, ev);
|
|
5076
5523
|
break;
|
|
5077
|
-
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
|
+
});
|
|
5078
5531
|
break;
|
|
5532
|
+
}
|
|
5079
5533
|
}
|
|
5080
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
|
+
});
|
|
5081
5543
|
events.emit("provider.stream_error", {
|
|
5082
5544
|
ctx,
|
|
5083
|
-
eventType:
|
|
5084
|
-
msg:
|
|
5545
|
+
eventType: String(evAny.type),
|
|
5546
|
+
msg: errMsg
|
|
5085
5547
|
});
|
|
5086
5548
|
}
|
|
5087
5549
|
}
|
|
5088
5550
|
} catch (err) {
|
|
5089
5551
|
if (signal.aborted) {
|
|
5090
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
|
+
});
|
|
5091
5560
|
return buildResponse(state);
|
|
5092
5561
|
}
|
|
5093
5562
|
throw err;
|
|
@@ -5109,10 +5578,29 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
5109
5578
|
} catch {
|
|
5110
5579
|
}
|
|
5111
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
|
+
});
|
|
5112
5591
|
return buildResponse(state);
|
|
5113
5592
|
}
|
|
5114
5593
|
|
|
5115
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
|
+
}
|
|
5116
5604
|
async function runProviderWithRetry(opts) {
|
|
5117
5605
|
const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
|
|
5118
5606
|
let attempt = 0;
|
|
@@ -5123,12 +5611,22 @@ async function runProviderWithRetry(opts) {
|
|
|
5123
5611
|
"provider.streaming": provider.capabilities.streaming,
|
|
5124
5612
|
"provider.attempt": attempt
|
|
5125
5613
|
});
|
|
5614
|
+
logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
|
|
5126
5615
|
try {
|
|
5127
|
-
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 });
|
|
5128
5617
|
span?.setAttribute("provider.stopReason", res.stopReason);
|
|
5129
5618
|
span?.setAttribute("provider.usage_in", res.usage.input);
|
|
5130
5619
|
span?.setAttribute("provider.usage_out", res.usage.output);
|
|
5131
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
|
+
});
|
|
5132
5630
|
return res;
|
|
5133
5631
|
} catch (err) {
|
|
5134
5632
|
if (err instanceof Error) span?.recordError(err);
|
|
@@ -5147,11 +5645,27 @@ async function runProviderWithRetry(opts) {
|
|
|
5147
5645
|
retryable: false
|
|
5148
5646
|
});
|
|
5149
5647
|
}
|
|
5150
|
-
|
|
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);
|
|
5151
5657
|
}
|
|
5152
5658
|
const delay = Math.round(retry.delayMs(attempt));
|
|
5153
5659
|
const attemptNum = attempt + 1;
|
|
5154
|
-
|
|
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
|
+
});
|
|
5155
5669
|
if (isProviderErr) {
|
|
5156
5670
|
events.emit("provider.retry", {
|
|
5157
5671
|
providerId: err.providerId,
|
|
@@ -5238,22 +5752,31 @@ function estimateToolResultTokens(content) {
|
|
|
5238
5752
|
function estimateTextTokens(text) {
|
|
5239
5753
|
return RoughTokenEstimate(text);
|
|
5240
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
|
+
}
|
|
5241
5766
|
function estimateMessageTokens(messages) {
|
|
5242
5767
|
let total = 0;
|
|
5243
5768
|
for (const m of messages) {
|
|
5244
|
-
if (typeof m.
|
|
5245
|
-
total +=
|
|
5246
|
-
|
|
5247
|
-
for (const b of m.content) {
|
|
5248
|
-
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
5249
|
-
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
5250
|
-
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
5251
|
-
}
|
|
5769
|
+
if (typeof m._estTokens === "number" && m._estTokens > 0) {
|
|
5770
|
+
total += m._estTokens;
|
|
5771
|
+
continue;
|
|
5252
5772
|
}
|
|
5773
|
+
total += computeMessageTokens(m);
|
|
5253
5774
|
}
|
|
5254
5775
|
return total;
|
|
5255
5776
|
}
|
|
5256
5777
|
function estimateToolDefTokens(tool) {
|
|
5778
|
+
const cached = tool._estDefTokens;
|
|
5779
|
+
if (typeof cached === "number" && cached > 0) return cached;
|
|
5257
5780
|
return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
|
|
5258
5781
|
}
|
|
5259
5782
|
function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
|
|
@@ -5263,6 +5786,11 @@ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = C
|
|
|
5263
5786
|
} else if (Array.isArray(messages)) {
|
|
5264
5787
|
for (const m of messages) {
|
|
5265
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
|
+
}
|
|
5266
5794
|
const content = m.content;
|
|
5267
5795
|
if (typeof content === "string") {
|
|
5268
5796
|
messagesTokens += RoughTokenEstimate(content);
|
|
@@ -5355,6 +5883,18 @@ function findPreserveStart(messages, preserveK) {
|
|
|
5355
5883
|
}
|
|
5356
5884
|
function eliseOldToolResults(messages, opts) {
|
|
5357
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 };
|
|
5358
5898
|
let saved = 0;
|
|
5359
5899
|
let changed = false;
|
|
5360
5900
|
const next = new Array(messages.length);
|
|
@@ -6248,6 +6788,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
6248
6788
|
static NOOP_RETRY_DELTA_TOKENS = 2e3;
|
|
6249
6789
|
/** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
|
|
6250
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;
|
|
6251
6800
|
/**
|
|
6252
6801
|
* @param compactor Compactor to use for compaction.
|
|
6253
6802
|
* @param maxContext Provider's max context window in tokens.
|
|
@@ -6283,12 +6832,24 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
6283
6832
|
}
|
|
6284
6833
|
handler() {
|
|
6285
6834
|
return async (ctx, next) => {
|
|
6286
|
-
const
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
)
|
|
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
|
+
}
|
|
6292
6853
|
const load = tokens / this._maxContext;
|
|
6293
6854
|
const policy = this.policyProvider?.(ctx);
|
|
6294
6855
|
const thresholds = policy?.thresholds ?? {
|
|
@@ -6525,7 +7086,7 @@ function createToolOutputSerializer(opts = {}) {
|
|
|
6525
7086
|
}
|
|
6526
7087
|
|
|
6527
7088
|
// src/execution/tool-executor.ts
|
|
6528
|
-
var ToolExecutor = class {
|
|
7089
|
+
var ToolExecutor = class _ToolExecutor {
|
|
6529
7090
|
constructor(registry, opts) {
|
|
6530
7091
|
this.registry = registry;
|
|
6531
7092
|
this.opts = opts;
|
|
@@ -6537,6 +7098,10 @@ var ToolExecutor = class {
|
|
|
6537
7098
|
}
|
|
6538
7099
|
registry;
|
|
6539
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;
|
|
6540
7105
|
serializer;
|
|
6541
7106
|
iterationTimeoutMs;
|
|
6542
7107
|
maxToolTimeoutMs;
|
|
@@ -6582,9 +7147,6 @@ Please call the tool again with arguments that match its inputSchema. You can us
|
|
|
6582
7147
|
return { result, tool, durationMs: Date.now() - start };
|
|
6583
7148
|
}
|
|
6584
7149
|
const toolDangerousCaps = getDangerousCapabilities(tool);
|
|
6585
|
-
if (toolDangerousCaps.length > 0) {
|
|
6586
|
-
if (this.opts.events) ;
|
|
6587
|
-
}
|
|
6588
7150
|
if (hasMalformedArguments(use.input)) {
|
|
6589
7151
|
const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
|
|
6590
7152
|
budget = this.decrementBudget(result, budget);
|
|
@@ -6822,17 +7384,48 @@ ${post.additionalContext}` };
|
|
|
6822
7384
|
throw new Error(`Tool "${tool.name}" does not support streaming execution`);
|
|
6823
7385
|
}
|
|
6824
7386
|
const stream = tool.executeStream(input, ctx, { signal });
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
break;
|
|
6830
|
-
}
|
|
7387
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
7388
|
+
let progressTail = "";
|
|
7389
|
+
let lastProgressEmitAt = 0;
|
|
7390
|
+
const emitProgress = (ev) => {
|
|
6831
7391
|
this.opts.events?.emit("tool.progress", {
|
|
6832
7392
|
name: tool.name,
|
|
6833
7393
|
id: toolUseId ?? "<unknown>",
|
|
6834
7394
|
event: ev
|
|
6835
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);
|
|
6836
7429
|
}
|
|
6837
7430
|
if (!sawFinal) {
|
|
6838
7431
|
throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
|
|
@@ -6943,9 +7536,11 @@ function extractMalformedRaw(input) {
|
|
|
6943
7536
|
|
|
6944
7537
|
// src/utils/assert-never.ts
|
|
6945
7538
|
function assertNever(x, message) {
|
|
6946
|
-
|
|
7539
|
+
const err = new Error(
|
|
6947
7540
|
`Unhandled case: ${JSON.stringify(x)}`
|
|
6948
7541
|
);
|
|
7542
|
+
err.name = "AssertNeverError";
|
|
7543
|
+
throw err;
|
|
6949
7544
|
}
|
|
6950
7545
|
|
|
6951
7546
|
// src/execution/autonomous-runner.ts
|
|
@@ -6956,7 +7551,13 @@ var DoneConditionChecker = class {
|
|
|
6956
7551
|
const result = compileUserRegex(condition.pattern, "");
|
|
6957
7552
|
this.compiledRegex = result.ok ? result.regex : null;
|
|
6958
7553
|
if (!result.ok) {
|
|
6959
|
-
console.warn(
|
|
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
|
+
}));
|
|
6960
7561
|
}
|
|
6961
7562
|
} else {
|
|
6962
7563
|
this.compiledRegex = null;
|
|
@@ -7132,9 +7733,13 @@ function projectSlug(absRoot) {
|
|
|
7132
7733
|
function slugify(name) {
|
|
7133
7734
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
7134
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
|
+
}
|
|
7135
7741
|
function resolveWstackPaths(opts) {
|
|
7136
|
-
const
|
|
7137
|
-
const globalRoot = opts.globalRoot ?? path11.join(home, ".wrongstack");
|
|
7742
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path11.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
7138
7743
|
const hash = projectHash(opts.projectRoot);
|
|
7139
7744
|
const slug = projectSlug(opts.projectRoot);
|
|
7140
7745
|
const projectDir = path11.join(globalRoot, "projects", slug);
|
|
@@ -7191,12 +7796,24 @@ async function loadGoal(filePath) {
|
|
|
7191
7796
|
try {
|
|
7192
7797
|
const parsed = JSON.parse(raw);
|
|
7193
7798
|
if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
|
|
7194
|
-
console.warn(
|
|
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
|
+
}));
|
|
7195
7806
|
return null;
|
|
7196
7807
|
}
|
|
7197
7808
|
return parsed;
|
|
7198
7809
|
} catch {
|
|
7199
|
-
console.warn(
|
|
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
|
+
}));
|
|
7200
7817
|
return null;
|
|
7201
7818
|
}
|
|
7202
7819
|
}
|
|
@@ -7310,7 +7927,14 @@ var EternalAutonomyEngine = class {
|
|
|
7310
7927
|
stop() {
|
|
7311
7928
|
this.stopRequested = true;
|
|
7312
7929
|
this.currentCtrl?.abort();
|
|
7313
|
-
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
|
+
}));
|
|
7314
7938
|
});
|
|
7315
7939
|
this.state = "stopped";
|
|
7316
7940
|
}
|
|
@@ -8277,6 +8901,7 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
8277
8901
|
function makeAgentSubagentRunner(opts) {
|
|
8278
8902
|
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
8279
8903
|
return async (task, ctx) => {
|
|
8904
|
+
const taskStartedAt = Date.now();
|
|
8280
8905
|
const factoryResult = await opts.factory(ctx.config);
|
|
8281
8906
|
const { agent, events } = factoryResult;
|
|
8282
8907
|
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
@@ -8373,7 +8998,7 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8373
8998
|
}),
|
|
8374
8999
|
events.on("provider.text_delta", (e) => {
|
|
8375
9000
|
ctx.budget.markActivity();
|
|
8376
|
-
streamingTextAcc = (streamingTextAcc + e.text).slice(-
|
|
9001
|
+
streamingTextAcc = (streamingTextAcc + e.text).slice(-2e3);
|
|
8377
9002
|
})
|
|
8378
9003
|
);
|
|
8379
9004
|
const onParentAbort = () => aborter.abort();
|
|
@@ -8381,6 +9006,15 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8381
9006
|
let result;
|
|
8382
9007
|
try {
|
|
8383
9008
|
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
9009
|
+
events.emit("subagent.task_completed", {
|
|
9010
|
+
subagentId: ctx.subagentId,
|
|
9011
|
+
taskId: task.id,
|
|
9012
|
+
status: result.status === "done" ? "success" : "failed",
|
|
9013
|
+
iterations: result.iterations,
|
|
9014
|
+
toolCalls: ctx.budget.usage().toolCalls,
|
|
9015
|
+
durationMs: Date.now() - taskStartedAt,
|
|
9016
|
+
finalText: result.finalText?.trim() || void 0
|
|
9017
|
+
});
|
|
8384
9018
|
} finally {
|
|
8385
9019
|
detachFleet?.();
|
|
8386
9020
|
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
@@ -8416,21 +9050,40 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8416
9050
|
if (budgetError) throw budgetError;
|
|
8417
9051
|
}
|
|
8418
9052
|
if (result.status === "failed") {
|
|
8419
|
-
throw result.error instanceof
|
|
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
|
+
});
|
|
8420
9058
|
}
|
|
8421
9059
|
if (result.status === "aborted") {
|
|
8422
|
-
throw new
|
|
9060
|
+
throw new AgentError({
|
|
9061
|
+
message: "agent aborted",
|
|
9062
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
9063
|
+
});
|
|
8423
9064
|
}
|
|
8424
9065
|
if (result.status === "max_iterations") {
|
|
8425
|
-
throw new
|
|
9066
|
+
throw new AgentError({
|
|
9067
|
+
message: "agent exhausted iteration limit",
|
|
9068
|
+
code: ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
9069
|
+
recoverable: true
|
|
9070
|
+
});
|
|
8426
9071
|
}
|
|
8427
9072
|
const usage = ctx.budget.usage();
|
|
8428
9073
|
const finalText = (result.finalText ?? "").trim();
|
|
8429
9074
|
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
8430
|
-
throw new
|
|
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
|
+
});
|
|
8431
9080
|
}
|
|
8432
9081
|
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
8433
|
-
throw new
|
|
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
|
+
});
|
|
8434
9087
|
}
|
|
8435
9088
|
return {
|
|
8436
9089
|
result: result.finalText,
|
|
@@ -8462,11 +9115,11 @@ var HEAVY_BUDGET = {
|
|
|
8462
9115
|
};
|
|
8463
9116
|
var TOOLS = {
|
|
8464
9117
|
/** Pure read/inspect — safe for analysis and review agents. */
|
|
8465
|
-
read: ["read", "grep", "glob", "search", "tree"],
|
|
9118
|
+
read: ["read", "grep", "glob", "search", "tree", "mailbox"],
|
|
8466
9119
|
/** Read + structured inspection (logs, diffs, json, dependency audit). */
|
|
8467
|
-
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit"],
|
|
9120
|
+
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
|
|
8468
9121
|
/** Read + edit (no shell). For agents that write code/docs but don't run it. */
|
|
8469
|
-
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch"],
|
|
9122
|
+
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
|
|
8470
9123
|
/** Full build loop: edit + run (lint/format/typecheck/test/bash). */
|
|
8471
9124
|
build: [
|
|
8472
9125
|
"read",
|
|
@@ -8483,16 +9136,17 @@ var TOOLS = {
|
|
|
8483
9136
|
"lint",
|
|
8484
9137
|
"format",
|
|
8485
9138
|
"typecheck",
|
|
8486
|
-
"test"
|
|
9139
|
+
"test",
|
|
9140
|
+
"mailbox"
|
|
8487
9141
|
],
|
|
8488
9142
|
/** Version control. */
|
|
8489
9143
|
vcs: ["read", "grep", "glob", "git", "diff"],
|
|
8490
9144
|
/** Dependency management + CVE audit. */
|
|
8491
|
-
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json"],
|
|
9145
|
+
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
|
|
8492
9146
|
/** Documentation authoring. */
|
|
8493
|
-
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document"],
|
|
9147
|
+
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
|
|
8494
9148
|
/** Web research. */
|
|
8495
|
-
research: ["read", "grep", "glob", "search", "fetch"]
|
|
9149
|
+
research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
|
|
8496
9150
|
};
|
|
8497
9151
|
|
|
8498
9152
|
// src/coordination/agents/phase1-discovery.ts
|
|
@@ -9290,15 +9944,44 @@ Working rules:
|
|
|
9290
9944
|
id: "e2e",
|
|
9291
9945
|
name: "E2E",
|
|
9292
9946
|
role: "e2e",
|
|
9293
|
-
tools: [
|
|
9947
|
+
tools: [
|
|
9948
|
+
...TOOLS.build,
|
|
9949
|
+
"fetch",
|
|
9950
|
+
"playwright_navigate",
|
|
9951
|
+
"playwright_screenshot",
|
|
9952
|
+
"playwright_click",
|
|
9953
|
+
"playwright_type",
|
|
9954
|
+
"playwright_evaluate",
|
|
9955
|
+
"playwright_select_option",
|
|
9956
|
+
"playwright_hover",
|
|
9957
|
+
"playwright_fill_form",
|
|
9958
|
+
"playwright_wait_for",
|
|
9959
|
+
"playwright_press_key",
|
|
9960
|
+
"playwright_drag"
|
|
9961
|
+
],
|
|
9294
9962
|
prompt: `You are the E2E agent. Your job is end-to-end testing: drive the whole
|
|
9295
9963
|
system the way a user would and verify the full flow works across boundaries.
|
|
9296
9964
|
|
|
9297
9965
|
Scope:
|
|
9298
9966
|
- Author end-to-end scenarios that exercise real user journeys
|
|
9299
9967
|
- Drive UI/CLI/API across process and network boundaries
|
|
9968
|
+
- Use Playwright browser tools (navigate, click, type, screenshot, evaluate)
|
|
9969
|
+
to automate web UI flows \u2014 open pages, interact with forms, capture evidence
|
|
9300
9970
|
- Set up and tear down realistic test state
|
|
9301
|
-
- Capture failures with enough detail to reproduce (screenshots, logs)
|
|
9971
|
+
- Capture failures with enough detail to reproduce (screenshots, logs, page HTML)
|
|
9972
|
+
|
|
9973
|
+
Playwright tools available (require the "playwright" MCP server to be enabled):
|
|
9974
|
+
playwright_navigate(url) \u2014 open a page at the given URL
|
|
9975
|
+
playwright_screenshot() \u2014 capture a full-page or viewport screenshot
|
|
9976
|
+
playwright_click(selector) \u2014 click on an element matching a CSS selector
|
|
9977
|
+
playwright_type(selector, text) \u2014 type text into a focused input element
|
|
9978
|
+
playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
|
|
9979
|
+
playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
|
|
9980
|
+
playwright_hover(selector) \u2014 hover the mouse over an element
|
|
9981
|
+
playwright_fill_form(fields) \u2014 fill multiple form fields in one call
|
|
9982
|
+
playwright_wait_for(selector) \u2014 block until an element appears on the page
|
|
9983
|
+
playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
|
|
9984
|
+
playwright_drag(from, to) \u2014 drag an element from one selector to another
|
|
9302
9985
|
|
|
9303
9986
|
Input format you accept:
|
|
9304
9987
|
{ "task": "scenario | smoke | journey", "flow": "<user journey>", "surface": "ui | cli | api" }
|
|
@@ -9312,8 +9995,10 @@ Output: Markdown e2e report:
|
|
|
9312
9995
|
Working rules:
|
|
9313
9996
|
- Test the real flow end to end; don't stub the thing under test
|
|
9314
9997
|
- Make scenarios deterministic \u2014 control time, randomness, and external state
|
|
9315
|
-
- On failure, capture artifacts (logs
|
|
9316
|
-
- Keep scenarios independent so one failure doesn't cascade
|
|
9998
|
+
- On failure, capture artifacts (screenshots, page HTML, logs) for reproduction
|
|
9999
|
+
- Keep scenarios independent so one failure doesn't cascade
|
|
10000
|
+
- For browser tests: playwright_navigate first, then interact, then playwright_screenshot as evidence
|
|
10001
|
+
- If playwright tools are unavailable, report it and fall back to API/CLI testing`
|
|
9317
10002
|
},
|
|
9318
10003
|
budget: HEAVY_BUDGET,
|
|
9319
10004
|
capability: {
|
|
@@ -9326,10 +10011,106 @@ Working rules:
|
|
|
9326
10011
|
"user journey",
|
|
9327
10012
|
"smoke test",
|
|
9328
10013
|
"playwright",
|
|
10014
|
+
"browser",
|
|
10015
|
+
"screenshot",
|
|
10016
|
+
"web ui",
|
|
10017
|
+
"headless",
|
|
9329
10018
|
"cypress",
|
|
9330
10019
|
"full flow",
|
|
9331
10020
|
"browser test",
|
|
9332
|
-
"acceptance test"
|
|
10021
|
+
"acceptance test",
|
|
10022
|
+
"navigate",
|
|
10023
|
+
"click",
|
|
10024
|
+
"form fill",
|
|
10025
|
+
"dom",
|
|
10026
|
+
"page load"
|
|
10027
|
+
]
|
|
10028
|
+
}
|
|
10029
|
+
},
|
|
10030
|
+
{
|
|
10031
|
+
config: {
|
|
10032
|
+
id: "browser",
|
|
10033
|
+
name: "Browser",
|
|
10034
|
+
role: "browser",
|
|
10035
|
+
tools: [
|
|
10036
|
+
...TOOLS.read,
|
|
10037
|
+
"fetch",
|
|
10038
|
+
"playwright_navigate",
|
|
10039
|
+
"playwright_screenshot",
|
|
10040
|
+
"playwright_click",
|
|
10041
|
+
"playwright_type",
|
|
10042
|
+
"playwright_evaluate",
|
|
10043
|
+
"playwright_select_option",
|
|
10044
|
+
"playwright_hover",
|
|
10045
|
+
"playwright_fill_form",
|
|
10046
|
+
"playwright_wait_for",
|
|
10047
|
+
"playwright_press_key",
|
|
10048
|
+
"playwright_drag"
|
|
10049
|
+
],
|
|
10050
|
+
prompt: `You are the Browser agent. Your job is browser automation: open web pages,
|
|
10051
|
+
interact with them, extract data, capture screenshots, and return structured
|
|
10052
|
+
results. You are a read-focused agent \u2014 you drive the browser, not the filesystem.
|
|
10053
|
+
|
|
10054
|
+
Scope:
|
|
10055
|
+
- Navigate to URLs and wait for pages to load
|
|
10056
|
+
- Take full-page or element screenshots as evidence
|
|
10057
|
+
- Click buttons, fill forms, select options, type text \u2014 full user simulation
|
|
10058
|
+
- Extract page content: text, HTML, element attributes, data tables
|
|
10059
|
+
- Evaluate JavaScript in the page context to extract structured data
|
|
10060
|
+
- Verify visual state (element visibility, text content, attribute values)
|
|
10061
|
+
|
|
10062
|
+
Playwright tools available (require the "playwright" MCP server to be enabled):
|
|
10063
|
+
playwright_navigate(url) \u2014 open a page at the given URL
|
|
10064
|
+
playwright_screenshot() \u2014 capture a full-page or viewport screenshot
|
|
10065
|
+
playwright_click(selector) \u2014 click on an element matching a CSS selector
|
|
10066
|
+
playwright_type(selector, text) \u2014 type text into a focused input element
|
|
10067
|
+
playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
|
|
10068
|
+
playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
|
|
10069
|
+
playwright_hover(selector) \u2014 hover the mouse over an element
|
|
10070
|
+
playwright_fill_form(fields) \u2014 fill multiple form fields in one call
|
|
10071
|
+
playwright_wait_for(selector) \u2014 block until an element appears on the page
|
|
10072
|
+
playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
|
|
10073
|
+
playwright_drag(from, to) \u2014 drag an element from one selector to another
|
|
10074
|
+
|
|
10075
|
+
Input format you accept:
|
|
10076
|
+
{ "task": "navigate | screenshot | extract | interact | verify", "url": "<url>", "steps": ["step1", "step2"] }
|
|
10077
|
+
|
|
10078
|
+
Output: Structured markdown report:
|
|
10079
|
+
- ## Page (URL, title, load status)
|
|
10080
|
+
- ## Actions Taken (step-by-step with timestamps)
|
|
10081
|
+
- ## Results (extracted data, element states, verification results)
|
|
10082
|
+
- ## Screenshots (list attached screenshot references)
|
|
10083
|
+
- ## Errors (any failures with stack traces)
|
|
10084
|
+
|
|
10085
|
+
Working rules:
|
|
10086
|
+
- Always playwright_navigate first before any interaction
|
|
10087
|
+
- Always playwright_wait_for after navigation to ensure the page is ready
|
|
10088
|
+
- playwright_screenshot is your primary evidence \u2014 use it before and after interactions
|
|
10089
|
+
- Use playwright_evaluate for structured data extraction (JSON, text content)
|
|
10090
|
+
- If a selector fails, try alternative selectors before giving up
|
|
10091
|
+
- Report exact CSS selectors used \u2014 they're part of the evidence
|
|
10092
|
+
- If playwright tools are unavailable, report the error immediately \u2014 do not guess`
|
|
10093
|
+
},
|
|
10094
|
+
budget: MEDIUM_BUDGET,
|
|
10095
|
+
capability: {
|
|
10096
|
+
phase: "verify",
|
|
10097
|
+
summary: "Browser automation: opens pages, clicks, types, screenshots, extracts data via Playwright headless Chromium.",
|
|
10098
|
+
keywords: [
|
|
10099
|
+
"browser",
|
|
10100
|
+
"screenshot",
|
|
10101
|
+
"navigate",
|
|
10102
|
+
"web page",
|
|
10103
|
+
"scrape",
|
|
10104
|
+
"crawl",
|
|
10105
|
+
"headless",
|
|
10106
|
+
"chrome",
|
|
10107
|
+
"open url",
|
|
10108
|
+
"capture",
|
|
10109
|
+
"page title",
|
|
10110
|
+
"extract data",
|
|
10111
|
+
"fill form",
|
|
10112
|
+
"click button",
|
|
10113
|
+
"take screenshot"
|
|
9333
10114
|
]
|
|
9334
10115
|
}
|
|
9335
10116
|
},
|
|
@@ -10778,7 +11559,7 @@ Working rules:
|
|
|
10778
11559
|
id: "tech-stack",
|
|
10779
11560
|
name: "Tech Stack Validator",
|
|
10780
11561
|
role: "tech-stack",
|
|
10781
|
-
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
|
|
11562
|
+
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
|
|
10782
11563
|
prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
|
|
10783
11564
|
before any package, library, or framework choice is committed.
|
|
10784
11565
|
|
|
@@ -10786,6 +11567,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
|
|
|
10786
11567
|
You are the "this isn't code, this is 10-year-old technology" agent. Intervene
|
|
10787
11568
|
hard when the LLM hallucinates a version number or suggests dead tech.
|
|
10788
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
|
+
|
|
10789
11580
|
## Critical rules
|
|
10790
11581
|
|
|
10791
11582
|
1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
|
|
@@ -10844,11 +11635,11 @@ When APPROVED:
|
|
|
10844
11635
|
**Install**: pnpm add <name>@^<major>.<minor>.0`
|
|
10845
11636
|
},
|
|
10846
11637
|
budget: {
|
|
10847
|
-
timeoutMs:
|
|
10848
|
-
maxIterations:
|
|
10849
|
-
maxToolCalls:
|
|
10850
|
-
maxTokens:
|
|
10851
|
-
maxCostUsd: 0.
|
|
11638
|
+
timeoutMs: 12e4,
|
|
11639
|
+
maxIterations: 10,
|
|
11640
|
+
maxToolCalls: 40,
|
|
11641
|
+
maxTokens: 6e4,
|
|
11642
|
+
maxCostUsd: 0.25
|
|
10852
11643
|
},
|
|
10853
11644
|
capability: {
|
|
10854
11645
|
phase: "meta",
|
|
@@ -11049,6 +11840,9 @@ Do not add prose, markdown, or code fences.`;
|
|
|
11049
11840
|
|
|
11050
11841
|
// src/coordination/coordinator/error-classifier.ts
|
|
11051
11842
|
function classifySubagentError(err, hints = {}) {
|
|
11843
|
+
if (err instanceof AgentError && err.cause) {
|
|
11844
|
+
return classifySubagentError(err.cause, hints);
|
|
11845
|
+
}
|
|
11052
11846
|
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
11053
11847
|
if (err instanceof ProviderError) {
|
|
11054
11848
|
const baseMessage2 = err.describe();
|
|
@@ -11081,7 +11875,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
11081
11875
|
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
11082
11876
|
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
11083
11877
|
}
|
|
11084
|
-
if (/empty response
|
|
11878
|
+
if (/empty response/i.test(baseMessage)) {
|
|
11085
11879
|
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
11086
11880
|
}
|
|
11087
11881
|
if (/^tool failed: /i.test(baseMessage)) {
|
|
@@ -12245,7 +13039,14 @@ var ParallelEternalEngine = class {
|
|
|
12245
13039
|
}
|
|
12246
13040
|
stop() {
|
|
12247
13041
|
this.stopRequested = true;
|
|
12248
|
-
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
|
+
}));
|
|
12249
13050
|
});
|
|
12250
13051
|
this.state = "stopped";
|
|
12251
13052
|
}
|
|
@@ -12845,24 +13646,36 @@ var InMemoryAgentBridge = class {
|
|
|
12845
13646
|
return () => this.subscriptions.delete(handler);
|
|
12846
13647
|
}
|
|
12847
13648
|
async request(msg, timeoutMs) {
|
|
12848
|
-
if (this.stopped) throw new
|
|
13649
|
+
if (this.stopped) throw new AgentError({
|
|
13650
|
+
message: "Bridge is stopped",
|
|
13651
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13652
|
+
});
|
|
12849
13653
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
12850
13654
|
const correlationId = msg.id;
|
|
12851
13655
|
if (this.inflightGuards.has(correlationId)) {
|
|
12852
|
-
throw new
|
|
12853
|
-
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids
|
|
12854
|
-
|
|
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
|
+
});
|
|
12855
13661
|
}
|
|
12856
13662
|
this.inflightGuards.add(correlationId);
|
|
12857
13663
|
return new Promise((resolve5, reject) => {
|
|
12858
13664
|
const timer = setTimeout(() => {
|
|
12859
13665
|
this.inflightGuards.delete(correlationId);
|
|
12860
13666
|
this.pendingRequests.delete(correlationId);
|
|
12861
|
-
reject(new
|
|
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
|
+
}));
|
|
12862
13672
|
}, timeout);
|
|
12863
13673
|
if (!this.inflightGuards.has(correlationId)) {
|
|
12864
13674
|
clearTimeout(timer);
|
|
12865
|
-
reject(new
|
|
13675
|
+
reject(new AgentError({
|
|
13676
|
+
message: "Bridge stopped",
|
|
13677
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13678
|
+
}));
|
|
12866
13679
|
return;
|
|
12867
13680
|
}
|
|
12868
13681
|
this.pendingRequests.set(correlationId, {
|
|
@@ -12883,7 +13696,10 @@ var InMemoryAgentBridge = class {
|
|
|
12883
13696
|
this.stopped = true;
|
|
12884
13697
|
for (const [, p] of this.pendingRequests) {
|
|
12885
13698
|
clearTimeout(p.timer);
|
|
12886
|
-
p.reject(new
|
|
13699
|
+
p.reject(new AgentError({
|
|
13700
|
+
message: "Bridge stopped",
|
|
13701
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13702
|
+
}));
|
|
12887
13703
|
}
|
|
12888
13704
|
this.pendingRequests.clear();
|
|
12889
13705
|
this.inflightGuards.clear();
|
|
@@ -13654,7 +14470,23 @@ Bridge contract:
|
|
|
13654
14470
|
subagents' context. Those are not yours to read.
|
|
13655
14471
|
- Your final task output is what the Director sees. Be concise,
|
|
13656
14472
|
structured, and self-contained \u2014 assume the Director will paste your
|
|
13657
|
-
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.`;
|
|
13658
14490
|
function composeDirectorPrompt(parts = {}) {
|
|
13659
14491
|
const sections = [];
|
|
13660
14492
|
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
@@ -16704,6 +17536,77 @@ Remember: your job is to make the user a better developer, not just to complete
|
|
|
16704
17536
|
tags: ["teaching", "mentor", "learning"],
|
|
16705
17537
|
toolPreferences: ["read", "edit", "explain"],
|
|
16706
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"]
|
|
16707
17610
|
}
|
|
16708
17611
|
];
|
|
16709
17612
|
|
|
@@ -17329,7 +18232,10 @@ var TaskTracker = class {
|
|
|
17329
18232
|
return this.graph;
|
|
17330
18233
|
}
|
|
17331
18234
|
addNode(node) {
|
|
17332
|
-
if (!this.graph) throw new
|
|
18235
|
+
if (!this.graph) throw new SddError({
|
|
18236
|
+
message: "No graph loaded",
|
|
18237
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18238
|
+
});
|
|
17333
18239
|
const now = Date.now();
|
|
17334
18240
|
const newNode = {
|
|
17335
18241
|
...node,
|
|
@@ -17347,7 +18253,10 @@ var TaskTracker = class {
|
|
|
17347
18253
|
return newNode;
|
|
17348
18254
|
}
|
|
17349
18255
|
addEdge(from, to, type = "depends_on") {
|
|
17350
|
-
if (!this.graph) throw new
|
|
18256
|
+
if (!this.graph) throw new SddError({
|
|
18257
|
+
message: "No graph loaded",
|
|
18258
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18259
|
+
});
|
|
17351
18260
|
this.graph.edges.push({
|
|
17352
18261
|
id: crypto.randomUUID(),
|
|
17353
18262
|
from,
|
|
@@ -17358,9 +18267,16 @@ var TaskTracker = class {
|
|
|
17358
18267
|
this.persist();
|
|
17359
18268
|
}
|
|
17360
18269
|
updateNodeStatus(id, status, reason) {
|
|
17361
|
-
if (!this.graph) throw new
|
|
18270
|
+
if (!this.graph) throw new SddError({
|
|
18271
|
+
message: "No graph loaded",
|
|
18272
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18273
|
+
});
|
|
17362
18274
|
const node = this.graph.nodes.get(id);
|
|
17363
|
-
if (!node) throw new
|
|
18275
|
+
if (!node) throw new SddError({
|
|
18276
|
+
message: `Node ${id} not found`,
|
|
18277
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18278
|
+
context: { nodeId: id }
|
|
18279
|
+
});
|
|
17364
18280
|
const from = node.status;
|
|
17365
18281
|
const now = Date.now();
|
|
17366
18282
|
node.status = status;
|
|
@@ -17383,9 +18299,16 @@ var TaskTracker = class {
|
|
|
17383
18299
|
this.persist();
|
|
17384
18300
|
}
|
|
17385
18301
|
updateNode(id, patch) {
|
|
17386
|
-
if (!this.graph) throw new
|
|
18302
|
+
if (!this.graph) throw new SddError({
|
|
18303
|
+
message: "No graph loaded",
|
|
18304
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18305
|
+
});
|
|
17387
18306
|
const node = this.graph.nodes.get(id);
|
|
17388
|
-
if (!node) throw new
|
|
18307
|
+
if (!node) throw new SddError({
|
|
18308
|
+
message: `Node ${id} not found`,
|
|
18309
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18310
|
+
context: { nodeId: id }
|
|
18311
|
+
});
|
|
17389
18312
|
if (patch.title !== void 0) node.title = patch.title;
|
|
17390
18313
|
if (patch.description !== void 0) node.description = patch.description;
|
|
17391
18314
|
if (patch.priority !== void 0) node.priority = patch.priority;
|
|
@@ -17504,7 +18427,12 @@ var TaskTracker = class {
|
|
|
17504
18427
|
persist() {
|
|
17505
18428
|
if (!this.graph) return;
|
|
17506
18429
|
this.opts.store.saveGraph(this.graph).catch((err) => {
|
|
17507
|
-
this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(
|
|
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
|
+
}));
|
|
17508
18436
|
});
|
|
17509
18437
|
}
|
|
17510
18438
|
};
|
|
@@ -17557,12 +18485,14 @@ var TaskFlow = class {
|
|
|
17557
18485
|
const analysis = parser.analyze(this.spec);
|
|
17558
18486
|
this.emit("spec.analyzed", { analysis });
|
|
17559
18487
|
if (analysis.completeness < 50) {
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
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 }
|
|
17563
18492
|
});
|
|
18493
|
+
this.emit("error", { phase: "analyzing", error: err });
|
|
17564
18494
|
this.setPhase("failed");
|
|
17565
|
-
throw
|
|
18495
|
+
throw err;
|
|
17566
18496
|
}
|
|
17567
18497
|
this.setPhase("generating");
|
|
17568
18498
|
const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
|
|
@@ -17570,7 +18500,11 @@ var TaskFlow = class {
|
|
|
17570
18500
|
return this.graph;
|
|
17571
18501
|
}
|
|
17572
18502
|
async execute(ctx) {
|
|
17573
|
-
if (!this.graph) throw new
|
|
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
|
+
});
|
|
17574
18508
|
this.setPhase("executing");
|
|
17575
18509
|
this.stopped = false;
|
|
17576
18510
|
const pendingTasks = this.getExecutableTasks();
|
|
@@ -17610,7 +18544,11 @@ var TaskFlow = class {
|
|
|
17610
18544
|
}
|
|
17611
18545
|
async reviewTask(taskId, approved, comment) {
|
|
17612
18546
|
const task = this.opts.tracker.getNode(taskId);
|
|
17613
|
-
if (!task) throw new
|
|
18547
|
+
if (!task) throw new SddError({
|
|
18548
|
+
message: `Task ${taskId} not found`,
|
|
18549
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18550
|
+
context: { taskId }
|
|
18551
|
+
});
|
|
17614
18552
|
if (approved) {
|
|
17615
18553
|
this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
|
|
17616
18554
|
this.emit("task.completed", { taskId });
|
|
@@ -18254,7 +19192,11 @@ var AISpecBuilder = class {
|
|
|
18254
19192
|
switch (this.session.phase) {
|
|
18255
19193
|
case "questioning":
|
|
18256
19194
|
if (!this.session.spec) {
|
|
18257
|
-
throw new
|
|
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
|
+
});
|
|
18258
19200
|
}
|
|
18259
19201
|
this.session.phase = "spec_review";
|
|
18260
19202
|
break;
|
|
@@ -18312,7 +19254,11 @@ var AISpecBuilder = class {
|
|
|
18312
19254
|
*/
|
|
18313
19255
|
async saveSpec() {
|
|
18314
19256
|
if (!this.session.spec) {
|
|
18315
|
-
throw new
|
|
19257
|
+
throw new SddError({
|
|
19258
|
+
message: "No spec to save.",
|
|
19259
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
19260
|
+
context: { sessionId: this.session.id }
|
|
19261
|
+
});
|
|
18316
19262
|
}
|
|
18317
19263
|
await this.store.save(this.session.spec);
|
|
18318
19264
|
return this.session.spec;
|
|
@@ -18327,17 +19273,30 @@ var AISpecBuilder = class {
|
|
|
18327
19273
|
try {
|
|
18328
19274
|
parsed = JSON.parse(jsonStr);
|
|
18329
19275
|
} catch (e) {
|
|
18330
|
-
throw new
|
|
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
|
+
});
|
|
18331
19282
|
}
|
|
18332
19283
|
if (!parsed || typeof parsed !== "object") {
|
|
18333
|
-
throw new
|
|
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
|
+
});
|
|
18334
19289
|
}
|
|
18335
19290
|
const raw = parsed;
|
|
18336
19291
|
const now = Date.now();
|
|
18337
19292
|
const title = String(raw.title ?? this.session.title ?? "Untitled");
|
|
18338
19293
|
const overview = String(raw.overview ?? "");
|
|
18339
19294
|
if (!overview || overview === "undefined") {
|
|
18340
|
-
throw new
|
|
19295
|
+
throw new SddError({
|
|
19296
|
+
message: "Spec must have an overview",
|
|
19297
|
+
code: ERROR_CODES.SDD_VALIDATION_FAILED,
|
|
19298
|
+
context: { field: "overview", title }
|
|
19299
|
+
});
|
|
18341
19300
|
}
|
|
18342
19301
|
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
18343
19302
|
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
@@ -19405,7 +20364,10 @@ var SddParallelRun = class {
|
|
|
19405
20364
|
"\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
|
|
19406
20365
|
"\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
|
|
19407
20366
|
].join("\n");
|
|
19408
|
-
if (!this.coordinator) throw new
|
|
20367
|
+
if (!this.coordinator) throw new SddError({
|
|
20368
|
+
message: "SDD parallel runner requires a coordinator",
|
|
20369
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
20370
|
+
});
|
|
19409
20371
|
const coordinator = this.coordinator;
|
|
19410
20372
|
const spawns = subagentIds.map(
|
|
19411
20373
|
(subagentId) => coordinator.spawn({
|
|
@@ -19417,7 +20379,10 @@ var SddParallelRun = class {
|
|
|
19417
20379
|
);
|
|
19418
20380
|
const spawnResults = await Promise.all(spawns);
|
|
19419
20381
|
if (!spawnResults.every((r) => Boolean(r.subagentId))) {
|
|
19420
|
-
throw new
|
|
20382
|
+
throw new SddError({
|
|
20383
|
+
message: "One or more subagent spawns failed",
|
|
20384
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
20385
|
+
});
|
|
19421
20386
|
}
|
|
19422
20387
|
const assignPromises = tasks.map((task, i) => {
|
|
19423
20388
|
const spec = {
|
|
@@ -20533,6 +21498,14 @@ var zaiVisionServer = () => ({
|
|
|
20533
21498
|
],
|
|
20534
21499
|
permission: "auto"
|
|
20535
21500
|
});
|
|
21501
|
+
var playwrightServer = () => ({
|
|
21502
|
+
name: "playwright",
|
|
21503
|
+
description: "Browser automation \u2014 navigate, screenshot, click, type, evaluate JS (headless Chromium)",
|
|
21504
|
+
transport: "stdio",
|
|
21505
|
+
command: "npx",
|
|
21506
|
+
args: ["-y", "@modelcontextprotocol/server-playwright"],
|
|
21507
|
+
permission: "confirm"
|
|
21508
|
+
});
|
|
20536
21509
|
var miniMaxVisionServer = () => ({
|
|
20537
21510
|
name: "minimax-vision",
|
|
20538
21511
|
description: "MiniMax MCP \u2014 image understanding via understand_image",
|
|
@@ -20559,7 +21532,8 @@ var allServers = () => ({
|
|
|
20559
21532
|
"google-maps": { ...googleMapsServer(), enabled: false },
|
|
20560
21533
|
sentinel: { ...sentinelServer(), enabled: false },
|
|
20561
21534
|
"zai-vision": { ...zaiVisionServer(), enabled: false },
|
|
20562
|
-
"minimax-vision": { ...miniMaxVisionServer(), enabled: false }
|
|
21535
|
+
"minimax-vision": { ...miniMaxVisionServer(), enabled: false },
|
|
21536
|
+
playwright: { ...playwrightServer(), enabled: false }
|
|
20563
21537
|
});
|
|
20564
21538
|
|
|
20565
21539
|
export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigMigrationError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPermissionPolicy, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, Director, DirectorStateCheckpoint, DoneConditionChecker, EternalAutonomyEngine, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetSpawnBudgetError, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SddParallelRun, SddTaskDecomposer, SelectiveCompactor, SessionAnalyzer, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolExecutor, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, clearPlan, composeDirectorPrompt, composeSubagentPrompt, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDelegateTool, createMessage, createSessionEventBridge, createStrategyCompactor, decryptConfigSecrets, deriveTodosFromPlanItem, dispatchAgent, emptyPlan, encryptConfigSecrets, everArtServer, filesystemServer, formatPlan, formatPlanTemplates, getAgentDefinition, getPlanTemplate, getTemplate, githubServer, googleMapsServer, listPlanTemplates, listTemplates, loadDirectorState, loadPlan, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, migratePlaintextSecrets, miniMaxVisionServer, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, resolveAuditLevel, resolveSessionLoggingConfig, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, savePlan, saveTodosCheckpoint, scoreAgents, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, templateToMarkdown, wireMetricsToEvents, zaiVisionServer };
|