@wrongstack/core 0.155.0 → 0.250.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-BbZU5TPN.d.ts → agent-bridge-4gc0vfW2.d.ts} +1 -1
- package/dist/{agent-subagent-runner-Bsueu0J2.d.ts → agent-subagent-runner-Dz-9kiE6.d.ts} +9 -8
- package/dist/{brain-CS_B0vIE.d.ts → brain-sCZ3lCjq.d.ts} +26 -2
- package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
- package/dist/{config-BaVThgnT.d.ts → config-eSsrto5d.d.ts} +8 -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 +1986 -146
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +1110 -296
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +229 -56
- 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-CbV8pXLD.d.ts → goal-preamble-BjJpnLW4.d.ts} +19 -10
- package/dist/{index-CI1hRfPt.d.ts → index-Dy8OwfBD.d.ts} +8 -8
- package/dist/{index-B5wz-GXm.d.ts → index-IehiNryU.d.ts} +7 -5
- package/dist/index.d.ts +438 -128
- package/dist/index.js +4989 -849
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +61 -13
- 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-CPERR2De.d.ts → mcp-servers-DfXxCASH.d.ts} +3 -3
- 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-DpanBg8D.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-BSKSFNhv.d.ts → multi-agent-coordinator-CnbEqpv0.d.ts} +8 -8
- package/dist/{null-fleet-bus-CGOez8Le.d.ts → null-fleet-bus-Do1OLYpj.d.ts} +7 -7
- package/dist/observability/index.d.ts +2 -2
- package/dist/package-outdated-watcher-CA5GGB4C.d.ts +560 -0
- package/dist/{parallel-eternal-engine-CYoTKjsz.d.ts → parallel-eternal-engine-UZg1xOzE.d.ts} +13 -9
- package/dist/{path-resolver-DuhlmPil.d.ts → path-resolver-BaP06Owy.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-D1n-gQI-.d.ts +493 -0
- package/dist/{plan-templates-DbH7lg-t.d.ts → plan-templates-BUVRY0pU.d.ts} +18 -7
- package/dist/{provider-runner-Cocq0O9E.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 +221 -87
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-w8MbUe2Q.d.ts → secret-vault-CeVNiy_f.d.ts} +3 -2
- package/dist/security/index.d.ts +5 -4
- package/dist/security/index.js +155 -13
- 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 +150 -12
- package/dist/storage/index.js +1041 -214
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +67 -20
- package/dist/types/index.js +562 -55
- 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 +25 -4
- package/dist/utils/index.js +45 -14
- 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
|
};
|
|
@@ -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;
|
|
@@ -2053,6 +2457,26 @@ var KEY_BYTES = 32;
|
|
|
2053
2457
|
var IV_BYTES = 12;
|
|
2054
2458
|
var TAG_BYTES = 16;
|
|
2055
2459
|
var ALGO = "aes-256-gcm";
|
|
2460
|
+
var KEY_FILE_MODE = 384;
|
|
2461
|
+
function checkKeyFilePermissions(keyFile) {
|
|
2462
|
+
if (process.platform === "win32") return;
|
|
2463
|
+
try {
|
|
2464
|
+
const stat6 = fs.statSync(keyFile);
|
|
2465
|
+
const actualMode = stat6.mode & 511;
|
|
2466
|
+
if (actualMode !== KEY_FILE_MODE) {
|
|
2467
|
+
console.warn(JSON.stringify({
|
|
2468
|
+
level: "warn",
|
|
2469
|
+
event: "vault.key_file_wrong_permissions",
|
|
2470
|
+
message: `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`,
|
|
2471
|
+
keyFile,
|
|
2472
|
+
expectedMode: KEY_FILE_MODE,
|
|
2473
|
+
actualMode,
|
|
2474
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2475
|
+
}));
|
|
2476
|
+
}
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2056
2480
|
var DefaultSecretVault = class {
|
|
2057
2481
|
keyFile;
|
|
2058
2482
|
key;
|
|
@@ -2076,14 +2500,26 @@ var DefaultSecretVault = class {
|
|
|
2076
2500
|
const rest = value.slice(ENCRYPTED_PREFIX.length);
|
|
2077
2501
|
const parts = rest.split(":");
|
|
2078
2502
|
if (parts.length !== 3) {
|
|
2079
|
-
throw new
|
|
2503
|
+
throw new ConfigError({
|
|
2504
|
+
message: "SecretVault: malformed encrypted value",
|
|
2505
|
+
code: ERROR_CODES.CONFIG_PARSE_FAILED,
|
|
2506
|
+
context: { field: "encrypted_value" }
|
|
2507
|
+
});
|
|
2080
2508
|
}
|
|
2081
2509
|
const [ivB64, tagB64, ctB64] = parts;
|
|
2082
2510
|
const iv = Buffer.from(ivB64, "base64");
|
|
2083
2511
|
const tag = Buffer.from(tagB64, "base64");
|
|
2084
2512
|
const ct = Buffer.from(ctB64, "base64");
|
|
2085
|
-
if (iv.length !== IV_BYTES) throw new
|
|
2086
|
-
|
|
2513
|
+
if (iv.length !== IV_BYTES) throw new ConfigError({
|
|
2514
|
+
message: "SecretVault: bad IV length",
|
|
2515
|
+
code: ERROR_CODES.CONFIG_PARSE_FAILED,
|
|
2516
|
+
context: { expected: IV_BYTES, actual: iv.length }
|
|
2517
|
+
});
|
|
2518
|
+
if (tag.length !== TAG_BYTES) throw new ConfigError({
|
|
2519
|
+
message: "SecretVault: bad tag length",
|
|
2520
|
+
code: ERROR_CODES.CONFIG_PARSE_FAILED,
|
|
2521
|
+
context: { expected: TAG_BYTES, actual: tag.length }
|
|
2522
|
+
});
|
|
2087
2523
|
const key = this.loadOrCreateKey();
|
|
2088
2524
|
const decipher = createDecipheriv(ALGO, key, iv);
|
|
2089
2525
|
decipher.setAuthTag(tag);
|
|
@@ -2093,30 +2529,36 @@ var DefaultSecretVault = class {
|
|
|
2093
2529
|
loadOrCreateKey() {
|
|
2094
2530
|
if (this.key) return this.key;
|
|
2095
2531
|
try {
|
|
2096
|
-
const buf =
|
|
2532
|
+
const buf = fs.readFileSync(this.keyFile);
|
|
2097
2533
|
if (buf.length !== KEY_BYTES) {
|
|
2098
|
-
throw new
|
|
2099
|
-
`SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key
|
|
2100
|
-
|
|
2534
|
+
throw new ConfigError({
|
|
2535
|
+
message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
|
|
2536
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2537
|
+
context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
|
|
2538
|
+
});
|
|
2101
2539
|
}
|
|
2102
2540
|
this.key = buf;
|
|
2541
|
+
checkKeyFilePermissions(this.keyFile);
|
|
2103
2542
|
return this.key;
|
|
2104
2543
|
} catch (err) {
|
|
2105
2544
|
if (err.code !== "ENOENT") throw err;
|
|
2106
2545
|
}
|
|
2107
|
-
|
|
2546
|
+
fs.mkdirSync(path11.dirname(this.keyFile), { recursive: true });
|
|
2108
2547
|
const key = randomBytes(KEY_BYTES);
|
|
2109
2548
|
try {
|
|
2110
|
-
|
|
2549
|
+
fs.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
|
|
2111
2550
|
} catch (err) {
|
|
2112
2551
|
if (err.code !== "EEXIST") throw err;
|
|
2113
|
-
const buf =
|
|
2552
|
+
const buf = fs.readFileSync(this.keyFile);
|
|
2114
2553
|
if (buf.length !== KEY_BYTES) {
|
|
2115
|
-
throw new
|
|
2116
|
-
`SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key
|
|
2117
|
-
|
|
2554
|
+
throw new ConfigError({
|
|
2555
|
+
message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
|
|
2556
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
2557
|
+
context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
|
|
2558
|
+
});
|
|
2118
2559
|
}
|
|
2119
2560
|
this.key = buf;
|
|
2561
|
+
checkKeyFilePermissions(this.keyFile);
|
|
2120
2562
|
return this.key;
|
|
2121
2563
|
}
|
|
2122
2564
|
this.key = key;
|
|
@@ -2177,7 +2619,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
|
|
|
2177
2619
|
await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2178
2620
|
await restrictFilePermissions(configPath);
|
|
2179
2621
|
}
|
|
2180
|
-
async function migratePlaintextSecrets(configPath, vault) {
|
|
2622
|
+
async function migratePlaintextSecrets(configPath, vault, logger) {
|
|
2181
2623
|
let raw;
|
|
2182
2624
|
try {
|
|
2183
2625
|
raw = await fsp.readFile(configPath, "utf8");
|
|
@@ -2194,11 +2636,14 @@ async function migratePlaintextSecrets(configPath, vault) {
|
|
|
2194
2636
|
const migrated = walkCount(parsed, vault, counter);
|
|
2195
2637
|
if (counter.n === 0) return { migrated: 0, file: configPath };
|
|
2196
2638
|
await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
|
|
2197
|
-
await restrictFilePermissions(
|
|
2639
|
+
await restrictFilePermissions(
|
|
2640
|
+
configPath,
|
|
2641
|
+
logger ? { warn: (msg) => logger.warn(msg) } : void 0
|
|
2642
|
+
);
|
|
2198
2643
|
return { migrated: counter.n, file: configPath };
|
|
2199
2644
|
}
|
|
2200
2645
|
async function restrictFilePermissions(filePath, opts) {
|
|
2201
|
-
const warn = ((msg) => console.warn(msg));
|
|
2646
|
+
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
2202
2647
|
if (process.platform === "win32") {
|
|
2203
2648
|
try {
|
|
2204
2649
|
const { execFile: execFile2 } = await import('child_process');
|
|
@@ -2480,7 +2925,13 @@ var DefaultConfigLoader = class {
|
|
|
2480
2925
|
cfg = deepMerge2(cfg, patch);
|
|
2481
2926
|
}
|
|
2482
2927
|
} catch (err) {
|
|
2483
|
-
console.warn(
|
|
2928
|
+
console.warn(JSON.stringify({
|
|
2929
|
+
level: "warn",
|
|
2930
|
+
event: "config.source_load_failed",
|
|
2931
|
+
source: src.name,
|
|
2932
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2933
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2934
|
+
}));
|
|
2484
2935
|
}
|
|
2485
2936
|
}
|
|
2486
2937
|
if (opts.cliFlags) {
|
|
@@ -2543,7 +2994,12 @@ var DefaultConfigLoader = class {
|
|
|
2543
2994
|
return parsed.value;
|
|
2544
2995
|
} catch (err) {
|
|
2545
2996
|
if (err.code === "ENOENT") return null;
|
|
2546
|
-
console.warn(
|
|
2997
|
+
console.warn(JSON.stringify({
|
|
2998
|
+
level: "warn",
|
|
2999
|
+
event: "config.sync_load_failed",
|
|
3000
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3001
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3002
|
+
}));
|
|
2547
3003
|
return null;
|
|
2548
3004
|
}
|
|
2549
3005
|
}
|
|
@@ -2553,33 +3009,63 @@ var DefaultConfigLoader = class {
|
|
|
2553
3009
|
raw = await fsp.readFile(file, "utf8");
|
|
2554
3010
|
} catch (err) {
|
|
2555
3011
|
if (err.code !== "ENOENT") {
|
|
2556
|
-
console.warn(
|
|
3012
|
+
console.warn(JSON.stringify({
|
|
3013
|
+
level: "warn",
|
|
3014
|
+
event: "config.read_failed",
|
|
3015
|
+
path: file,
|
|
3016
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3017
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3018
|
+
}));
|
|
2557
3019
|
}
|
|
2558
3020
|
return {};
|
|
2559
3021
|
}
|
|
2560
3022
|
const parsed = safeParse(raw);
|
|
2561
3023
|
if (!parsed.ok || !parsed.value) {
|
|
2562
|
-
console.warn(
|
|
2563
|
-
|
|
2564
|
-
|
|
3024
|
+
console.warn(JSON.stringify({
|
|
3025
|
+
level: "warn",
|
|
3026
|
+
event: "config.parse_failed",
|
|
3027
|
+
path: file,
|
|
3028
|
+
message: "invalid JSON \u2014 falling back to defaults for this layer",
|
|
3029
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3030
|
+
}));
|
|
2565
3031
|
return {};
|
|
2566
3032
|
}
|
|
2567
3033
|
return parsed.value;
|
|
2568
3034
|
}
|
|
2569
3035
|
validateBehavior(cfg) {
|
|
2570
|
-
if (cfg.version === void 0) throw new
|
|
2571
|
-
|
|
3036
|
+
if (cfg.version === void 0) throw new ConfigError({
|
|
3037
|
+
message: "Config: missing version field",
|
|
3038
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3039
|
+
context: { field: "version" }
|
|
3040
|
+
});
|
|
3041
|
+
if (cfg.version !== 1) throw new ConfigError({
|
|
3042
|
+
message: `Config: unsupported version ${cfg.version}`,
|
|
3043
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3044
|
+
context: { field: "version", actual: cfg.version }
|
|
3045
|
+
});
|
|
2572
3046
|
const c = cfg.context;
|
|
2573
|
-
if (!c) throw new
|
|
3047
|
+
if (!c) throw new ConfigError({
|
|
3048
|
+
message: "Config: missing context section",
|
|
3049
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3050
|
+
context: { field: "context" }
|
|
3051
|
+
});
|
|
2574
3052
|
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
2575
3053
|
for (const f of fields) {
|
|
2576
3054
|
const v = c[f];
|
|
2577
3055
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2578
|
-
throw new
|
|
3056
|
+
throw new ConfigError({
|
|
3057
|
+
message: `Config: context.${String(f)} must be a finite number (got ${typeof v})`,
|
|
3058
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3059
|
+
context: { field: `context.${String(f)}`, actualType: typeof v }
|
|
3060
|
+
});
|
|
2579
3061
|
}
|
|
2580
3062
|
}
|
|
2581
3063
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
2582
|
-
throw new
|
|
3064
|
+
throw new ConfigError({
|
|
3065
|
+
message: "Config: context thresholds must satisfy warn < soft < hard",
|
|
3066
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3067
|
+
context: { warn: c.warnThreshold, soft: c.softThreshold, hard: c.hardThreshold }
|
|
3068
|
+
});
|
|
2583
3069
|
}
|
|
2584
3070
|
if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
|
|
2585
3071
|
const known = listContextWindowModes().map((m) => m.id).join(", ");
|
|
@@ -2591,12 +3077,18 @@ var DefaultConfigLoader = class {
|
|
|
2591
3077
|
}
|
|
2592
3078
|
validateIdentity(cfg) {
|
|
2593
3079
|
if (!cfg.provider) {
|
|
2594
|
-
throw new
|
|
2595
|
-
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
2596
|
-
|
|
3080
|
+
throw new ConfigError({
|
|
3081
|
+
message: "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER.",
|
|
3082
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3083
|
+
context: { field: "provider" }
|
|
3084
|
+
});
|
|
2597
3085
|
}
|
|
2598
3086
|
if (!cfg.model) {
|
|
2599
|
-
throw new
|
|
3087
|
+
throw new ConfigError({
|
|
3088
|
+
message: "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.",
|
|
3089
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3090
|
+
context: { field: "model" }
|
|
3091
|
+
});
|
|
2600
3092
|
}
|
|
2601
3093
|
}
|
|
2602
3094
|
};
|
|
@@ -2697,7 +3189,10 @@ var RecoveryLock = class {
|
|
|
2697
3189
|
if (this.sessionStore) {
|
|
2698
3190
|
try {
|
|
2699
3191
|
const data = await this.sessionStore.load(lock.sessionId);
|
|
2700
|
-
const
|
|
3192
|
+
const lastEnd = data.events.findLastIndex((e) => e.type === "session_end");
|
|
3193
|
+
const closed = lastEnd >= 0 && !data.events.slice(lastEnd + 1).some(
|
|
3194
|
+
(e) => e.type === "user_input" || e.type === "llm_response" || e.type === "in_flight_start"
|
|
3195
|
+
);
|
|
2701
3196
|
if (closed) return null;
|
|
2702
3197
|
messageCount = data.messages.length;
|
|
2703
3198
|
} catch {
|
|
@@ -3242,6 +3737,7 @@ function isAllowed(type, level) {
|
|
|
3242
3737
|
}
|
|
3243
3738
|
function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
3244
3739
|
const normalizedLevel = level ?? "standard";
|
|
3740
|
+
const resolveWriter = typeof writer === "function" ? writer : () => writer;
|
|
3245
3741
|
const progressCounters = /* @__PURE__ */ new Map();
|
|
3246
3742
|
const toolProgressConfig = options.sampling?.toolProgress ?? {};
|
|
3247
3743
|
const TOOL_PROGRESS_SAMPLE_RATE = toolProgressConfig.sampleRate ?? 8;
|
|
@@ -3266,13 +3762,26 @@ function createSessionEventBridge(writer, level = "standard", options = {}) {
|
|
|
3266
3762
|
return isAllowed(type, normalizedLevel);
|
|
3267
3763
|
},
|
|
3268
3764
|
async append(event) {
|
|
3269
|
-
|
|
3765
|
+
const target = resolveWriter();
|
|
3766
|
+
if (!target) return;
|
|
3270
3767
|
if (!isAllowed(event.type, normalizedLevel)) return;
|
|
3271
3768
|
if (!shouldSample(event)) return;
|
|
3272
3769
|
try {
|
|
3273
|
-
await
|
|
3770
|
+
await target.append(event);
|
|
3274
3771
|
} catch (err) {
|
|
3275
3772
|
}
|
|
3773
|
+
},
|
|
3774
|
+
async appendBatch(events) {
|
|
3775
|
+
const target = resolveWriter();
|
|
3776
|
+
if (!target || events.length === 0) return;
|
|
3777
|
+
const allowed = events.filter(
|
|
3778
|
+
(e) => isAllowed(e.type, normalizedLevel) && shouldSample(e)
|
|
3779
|
+
);
|
|
3780
|
+
if (allowed.length === 0) return;
|
|
3781
|
+
try {
|
|
3782
|
+
await target.appendBatch(allowed);
|
|
3783
|
+
} catch {
|
|
3784
|
+
}
|
|
3276
3785
|
}
|
|
3277
3786
|
};
|
|
3278
3787
|
}
|
|
@@ -3326,10 +3835,12 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
|
3326
3835
|
try {
|
|
3327
3836
|
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
3328
3837
|
} catch (err) {
|
|
3329
|
-
console.warn(
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3838
|
+
console.warn(JSON.stringify({
|
|
3839
|
+
level: "warn",
|
|
3840
|
+
event: "todos_checkpoint.save_failed",
|
|
3841
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3842
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3843
|
+
}));
|
|
3333
3844
|
}
|
|
3334
3845
|
}
|
|
3335
3846
|
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
@@ -3339,7 +3850,13 @@ function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
|
3339
3850
|
const enqueueWrite = (todos) => {
|
|
3340
3851
|
writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos)).catch((err) => {
|
|
3341
3852
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3342
|
-
console.error(
|
|
3853
|
+
console.error(JSON.stringify({
|
|
3854
|
+
level: "error",
|
|
3855
|
+
event: "todos_checkpoint.write_chain_failed",
|
|
3856
|
+
sessionId,
|
|
3857
|
+
message: msg,
|
|
3858
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3859
|
+
}));
|
|
3343
3860
|
});
|
|
3344
3861
|
return writeChain;
|
|
3345
3862
|
};
|
|
@@ -3478,14 +3995,16 @@ function deriveTodosFromPlanItem(plan, idOrIndex, subtasks) {
|
|
|
3478
3995
|
id: `todo_${Date.now()}_plan`,
|
|
3479
3996
|
content: item.title,
|
|
3480
3997
|
status: "in_progress",
|
|
3481
|
-
activeForm: item.title
|
|
3998
|
+
activeForm: item.title,
|
|
3999
|
+
promotedFromPlan: item.id
|
|
3482
4000
|
});
|
|
3483
4001
|
if (subtasks && subtasks.length > 0) {
|
|
3484
4002
|
for (const st of subtasks) {
|
|
3485
4003
|
todos.push({
|
|
3486
4004
|
id: `todo_${Date.now()}_${randomUUID().slice(0, 6)}`,
|
|
3487
4005
|
content: st,
|
|
3488
|
-
status: "pending"
|
|
4006
|
+
status: "pending",
|
|
4007
|
+
promotedFromPlan: item.id
|
|
3489
4008
|
});
|
|
3490
4009
|
}
|
|
3491
4010
|
}
|
|
@@ -4390,90 +4909,10 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
4390
4909
|
}
|
|
4391
4910
|
};
|
|
4392
4911
|
|
|
4393
|
-
// src/
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
4397
|
-
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
4398
|
-
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
4399
|
-
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
4400
|
-
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
4401
|
-
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
4402
|
-
// Agent
|
|
4403
|
-
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
4404
|
-
AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
|
|
4405
|
-
AGENT_ABORTED: "AGENT_ABORTED",
|
|
4406
|
-
AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
|
|
4407
|
-
// File system
|
|
4408
|
-
FS_READ_FAILED: "FS_READ_FAILED",
|
|
4409
|
-
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED"};
|
|
4410
|
-
var WrongStackError = class extends Error {
|
|
4411
|
-
code;
|
|
4412
|
-
subsystem;
|
|
4413
|
-
severity;
|
|
4414
|
-
recoverable;
|
|
4415
|
-
context;
|
|
4416
|
-
constructor(opts) {
|
|
4417
|
-
super(opts.message, { cause: opts.cause });
|
|
4418
|
-
this.name = "WrongStackError";
|
|
4419
|
-
this.code = opts.code;
|
|
4420
|
-
this.subsystem = opts.subsystem;
|
|
4421
|
-
this.severity = opts.severity ?? "error";
|
|
4422
|
-
this.recoverable = opts.recoverable ?? false;
|
|
4423
|
-
this.context = opts.context;
|
|
4424
|
-
}
|
|
4425
|
-
/**
|
|
4426
|
-
* Render a one-line user-facing description.
|
|
4427
|
-
* Subclasses should override for domain-specific formatting.
|
|
4428
|
-
*/
|
|
4429
|
-
describe() {
|
|
4430
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
4431
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
4432
|
-
}
|
|
4433
|
-
};
|
|
4434
|
-
function formatContext(ctx) {
|
|
4435
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
4436
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
4437
|
-
}
|
|
4438
|
-
var AgentError = class extends WrongStackError {
|
|
4439
|
-
constructor(opts) {
|
|
4440
|
-
super({
|
|
4441
|
-
message: opts.message,
|
|
4442
|
-
code: opts.code,
|
|
4443
|
-
subsystem: "agent",
|
|
4444
|
-
severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
|
|
4445
|
-
recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
4446
|
-
context: opts.context,
|
|
4447
|
-
cause: opts.cause
|
|
4448
|
-
});
|
|
4449
|
-
this.name = "AgentError";
|
|
4450
|
-
}
|
|
4451
|
-
};
|
|
4452
|
-
function toWrongStackError(err, code = ERROR_CODES.AGENT_RUN_FAILED) {
|
|
4453
|
-
if (err instanceof WrongStackError) return err;
|
|
4454
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4455
|
-
return new AgentError({
|
|
4456
|
-
message,
|
|
4457
|
-
code: code === "UNKNOWN" ? ERROR_CODES.AGENT_RUN_FAILED : code,
|
|
4458
|
-
cause: err
|
|
4459
|
-
});
|
|
4912
|
+
// src/utils/string.ts
|
|
4913
|
+
function truncate(s, max) {
|
|
4914
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
4460
4915
|
}
|
|
4461
|
-
var FsError = class extends WrongStackError {
|
|
4462
|
-
path;
|
|
4463
|
-
constructor(opts) {
|
|
4464
|
-
super({
|
|
4465
|
-
message: opts.message,
|
|
4466
|
-
code: opts.code,
|
|
4467
|
-
subsystem: "fs",
|
|
4468
|
-
severity: "error",
|
|
4469
|
-
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
4470
|
-
context: { path: opts.path, ...opts.context },
|
|
4471
|
-
cause: opts.cause
|
|
4472
|
-
});
|
|
4473
|
-
this.name = "FsError";
|
|
4474
|
-
this.path = opts.path;
|
|
4475
|
-
}
|
|
4476
|
-
};
|
|
4477
4916
|
|
|
4478
4917
|
// src/types/provider.ts
|
|
4479
4918
|
var ProviderError = class extends WrongStackError {
|
|
@@ -4533,9 +4972,6 @@ function describeStatus(status, type) {
|
|
|
4533
4972
|
if (type) return `${type} (${status})`;
|
|
4534
4973
|
return `HTTP ${status}`;
|
|
4535
4974
|
}
|
|
4536
|
-
function truncate(s, n) {
|
|
4537
|
-
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
4538
|
-
}
|
|
4539
4975
|
function providerStatusToCode(status, type) {
|
|
4540
4976
|
if (status === 0) return ERROR_CODES.PROVIDER_NETWORK_ERROR;
|
|
4541
4977
|
if (type === "rate_limit_error" || status === 429) return ERROR_CODES.PROVIDER_RATE_LIMITED;
|
|
@@ -5030,8 +5466,9 @@ function handleMessageStop(state, ev) {
|
|
|
5030
5466
|
state.stopReason = ev.stopReason ?? "end_turn";
|
|
5031
5467
|
state.usage = ev.usage ?? { input: 0, output: 0 };
|
|
5032
5468
|
}
|
|
5033
|
-
async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
5469
|
+
async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
|
|
5034
5470
|
const state = createStreamingState(req.model);
|
|
5471
|
+
logger.debug("Stream started", { providerId: provider.id, model: req.model });
|
|
5035
5472
|
const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
|
|
5036
5473
|
try {
|
|
5037
5474
|
for (; ; ) {
|
|
@@ -5086,20 +5523,42 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
5086
5523
|
case "message_stop":
|
|
5087
5524
|
handleMessageStop(state, ev);
|
|
5088
5525
|
break;
|
|
5089
|
-
default:
|
|
5526
|
+
default: {
|
|
5527
|
+
const unknownEv = ev;
|
|
5528
|
+
logger.warn(`Stream received unknown event type: "${String(unknownEv.type)}"`, {
|
|
5529
|
+
providerId: provider.id,
|
|
5530
|
+
model: req.model,
|
|
5531
|
+
eventType: String(unknownEv.type)
|
|
5532
|
+
});
|
|
5090
5533
|
break;
|
|
5534
|
+
}
|
|
5091
5535
|
}
|
|
5092
5536
|
} catch (handlerErr) {
|
|
5537
|
+
const errMsg = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
|
|
5538
|
+
const evAny = ev;
|
|
5539
|
+
logger.warn(`Stream handler error for event type "${String(evAny.type)}": ${errMsg}`, {
|
|
5540
|
+
providerId: provider.id,
|
|
5541
|
+
model: req.model,
|
|
5542
|
+
eventType: String(evAny.type),
|
|
5543
|
+
errorMessage: errMsg
|
|
5544
|
+
});
|
|
5093
5545
|
events.emit("provider.stream_error", {
|
|
5094
5546
|
ctx,
|
|
5095
|
-
eventType:
|
|
5096
|
-
msg:
|
|
5547
|
+
eventType: String(evAny.type),
|
|
5548
|
+
msg: errMsg
|
|
5097
5549
|
});
|
|
5098
5550
|
}
|
|
5099
5551
|
}
|
|
5100
5552
|
} catch (err) {
|
|
5101
5553
|
if (signal.aborted) {
|
|
5102
5554
|
state.stopReason = "end_turn";
|
|
5555
|
+
logger.debug("Stream aborted \u2014 returning partial state", {
|
|
5556
|
+
providerId: provider.id,
|
|
5557
|
+
model: req.model,
|
|
5558
|
+
textBlockCount: state.textBuffers.length,
|
|
5559
|
+
toolBlockCount: state.tools.size,
|
|
5560
|
+
thinkingBlockCount: state.thinking.length
|
|
5561
|
+
});
|
|
5103
5562
|
return buildResponse(state);
|
|
5104
5563
|
}
|
|
5105
5564
|
throw err;
|
|
@@ -5121,10 +5580,29 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
5121
5580
|
} catch {
|
|
5122
5581
|
}
|
|
5123
5582
|
}
|
|
5583
|
+
logger.debug("Stream completed", {
|
|
5584
|
+
providerId: provider.id,
|
|
5585
|
+
model: req.model,
|
|
5586
|
+
stopReason: state.stopReason,
|
|
5587
|
+
textBlockCount: state.textBuffers.length,
|
|
5588
|
+
toolBlockCount: state.tools.size,
|
|
5589
|
+
thinkingBlockCount: state.thinking.length,
|
|
5590
|
+
usageInput: state.usage.input,
|
|
5591
|
+
usageOutput: state.usage.output
|
|
5592
|
+
});
|
|
5124
5593
|
return buildResponse(state);
|
|
5125
5594
|
}
|
|
5126
5595
|
|
|
5127
5596
|
// src/core/provider-runner.ts
|
|
5597
|
+
function providerLogCtx(p, r) {
|
|
5598
|
+
return {
|
|
5599
|
+
providerId: p.id,
|
|
5600
|
+
model: r.model,
|
|
5601
|
+
streaming: p.capabilities.streaming,
|
|
5602
|
+
msgCount: r.messages.length,
|
|
5603
|
+
toolCount: r.tools?.length ?? 0
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5128
5606
|
async function runProviderWithRetry(opts) {
|
|
5129
5607
|
const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
|
|
5130
5608
|
let attempt = 0;
|
|
@@ -5135,12 +5613,22 @@ async function runProviderWithRetry(opts) {
|
|
|
5135
5613
|
"provider.streaming": provider.capabilities.streaming,
|
|
5136
5614
|
"provider.attempt": attempt
|
|
5137
5615
|
});
|
|
5616
|
+
logger.debug(`Provider attempt ${attempt + 1} starting`, providerLogCtx(provider, request));
|
|
5138
5617
|
try {
|
|
5139
|
-
const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
|
|
5618
|
+
const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events, logger) : await provider.complete(request, { signal });
|
|
5140
5619
|
span?.setAttribute("provider.stopReason", res.stopReason);
|
|
5141
5620
|
span?.setAttribute("provider.usage_in", res.usage.input);
|
|
5142
5621
|
span?.setAttribute("provider.usage_out", res.usage.output);
|
|
5143
5622
|
span?.end();
|
|
5623
|
+
logger.debug("Provider call succeeded", {
|
|
5624
|
+
...providerLogCtx(provider, request),
|
|
5625
|
+
stopReason: res.stopReason,
|
|
5626
|
+
usageInput: res.usage.input,
|
|
5627
|
+
usageOutput: res.usage.output,
|
|
5628
|
+
cacheRead: res.usage.cacheRead,
|
|
5629
|
+
cacheWrite: res.usage.cacheWrite,
|
|
5630
|
+
attempts: attempt + 1
|
|
5631
|
+
});
|
|
5144
5632
|
return res;
|
|
5145
5633
|
} catch (err) {
|
|
5146
5634
|
if (err instanceof Error) span?.recordError(err);
|
|
@@ -5159,11 +5647,27 @@ async function runProviderWithRetry(opts) {
|
|
|
5159
5647
|
retryable: false
|
|
5160
5648
|
});
|
|
5161
5649
|
}
|
|
5162
|
-
|
|
5650
|
+
logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
|
|
5651
|
+
...providerLogCtx(provider, request),
|
|
5652
|
+
attempts: attempt + 1,
|
|
5653
|
+
errorDescription: description,
|
|
5654
|
+
status: isProviderErr ? err.status : void 0,
|
|
5655
|
+
errorName: err instanceof Error ? err.name : void 0,
|
|
5656
|
+
errorStack: err instanceof Error ? err.stack?.split("\n").slice(0, 3).join("\n") : void 0
|
|
5657
|
+
});
|
|
5658
|
+
throw toWrongStackError(err);
|
|
5163
5659
|
}
|
|
5164
5660
|
const delay = Math.round(retry.delayMs(attempt));
|
|
5165
5661
|
const attemptNum = attempt + 1;
|
|
5166
|
-
|
|
5662
|
+
const maxAttempts = retry.maxAttempts(isProviderErr ? err : errAsErr);
|
|
5663
|
+
logger.warn(`Provider retry ${attemptNum}/${maxAttempts} in ${delay}ms \u2014 ${description}`, {
|
|
5664
|
+
...providerLogCtx(provider, request),
|
|
5665
|
+
attempt: attemptNum,
|
|
5666
|
+
maxAttempts,
|
|
5667
|
+
delayMs: delay,
|
|
5668
|
+
errorDescription: description,
|
|
5669
|
+
status: isProviderErr ? err.status : void 0
|
|
5670
|
+
});
|
|
5167
5671
|
if (isProviderErr) {
|
|
5168
5672
|
events.emit("provider.retry", {
|
|
5169
5673
|
providerId: err.providerId,
|
|
@@ -5250,22 +5754,31 @@ function estimateToolResultTokens(content) {
|
|
|
5250
5754
|
function estimateTextTokens(text) {
|
|
5251
5755
|
return RoughTokenEstimate(text);
|
|
5252
5756
|
}
|
|
5757
|
+
function computeMessageTokens(msg) {
|
|
5758
|
+
if (typeof msg.content === "string") return estimateTextTokens(msg.content);
|
|
5759
|
+
let total = 0;
|
|
5760
|
+
for (const b of msg.content) {
|
|
5761
|
+
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
5762
|
+
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
5763
|
+
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
5764
|
+
else total += RoughTokenEstimate(JSON.stringify(b));
|
|
5765
|
+
}
|
|
5766
|
+
return total;
|
|
5767
|
+
}
|
|
5253
5768
|
function estimateMessageTokens(messages) {
|
|
5254
5769
|
let total = 0;
|
|
5255
5770
|
for (const m of messages) {
|
|
5256
|
-
if (typeof m.
|
|
5257
|
-
total +=
|
|
5258
|
-
|
|
5259
|
-
for (const b of m.content) {
|
|
5260
|
-
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
5261
|
-
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
5262
|
-
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
5263
|
-
}
|
|
5771
|
+
if (typeof m._estTokens === "number" && m._estTokens > 0) {
|
|
5772
|
+
total += m._estTokens;
|
|
5773
|
+
continue;
|
|
5264
5774
|
}
|
|
5775
|
+
total += computeMessageTokens(m);
|
|
5265
5776
|
}
|
|
5266
5777
|
return total;
|
|
5267
5778
|
}
|
|
5268
5779
|
function estimateToolDefTokens(tool) {
|
|
5780
|
+
const cached = tool._estDefTokens;
|
|
5781
|
+
if (typeof cached === "number" && cached > 0) return cached;
|
|
5269
5782
|
return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
|
|
5270
5783
|
}
|
|
5271
5784
|
function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
|
|
@@ -5275,6 +5788,11 @@ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = C
|
|
|
5275
5788
|
} else if (Array.isArray(messages)) {
|
|
5276
5789
|
for (const m of messages) {
|
|
5277
5790
|
if (typeof m === "object" && m !== null && "content" in m) {
|
|
5791
|
+
const cached = m._estTokens;
|
|
5792
|
+
if (typeof cached === "number" && cached > 0) {
|
|
5793
|
+
messagesTokens += cached;
|
|
5794
|
+
continue;
|
|
5795
|
+
}
|
|
5278
5796
|
const content = m.content;
|
|
5279
5797
|
if (typeof content === "string") {
|
|
5280
5798
|
messagesTokens += RoughTokenEstimate(content);
|
|
@@ -5367,6 +5885,18 @@ function findPreserveStart(messages, preserveK) {
|
|
|
5367
5885
|
}
|
|
5368
5886
|
function eliseOldToolResults(messages, opts) {
|
|
5369
5887
|
const preserveStart = findPreserveStart(messages, opts.preserveK);
|
|
5888
|
+
let hasOversized = false;
|
|
5889
|
+
for (let i = 0; i < preserveStart && !hasOversized; i++) {
|
|
5890
|
+
const msg = messages[i];
|
|
5891
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
5892
|
+
for (const b of msg.content) {
|
|
5893
|
+
if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
|
|
5894
|
+
hasOversized = true;
|
|
5895
|
+
break;
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
if (!hasOversized) return { messages, saved: 0, changed: false };
|
|
5370
5900
|
let saved = 0;
|
|
5371
5901
|
let changed = false;
|
|
5372
5902
|
const next = new Array(messages.length);
|
|
@@ -6260,6 +6790,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
6260
6790
|
static NOOP_RETRY_DELTA_TOKENS = 2e3;
|
|
6261
6791
|
/** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
|
|
6262
6792
|
lastNoopAttempt = null;
|
|
6793
|
+
/**
|
|
6794
|
+
* Cached token estimate from the last handler() invocation. When the
|
|
6795
|
+
* message count and tool count haven't changed since the last estimate
|
|
6796
|
+
* (autonomous idle loops), we skip the expensive O(n) token estimation
|
|
6797
|
+
* and reuse this value. Reset to -1 when the context changes.
|
|
6798
|
+
*/
|
|
6799
|
+
_cachedTokens = -1;
|
|
6800
|
+
_cachedMsgCount = -1;
|
|
6801
|
+
_cachedToolCount = -1;
|
|
6263
6802
|
/**
|
|
6264
6803
|
* @param compactor Compactor to use for compaction.
|
|
6265
6804
|
* @param maxContext Provider's max context window in tokens.
|
|
@@ -6295,12 +6834,24 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
6295
6834
|
}
|
|
6296
6835
|
handler() {
|
|
6297
6836
|
return async (ctx, next) => {
|
|
6298
|
-
const
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
)
|
|
6837
|
+
const msgCount = ctx.messages.length;
|
|
6838
|
+
const toolCount = (ctx.tools ?? []).length;
|
|
6839
|
+
let tokens;
|
|
6840
|
+
if (this._estimator) {
|
|
6841
|
+
tokens = this._estimator(ctx);
|
|
6842
|
+
} else if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && this._cachedTokens >= 0) {
|
|
6843
|
+
tokens = this._cachedTokens;
|
|
6844
|
+
} else {
|
|
6845
|
+
tokens = estimateRequestTokensCalibrated(
|
|
6846
|
+
ctx.messages,
|
|
6847
|
+
ctx.systemPrompt,
|
|
6848
|
+
ctx.tools ?? [],
|
|
6849
|
+
`${ctx.provider?.id ?? "unknown"}/${ctx.model}`
|
|
6850
|
+
).total;
|
|
6851
|
+
this._cachedTokens = tokens;
|
|
6852
|
+
this._cachedMsgCount = msgCount;
|
|
6853
|
+
this._cachedToolCount = toolCount;
|
|
6854
|
+
}
|
|
6304
6855
|
const load = tokens / this._maxContext;
|
|
6305
6856
|
const policy = this.policyProvider?.(ctx);
|
|
6306
6857
|
const thresholds = policy?.thresholds ?? {
|
|
@@ -6537,7 +7088,7 @@ function createToolOutputSerializer(opts = {}) {
|
|
|
6537
7088
|
}
|
|
6538
7089
|
|
|
6539
7090
|
// src/execution/tool-executor.ts
|
|
6540
|
-
var ToolExecutor = class {
|
|
7091
|
+
var ToolExecutor = class _ToolExecutor {
|
|
6541
7092
|
constructor(registry, opts) {
|
|
6542
7093
|
this.registry = registry;
|
|
6543
7094
|
this.opts = opts;
|
|
@@ -6549,6 +7100,10 @@ var ToolExecutor = class {
|
|
|
6549
7100
|
}
|
|
6550
7101
|
registry;
|
|
6551
7102
|
opts;
|
|
7103
|
+
/** Minimum gap between coalesced `partial_output` tool.progress emits. */
|
|
7104
|
+
static PROGRESS_EMIT_INTERVAL_MS = 100;
|
|
7105
|
+
/** Max chars of accumulated stream text carried per coalesced emit. */
|
|
7106
|
+
static PROGRESS_TAIL_CHARS = 16384;
|
|
6552
7107
|
serializer;
|
|
6553
7108
|
iterationTimeoutMs;
|
|
6554
7109
|
maxToolTimeoutMs;
|
|
@@ -6594,9 +7149,6 @@ Please call the tool again with arguments that match its inputSchema. You can us
|
|
|
6594
7149
|
return { result, tool, durationMs: Date.now() - start };
|
|
6595
7150
|
}
|
|
6596
7151
|
const toolDangerousCaps = getDangerousCapabilities(tool);
|
|
6597
|
-
if (toolDangerousCaps.length > 0) {
|
|
6598
|
-
if (this.opts.events) ;
|
|
6599
|
-
}
|
|
6600
7152
|
if (hasMalformedArguments(use.input)) {
|
|
6601
7153
|
const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
|
|
6602
7154
|
budget = this.decrementBudget(result, budget);
|
|
@@ -6834,17 +7386,48 @@ ${post.additionalContext}` };
|
|
|
6834
7386
|
throw new Error(`Tool "${tool.name}" does not support streaming execution`);
|
|
6835
7387
|
}
|
|
6836
7388
|
const stream = tool.executeStream(input, ctx, { signal });
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
break;
|
|
6842
|
-
}
|
|
7389
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
7390
|
+
let progressTail = "";
|
|
7391
|
+
let lastProgressEmitAt = 0;
|
|
7392
|
+
const emitProgress = (ev) => {
|
|
6843
7393
|
this.opts.events?.emit("tool.progress", {
|
|
6844
7394
|
name: tool.name,
|
|
6845
7395
|
id: toolUseId ?? "<unknown>",
|
|
6846
7396
|
event: ev
|
|
6847
7397
|
});
|
|
7398
|
+
};
|
|
7399
|
+
const flushProgressTail = (force) => {
|
|
7400
|
+
if (progressTail.length === 0) return;
|
|
7401
|
+
const now = Date.now();
|
|
7402
|
+
if (!force && now - lastProgressEmitAt < _ToolExecutor.PROGRESS_EMIT_INTERVAL_MS) return;
|
|
7403
|
+
const text = progressTail;
|
|
7404
|
+
progressTail = "";
|
|
7405
|
+
lastProgressEmitAt = now;
|
|
7406
|
+
emitProgress({ type: "partial_output", text });
|
|
7407
|
+
};
|
|
7408
|
+
try {
|
|
7409
|
+
while (true) {
|
|
7410
|
+
const { done, value: ev } = await iter.next();
|
|
7411
|
+
if (done) break;
|
|
7412
|
+
if (ev.type === "final") {
|
|
7413
|
+
finalOutput = ev.output;
|
|
7414
|
+
sawFinal = true;
|
|
7415
|
+
break;
|
|
7416
|
+
}
|
|
7417
|
+
if (ev.type === "partial_output" && typeof ev.text === "string") {
|
|
7418
|
+
progressTail += ev.text;
|
|
7419
|
+
if (progressTail.length > _ToolExecutor.PROGRESS_TAIL_CHARS) {
|
|
7420
|
+
progressTail = progressTail.slice(-_ToolExecutor.PROGRESS_TAIL_CHARS);
|
|
7421
|
+
}
|
|
7422
|
+
flushProgressTail(false);
|
|
7423
|
+
continue;
|
|
7424
|
+
}
|
|
7425
|
+
flushProgressTail(true);
|
|
7426
|
+
emitProgress(ev);
|
|
7427
|
+
}
|
|
7428
|
+
flushProgressTail(true);
|
|
7429
|
+
} finally {
|
|
7430
|
+
await iter.return?.(void 0);
|
|
6848
7431
|
}
|
|
6849
7432
|
if (!sawFinal) {
|
|
6850
7433
|
throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
|
|
@@ -6955,9 +7538,11 @@ function extractMalformedRaw(input) {
|
|
|
6955
7538
|
|
|
6956
7539
|
// src/utils/assert-never.ts
|
|
6957
7540
|
function assertNever(x, message) {
|
|
6958
|
-
|
|
7541
|
+
const err = new Error(
|
|
6959
7542
|
`Unhandled case: ${JSON.stringify(x)}`
|
|
6960
7543
|
);
|
|
7544
|
+
err.name = "AssertNeverError";
|
|
7545
|
+
throw err;
|
|
6961
7546
|
}
|
|
6962
7547
|
|
|
6963
7548
|
// src/execution/autonomous-runner.ts
|
|
@@ -6968,7 +7553,13 @@ var DoneConditionChecker = class {
|
|
|
6968
7553
|
const result = compileUserRegex(condition.pattern, "");
|
|
6969
7554
|
this.compiledRegex = result.ok ? result.regex : null;
|
|
6970
7555
|
if (!result.ok) {
|
|
6971
|
-
console.warn(
|
|
7556
|
+
console.warn(JSON.stringify({
|
|
7557
|
+
level: "warn",
|
|
7558
|
+
event: "autonomous.done_condition_invalid_regex",
|
|
7559
|
+
pattern: condition.pattern,
|
|
7560
|
+
reason: result.reason,
|
|
7561
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7562
|
+
}));
|
|
6972
7563
|
}
|
|
6973
7564
|
} else {
|
|
6974
7565
|
this.compiledRegex = null;
|
|
@@ -7144,9 +7735,13 @@ function projectSlug(absRoot) {
|
|
|
7144
7735
|
function slugify(name) {
|
|
7145
7736
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
7146
7737
|
}
|
|
7738
|
+
function wstackGlobalRoot() {
|
|
7739
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
7740
|
+
if (fromEnv && fromEnv.trim().length > 0) return path11.resolve(fromEnv);
|
|
7741
|
+
return path11.join(os.homedir(), ".wrongstack");
|
|
7742
|
+
}
|
|
7147
7743
|
function resolveWstackPaths(opts) {
|
|
7148
|
-
const
|
|
7149
|
-
const globalRoot = opts.globalRoot ?? path11.join(home, ".wrongstack");
|
|
7744
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path11.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
7150
7745
|
const hash = projectHash(opts.projectRoot);
|
|
7151
7746
|
const slug = projectSlug(opts.projectRoot);
|
|
7152
7747
|
const projectDir = path11.join(globalRoot, "projects", slug);
|
|
@@ -7203,12 +7798,24 @@ async function loadGoal(filePath) {
|
|
|
7203
7798
|
try {
|
|
7204
7799
|
const parsed = JSON.parse(raw);
|
|
7205
7800
|
if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
|
|
7206
|
-
console.warn(
|
|
7801
|
+
console.warn(JSON.stringify({
|
|
7802
|
+
level: "warn",
|
|
7803
|
+
event: "goal_store.invalid_schema",
|
|
7804
|
+
path: filePath,
|
|
7805
|
+
message: "invalid schema \u2014 consider deleting and re-creating",
|
|
7806
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7807
|
+
}));
|
|
7207
7808
|
return null;
|
|
7208
7809
|
}
|
|
7209
7810
|
return parsed;
|
|
7210
7811
|
} catch {
|
|
7211
|
-
console.warn(
|
|
7812
|
+
console.warn(JSON.stringify({
|
|
7813
|
+
level: "warn",
|
|
7814
|
+
event: "goal_store.parse_failed",
|
|
7815
|
+
path: filePath,
|
|
7816
|
+
message: "JSON parse failed \u2014 consider deleting and re-creating",
|
|
7817
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7818
|
+
}));
|
|
7212
7819
|
return null;
|
|
7213
7820
|
}
|
|
7214
7821
|
}
|
|
@@ -7322,7 +7929,14 @@ var EternalAutonomyEngine = class {
|
|
|
7322
7929
|
stop() {
|
|
7323
7930
|
this.stopRequested = true;
|
|
7324
7931
|
this.currentCtrl?.abort();
|
|
7325
|
-
void this.persistEngineState("stopped").catch(() => {
|
|
7932
|
+
void this.persistEngineState("stopped").catch((err) => {
|
|
7933
|
+
console.error(JSON.stringify({
|
|
7934
|
+
level: "error",
|
|
7935
|
+
event: "engine.persist_state_failed",
|
|
7936
|
+
message: err instanceof Error ? err.message : String(err),
|
|
7937
|
+
context: { expectedState: "stopped" },
|
|
7938
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7939
|
+
}));
|
|
7326
7940
|
});
|
|
7327
7941
|
this.state = "stopped";
|
|
7328
7942
|
}
|
|
@@ -8438,21 +9052,40 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8438
9052
|
if (budgetError) throw budgetError;
|
|
8439
9053
|
}
|
|
8440
9054
|
if (result.status === "failed") {
|
|
8441
|
-
throw result.error instanceof
|
|
9055
|
+
throw result.error instanceof AgentError ? result.error : new AgentError({
|
|
9056
|
+
message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
|
|
9057
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
9058
|
+
cause: result.error
|
|
9059
|
+
});
|
|
8442
9060
|
}
|
|
8443
9061
|
if (result.status === "aborted") {
|
|
8444
|
-
throw new
|
|
9062
|
+
throw new AgentError({
|
|
9063
|
+
message: "agent aborted",
|
|
9064
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
9065
|
+
});
|
|
8445
9066
|
}
|
|
8446
9067
|
if (result.status === "max_iterations") {
|
|
8447
|
-
throw new
|
|
9068
|
+
throw new AgentError({
|
|
9069
|
+
message: "agent exhausted iteration limit",
|
|
9070
|
+
code: ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
9071
|
+
recoverable: true
|
|
9072
|
+
});
|
|
8448
9073
|
}
|
|
8449
9074
|
const usage = ctx.budget.usage();
|
|
8450
9075
|
const finalText = (result.finalText ?? "").trim();
|
|
8451
9076
|
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
8452
|
-
throw new
|
|
9077
|
+
throw new AgentError({
|
|
9078
|
+
message: "empty response \u2014 agent produced no text and no tool calls",
|
|
9079
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
9080
|
+
context: { iterations: result.iterations }
|
|
9081
|
+
});
|
|
8453
9082
|
}
|
|
8454
9083
|
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
8455
|
-
throw new
|
|
9084
|
+
throw new AgentError({
|
|
9085
|
+
message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
|
|
9086
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
9087
|
+
context: { tool: lastToolFailed, iterations: result.iterations }
|
|
9088
|
+
});
|
|
8456
9089
|
}
|
|
8457
9090
|
return {
|
|
8458
9091
|
result: result.finalText,
|
|
@@ -8484,11 +9117,11 @@ var HEAVY_BUDGET = {
|
|
|
8484
9117
|
};
|
|
8485
9118
|
var TOOLS = {
|
|
8486
9119
|
/** Pure read/inspect — safe for analysis and review agents. */
|
|
8487
|
-
read: ["read", "grep", "glob", "search", "tree"],
|
|
9120
|
+
read: ["read", "grep", "glob", "search", "tree", "mailbox"],
|
|
8488
9121
|
/** Read + structured inspection (logs, diffs, json, dependency audit). */
|
|
8489
|
-
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit"],
|
|
9122
|
+
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
|
|
8490
9123
|
/** Read + edit (no shell). For agents that write code/docs but don't run it. */
|
|
8491
|
-
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch"],
|
|
9124
|
+
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
|
|
8492
9125
|
/** Full build loop: edit + run (lint/format/typecheck/test/bash). */
|
|
8493
9126
|
build: [
|
|
8494
9127
|
"read",
|
|
@@ -8505,16 +9138,17 @@ var TOOLS = {
|
|
|
8505
9138
|
"lint",
|
|
8506
9139
|
"format",
|
|
8507
9140
|
"typecheck",
|
|
8508
|
-
"test"
|
|
9141
|
+
"test",
|
|
9142
|
+
"mailbox"
|
|
8509
9143
|
],
|
|
8510
9144
|
/** Version control. */
|
|
8511
9145
|
vcs: ["read", "grep", "glob", "git", "diff"],
|
|
8512
9146
|
/** Dependency management + CVE audit. */
|
|
8513
|
-
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json"],
|
|
9147
|
+
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
|
|
8514
9148
|
/** Documentation authoring. */
|
|
8515
|
-
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document"],
|
|
9149
|
+
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
|
|
8516
9150
|
/** Web research. */
|
|
8517
|
-
research: ["read", "grep", "glob", "search", "fetch"]
|
|
9151
|
+
research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
|
|
8518
9152
|
};
|
|
8519
9153
|
|
|
8520
9154
|
// src/coordination/agents/phase1-discovery.ts
|
|
@@ -10927,7 +11561,7 @@ Working rules:
|
|
|
10927
11561
|
id: "tech-stack",
|
|
10928
11562
|
name: "Tech Stack Validator",
|
|
10929
11563
|
role: "tech-stack",
|
|
10930
|
-
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
|
|
11564
|
+
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
|
|
10931
11565
|
prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
|
|
10932
11566
|
before any package, library, or framework choice is committed.
|
|
10933
11567
|
|
|
@@ -10935,6 +11569,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
|
|
|
10935
11569
|
You are the "this isn't code, this is 10-year-old technology" agent. Intervene
|
|
10936
11570
|
hard when the LLM hallucinates a version number or suggests dead tech.
|
|
10937
11571
|
|
|
11572
|
+
## Before you begin
|
|
11573
|
+
|
|
11574
|
+
Check the inter-agent mailbox for pending tasks. Other agents or the file-watcher
|
|
11575
|
+
may have left assign messages with dependency files to audit:
|
|
11576
|
+
- mailbox action=check
|
|
11577
|
+
|
|
11578
|
+
If you find an assign message, use the specified file path and packages.
|
|
11579
|
+
When done, post results back:
|
|
11580
|
+
- mailbox action=send to=<sender> type=result subject="Tech stack audit results" body="..."
|
|
11581
|
+
|
|
10938
11582
|
## Critical rules
|
|
10939
11583
|
|
|
10940
11584
|
1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
|
|
@@ -10993,11 +11637,11 @@ When APPROVED:
|
|
|
10993
11637
|
**Install**: pnpm add <name>@^<major>.<minor>.0`
|
|
10994
11638
|
},
|
|
10995
11639
|
budget: {
|
|
10996
|
-
timeoutMs:
|
|
10997
|
-
maxIterations:
|
|
10998
|
-
maxToolCalls:
|
|
10999
|
-
maxTokens:
|
|
11000
|
-
maxCostUsd: 0.
|
|
11640
|
+
timeoutMs: 12e4,
|
|
11641
|
+
maxIterations: 10,
|
|
11642
|
+
maxToolCalls: 40,
|
|
11643
|
+
maxTokens: 6e4,
|
|
11644
|
+
maxCostUsd: 0.25
|
|
11001
11645
|
},
|
|
11002
11646
|
capability: {
|
|
11003
11647
|
phase: "meta",
|
|
@@ -11198,6 +11842,9 @@ Do not add prose, markdown, or code fences.`;
|
|
|
11198
11842
|
|
|
11199
11843
|
// src/coordination/coordinator/error-classifier.ts
|
|
11200
11844
|
function classifySubagentError(err, hints = {}) {
|
|
11845
|
+
if (err instanceof AgentError && err.cause) {
|
|
11846
|
+
return classifySubagentError(err.cause, hints);
|
|
11847
|
+
}
|
|
11201
11848
|
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
11202
11849
|
if (err instanceof ProviderError) {
|
|
11203
11850
|
const baseMessage2 = err.describe();
|
|
@@ -11230,7 +11877,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
11230
11877
|
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
11231
11878
|
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
11232
11879
|
}
|
|
11233
|
-
if (/empty response
|
|
11880
|
+
if (/empty response/i.test(baseMessage)) {
|
|
11234
11881
|
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
11235
11882
|
}
|
|
11236
11883
|
if (/^tool failed: /i.test(baseMessage)) {
|
|
@@ -12394,7 +13041,14 @@ var ParallelEternalEngine = class {
|
|
|
12394
13041
|
}
|
|
12395
13042
|
stop() {
|
|
12396
13043
|
this.stopRequested = true;
|
|
12397
|
-
void this.persistState("stopped").catch(() => {
|
|
13044
|
+
void this.persistState("stopped").catch((err) => {
|
|
13045
|
+
console.error(JSON.stringify({
|
|
13046
|
+
level: "error",
|
|
13047
|
+
event: "engine.persist_state_failed",
|
|
13048
|
+
message: err instanceof Error ? err.message : String(err),
|
|
13049
|
+
context: { expectedState: "stopped" },
|
|
13050
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
13051
|
+
}));
|
|
12398
13052
|
});
|
|
12399
13053
|
this.state = "stopped";
|
|
12400
13054
|
}
|
|
@@ -12994,24 +13648,36 @@ var InMemoryAgentBridge = class {
|
|
|
12994
13648
|
return () => this.subscriptions.delete(handler);
|
|
12995
13649
|
}
|
|
12996
13650
|
async request(msg, timeoutMs) {
|
|
12997
|
-
if (this.stopped) throw new
|
|
13651
|
+
if (this.stopped) throw new AgentError({
|
|
13652
|
+
message: "Bridge is stopped",
|
|
13653
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13654
|
+
});
|
|
12998
13655
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
12999
13656
|
const correlationId = msg.id;
|
|
13000
13657
|
if (this.inflightGuards.has(correlationId)) {
|
|
13001
|
-
throw new
|
|
13002
|
-
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids
|
|
13003
|
-
|
|
13658
|
+
throw new AgentError({
|
|
13659
|
+
message: `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`,
|
|
13660
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
13661
|
+
context: { correlationId }
|
|
13662
|
+
});
|
|
13004
13663
|
}
|
|
13005
13664
|
this.inflightGuards.add(correlationId);
|
|
13006
13665
|
return new Promise((resolve5, reject) => {
|
|
13007
13666
|
const timer = setTimeout(() => {
|
|
13008
13667
|
this.inflightGuards.delete(correlationId);
|
|
13009
13668
|
this.pendingRequests.delete(correlationId);
|
|
13010
|
-
reject(new
|
|
13669
|
+
reject(new AgentError({
|
|
13670
|
+
message: `Request ${correlationId} timed out after ${timeout}ms`,
|
|
13671
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
13672
|
+
context: { correlationId, timeoutMs: timeout }
|
|
13673
|
+
}));
|
|
13011
13674
|
}, timeout);
|
|
13012
13675
|
if (!this.inflightGuards.has(correlationId)) {
|
|
13013
13676
|
clearTimeout(timer);
|
|
13014
|
-
reject(new
|
|
13677
|
+
reject(new AgentError({
|
|
13678
|
+
message: "Bridge stopped",
|
|
13679
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13680
|
+
}));
|
|
13015
13681
|
return;
|
|
13016
13682
|
}
|
|
13017
13683
|
this.pendingRequests.set(correlationId, {
|
|
@@ -13032,7 +13698,10 @@ var InMemoryAgentBridge = class {
|
|
|
13032
13698
|
this.stopped = true;
|
|
13033
13699
|
for (const [, p] of this.pendingRequests) {
|
|
13034
13700
|
clearTimeout(p.timer);
|
|
13035
|
-
p.reject(new
|
|
13701
|
+
p.reject(new AgentError({
|
|
13702
|
+
message: "Bridge stopped",
|
|
13703
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
13704
|
+
}));
|
|
13036
13705
|
}
|
|
13037
13706
|
this.pendingRequests.clear();
|
|
13038
13707
|
this.inflightGuards.clear();
|
|
@@ -13803,7 +14472,23 @@ Bridge contract:
|
|
|
13803
14472
|
subagents' context. Those are not yours to read.
|
|
13804
14473
|
- Your final task output is what the Director sees. Be concise,
|
|
13805
14474
|
structured, and self-contained \u2014 assume the Director will paste your
|
|
13806
|
-
output into its own context
|
|
14475
|
+
output into its own context.
|
|
14476
|
+
|
|
14477
|
+
Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` tools):
|
|
14478
|
+
- You are part of a project-wide fleet that may span other terminals and
|
|
14479
|
+
WebUIs. Your mailbox identity is \`<your-name>@<session-tag>\` (unique
|
|
14480
|
+
per session); mail addressed to you, to your bare name, or broadcast
|
|
14481
|
+
to \`*\` is injected into your conversation automatically before each
|
|
14482
|
+
step \u2014 read it once, it is marked read.
|
|
14483
|
+
- Broadcast milestones: when you complete a significant piece of work,
|
|
14484
|
+
\`mail_send to="*"\` a one-line summary so parallel agents don't collide
|
|
14485
|
+
with or duplicate it.
|
|
14486
|
+
- Hand off matching work: if another online agent's role fits a follow-up
|
|
14487
|
+
better (e.g. a reviewer while you just wrote code), \`mail_send\` it to
|
|
14488
|
+
their exact id instead of doing everything yourself. Discover ids with
|
|
14489
|
+
\`mailbox action=online\`.
|
|
14490
|
+
- Answer your mail: reply to the sender's exact \`from\` id. When done with
|
|
14491
|
+
an assigned task, post a \`result\` back to whoever assigned it.`;
|
|
13807
14492
|
function composeDirectorPrompt(parts = {}) {
|
|
13808
14493
|
const sections = [];
|
|
13809
14494
|
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
@@ -16853,6 +17538,77 @@ Remember: your job is to make the user a better developer, not just to complete
|
|
|
16853
17538
|
tags: ["teaching", "mentor", "learning"],
|
|
16854
17539
|
toolPreferences: ["read", "edit", "explain"],
|
|
16855
17540
|
suggestedSkills: ["prompt-engineering", "skill-creator", "node-modern", "typescript-strict"]
|
|
17541
|
+
},
|
|
17542
|
+
{
|
|
17543
|
+
id: "research-web",
|
|
17544
|
+
name: "Research Web",
|
|
17545
|
+
description: "Current-data research \u2014 search web, verify, inject findings into context",
|
|
17546
|
+
prompt: `## Research Web Mode
|
|
17547
|
+
|
|
17548
|
+
You are in research mode. Your role: find, verify, and incorporate
|
|
17549
|
+
current web data. Your training data is stale \u2014 every factual claim
|
|
17550
|
+
about version numbers, API surfaces, package status, or ecosystem
|
|
17551
|
+
changes must be verified against live sources.
|
|
17552
|
+
|
|
17553
|
+
### When to research
|
|
17554
|
+
- The user asks "is this still the case?", "what's current?", "latest version?"
|
|
17555
|
+
- You're about to claim a version number, deprecation, or API change
|
|
17556
|
+
- You're comparing tools, packages, or approaches released in the last 12 months
|
|
17557
|
+
- You realize your knowledge may be >6 months old on a fast-moving topic
|
|
17558
|
+
|
|
17559
|
+
### Research methodology
|
|
17560
|
+
1. **Search first, fetch selectively.** Use web_search with 5-8 results for
|
|
17561
|
+
broad queries. Then web_fetch the 1-2 most authoritative results for detail.
|
|
17562
|
+
Don't fetch every result \u2014 you'll burn tokens on noise.
|
|
17563
|
+
2. **Cross-reference.** One source is a data point. Two sources that agree
|
|
17564
|
+
is a signal. Three is confirmation. Flag single-source claims as tentative.
|
|
17565
|
+
3. **Cite sources.** Every factual claim from web data must include where it
|
|
17566
|
+
came from: domain name, and date if visible on the page.
|
|
17567
|
+
4. **Know when to stop.** 2-3 searches + 1-2 fetches is usually sufficient.
|
|
17568
|
+
If you're on your 5th search without a clear answer, pause and tell the user
|
|
17569
|
+
what you've found and what's still unclear \u2014 let them decide to dig deeper.
|
|
17570
|
+
5. **Inject findings for reuse.** After gathering current data, use
|
|
17571
|
+
context_manager with add_note to inject a structured "Research Findings"
|
|
17572
|
+
block into the conversation. Future turns see this and don't re-search.
|
|
17573
|
+
|
|
17574
|
+
### Self-injection pattern
|
|
17575
|
+
When you discover current data mid-research, inject it so subsequent turns
|
|
17576
|
+
benefit without re-searching:
|
|
17577
|
+
|
|
17578
|
+
web_search("Next.js middleware breaking changes 2025")
|
|
17579
|
+
\u2192 Surfaced: Next.js 15.2 changed middleware runtime from edge to node
|
|
17580
|
+
web_fetch("https://nextjs.org/docs/messages/middleware-upgrade-guide")
|
|
17581
|
+
\u2192 Confirmed: middleware now runs on Node.js runtime by default
|
|
17582
|
+
context_manager: add_note(
|
|
17583
|
+
"## Research: Next.js middleware
|
|
17584
|
+
- Next.js 15.2: middleware defaults to Node.js runtime (was edge)
|
|
17585
|
+
- Breaking: edge-only APIs (crypto.subtle, WebSocket) no longer available
|
|
17586
|
+
- Migration: use node:* equivalents or set runtime: 'edge' explicitly
|
|
17587
|
+
- Source: nextjs.org/docs/messages/middleware-upgrade-guide"
|
|
17588
|
+
)
|
|
17589
|
+
|
|
17590
|
+
The add_note persists in conversation \u2014 you won't re-search on the next turn.
|
|
17591
|
+
|
|
17592
|
+
### Anti-patterns
|
|
17593
|
+
- Don't research things already in the conversation context (including
|
|
17594
|
+
earlier add_note blocks you injected)
|
|
17595
|
+
- Don't treat a single web search result as ground truth \u2014 cross-reference
|
|
17596
|
+
- Don't inject raw JSON or search result dumps via add_note \u2014 summarize
|
|
17597
|
+
- Don't research while the user is waiting for a quick code edit \u2014 toggle
|
|
17598
|
+
research-web mode only during analysis/discussion phases
|
|
17599
|
+
- Don't research-loop: 5+ searches on one topic \u2192 stop and ask the user
|
|
17600
|
+
|
|
17601
|
+
### Exiting research mode
|
|
17602
|
+
When the user no longer needs current-data research, suggest switching back
|
|
17603
|
+
to the previous mode. You stay in research mode until explicitly told to
|
|
17604
|
+
switch \u2014 but don't force web searches on every turn. The methodology rules
|
|
17605
|
+
above already gate when to actually search.
|
|
17606
|
+
|
|
17607
|
+
When you're done with research: suggest the user run \`/mode default\` or
|
|
17608
|
+
their previous mode.`,
|
|
17609
|
+
tags: ["research", "web", "current-data", "up-to-date"],
|
|
17610
|
+
toolPreferences: ["web_search", "web_fetch", "search", "fetch", "context_manager"],
|
|
17611
|
+
suggestedSkills: ["research-web", "tech-stack", "node-modern", "security-scanner", "react-modern"]
|
|
16856
17612
|
}
|
|
16857
17613
|
];
|
|
16858
17614
|
|
|
@@ -17478,7 +18234,10 @@ var TaskTracker = class {
|
|
|
17478
18234
|
return this.graph;
|
|
17479
18235
|
}
|
|
17480
18236
|
addNode(node) {
|
|
17481
|
-
if (!this.graph) throw new
|
|
18237
|
+
if (!this.graph) throw new SddError({
|
|
18238
|
+
message: "No graph loaded",
|
|
18239
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18240
|
+
});
|
|
17482
18241
|
const now = Date.now();
|
|
17483
18242
|
const newNode = {
|
|
17484
18243
|
...node,
|
|
@@ -17496,7 +18255,10 @@ var TaskTracker = class {
|
|
|
17496
18255
|
return newNode;
|
|
17497
18256
|
}
|
|
17498
18257
|
addEdge(from, to, type = "depends_on") {
|
|
17499
|
-
if (!this.graph) throw new
|
|
18258
|
+
if (!this.graph) throw new SddError({
|
|
18259
|
+
message: "No graph loaded",
|
|
18260
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18261
|
+
});
|
|
17500
18262
|
this.graph.edges.push({
|
|
17501
18263
|
id: crypto.randomUUID(),
|
|
17502
18264
|
from,
|
|
@@ -17507,9 +18269,16 @@ var TaskTracker = class {
|
|
|
17507
18269
|
this.persist();
|
|
17508
18270
|
}
|
|
17509
18271
|
updateNodeStatus(id, status, reason) {
|
|
17510
|
-
if (!this.graph) throw new
|
|
18272
|
+
if (!this.graph) throw new SddError({
|
|
18273
|
+
message: "No graph loaded",
|
|
18274
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18275
|
+
});
|
|
17511
18276
|
const node = this.graph.nodes.get(id);
|
|
17512
|
-
if (!node) throw new
|
|
18277
|
+
if (!node) throw new SddError({
|
|
18278
|
+
message: `Node ${id} not found`,
|
|
18279
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18280
|
+
context: { nodeId: id }
|
|
18281
|
+
});
|
|
17513
18282
|
const from = node.status;
|
|
17514
18283
|
const now = Date.now();
|
|
17515
18284
|
node.status = status;
|
|
@@ -17532,9 +18301,16 @@ var TaskTracker = class {
|
|
|
17532
18301
|
this.persist();
|
|
17533
18302
|
}
|
|
17534
18303
|
updateNode(id, patch) {
|
|
17535
|
-
if (!this.graph) throw new
|
|
18304
|
+
if (!this.graph) throw new SddError({
|
|
18305
|
+
message: "No graph loaded",
|
|
18306
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
18307
|
+
});
|
|
17536
18308
|
const node = this.graph.nodes.get(id);
|
|
17537
|
-
if (!node) throw new
|
|
18309
|
+
if (!node) throw new SddError({
|
|
18310
|
+
message: `Node ${id} not found`,
|
|
18311
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18312
|
+
context: { nodeId: id }
|
|
18313
|
+
});
|
|
17538
18314
|
if (patch.title !== void 0) node.title = patch.title;
|
|
17539
18315
|
if (patch.description !== void 0) node.description = patch.description;
|
|
17540
18316
|
if (patch.priority !== void 0) node.priority = patch.priority;
|
|
@@ -17653,7 +18429,12 @@ var TaskTracker = class {
|
|
|
17653
18429
|
persist() {
|
|
17654
18430
|
if (!this.graph) return;
|
|
17655
18431
|
this.opts.store.saveGraph(this.graph).catch((err) => {
|
|
17656
|
-
this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(
|
|
18432
|
+
this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(JSON.stringify({
|
|
18433
|
+
level: "warn",
|
|
18434
|
+
event: "task_tracker.save_graph_failed",
|
|
18435
|
+
message: err instanceof Error ? err.message : String(err),
|
|
18436
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
18437
|
+
}));
|
|
17657
18438
|
});
|
|
17658
18439
|
}
|
|
17659
18440
|
};
|
|
@@ -17706,12 +18487,14 @@ var TaskFlow = class {
|
|
|
17706
18487
|
const analysis = parser.analyze(this.spec);
|
|
17707
18488
|
this.emit("spec.analyzed", { analysis });
|
|
17708
18489
|
if (analysis.completeness < 50) {
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
18490
|
+
const err = new SddError({
|
|
18491
|
+
message: `Spec completeness too low: ${analysis.completeness}%`,
|
|
18492
|
+
code: ERROR_CODES.SDD_VALIDATION_FAILED,
|
|
18493
|
+
context: { completeness: analysis.completeness }
|
|
17712
18494
|
});
|
|
18495
|
+
this.emit("error", { phase: "analyzing", error: err });
|
|
17713
18496
|
this.setPhase("failed");
|
|
17714
|
-
throw
|
|
18497
|
+
throw err;
|
|
17715
18498
|
}
|
|
17716
18499
|
this.setPhase("generating");
|
|
17717
18500
|
const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
|
|
@@ -17719,7 +18502,11 @@ var TaskFlow = class {
|
|
|
17719
18502
|
return this.graph;
|
|
17720
18503
|
}
|
|
17721
18504
|
async execute(ctx) {
|
|
17722
|
-
if (!this.graph) throw new
|
|
18505
|
+
if (!this.graph) throw new SddError({
|
|
18506
|
+
message: "No graph loaded. Call fromSpec first.",
|
|
18507
|
+
code: ERROR_CODES.SDD_INVALID_STATE,
|
|
18508
|
+
context: { phase: this.phase }
|
|
18509
|
+
});
|
|
17723
18510
|
this.setPhase("executing");
|
|
17724
18511
|
this.stopped = false;
|
|
17725
18512
|
const pendingTasks = this.getExecutableTasks();
|
|
@@ -17759,7 +18546,11 @@ var TaskFlow = class {
|
|
|
17759
18546
|
}
|
|
17760
18547
|
async reviewTask(taskId, approved, comment) {
|
|
17761
18548
|
const task = this.opts.tracker.getNode(taskId);
|
|
17762
|
-
if (!task) throw new
|
|
18549
|
+
if (!task) throw new SddError({
|
|
18550
|
+
message: `Task ${taskId} not found`,
|
|
18551
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
18552
|
+
context: { taskId }
|
|
18553
|
+
});
|
|
17763
18554
|
if (approved) {
|
|
17764
18555
|
this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
|
|
17765
18556
|
this.emit("task.completed", { taskId });
|
|
@@ -18403,7 +19194,11 @@ var AISpecBuilder = class {
|
|
|
18403
19194
|
switch (this.session.phase) {
|
|
18404
19195
|
case "questioning":
|
|
18405
19196
|
if (!this.session.spec) {
|
|
18406
|
-
throw new
|
|
19197
|
+
throw new SddError({
|
|
19198
|
+
message: "Cannot approve: no spec generated yet.",
|
|
19199
|
+
code: ERROR_CODES.SDD_INVALID_STATE,
|
|
19200
|
+
context: { phase: "questioning", sessionId: this.session.id }
|
|
19201
|
+
});
|
|
18407
19202
|
}
|
|
18408
19203
|
this.session.phase = "spec_review";
|
|
18409
19204
|
break;
|
|
@@ -18461,7 +19256,11 @@ var AISpecBuilder = class {
|
|
|
18461
19256
|
*/
|
|
18462
19257
|
async saveSpec() {
|
|
18463
19258
|
if (!this.session.spec) {
|
|
18464
|
-
throw new
|
|
19259
|
+
throw new SddError({
|
|
19260
|
+
message: "No spec to save.",
|
|
19261
|
+
code: ERROR_CODES.SDD_NOT_READY,
|
|
19262
|
+
context: { sessionId: this.session.id }
|
|
19263
|
+
});
|
|
18465
19264
|
}
|
|
18466
19265
|
await this.store.save(this.session.spec);
|
|
18467
19266
|
return this.session.spec;
|
|
@@ -18476,17 +19275,30 @@ var AISpecBuilder = class {
|
|
|
18476
19275
|
try {
|
|
18477
19276
|
parsed = JSON.parse(jsonStr);
|
|
18478
19277
|
} catch (e) {
|
|
18479
|
-
throw new
|
|
19278
|
+
throw new SddError({
|
|
19279
|
+
message: "Invalid JSON for spec",
|
|
19280
|
+
code: ERROR_CODES.SDD_PARSE_FAILED,
|
|
19281
|
+
cause: e,
|
|
19282
|
+
context: { detail: e instanceof Error ? e.message : "parse error" }
|
|
19283
|
+
});
|
|
18480
19284
|
}
|
|
18481
19285
|
if (!parsed || typeof parsed !== "object") {
|
|
18482
|
-
throw new
|
|
19286
|
+
throw new SddError({
|
|
19287
|
+
message: "Spec JSON must be an object",
|
|
19288
|
+
code: ERROR_CODES.SDD_VALIDATION_FAILED,
|
|
19289
|
+
context: { actualType: typeof parsed }
|
|
19290
|
+
});
|
|
18483
19291
|
}
|
|
18484
19292
|
const raw = parsed;
|
|
18485
19293
|
const now = Date.now();
|
|
18486
19294
|
const title = String(raw.title ?? this.session.title ?? "Untitled");
|
|
18487
19295
|
const overview = String(raw.overview ?? "");
|
|
18488
19296
|
if (!overview || overview === "undefined") {
|
|
18489
|
-
throw new
|
|
19297
|
+
throw new SddError({
|
|
19298
|
+
message: "Spec must have an overview",
|
|
19299
|
+
code: ERROR_CODES.SDD_VALIDATION_FAILED,
|
|
19300
|
+
context: { field: "overview", title }
|
|
19301
|
+
});
|
|
18490
19302
|
}
|
|
18491
19303
|
const rawSections = Array.isArray(raw.sections) ? raw.sections : [];
|
|
18492
19304
|
const sections = rawSections.filter((s) => s && typeof s === "object").map((s) => ({
|
|
@@ -18769,14 +19581,14 @@ function renderNode(graph, nodeId, lines, rendered, childrenMap, compact, prefix
|
|
|
18769
19581
|
const icon = STATUS_ICON[node.status];
|
|
18770
19582
|
const prioIcon = PRIORITY_ICON[node.priority];
|
|
18771
19583
|
const typeIcon = TYPE_ICON[node.type];
|
|
18772
|
-
const title = compact ?
|
|
19584
|
+
const title = compact ? truncate(node.title, 40) : node.title;
|
|
18773
19585
|
const blockedBy = childrenMap.get(nodeId) ?? [];
|
|
18774
19586
|
const depsStr = blockedBy.length > 0 ? ` \u2190 [${blockedBy.map((d) => graph.nodes.get(d)?.title?.slice(0, 12) ?? "?").join(", ")}]` : "";
|
|
18775
19587
|
lines.push(`${prefix}${icon} ${typeIcon} ${prioIcon} ${title}${depsStr}`);
|
|
18776
19588
|
if (!compact && node.description) {
|
|
18777
19589
|
const descLines = node.description.split("\n").slice(0, 3);
|
|
18778
19590
|
for (const dl of descLines) {
|
|
18779
|
-
lines.push(`${prefix} \u2514 ${
|
|
19591
|
+
lines.push(`${prefix} \u2514 ${truncate(dl, 60)}`);
|
|
18780
19592
|
}
|
|
18781
19593
|
}
|
|
18782
19594
|
const dependents = graph.edges.filter((e) => e.type === "depends_on" && e.to === nodeId).map((e) => e.from).filter((id) => graph.nodes.has(id));
|
|
@@ -18852,10 +19664,6 @@ function renderSpecAnalysis(spec, analysis) {
|
|
|
18852
19664
|
}
|
|
18853
19665
|
return lines.join("\n");
|
|
18854
19666
|
}
|
|
18855
|
-
function truncate2(str, maxLen) {
|
|
18856
|
-
if (str.length <= maxLen) return str;
|
|
18857
|
-
return str.slice(0, maxLen - 1) + "\u2026";
|
|
18858
|
-
}
|
|
18859
19667
|
|
|
18860
19668
|
// src/sdd/critical-path.ts
|
|
18861
19669
|
function analyzeCriticalPath(graph) {
|
|
@@ -19554,7 +20362,10 @@ var SddParallelRun = class {
|
|
|
19554
20362
|
"\u2022 Do not ask before routine in-project tool use; if a permission gate appears, wait for that flow.",
|
|
19555
20363
|
"\u2022 Keep output concise \u2014 summarize changes, do not transcribe files."
|
|
19556
20364
|
].join("\n");
|
|
19557
|
-
if (!this.coordinator) throw new
|
|
20365
|
+
if (!this.coordinator) throw new SddError({
|
|
20366
|
+
message: "SDD parallel runner requires a coordinator",
|
|
20367
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
20368
|
+
});
|
|
19558
20369
|
const coordinator = this.coordinator;
|
|
19559
20370
|
const spawns = subagentIds.map(
|
|
19560
20371
|
(subagentId) => coordinator.spawn({
|
|
@@ -19566,7 +20377,10 @@ var SddParallelRun = class {
|
|
|
19566
20377
|
);
|
|
19567
20378
|
const spawnResults = await Promise.all(spawns);
|
|
19568
20379
|
if (!spawnResults.every((r) => Boolean(r.subagentId))) {
|
|
19569
|
-
throw new
|
|
20380
|
+
throw new SddError({
|
|
20381
|
+
message: "One or more subagent spawns failed",
|
|
20382
|
+
code: ERROR_CODES.SDD_INVALID_STATE
|
|
20383
|
+
});
|
|
19570
20384
|
}
|
|
19571
20385
|
const assignPromises = tasks.map((task, i) => {
|
|
19572
20386
|
const spec = {
|