@wrongstack/core 0.1.3 → 0.1.7
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/LICENSE +17 -13
- package/README.md +155 -0
- package/dist/defaults/index.d.ts +68 -33
- package/dist/defaults/index.js +559 -239
- package/dist/defaults/index.js.map +1 -1
- package/dist/index.d.ts +26 -12
- package/dist/index.js +745 -322
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +5 -4
- package/dist/kernel/index.js +2 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{secret-scrubber-qU3AwEiI.d.ts → mode-Pjt5vMS6.d.ts} +94 -3
- package/dist/{provider-DovtyuM8.d.ts → provider-txgB0Oq9.d.ts} +27 -30
- package/dist/{session-reader-DR4u3bu9.d.ts → session-reader-7AutWHut.d.ts} +13 -32
- package/dist/system-prompt-vAB0F54-.d.ts +23 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.js +34 -15
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +16 -11
- package/dist/utils/index.js +40 -13
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-D24ynAz1.d.ts → wstack-paths-BGu2INTm.d.ts} +7 -0
- package/package.json +17 -4
- package/dist/system-prompt--mzZnenv.d.ts +0 -16
package/dist/defaults/index.js
CHANGED
|
@@ -180,9 +180,11 @@ var DefaultPathResolver = class {
|
|
|
180
180
|
ensureInsideRoot(absPath) {
|
|
181
181
|
const resolved = this.resolve(absPath);
|
|
182
182
|
if (!this.isInsideRoot(resolved)) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
|
|
184
|
+
const err = new Error(`Path "${display}" resolves outside the project root`);
|
|
185
|
+
err.fullPath = absPath;
|
|
186
|
+
err.projectRoot = this.projectRoot;
|
|
187
|
+
throw err;
|
|
186
188
|
}
|
|
187
189
|
return resolved;
|
|
188
190
|
}
|
|
@@ -352,7 +354,9 @@ var DefaultSessionStore = class {
|
|
|
352
354
|
try {
|
|
353
355
|
handle = await fsp.open(file, "a", 384);
|
|
354
356
|
} catch (err) {
|
|
355
|
-
throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}
|
|
357
|
+
throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`, {
|
|
358
|
+
cause: err
|
|
359
|
+
});
|
|
356
360
|
}
|
|
357
361
|
try {
|
|
358
362
|
return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
|
|
@@ -370,7 +374,8 @@ var DefaultSessionStore = class {
|
|
|
370
374
|
handle = await fsp.open(file, "a", 384);
|
|
371
375
|
} catch (err) {
|
|
372
376
|
throw new Error(
|
|
373
|
-
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}
|
|
377
|
+
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
|
|
378
|
+
{ cause: err }
|
|
374
379
|
);
|
|
375
380
|
}
|
|
376
381
|
const writer = new FileSessionWriter(
|
|
@@ -393,7 +398,10 @@ var DefaultSessionStore = class {
|
|
|
393
398
|
const events = [];
|
|
394
399
|
for (const line of lines) {
|
|
395
400
|
try {
|
|
396
|
-
|
|
401
|
+
const parsed = JSON.parse(line);
|
|
402
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
403
|
+
events.push(parsed);
|
|
404
|
+
}
|
|
397
405
|
} catch {
|
|
398
406
|
}
|
|
399
407
|
}
|
|
@@ -410,7 +418,11 @@ var DefaultSessionStore = class {
|
|
|
410
418
|
ids.map((id) => this.summaryFor(id).catch(() => null))
|
|
411
419
|
);
|
|
412
420
|
const out = sessions.filter((s) => s !== null);
|
|
413
|
-
out.sort((a, b) =>
|
|
421
|
+
out.sort((a, b) => {
|
|
422
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
423
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
424
|
+
return a.id.localeCompare(b.id);
|
|
425
|
+
});
|
|
414
426
|
return out.slice(0, limit);
|
|
415
427
|
} catch {
|
|
416
428
|
return [];
|
|
@@ -556,6 +568,8 @@ var FileSessionWriter = class {
|
|
|
556
568
|
filePath;
|
|
557
569
|
initDone = false;
|
|
558
570
|
resumed;
|
|
571
|
+
appendFailCount = 0;
|
|
572
|
+
lastAppendWarnAt = 0;
|
|
559
573
|
async writeSessionStart() {
|
|
560
574
|
if (this.initDone || this.closed) return;
|
|
561
575
|
this.initDone = true;
|
|
@@ -584,7 +598,19 @@ var FileSessionWriter = class {
|
|
|
584
598
|
await this.handle.appendFile(`${JSON.stringify(event)}
|
|
585
599
|
`, "utf8");
|
|
586
600
|
} catch (err) {
|
|
587
|
-
|
|
601
|
+
this.appendFailCount++;
|
|
602
|
+
const now = Date.now();
|
|
603
|
+
if (now - this.lastAppendWarnAt > 5e3) {
|
|
604
|
+
const suppressed = this.appendFailCount - 1;
|
|
605
|
+
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
606
|
+
console.warn(
|
|
607
|
+
"[session] append failed:",
|
|
608
|
+
err instanceof Error ? err.message : String(err),
|
|
609
|
+
tail
|
|
610
|
+
);
|
|
611
|
+
this.lastAppendWarnAt = now;
|
|
612
|
+
this.appendFailCount = 0;
|
|
613
|
+
}
|
|
588
614
|
}
|
|
589
615
|
}
|
|
590
616
|
/**
|
|
@@ -790,6 +816,15 @@ function mergeAdjacentText(blocks) {
|
|
|
790
816
|
var MAX_BYTES_TOTAL = 32e3;
|
|
791
817
|
var DefaultMemoryStore = class {
|
|
792
818
|
files;
|
|
819
|
+
/**
|
|
820
|
+
* Per-scope serialization queue. `remember` / `forget` / `consolidate` /
|
|
821
|
+
* `clear` are read-modify-write against a single file; without a lock,
|
|
822
|
+
* two concurrent calls on the same scope can read the same baseline and
|
|
823
|
+
* the later write silently drops the earlier entry. We chain each
|
|
824
|
+
* mutation onto the prior promise for the same scope so they run in
|
|
825
|
+
* issue order. Different scopes still proceed in parallel.
|
|
826
|
+
*/
|
|
827
|
+
writeChain = /* @__PURE__ */ new Map();
|
|
793
828
|
constructor(opts) {
|
|
794
829
|
this.files = {
|
|
795
830
|
"project-agents": opts.paths.inProjectAgentsFile,
|
|
@@ -797,6 +832,18 @@ var DefaultMemoryStore = class {
|
|
|
797
832
|
"user-memory": opts.paths.globalMemory
|
|
798
833
|
};
|
|
799
834
|
}
|
|
835
|
+
async runSerialized(scope, work) {
|
|
836
|
+
const prior = this.writeChain.get(scope) ?? Promise.resolve();
|
|
837
|
+
const next = prior.catch(() => void 0).then(work);
|
|
838
|
+
this.writeChain.set(scope, next);
|
|
839
|
+
try {
|
|
840
|
+
return await next;
|
|
841
|
+
} finally {
|
|
842
|
+
if (this.writeChain.get(scope) === next) {
|
|
843
|
+
this.writeChain.delete(scope);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
800
847
|
async readAll() {
|
|
801
848
|
const parts = [];
|
|
802
849
|
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
@@ -815,27 +862,32 @@ ${body.trim()}`);
|
|
|
815
862
|
}
|
|
816
863
|
}
|
|
817
864
|
async remember(text, scope = "project-memory") {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
865
|
+
return this.runSerialized(scope, async () => {
|
|
866
|
+
const file = this.files[scope];
|
|
867
|
+
await ensureDir(path2.dirname(file));
|
|
868
|
+
let existing = "";
|
|
869
|
+
try {
|
|
870
|
+
existing = await fsp.readFile(file, "utf8");
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
874
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
875
|
+
const entry = `
|
|
828
876
|
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
829
877
|
`;
|
|
830
|
-
|
|
878
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
831
879
|
${entry}`;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
880
|
+
await atomicWrite(file, next);
|
|
881
|
+
const buf = Buffer.byteLength(next, "utf8");
|
|
882
|
+
if (buf > MAX_BYTES_TOTAL) {
|
|
883
|
+
await this.consolidateUnsafe(scope);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
837
886
|
}
|
|
838
887
|
async forget(query, scope = "project-memory") {
|
|
888
|
+
return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
|
|
889
|
+
}
|
|
890
|
+
async forgetUnsafe(query, scope) {
|
|
839
891
|
const file = this.files[scope];
|
|
840
892
|
let existing;
|
|
841
893
|
try {
|
|
@@ -872,6 +924,9 @@ ${entry}`;
|
|
|
872
924
|
return removed;
|
|
873
925
|
}
|
|
874
926
|
async consolidate(scope) {
|
|
927
|
+
return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
|
|
928
|
+
}
|
|
929
|
+
async consolidateUnsafe(scope) {
|
|
875
930
|
const file = this.files[scope];
|
|
876
931
|
let existing;
|
|
877
932
|
try {
|
|
@@ -902,12 +957,14 @@ ${entry}`;
|
|
|
902
957
|
}
|
|
903
958
|
async clear(scope) {
|
|
904
959
|
if (scope) {
|
|
905
|
-
await atomicWrite(this.files[scope], "");
|
|
906
|
-
|
|
907
|
-
for (const s of ["project-agents", "project-memory", "user-memory"]) {
|
|
908
|
-
await atomicWrite(this.files[s], "");
|
|
909
|
-
}
|
|
960
|
+
await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
|
|
961
|
+
return;
|
|
910
962
|
}
|
|
963
|
+
await Promise.all(
|
|
964
|
+
["project-agents", "project-memory", "user-memory"].map(
|
|
965
|
+
(s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
|
|
966
|
+
)
|
|
967
|
+
);
|
|
911
968
|
}
|
|
912
969
|
};
|
|
913
970
|
function labelOf(scope) {
|
|
@@ -955,9 +1012,27 @@ var PATTERNS = [
|
|
|
955
1012
|
regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
|
|
956
1013
|
}
|
|
957
1014
|
];
|
|
1015
|
+
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
958
1016
|
var DefaultSecretScrubber = class {
|
|
959
1017
|
scrub(text) {
|
|
960
1018
|
if (!text) return text;
|
|
1019
|
+
if (text.length <= SCRUB_CHUNK_BYTES) {
|
|
1020
|
+
return this.scrubOne(text);
|
|
1021
|
+
}
|
|
1022
|
+
const out = [];
|
|
1023
|
+
let i = 0;
|
|
1024
|
+
while (i < text.length) {
|
|
1025
|
+
let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
|
|
1026
|
+
if (end < text.length) {
|
|
1027
|
+
const nl = text.lastIndexOf("\n", end);
|
|
1028
|
+
if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
|
|
1029
|
+
}
|
|
1030
|
+
out.push(this.scrubOne(text.slice(i, end)));
|
|
1031
|
+
i = end;
|
|
1032
|
+
}
|
|
1033
|
+
return out.join("");
|
|
1034
|
+
}
|
|
1035
|
+
scrubOne(text) {
|
|
961
1036
|
let out = text;
|
|
962
1037
|
for (const p of PATTERNS) {
|
|
963
1038
|
out = out.replace(p.regex, (_match, group1, group2) => {
|
|
@@ -1062,7 +1137,17 @@ var DefaultSecretVault = class {
|
|
|
1062
1137
|
}
|
|
1063
1138
|
};
|
|
1064
1139
|
function decryptConfigSecrets(cfg, vault) {
|
|
1065
|
-
return walk(cfg, vault, (v) =>
|
|
1140
|
+
return walk(cfg, vault, (v, key) => {
|
|
1141
|
+
try {
|
|
1142
|
+
return vault.decrypt(v);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
console.warn(
|
|
1145
|
+
`[secret-vault] Failed to decrypt "${key}":`,
|
|
1146
|
+
err instanceof Error ? err.message : err
|
|
1147
|
+
);
|
|
1148
|
+
return "";
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1066
1151
|
}
|
|
1067
1152
|
function encryptConfigSecrets(cfg, vault) {
|
|
1068
1153
|
return walk(cfg, vault, (v) => vault.encrypt(v));
|
|
@@ -1076,7 +1161,7 @@ function walk(node, vault, transform) {
|
|
|
1076
1161
|
const out = {};
|
|
1077
1162
|
for (const [k, v] of Object.entries(node)) {
|
|
1078
1163
|
if (typeof v === "string" && isSecretField(k)) {
|
|
1079
|
-
out[k] = transform(v);
|
|
1164
|
+
out[k] = transform(v, k);
|
|
1080
1165
|
} else if (typeof v === "object" && v !== null) {
|
|
1081
1166
|
out[k] = walk(v, vault, transform);
|
|
1082
1167
|
} else {
|
|
@@ -1150,9 +1235,11 @@ function walkCount(node, vault, counter) {
|
|
|
1150
1235
|
}
|
|
1151
1236
|
return out;
|
|
1152
1237
|
}
|
|
1238
|
+
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1153
1239
|
function deepMerge(a, b) {
|
|
1154
1240
|
const out = { ...a };
|
|
1155
1241
|
for (const [k, v] of Object.entries(b)) {
|
|
1242
|
+
if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
1156
1243
|
const existing = out[k];
|
|
1157
1244
|
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1158
1245
|
out[k] = deepMerge(existing, v);
|
|
@@ -1165,7 +1252,22 @@ function deepMerge(a, b) {
|
|
|
1165
1252
|
|
|
1166
1253
|
// src/utils/glob-match.ts
|
|
1167
1254
|
function escapeRegex(s) {
|
|
1168
|
-
return s.replace(/[.+^${}()
|
|
1255
|
+
return s.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
1256
|
+
}
|
|
1257
|
+
var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
|
|
1258
|
+
var CACHE_MAX_SIZE = 2e3;
|
|
1259
|
+
function getCachedGlob(pattern) {
|
|
1260
|
+
const cached = COMPILED_GLOB_CACHE.get(pattern);
|
|
1261
|
+
if (cached) return cached;
|
|
1262
|
+
if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
|
|
1263
|
+
const keys = [...COMPILED_GLOB_CACHE.keys()];
|
|
1264
|
+
for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
|
|
1265
|
+
COMPILED_GLOB_CACHE.delete(keys[i]);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const re = compileGlob(pattern);
|
|
1269
|
+
COMPILED_GLOB_CACHE.set(pattern, re);
|
|
1270
|
+
return re;
|
|
1169
1271
|
}
|
|
1170
1272
|
function compileGlob(pattern) {
|
|
1171
1273
|
let i = 0;
|
|
@@ -1214,7 +1316,7 @@ function compileGlob(pattern) {
|
|
|
1214
1316
|
return new RegExp(re);
|
|
1215
1317
|
}
|
|
1216
1318
|
function matchGlob(pattern, input) {
|
|
1217
|
-
return
|
|
1319
|
+
return getCachedGlob(pattern).test(input);
|
|
1218
1320
|
}
|
|
1219
1321
|
function matchAny(patterns, input) {
|
|
1220
1322
|
return patterns.some((p) => matchGlob(p, input));
|
|
@@ -1261,7 +1363,7 @@ var DefaultPermissionPolicy = class {
|
|
|
1261
1363
|
if (!this.loaded) await this.reload();
|
|
1262
1364
|
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
1263
1365
|
const entry = this.policy[tool.name] ?? namespaceEntry;
|
|
1264
|
-
const subject = this.subjectFor(tool.name, input);
|
|
1366
|
+
const subject = this.subjectFor(tool.name, input, tool.subjectKey);
|
|
1265
1367
|
if (entry?.deny && subject && matchAny(entry.deny, subject)) {
|
|
1266
1368
|
return { permission: "deny", source: "deny", reason: "matched deny pattern" };
|
|
1267
1369
|
}
|
|
@@ -1309,16 +1411,23 @@ var DefaultPermissionPolicy = class {
|
|
|
1309
1411
|
throw err;
|
|
1310
1412
|
}
|
|
1311
1413
|
}
|
|
1312
|
-
subjectFor(toolName, input) {
|
|
1414
|
+
subjectFor(toolName, input, subjectKey) {
|
|
1313
1415
|
if (!input || typeof input !== "object") return void 0;
|
|
1314
1416
|
const obj = input;
|
|
1315
1417
|
const globChars = /[*?\[\]]/g;
|
|
1316
1418
|
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
1419
|
+
const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
|
|
1420
|
+
if (subjectKey) {
|
|
1421
|
+
const v = obj[subjectKey];
|
|
1422
|
+
if (typeof v === "string") {
|
|
1423
|
+
return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1317
1426
|
if (toolName === "bash" && typeof obj.command === "string") {
|
|
1318
1427
|
return escapeGlob(obj.command);
|
|
1319
1428
|
}
|
|
1320
1429
|
if (typeof obj.path === "string") {
|
|
1321
|
-
return
|
|
1430
|
+
return normalizePath(obj.path);
|
|
1322
1431
|
}
|
|
1323
1432
|
if (typeof obj.url === "string") {
|
|
1324
1433
|
return escapeGlob(obj.url);
|
|
@@ -1464,14 +1573,15 @@ function providerStatusToCode(status, type) {
|
|
|
1464
1573
|
}
|
|
1465
1574
|
|
|
1466
1575
|
// src/defaults/retry-policy.ts
|
|
1467
|
-
var DefaultRetryPolicy = class {
|
|
1576
|
+
var DefaultRetryPolicy = class _DefaultRetryPolicy {
|
|
1577
|
+
static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
|
|
1468
1578
|
shouldRetry(err, attempt) {
|
|
1469
1579
|
if (err instanceof ProviderError) {
|
|
1470
1580
|
if (!err.retryable) return false;
|
|
1471
1581
|
return attempt < this.maxAttempts(err);
|
|
1472
1582
|
}
|
|
1473
1583
|
const msg = err.message ?? "";
|
|
1474
|
-
const isNetwork =
|
|
1584
|
+
const isNetwork = _DefaultRetryPolicy.NETWORK_ERR_RE.test(msg);
|
|
1475
1585
|
if (isNetwork) return attempt < 2;
|
|
1476
1586
|
return false;
|
|
1477
1587
|
}
|
|
@@ -1493,55 +1603,43 @@ var DefaultRetryPolicy = class {
|
|
|
1493
1603
|
};
|
|
1494
1604
|
|
|
1495
1605
|
// src/defaults/error-handler.ts
|
|
1606
|
+
var CONTEXT_OVERFLOW_RE = /context|too long|tokens/i;
|
|
1607
|
+
var NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
|
|
1496
1608
|
function buildRecoveryStrategies(opts) {
|
|
1497
1609
|
return [
|
|
1498
1610
|
{
|
|
1499
1611
|
label: "context_overflow_reduce",
|
|
1500
1612
|
compactor: opts?.compactor,
|
|
1501
1613
|
async attempt(err, ctx) {
|
|
1502
|
-
if (err instanceof ProviderError
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
stopReason: "end_turn",
|
|
1510
|
-
usage: { input: 0, output: 0 },
|
|
1511
|
-
model: ctx.model
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
} catch {
|
|
1614
|
+
if (!(err instanceof ProviderError)) return null;
|
|
1615
|
+
if (err.status !== 413 && !CONTEXT_OVERFLOW_RE.test(err.message)) return null;
|
|
1616
|
+
if (this.compactor) {
|
|
1617
|
+
try {
|
|
1618
|
+
const report = await this.compactor.compact(ctx, { aggressive: true });
|
|
1619
|
+
if (report.after < report.before) {
|
|
1620
|
+
return { action: "retry", reason: "context_compacted" };
|
|
1515
1621
|
}
|
|
1622
|
+
} catch {
|
|
1516
1623
|
}
|
|
1517
|
-
return null;
|
|
1518
1624
|
}
|
|
1519
1625
|
return null;
|
|
1520
1626
|
}
|
|
1521
1627
|
},
|
|
1522
1628
|
{
|
|
1523
1629
|
label: "rate_limit_backoff",
|
|
1524
|
-
async attempt(err
|
|
1525
|
-
if (err instanceof ProviderError
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
|
|
1531
|
-
stopReason: "end_turn",
|
|
1532
|
-
usage: { input: 0, output: 0 },
|
|
1533
|
-
model: ctx.model
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
return null;
|
|
1630
|
+
async attempt(err) {
|
|
1631
|
+
if (!(err instanceof ProviderError) || err.status !== 429) return null;
|
|
1632
|
+
const delayMs = err.body?.retryAfterMs ?? 5e3;
|
|
1633
|
+
const delay = Math.max(1e3, Math.min(delayMs, 6e4));
|
|
1634
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1635
|
+
return { action: "retry", reason: "rate_limit_backoff" };
|
|
1537
1636
|
}
|
|
1538
1637
|
},
|
|
1539
1638
|
{
|
|
1540
1639
|
label: "downgrade_model",
|
|
1541
1640
|
async attempt(err, ctx) {
|
|
1542
|
-
if (err instanceof ProviderError
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1641
|
+
if (!(err instanceof ProviderError)) return null;
|
|
1642
|
+
if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
|
|
1545
1643
|
return null;
|
|
1546
1644
|
}
|
|
1547
1645
|
}
|
|
@@ -1564,12 +1662,12 @@ var DefaultErrorHandler = class {
|
|
|
1564
1662
|
if (err.status === 429) return { kind: "rate_limit", retryable: true };
|
|
1565
1663
|
if (err.status === 529) return { kind: "overloaded", retryable: true };
|
|
1566
1664
|
if (err.status >= 500) return { kind: "server", retryable: true };
|
|
1567
|
-
if (err.status === 413 ||
|
|
1665
|
+
if (err.status === 413 || CONTEXT_OVERFLOW_RE.test(err.message)) {
|
|
1568
1666
|
return { kind: "context_overflow", retryable: false };
|
|
1569
1667
|
}
|
|
1570
1668
|
if (err.status >= 400) return { kind: "client", retryable: false };
|
|
1571
1669
|
}
|
|
1572
|
-
if (err instanceof Error &&
|
|
1670
|
+
if (err instanceof Error && NETWORK_ERR_RE.test(err.message)) {
|
|
1573
1671
|
return { kind: "network", retryable: true };
|
|
1574
1672
|
}
|
|
1575
1673
|
return { kind: "unknown", retryable: false };
|
|
@@ -1633,13 +1731,28 @@ var DefaultSkillLoader = class {
|
|
|
1633
1731
|
async manifestText() {
|
|
1634
1732
|
const skills = await this.list();
|
|
1635
1733
|
if (skills.length === 0) return "";
|
|
1734
|
+
const entries = await this.listEntries();
|
|
1636
1735
|
const lines = ["## Available skills"];
|
|
1637
|
-
for (const
|
|
1638
|
-
|
|
1639
|
-
lines.push(
|
|
1736
|
+
for (const e of entries) {
|
|
1737
|
+
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
|
|
1738
|
+
lines.push(`- **${e.name}**${scopeTag}`);
|
|
1739
|
+
lines.push(` Use when: ${e.trigger}`);
|
|
1640
1740
|
}
|
|
1641
1741
|
return lines.join("\n");
|
|
1642
1742
|
}
|
|
1743
|
+
async listEntries() {
|
|
1744
|
+
const skills = await this.list();
|
|
1745
|
+
const entries = [];
|
|
1746
|
+
for (const s of skills) {
|
|
1747
|
+
try {
|
|
1748
|
+
const raw = await fsp.readFile(s.path, "utf8");
|
|
1749
|
+
const { trigger, scope } = parseDescription(raw);
|
|
1750
|
+
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
1751
|
+
} catch {
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return entries;
|
|
1755
|
+
}
|
|
1643
1756
|
async readBody(name) {
|
|
1644
1757
|
const m = await this.find(name);
|
|
1645
1758
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
@@ -1682,6 +1795,19 @@ function parseFrontmatter(raw) {
|
|
|
1682
1795
|
flush();
|
|
1683
1796
|
return out;
|
|
1684
1797
|
}
|
|
1798
|
+
function parseDescription(raw) {
|
|
1799
|
+
const fm = parseFrontmatter(raw);
|
|
1800
|
+
const desc = fm.description ?? "";
|
|
1801
|
+
const firstSentenceEnd = desc.indexOf(". ");
|
|
1802
|
+
const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
|
|
1803
|
+
const scope = [];
|
|
1804
|
+
const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
|
|
1805
|
+
if (coversMatch) {
|
|
1806
|
+
const items = coversMatch[1].replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1807
|
+
scope.push(...items);
|
|
1808
|
+
}
|
|
1809
|
+
return { trigger, scope };
|
|
1810
|
+
}
|
|
1685
1811
|
var BEHAVIOR_DEFAULTS = {
|
|
1686
1812
|
version: 1,
|
|
1687
1813
|
context: {
|
|
@@ -1730,11 +1856,13 @@ var ENV_MAP = {
|
|
|
1730
1856
|
function isPrimitiveArray(a) {
|
|
1731
1857
|
return a.every((v) => v === null || typeof v !== "object");
|
|
1732
1858
|
}
|
|
1859
|
+
var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1733
1860
|
function deepMerge2(base, patch) {
|
|
1734
1861
|
if (typeof base !== "object" || base === null) return patch ?? base;
|
|
1735
1862
|
if (typeof patch !== "object" || patch === null) return base;
|
|
1736
1863
|
const out = { ...base };
|
|
1737
1864
|
for (const [k, v] of Object.entries(patch)) {
|
|
1865
|
+
if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
|
|
1738
1866
|
const existing = out[k];
|
|
1739
1867
|
if (Array.isArray(v)) {
|
|
1740
1868
|
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
@@ -1802,8 +1930,12 @@ var DefaultConfigLoader = class {
|
|
|
1802
1930
|
if (cfg.providers) {
|
|
1803
1931
|
for (const pcfg of Object.values(cfg.providers)) {
|
|
1804
1932
|
if (!pcfg || typeof pcfg !== "object") continue;
|
|
1805
|
-
const
|
|
1806
|
-
if (!Array.isArray(
|
|
1933
|
+
const rawKeys = pcfg.apiKeys;
|
|
1934
|
+
if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
|
|
1935
|
+
const keys = rawKeys.filter(
|
|
1936
|
+
(k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
|
|
1937
|
+
);
|
|
1938
|
+
if (keys.length === 0) continue;
|
|
1807
1939
|
const existing = pcfg.apiKey;
|
|
1808
1940
|
if (existing && existing.length > 0) continue;
|
|
1809
1941
|
const activeLabel = pcfg.activeKey;
|
|
@@ -1814,23 +1946,42 @@ var DefaultConfigLoader = class {
|
|
|
1814
1946
|
}
|
|
1815
1947
|
}
|
|
1816
1948
|
this.validateBehavior(cfg);
|
|
1817
|
-
if (this.strict)
|
|
1949
|
+
if (this.strict) {
|
|
1950
|
+
this.validateIdentity(cfg);
|
|
1951
|
+
}
|
|
1818
1952
|
return Object.freeze(cfg);
|
|
1819
1953
|
}
|
|
1820
1954
|
async readJson(file) {
|
|
1955
|
+
let raw;
|
|
1821
1956
|
try {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
if (
|
|
1825
|
-
|
|
1957
|
+
raw = await fsp.readFile(file, "utf8");
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
if (err.code !== "ENOENT") {
|
|
1960
|
+
console.warn(`[config] Failed to read "${file}":`, err);
|
|
1961
|
+
}
|
|
1962
|
+
return {};
|
|
1826
1963
|
}
|
|
1827
|
-
|
|
1964
|
+
const parsed = safeParse(raw);
|
|
1965
|
+
if (!parsed.ok || !parsed.value) {
|
|
1966
|
+
console.warn(
|
|
1967
|
+
`[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
|
|
1968
|
+
);
|
|
1969
|
+
return {};
|
|
1970
|
+
}
|
|
1971
|
+
return parsed.value;
|
|
1828
1972
|
}
|
|
1829
1973
|
validateBehavior(cfg) {
|
|
1830
1974
|
if (cfg.version === void 0) throw new Error("Config: missing version field");
|
|
1831
1975
|
if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
|
|
1832
1976
|
const c = cfg.context;
|
|
1833
1977
|
if (!c) throw new Error("Config: missing context section");
|
|
1978
|
+
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
1979
|
+
for (const f of fields) {
|
|
1980
|
+
const v = c[f];
|
|
1981
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
1982
|
+
throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1834
1985
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
1835
1986
|
throw new Error("Config: context thresholds must satisfy warn < soft < hard");
|
|
1836
1987
|
}
|
|
@@ -1878,7 +2029,8 @@ var DefaultConfigStore = class {
|
|
|
1878
2029
|
for (const w of this.watchers) {
|
|
1879
2030
|
try {
|
|
1880
2031
|
w(next, prev);
|
|
1881
|
-
} catch {
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
console.error("[config-store] watcher threw:", err);
|
|
1882
2034
|
}
|
|
1883
2035
|
}
|
|
1884
2036
|
return next;
|
|
@@ -1955,21 +2107,33 @@ var DEFAULT_CONFIG_MIGRATIONS = [];
|
|
|
1955
2107
|
|
|
1956
2108
|
// src/utils/token-estimate.ts
|
|
1957
2109
|
var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
2110
|
+
var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
|
|
2111
|
+
var ESTIMATE_CACHE_MAX_SIZE = 1e4;
|
|
2112
|
+
function getCachedEstimate(key, compute) {
|
|
2113
|
+
const existing = ESTIMATE_CACHE.get(key);
|
|
2114
|
+
if (existing !== void 0) return existing;
|
|
2115
|
+
if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
|
|
2116
|
+
const keys = [...ESTIMATE_CACHE.keys()];
|
|
2117
|
+
for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
|
|
2118
|
+
ESTIMATE_CACHE.delete(keys[i]);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const estimate = compute();
|
|
2122
|
+
ESTIMATE_CACHE.set(key, estimate);
|
|
2123
|
+
return estimate;
|
|
2124
|
+
}
|
|
1958
2125
|
function estimateToolInputTokens(input) {
|
|
1959
2126
|
if (typeof input === "string") return RoughTokenEstimate(input);
|
|
1960
|
-
if (input
|
|
1961
|
-
return input
|
|
1962
|
-
}
|
|
1963
|
-
const str = typeof input === "object" ? JSON.stringify(input) : String(input);
|
|
1964
|
-
const estimate = RoughTokenEstimate(str);
|
|
1965
|
-
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
1966
|
-
input.__tokenEstimate = estimate;
|
|
2127
|
+
if (input === null || typeof input !== "object") {
|
|
2128
|
+
return RoughTokenEstimate(String(input));
|
|
1967
2129
|
}
|
|
1968
|
-
|
|
2130
|
+
const key = JSON.stringify(input);
|
|
2131
|
+
return getCachedEstimate(key, () => RoughTokenEstimate(key));
|
|
1969
2132
|
}
|
|
1970
2133
|
function estimateToolResultTokens(content) {
|
|
1971
2134
|
if (typeof content === "string") return RoughTokenEstimate(content);
|
|
1972
|
-
|
|
2135
|
+
const key = JSON.stringify(content);
|
|
2136
|
+
return getCachedEstimate(key, () => RoughTokenEstimate(key));
|
|
1973
2137
|
}
|
|
1974
2138
|
function estimateTextTokens(text) {
|
|
1975
2139
|
return RoughTokenEstimate(text);
|
|
@@ -2010,9 +2174,18 @@ var HybridCompactor = class {
|
|
|
2010
2174
|
}
|
|
2011
2175
|
}
|
|
2012
2176
|
let saved = 0;
|
|
2013
|
-
|
|
2177
|
+
let changed = false;
|
|
2178
|
+
const nextMessages = new Array(messages.length);
|
|
2179
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2014
2180
|
const msg = messages[i];
|
|
2015
|
-
if (
|
|
2181
|
+
if (i >= preserveStart) {
|
|
2182
|
+
nextMessages[i] = msg;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
2186
|
+
nextMessages[i] = msg;
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2016
2189
|
const newContent = msg.content.map((b) => {
|
|
2017
2190
|
if (b.type !== "tool_result") return b;
|
|
2018
2191
|
const tokens = estimateToolResultTokens(b.content);
|
|
@@ -2026,8 +2199,14 @@ var HybridCompactor = class {
|
|
|
2026
2199
|
};
|
|
2027
2200
|
return elided;
|
|
2028
2201
|
});
|
|
2029
|
-
|
|
2202
|
+
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
2203
|
+
nextMessages[i] = msg;
|
|
2204
|
+
} else {
|
|
2205
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
2206
|
+
changed = true;
|
|
2207
|
+
}
|
|
2030
2208
|
}
|
|
2209
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
2031
2210
|
return saved;
|
|
2032
2211
|
}
|
|
2033
2212
|
collapseAncientTurns(ctx) {
|
|
@@ -2061,10 +2240,10 @@ var HybridCompactor = class {
|
|
|
2061
2240
|
let total = 0;
|
|
2062
2241
|
for (const m of messages) {
|
|
2063
2242
|
if (typeof m.content === "string") {
|
|
2064
|
-
total +=
|
|
2243
|
+
total += this.estimator(m.content);
|
|
2065
2244
|
} else {
|
|
2066
2245
|
for (const b of m.content) {
|
|
2067
|
-
if (b.type === "text") total +=
|
|
2246
|
+
if (b.type === "text") total += this.estimator(b.text);
|
|
2068
2247
|
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
2069
2248
|
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
2070
2249
|
}
|
|
@@ -2109,7 +2288,7 @@ var IntelligentCompactor = class {
|
|
|
2109
2288
|
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
2110
2289
|
const reductions = [];
|
|
2111
2290
|
const load = beforeTokens / this.maxContext;
|
|
2112
|
-
const aggressive = opts.aggressive ?? load >= this.softThreshold;
|
|
2291
|
+
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
2113
2292
|
const saved1 = this.eliseOldToolResults(ctx);
|
|
2114
2293
|
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
2115
2294
|
if (aggressive) {
|
|
@@ -2218,9 +2397,18 @@ var IntelligentCompactor = class {
|
|
|
2218
2397
|
}
|
|
2219
2398
|
}
|
|
2220
2399
|
let saved = 0;
|
|
2221
|
-
|
|
2400
|
+
let changed = false;
|
|
2401
|
+
const nextMessages = new Array(messages.length);
|
|
2402
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2222
2403
|
const msg = messages[i];
|
|
2223
|
-
if (
|
|
2404
|
+
if (i >= preserveStart) {
|
|
2405
|
+
nextMessages[i] = msg;
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2408
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
2409
|
+
nextMessages[i] = msg;
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2224
2412
|
const newContent = msg.content.map((b) => {
|
|
2225
2413
|
if (b.type !== "tool_result") return b;
|
|
2226
2414
|
const tokens = estimateToolResultTokens(b.content);
|
|
@@ -2233,8 +2421,14 @@ var IntelligentCompactor = class {
|
|
|
2233
2421
|
is_error: b.is_error
|
|
2234
2422
|
};
|
|
2235
2423
|
});
|
|
2236
|
-
|
|
2424
|
+
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
2425
|
+
nextMessages[i] = msg;
|
|
2426
|
+
} else {
|
|
2427
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
2428
|
+
changed = true;
|
|
2429
|
+
}
|
|
2237
2430
|
}
|
|
2431
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
2238
2432
|
return saved;
|
|
2239
2433
|
}
|
|
2240
2434
|
hasTextContent(m) {
|
|
@@ -2387,7 +2581,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
2387
2581
|
const jsonEnd = raw.lastIndexOf("}");
|
|
2388
2582
|
if (jsonStart === -1 || jsonEnd === -1) {
|
|
2389
2583
|
return this.fallbackSelect(
|
|
2390
|
-
Array.from({ length: messageCount }, (
|
|
2584
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
2391
2585
|
this.maxContextTokens
|
|
2392
2586
|
);
|
|
2393
2587
|
}
|
|
@@ -2396,7 +2590,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
2396
2590
|
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
2397
2591
|
} catch {
|
|
2398
2592
|
return this.fallbackSelect(
|
|
2399
|
-
Array.from({ length: messageCount }, (
|
|
2593
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
2400
2594
|
this.maxContextTokens
|
|
2401
2595
|
);
|
|
2402
2596
|
}
|
|
@@ -2449,7 +2643,7 @@ var SelectiveCompactor = class {
|
|
|
2449
2643
|
const savedElision = this.eliseOldToolResults(ctx);
|
|
2450
2644
|
if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
|
|
2451
2645
|
const afterPhase1 = this.estimateTokens(ctx.messages);
|
|
2452
|
-
const targetBudget = this.computeTargetBudget(load
|
|
2646
|
+
const targetBudget = this.computeTargetBudget(load);
|
|
2453
2647
|
if (afterPhase1 > targetBudget) {
|
|
2454
2648
|
const savedSelective = await this.runSelector(ctx, targetBudget);
|
|
2455
2649
|
if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
|
|
@@ -2467,7 +2661,7 @@ var SelectiveCompactor = class {
|
|
|
2467
2661
|
try {
|
|
2468
2662
|
result = await this.selector.select(ctx.messages, targetBudget);
|
|
2469
2663
|
} catch {
|
|
2470
|
-
return this.aggressiveRecencyTrim(ctx
|
|
2664
|
+
return this.aggressiveRecencyTrim(ctx);
|
|
2471
2665
|
}
|
|
2472
2666
|
await this.executePlan(ctx, result);
|
|
2473
2667
|
const after = this.estimateTokens(ctx.messages);
|
|
@@ -2522,9 +2716,8 @@ Summarize the following message range:`;
|
|
|
2522
2716
|
* Fallback when selector fails: aggressively trim from the oldest end
|
|
2523
2717
|
* until we hit targetBudget.
|
|
2524
2718
|
*/
|
|
2525
|
-
aggressiveRecencyTrim(ctx
|
|
2719
|
+
aggressiveRecencyTrim(ctx) {
|
|
2526
2720
|
const messages = ctx.messages;
|
|
2527
|
-
this.estimateTokens(messages);
|
|
2528
2721
|
const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
|
|
2529
2722
|
if (preserveIdx <= 0) return 0;
|
|
2530
2723
|
let boundary = preserveIdx;
|
|
@@ -2545,7 +2738,7 @@ Summarize the following message range:`;
|
|
|
2545
2738
|
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
2546
2739
|
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
2547
2740
|
}
|
|
2548
|
-
computeTargetBudget(load
|
|
2741
|
+
computeTargetBudget(load) {
|
|
2549
2742
|
if (load >= this.hardThreshold) {
|
|
2550
2743
|
return Math.floor(this.maxContext * 0.5);
|
|
2551
2744
|
}
|
|
@@ -2567,9 +2760,18 @@ Summarize the following message range:`;
|
|
|
2567
2760
|
}
|
|
2568
2761
|
}
|
|
2569
2762
|
let saved = 0;
|
|
2570
|
-
|
|
2763
|
+
let changed = false;
|
|
2764
|
+
const nextMessages = new Array(messages.length);
|
|
2765
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2571
2766
|
const msg = messages[i];
|
|
2572
|
-
if (
|
|
2767
|
+
if (i >= preserveStart) {
|
|
2768
|
+
nextMessages[i] = msg;
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
2772
|
+
nextMessages[i] = msg;
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2573
2775
|
const newContent = msg.content.map((b) => {
|
|
2574
2776
|
if (b.type !== "tool_result") return b;
|
|
2575
2777
|
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
@@ -2583,8 +2785,14 @@ Summarize the following message range:`;
|
|
|
2583
2785
|
is_error: b.is_error
|
|
2584
2786
|
};
|
|
2585
2787
|
});
|
|
2586
|
-
|
|
2788
|
+
if (newContent.every((b, idx) => b === msg.content[idx])) {
|
|
2789
|
+
nextMessages[i] = msg;
|
|
2790
|
+
} else {
|
|
2791
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
2792
|
+
changed = true;
|
|
2793
|
+
}
|
|
2587
2794
|
}
|
|
2795
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
2588
2796
|
return saved;
|
|
2589
2797
|
}
|
|
2590
2798
|
hasTextContent(m) {
|
|
@@ -2620,46 +2828,78 @@ var AutoCompactionMiddleware = class {
|
|
|
2620
2828
|
name = "AutoCompaction";
|
|
2621
2829
|
compactor;
|
|
2622
2830
|
warnThreshold;
|
|
2623
|
-
// fraction of maxContext (0-1)
|
|
2624
2831
|
softThreshold;
|
|
2625
2832
|
hardThreshold;
|
|
2626
2833
|
maxContext;
|
|
2627
2834
|
estimator;
|
|
2628
2835
|
aggressiveOn;
|
|
2836
|
+
events;
|
|
2837
|
+
failureMode;
|
|
2629
2838
|
/**
|
|
2630
|
-
* @param compactor Compactor to use for compaction
|
|
2631
|
-
* @param maxContext Provider's max context window in tokens
|
|
2632
|
-
* @param estimator Token estimation function
|
|
2633
|
-
* @param thresholds
|
|
2634
|
-
* @param
|
|
2839
|
+
* @param compactor Compactor to use for compaction.
|
|
2840
|
+
* @param maxContext Provider's max context window in tokens.
|
|
2841
|
+
* @param estimator Token estimation function.
|
|
2842
|
+
* @param thresholds Threshold fractions (0-1) of maxContext.
|
|
2843
|
+
* @param opts Optional behavior. By default, failures at the
|
|
2844
|
+
* hard threshold throw AGENT_CONTEXT_OVERFLOW so
|
|
2845
|
+
* the agent does not continue into a likely
|
|
2846
|
+
* provider context overflow. Warn/soft failures
|
|
2847
|
+
* still emit compaction.failed and continue.
|
|
2635
2848
|
*/
|
|
2636
|
-
constructor(compactor, maxContext, estimator, thresholds,
|
|
2849
|
+
constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
|
|
2850
|
+
const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
|
|
2637
2851
|
this.compactor = compactor;
|
|
2638
2852
|
this.maxContext = maxContext;
|
|
2639
2853
|
this.estimator = estimator;
|
|
2640
2854
|
this.warnThreshold = thresholds.warn;
|
|
2641
2855
|
this.softThreshold = thresholds.soft;
|
|
2642
2856
|
this.hardThreshold = thresholds.hard;
|
|
2643
|
-
this.aggressiveOn = aggressiveOn;
|
|
2857
|
+
this.aggressiveOn = opts.aggressiveOn ?? "soft";
|
|
2858
|
+
this.events = opts.events;
|
|
2859
|
+
this.failureMode = opts.failureMode ?? "throw_on_hard";
|
|
2644
2860
|
}
|
|
2645
2861
|
handler() {
|
|
2646
2862
|
return async (ctx, next) => {
|
|
2647
2863
|
const tokens = this.estimator(ctx);
|
|
2648
2864
|
const load = tokens / this.maxContext;
|
|
2649
2865
|
if (load >= this.hardThreshold) {
|
|
2650
|
-
await this.compact(ctx, true);
|
|
2866
|
+
await this.compact(ctx, true, { level: "hard", tokens, load });
|
|
2651
2867
|
} else if (load >= this.softThreshold) {
|
|
2652
|
-
await this.compact(ctx, this.aggressiveOn !== "hard");
|
|
2868
|
+
await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
|
|
2653
2869
|
} else if (load >= this.warnThreshold) {
|
|
2654
|
-
await this.compact(ctx, false);
|
|
2870
|
+
await this.compact(ctx, false, { level: "warn", tokens, load });
|
|
2655
2871
|
}
|
|
2656
2872
|
return next(ctx);
|
|
2657
2873
|
};
|
|
2658
2874
|
}
|
|
2659
|
-
async compact(ctx, aggressive) {
|
|
2875
|
+
async compact(ctx, aggressive, pressure) {
|
|
2660
2876
|
try {
|
|
2661
2877
|
await this.compactor.compact(ctx, { aggressive });
|
|
2662
|
-
} catch {
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2880
|
+
const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
|
|
2881
|
+
this.events?.emit("compaction.failed", {
|
|
2882
|
+
err: error,
|
|
2883
|
+
aggressive,
|
|
2884
|
+
level: pressure.level,
|
|
2885
|
+
tokens: pressure.tokens,
|
|
2886
|
+
maxContext: this.maxContext,
|
|
2887
|
+
load: pressure.load,
|
|
2888
|
+
fatal
|
|
2889
|
+
});
|
|
2890
|
+
if (fatal) {
|
|
2891
|
+
throw new AgentError({
|
|
2892
|
+
message: `Auto-compaction failed at ${pressure.level} threshold`,
|
|
2893
|
+
code: "AGENT_CONTEXT_OVERFLOW",
|
|
2894
|
+
recoverable: true,
|
|
2895
|
+
context: {
|
|
2896
|
+
level: pressure.level,
|
|
2897
|
+
tokens: pressure.tokens,
|
|
2898
|
+
maxContext: this.maxContext
|
|
2899
|
+
},
|
|
2900
|
+
cause: err
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2663
2903
|
}
|
|
2664
2904
|
}
|
|
2665
2905
|
};
|
|
@@ -3173,8 +3413,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3173
3413
|
const context = {
|
|
3174
3414
|
subagentId: id,
|
|
3175
3415
|
tasks: [],
|
|
3176
|
-
//
|
|
3177
|
-
// bidirectional bridge is created.
|
|
3416
|
+
// Wired later by the caller via setSubagentBridge() once the
|
|
3417
|
+
// bidirectional bridge is created. Readers must null-check / use
|
|
3418
|
+
// hasParentBridge() — the type now reflects this.
|
|
3178
3419
|
parentBridge: null,
|
|
3179
3420
|
doneCondition: this.config.doneCondition,
|
|
3180
3421
|
maxConcurrent: this.config.maxConcurrent ?? 4
|
|
@@ -3219,9 +3460,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3219
3460
|
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
3220
3461
|
}
|
|
3221
3462
|
async stopAll() {
|
|
3222
|
-
|
|
3223
|
-
await this.stop(id);
|
|
3224
|
-
}
|
|
3463
|
+
await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
|
|
3225
3464
|
}
|
|
3226
3465
|
getStatus() {
|
|
3227
3466
|
return {
|
|
@@ -3257,7 +3496,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3257
3496
|
if (!subagentId) return;
|
|
3258
3497
|
const task = this.pendingTasks.shift();
|
|
3259
3498
|
if (!task) return;
|
|
3260
|
-
|
|
3499
|
+
this.runDispatched(subagentId, task).catch((err) => {
|
|
3500
|
+
this.recordCompletion({
|
|
3501
|
+
subagentId,
|
|
3502
|
+
taskId: task.id,
|
|
3503
|
+
status: "failed",
|
|
3504
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3505
|
+
iterations: 0,
|
|
3506
|
+
toolCalls: 0,
|
|
3507
|
+
durationMs: 0
|
|
3508
|
+
});
|
|
3509
|
+
});
|
|
3261
3510
|
}
|
|
3262
3511
|
}
|
|
3263
3512
|
canDispatch() {
|
|
@@ -3277,7 +3526,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3277
3526
|
subagent.currentTask = task.id;
|
|
3278
3527
|
task.subagentId = subagentId;
|
|
3279
3528
|
subagent.context.tasks.push(task);
|
|
3280
|
-
this.inFlight++;
|
|
3281
3529
|
this.emit("task.assigned", { task, subagentId });
|
|
3282
3530
|
const budget = new SubagentBudget({
|
|
3283
3531
|
maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
|
|
@@ -3287,6 +3535,10 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3287
3535
|
timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
|
|
3288
3536
|
});
|
|
3289
3537
|
subagent.activeBudget = budget;
|
|
3538
|
+
if (!this.runner) {
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
this.inFlight++;
|
|
3290
3542
|
const startTime = Date.now();
|
|
3291
3543
|
const runCtx = {
|
|
3292
3544
|
subagentId,
|
|
@@ -3296,9 +3548,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3296
3548
|
bridge: subagent.context.parentBridge || null
|
|
3297
3549
|
};
|
|
3298
3550
|
let result;
|
|
3299
|
-
if (!this.runner) {
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
3551
|
budget.start();
|
|
3303
3552
|
try {
|
|
3304
3553
|
const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
|
|
@@ -3313,13 +3562,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3313
3562
|
};
|
|
3314
3563
|
} catch (err) {
|
|
3315
3564
|
const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
|
|
3565
|
+
const usage = budget.usage();
|
|
3316
3566
|
result = {
|
|
3317
3567
|
subagentId,
|
|
3318
3568
|
taskId: task.id,
|
|
3319
3569
|
status,
|
|
3320
3570
|
error: err instanceof Error ? err.message : String(err),
|
|
3321
|
-
iterations:
|
|
3322
|
-
toolCalls:
|
|
3571
|
+
iterations: usage.iterations,
|
|
3572
|
+
toolCalls: usage.toolCalls,
|
|
3323
3573
|
durationMs: Date.now() - startTime
|
|
3324
3574
|
};
|
|
3325
3575
|
}
|
|
@@ -3344,11 +3594,24 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3344
3594
|
recordCompletion(result) {
|
|
3345
3595
|
this.completedResults.push(result);
|
|
3346
3596
|
this.totalIterations += result.iterations;
|
|
3347
|
-
|
|
3597
|
+
if (this.inFlight > 0) {
|
|
3598
|
+
this.inFlight--;
|
|
3599
|
+
} else if (this.runner) {
|
|
3600
|
+
this.emit("warning", {
|
|
3601
|
+
type: "inFlight_underflow",
|
|
3602
|
+
taskId: result.taskId,
|
|
3603
|
+
subagentId: result.subagentId
|
|
3604
|
+
});
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3348
3607
|
const subagent = this.subagents.get(result.subagentId);
|
|
3349
3608
|
if (subagent && subagent.status !== "stopped") {
|
|
3350
|
-
|
|
3609
|
+
const failed = result.status === "failed" || result.status === "timeout";
|
|
3610
|
+
subagent.status = failed ? "error" : "idle";
|
|
3351
3611
|
subagent.currentTask = void 0;
|
|
3612
|
+
if (subagent.abortController.signal.aborted) {
|
|
3613
|
+
subagent.abortController = new AbortController();
|
|
3614
|
+
}
|
|
3352
3615
|
if (subagent.status === "error") {
|
|
3353
3616
|
queueMicrotask(() => {
|
|
3354
3617
|
if (subagent.status === "error") subagent.status = "idle";
|
|
@@ -3536,6 +3799,11 @@ var InMemoryAgentBridge = class {
|
|
|
3536
3799
|
if (this.stopped) throw new Error("Bridge is stopped");
|
|
3537
3800
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
3538
3801
|
const correlationId = msg.id;
|
|
3802
|
+
if (this.pendingRequests.has(correlationId)) {
|
|
3803
|
+
throw new Error(
|
|
3804
|
+
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3539
3807
|
return new Promise((resolve3, reject) => {
|
|
3540
3808
|
const timer = setTimeout(() => {
|
|
3541
3809
|
this.pendingRequests.delete(correlationId);
|
|
@@ -3576,8 +3844,10 @@ function createMessage(type, from, payload, to) {
|
|
|
3576
3844
|
var DoneConditionChecker = class {
|
|
3577
3845
|
constructor(condition) {
|
|
3578
3846
|
this.condition = condition;
|
|
3847
|
+
this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
|
|
3579
3848
|
}
|
|
3580
3849
|
condition;
|
|
3850
|
+
compiledRegex;
|
|
3581
3851
|
check(state) {
|
|
3582
3852
|
switch (this.condition.type) {
|
|
3583
3853
|
case "iterations":
|
|
@@ -3591,11 +3861,8 @@ var DoneConditionChecker = class {
|
|
|
3591
3861
|
}
|
|
3592
3862
|
break;
|
|
3593
3863
|
case "output_match":
|
|
3594
|
-
if (this.
|
|
3595
|
-
|
|
3596
|
-
if (regex.test(state.lastOutput)) {
|
|
3597
|
-
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
3598
|
-
}
|
|
3864
|
+
if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
|
|
3865
|
+
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
3599
3866
|
}
|
|
3600
3867
|
break;
|
|
3601
3868
|
}
|
|
@@ -3652,7 +3919,8 @@ var AutonomousRunner = class {
|
|
|
3652
3919
|
return failedResult;
|
|
3653
3920
|
}
|
|
3654
3921
|
} catch (e) {
|
|
3655
|
-
|
|
3922
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3923
|
+
if (msg.includes("timeout")) {
|
|
3656
3924
|
const timeoutResult = {
|
|
3657
3925
|
status: "failed",
|
|
3658
3926
|
error: toWrongStackError(e),
|
|
@@ -3681,14 +3949,11 @@ var AutonomousRunner = class {
|
|
|
3681
3949
|
|
|
3682
3950
|
// src/defaults/spec-parser.ts
|
|
3683
3951
|
var SpecParser = class {
|
|
3684
|
-
constructor(opts = {}) {
|
|
3685
|
-
this.opts = opts;
|
|
3686
|
-
}
|
|
3687
|
-
opts;
|
|
3688
3952
|
parse(content) {
|
|
3689
3953
|
const lines = content.split("\n");
|
|
3690
3954
|
const sections = this.extractSections(lines);
|
|
3691
3955
|
const requirements = this.extractRequirements(lines);
|
|
3956
|
+
const now = Date.now();
|
|
3692
3957
|
return {
|
|
3693
3958
|
id: crypto.randomUUID(),
|
|
3694
3959
|
title: this.extractTitle(lines),
|
|
@@ -3697,8 +3962,8 @@ var SpecParser = class {
|
|
|
3697
3962
|
overview: this.extractOverview(lines),
|
|
3698
3963
|
sections,
|
|
3699
3964
|
requirements,
|
|
3700
|
-
createdAt:
|
|
3701
|
-
updatedAt:
|
|
3965
|
+
createdAt: now,
|
|
3966
|
+
updatedAt: now
|
|
3702
3967
|
};
|
|
3703
3968
|
}
|
|
3704
3969
|
extractTitle(lines) {
|
|
@@ -3790,20 +4055,13 @@ var SpecParser = class {
|
|
|
3790
4055
|
parseRequirementLine(line, id) {
|
|
3791
4056
|
const trimmed = line.trim();
|
|
3792
4057
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
"non-functional": "non-functional",
|
|
3796
|
-
"security": "security",
|
|
3797
|
-
"performance": "performance",
|
|
3798
|
-
"ux": "ux"
|
|
3799
|
-
};
|
|
4058
|
+
const lower = trimmed.toLowerCase();
|
|
4059
|
+
const types = ["functional", "non-functional", "security", "performance", "ux"];
|
|
3800
4060
|
let type = "functional";
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
if (trimmed.toLowerCase().includes(`[${key}]`)) {
|
|
3804
|
-
type = val;
|
|
3805
|
-
}
|
|
4061
|
+
for (const t of types) {
|
|
4062
|
+
if (lower.includes(`[${t}]`)) type = t;
|
|
3806
4063
|
}
|
|
4064
|
+
let priority = "medium";
|
|
3807
4065
|
if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
|
|
3808
4066
|
priority = "critical";
|
|
3809
4067
|
} else if (trimmed.includes("[high]")) {
|
|
@@ -3893,9 +4151,10 @@ var SpecParser = class {
|
|
|
3893
4151
|
warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
|
|
3894
4152
|
}
|
|
3895
4153
|
}
|
|
4154
|
+
const reqIds = new Set(spec.requirements.map((r) => r.id));
|
|
3896
4155
|
const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
|
|
3897
4156
|
for (const id of blockedByIds) {
|
|
3898
|
-
if (!
|
|
4157
|
+
if (!reqIds.has(id)) {
|
|
3899
4158
|
errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
|
|
3900
4159
|
}
|
|
3901
4160
|
}
|
|
@@ -3925,25 +4184,21 @@ var TaskGenerator = class {
|
|
|
3925
4184
|
status: "pending"
|
|
3926
4185
|
});
|
|
3927
4186
|
}
|
|
3928
|
-
const
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3938
|
-
this.opts.taskTracker.addNode(task);
|
|
3939
|
-
}
|
|
3940
|
-
for (const req of mediumReqs) {
|
|
3941
|
-
const task = this.createTaskFromRequirement(req, spec.title);
|
|
3942
|
-
this.opts.taskTracker.addNode(task);
|
|
4187
|
+
const byPriority = {
|
|
4188
|
+
critical: [],
|
|
4189
|
+
high: [],
|
|
4190
|
+
medium: [],
|
|
4191
|
+
low: []
|
|
4192
|
+
};
|
|
4193
|
+
for (const req of spec.requirements) {
|
|
4194
|
+
const bucket = byPriority[req.priority] ?? byPriority.medium;
|
|
4195
|
+
bucket.push(req);
|
|
3943
4196
|
}
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
4197
|
+
const order = ["critical", "high", "medium", "low"];
|
|
4198
|
+
for (const p of order) {
|
|
4199
|
+
for (const req of byPriority[p]) {
|
|
4200
|
+
this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
|
|
4201
|
+
}
|
|
3947
4202
|
}
|
|
3948
4203
|
if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
|
|
3949
4204
|
const apiParent = this.opts.taskTracker.addNode({
|
|
@@ -3977,17 +4232,15 @@ var TaskGenerator = class {
|
|
|
3977
4232
|
});
|
|
3978
4233
|
return graph;
|
|
3979
4234
|
}
|
|
3980
|
-
createTaskFromRequirement(req
|
|
3981
|
-
const type = this.mapRequirementType(req.type);
|
|
3982
|
-
const tags = [req.type, req.priority];
|
|
4235
|
+
createTaskFromRequirement(req) {
|
|
3983
4236
|
return {
|
|
3984
4237
|
title: req.description,
|
|
3985
|
-
description: this.buildDescription(req
|
|
3986
|
-
type,
|
|
3987
|
-
priority:
|
|
4238
|
+
description: this.buildDescription(req),
|
|
4239
|
+
type: this.mapRequirementType(req.type),
|
|
4240
|
+
priority: req.priority,
|
|
3988
4241
|
status: "pending",
|
|
3989
4242
|
specRequirementId: req.id,
|
|
3990
|
-
tags,
|
|
4243
|
+
tags: [req.type, req.priority],
|
|
3991
4244
|
estimateHours: this.estimateHours(req)
|
|
3992
4245
|
};
|
|
3993
4246
|
}
|
|
@@ -4002,7 +4255,7 @@ var TaskGenerator = class {
|
|
|
4002
4255
|
estimateHours: this.estimateForEndpoint(endpoint)
|
|
4003
4256
|
};
|
|
4004
4257
|
}
|
|
4005
|
-
buildDescription(req
|
|
4258
|
+
buildDescription(req) {
|
|
4006
4259
|
const lines = [
|
|
4007
4260
|
req.description,
|
|
4008
4261
|
"",
|
|
@@ -4036,20 +4289,6 @@ var TaskGenerator = class {
|
|
|
4036
4289
|
return "feature";
|
|
4037
4290
|
}
|
|
4038
4291
|
}
|
|
4039
|
-
mapPriority(priority) {
|
|
4040
|
-
switch (priority) {
|
|
4041
|
-
case "critical":
|
|
4042
|
-
return "critical";
|
|
4043
|
-
case "high":
|
|
4044
|
-
return "high";
|
|
4045
|
-
case "medium":
|
|
4046
|
-
return "medium";
|
|
4047
|
-
case "low":
|
|
4048
|
-
return "low";
|
|
4049
|
-
default:
|
|
4050
|
-
return "medium";
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
4292
|
estimateHours(req) {
|
|
4054
4293
|
switch (req.priority) {
|
|
4055
4294
|
case "critical":
|
|
@@ -4120,16 +4359,33 @@ var DefaultTaskStore = class {
|
|
|
4120
4359
|
|
|
4121
4360
|
// src/types/task-graph.ts
|
|
4122
4361
|
function computeTaskProgress(graph) {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4362
|
+
let completed = 0, pending = 0, inProgress = 0, blocked = 0, failed = 0, review = 0;
|
|
4363
|
+
let estimatedHours = 0, actualHours = 0;
|
|
4364
|
+
for (const n of graph.nodes.values()) {
|
|
4365
|
+
switch (n.status) {
|
|
4366
|
+
case "completed":
|
|
4367
|
+
completed++;
|
|
4368
|
+
break;
|
|
4369
|
+
case "pending":
|
|
4370
|
+
pending++;
|
|
4371
|
+
break;
|
|
4372
|
+
case "in_progress":
|
|
4373
|
+
inProgress++;
|
|
4374
|
+
break;
|
|
4375
|
+
case "blocked":
|
|
4376
|
+
blocked++;
|
|
4377
|
+
break;
|
|
4378
|
+
case "failed":
|
|
4379
|
+
failed++;
|
|
4380
|
+
break;
|
|
4381
|
+
case "review":
|
|
4382
|
+
review++;
|
|
4383
|
+
break;
|
|
4384
|
+
}
|
|
4385
|
+
estimatedHours += n.estimateHours ?? 0;
|
|
4386
|
+
actualHours += n.actualHours ?? 0;
|
|
4387
|
+
}
|
|
4388
|
+
const total = graph.nodes.size;
|
|
4133
4389
|
return {
|
|
4134
4390
|
total,
|
|
4135
4391
|
pending,
|
|
@@ -4185,40 +4441,40 @@ var TaskTracker = class {
|
|
|
4185
4441
|
this.graph.rootNodes.push(newNode.id);
|
|
4186
4442
|
}
|
|
4187
4443
|
this.graph.updatedAt = now;
|
|
4188
|
-
this.
|
|
4444
|
+
this.persist();
|
|
4189
4445
|
return newNode;
|
|
4190
4446
|
}
|
|
4191
4447
|
addEdge(from, to, type = "depends_on") {
|
|
4192
4448
|
if (!this.graph) throw new Error("No graph loaded");
|
|
4193
|
-
|
|
4449
|
+
this.graph.edges.push({
|
|
4194
4450
|
id: crypto.randomUUID(),
|
|
4195
4451
|
from,
|
|
4196
4452
|
to,
|
|
4197
4453
|
type
|
|
4198
|
-
};
|
|
4199
|
-
this.graph.edges.push(edge);
|
|
4454
|
+
});
|
|
4200
4455
|
this.graph.updatedAt = Date.now();
|
|
4201
|
-
this.
|
|
4456
|
+
this.persist();
|
|
4202
4457
|
}
|
|
4203
4458
|
updateNodeStatus(id, status, reason) {
|
|
4204
4459
|
if (!this.graph) throw new Error("No graph loaded");
|
|
4205
4460
|
const node = this.graph.nodes.get(id);
|
|
4206
4461
|
if (!node) throw new Error(`Node ${id} not found`);
|
|
4207
4462
|
const from = node.status;
|
|
4463
|
+
const now = Date.now();
|
|
4208
4464
|
node.status = status;
|
|
4209
|
-
node.updatedAt =
|
|
4465
|
+
node.updatedAt = now;
|
|
4210
4466
|
if (status === "completed") {
|
|
4211
|
-
node.completedAt =
|
|
4467
|
+
node.completedAt = now;
|
|
4212
4468
|
}
|
|
4213
|
-
this.transitions.push({ from, to: status, timestamp:
|
|
4469
|
+
this.transitions.push({ from, to: status, timestamp: now, reason });
|
|
4214
4470
|
if (status === "completed") {
|
|
4215
4471
|
this.unblockDependents(id);
|
|
4216
4472
|
}
|
|
4217
4473
|
if (status === "in_progress") {
|
|
4218
4474
|
this.checkAndBlockIfNeeded(id);
|
|
4219
4475
|
}
|
|
4220
|
-
this.graph.updatedAt =
|
|
4221
|
-
this.
|
|
4476
|
+
this.graph.updatedAt = now;
|
|
4477
|
+
this.persist();
|
|
4222
4478
|
}
|
|
4223
4479
|
getNode(id) {
|
|
4224
4480
|
return this.graph?.nodes.get(id);
|
|
@@ -4239,9 +4495,7 @@ var TaskTracker = class {
|
|
|
4239
4495
|
}
|
|
4240
4496
|
if (sort) {
|
|
4241
4497
|
nodes.sort((a, b) => {
|
|
4242
|
-
const
|
|
4243
|
-
const bVal = b[sort.field] ?? "";
|
|
4244
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
4498
|
+
const cmp = compareByField(a, b, sort.field);
|
|
4245
4499
|
return sort.direction === "asc" ? cmp : -cmp;
|
|
4246
4500
|
});
|
|
4247
4501
|
}
|
|
@@ -4320,7 +4574,47 @@ var TaskTracker = class {
|
|
|
4320
4574
|
}
|
|
4321
4575
|
}
|
|
4322
4576
|
}
|
|
4577
|
+
/**
|
|
4578
|
+
* Fire-and-forget persistence with attached error handler.
|
|
4579
|
+
* Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
|
|
4580
|
+
* avoid forcing an async cascade through every caller; if the store
|
|
4581
|
+
* rejects, the configured `onPersistError` is invoked so failures are
|
|
4582
|
+
* surfaced instead of swallowed by an unhandled promise rejection.
|
|
4583
|
+
*/
|
|
4584
|
+
persist() {
|
|
4585
|
+
if (!this.graph) return;
|
|
4586
|
+
this.opts.store.saveGraph(this.graph).catch((err) => {
|
|
4587
|
+
if (this.opts.onPersistError) this.opts.onPersistError(err);
|
|
4588
|
+
else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
};
|
|
4592
|
+
var PRIORITY_RANK = {
|
|
4593
|
+
critical: 0,
|
|
4594
|
+
high: 1,
|
|
4595
|
+
medium: 2,
|
|
4596
|
+
low: 3
|
|
4597
|
+
};
|
|
4598
|
+
var STATUS_RANK = {
|
|
4599
|
+
in_progress: 0,
|
|
4600
|
+
pending: 1,
|
|
4601
|
+
review: 2,
|
|
4602
|
+
blocked: 3,
|
|
4603
|
+
failed: 4,
|
|
4604
|
+
completed: 5
|
|
4323
4605
|
};
|
|
4606
|
+
function compareByField(a, b, field) {
|
|
4607
|
+
switch (field) {
|
|
4608
|
+
case "priority":
|
|
4609
|
+
return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
|
|
4610
|
+
case "status":
|
|
4611
|
+
return STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
4612
|
+
case "createdAt":
|
|
4613
|
+
return a.createdAt - b.createdAt;
|
|
4614
|
+
case "updatedAt":
|
|
4615
|
+
return a.updatedAt - b.updatedAt;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4324
4618
|
|
|
4325
4619
|
// src/defaults/task-flow.ts
|
|
4326
4620
|
var TaskFlow = class {
|
|
@@ -4372,9 +4666,10 @@ var TaskFlow = class {
|
|
|
4372
4666
|
const task = batch[i];
|
|
4373
4667
|
if (!result || !task) continue;
|
|
4374
4668
|
if (result.status === "rejected") {
|
|
4375
|
-
|
|
4376
|
-
this.
|
|
4377
|
-
|
|
4669
|
+
const reason = result.reason;
|
|
4670
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
|
|
4671
|
+
this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
|
|
4672
|
+
ctx.onTaskFail?.(task, reason);
|
|
4378
4673
|
} else {
|
|
4379
4674
|
this.opts.tracker.updateNodeStatus(task.id, "completed");
|
|
4380
4675
|
this.emit("task.completed", { taskId: task.id, result: result.value });
|
|
@@ -4657,7 +4952,6 @@ function createToolOutputSerializer(opts = {}) {
|
|
|
4657
4952
|
}
|
|
4658
4953
|
const half = Math.floor(available / 2);
|
|
4659
4954
|
const first = text.slice(0, half);
|
|
4660
|
-
Buffer.byteLength(first, "utf8");
|
|
4661
4955
|
const second = text.slice(text.length - half);
|
|
4662
4956
|
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
4663
4957
|
}
|
|
@@ -4707,7 +5001,7 @@ var ToolExecutor = class {
|
|
|
4707
5001
|
return { result, tool, durationMs: Date.now() - start };
|
|
4708
5002
|
}
|
|
4709
5003
|
} else {
|
|
4710
|
-
const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
|
|
5004
|
+
const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
|
|
4711
5005
|
const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
|
|
4712
5006
|
return { result: pending, tool, durationMs: Date.now() - start };
|
|
4713
5007
|
}
|
|
@@ -4739,15 +5033,31 @@ var ToolExecutor = class {
|
|
|
4739
5033
|
span?.end();
|
|
4740
5034
|
}
|
|
4741
5035
|
};
|
|
5036
|
+
const safeRun = async (use) => {
|
|
5037
|
+
try {
|
|
5038
|
+
return await runOne(use);
|
|
5039
|
+
} catch (err) {
|
|
5040
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5041
|
+
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
5042
|
+
const result = {
|
|
5043
|
+
type: "tool_result",
|
|
5044
|
+
tool_use_id: use.id,
|
|
5045
|
+
content: `Tool "${use.name}" execution failed: ${scrubbed}`,
|
|
5046
|
+
is_error: true
|
|
5047
|
+
};
|
|
5048
|
+
budget = this.decrementBudget(result, budget);
|
|
5049
|
+
return { result, tool: this.registry.get(use.name), durationMs: 0 };
|
|
5050
|
+
}
|
|
5051
|
+
};
|
|
4742
5052
|
if (strategy === "sequential") {
|
|
4743
5053
|
const outputs = [];
|
|
4744
5054
|
for (const use of toolUses) {
|
|
4745
|
-
if (use) outputs.push(await
|
|
5055
|
+
if (use) outputs.push(await safeRun(use));
|
|
4746
5056
|
}
|
|
4747
5057
|
return { outputs, remainingBudget: budget };
|
|
4748
5058
|
}
|
|
4749
5059
|
if (strategy === "parallel") {
|
|
4750
|
-
const outputs = await Promise.all(toolUses.map((use) =>
|
|
5060
|
+
const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
|
|
4751
5061
|
return { outputs, remainingBudget: budget };
|
|
4752
5062
|
}
|
|
4753
5063
|
const nonMutating = [];
|
|
@@ -4758,10 +5068,10 @@ var ToolExecutor = class {
|
|
|
4758
5068
|
if (tool?.mutating) mutating.push(use);
|
|
4759
5069
|
else nonMutating.push(use);
|
|
4760
5070
|
}
|
|
4761
|
-
const firstPass = await Promise.all(nonMutating.map((use) =>
|
|
5071
|
+
const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
|
|
4762
5072
|
const secondPass = [];
|
|
4763
5073
|
for (const use of mutating) {
|
|
4764
|
-
secondPass.push(await
|
|
5074
|
+
secondPass.push(await safeRun(use));
|
|
4765
5075
|
}
|
|
4766
5076
|
return {
|
|
4767
5077
|
outputs: [...firstPass, ...secondPass],
|
|
@@ -4796,7 +5106,8 @@ var ToolExecutor = class {
|
|
|
4796
5106
|
}
|
|
4797
5107
|
async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
|
|
4798
5108
|
if (parentSignal.aborted) {
|
|
4799
|
-
|
|
5109
|
+
if (parentSignal.reason instanceof Error) throw parentSignal.reason;
|
|
5110
|
+
throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
4800
5111
|
}
|
|
4801
5112
|
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
4802
5113
|
const ctrl = new AbortController();
|
|
@@ -4865,16 +5176,23 @@ var ToolExecutor = class {
|
|
|
4865
5176
|
* Matches the logic in DefaultPermissionPolicy so the TUI shows the
|
|
4866
5177
|
* same subject that the trust file would use.
|
|
4867
5178
|
*/
|
|
4868
|
-
subjectFor(toolName, input) {
|
|
5179
|
+
subjectFor(toolName, input, subjectKey) {
|
|
4869
5180
|
if (!input || typeof input !== "object") return void 0;
|
|
4870
5181
|
const obj = input;
|
|
4871
5182
|
const globChars = /[*?\[\]]/g;
|
|
4872
5183
|
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
5184
|
+
const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
|
|
5185
|
+
if (subjectKey) {
|
|
5186
|
+
const v = obj[subjectKey];
|
|
5187
|
+
if (typeof v === "string") {
|
|
5188
|
+
return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
4873
5191
|
if (toolName === "bash" && typeof obj.command === "string") {
|
|
4874
5192
|
return escapeGlob(obj.command);
|
|
4875
5193
|
}
|
|
4876
5194
|
if (typeof obj.path === "string") {
|
|
4877
|
-
return
|
|
5195
|
+
return normalizePath(obj.path);
|
|
4878
5196
|
}
|
|
4879
5197
|
if (typeof obj.url === "string") {
|
|
4880
5198
|
return escapeGlob(obj.url);
|
|
@@ -5298,8 +5616,9 @@ var DefaultHealthRegistry = class {
|
|
|
5298
5616
|
return { status, timestamp: Date.now(), checks: results };
|
|
5299
5617
|
}
|
|
5300
5618
|
async runOne(check) {
|
|
5619
|
+
let timer = null;
|
|
5301
5620
|
const timeout = new Promise((resolve3) => {
|
|
5302
|
-
setTimeout(
|
|
5621
|
+
timer = setTimeout(
|
|
5303
5622
|
() => resolve3({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
|
|
5304
5623
|
this.timeoutMs
|
|
5305
5624
|
);
|
|
@@ -5308,6 +5627,8 @@ var DefaultHealthRegistry = class {
|
|
|
5308
5627
|
return await Promise.race([check.check(), timeout]);
|
|
5309
5628
|
} catch (err) {
|
|
5310
5629
|
return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
|
|
5630
|
+
} finally {
|
|
5631
|
+
if (timer) clearTimeout(timer);
|
|
5311
5632
|
}
|
|
5312
5633
|
}
|
|
5313
5634
|
};
|
|
@@ -5946,7 +6267,6 @@ function createContextManagerTool(opts = {}) {
|
|
|
5946
6267
|
notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
|
|
5947
6268
|
};
|
|
5948
6269
|
}
|
|
5949
|
-
messages.slice(from, to + 1);
|
|
5950
6270
|
const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
|
|
5951
6271
|
const summaryMsg = {
|
|
5952
6272
|
role: "system",
|