@wrongstack/core 0.264.0 → 0.265.1
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-D8sa1vtv.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
- package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +113 -11
- package/dist/{brain-O1IdKPaK.d.ts → brain-BXd_61kQ.d.ts} +31 -2
- package/dist/{compactor-BBy0rCtB.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
- package/dist/{config-Dz2F3H2K.d.ts → config-BMCj_XDs.d.ts} +80 -12
- package/dist/{context-BGSpZNSE.d.ts → context-MRk5PhNv.d.ts} +26 -12
- package/dist/coordination/index.d.ts +77 -21
- package/dist/coordination/index.js +557 -159
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
- package/dist/defaults/index.d.ts +28 -28
- package/dist/defaults/index.js +609 -195
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +16 -16
- package/dist/execution/index.js +394 -155
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +2 -2
- package/dist/execution/prompt-enhancer.js +1 -1
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-DvHDSKSe.d.ts} +14 -10
- package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
- package/dist/{index-CYIQrXVF.d.ts → index-B-ch8K9C.d.ts} +8 -8
- package/dist/{index-CbLSI66_.d.ts → index-CEDeNodM.d.ts} +5 -5
- package/dist/index.d.ts +183 -52
- package/dist/index.js +1779 -673
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +12 -8
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +1 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-DzxuZnNz.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
- package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +74 -30
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-B_siPxqN.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +1 -1
- package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
- package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
- package/dist/{permission-4yvGmMRB.d.ts → permission-B9SB45lp.d.ts} +1 -1
- package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
- package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-DPDxH_7m.d.ts} +3 -3
- package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
- package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
- package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +274 -93
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
- package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
- package/dist/security/index.d.ts +5 -5
- package/dist/security/index.js +204 -23
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-gIuhRTkN.d.ts → selector-CzHh_igB.d.ts} +1 -1
- package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +1 -1
- package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
- package/dist/storage/index.d.ts +112 -15
- package/dist/storage/index.js +419 -81
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/types/index.d.ts +21 -21
- package/dist/types/index.js +261 -53
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +3 -5
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
- package/package.json +1 -1
- package/skills/api-design/SKILL.md +1 -1
- package/skills/audit-log/SKILL.md +6 -6
- package/skills/bug-hunter/SKILL.md +5 -5
- package/skills/chimera/SKILL.md +4 -4
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/git-flow/SKILL.md +3 -3
- package/skills/multi-agent/SKILL.md +3 -3
- package/skills/node-modern/SKILL.md +1 -0
- package/skills/observability/SKILL.md +2 -2
- package/skills/output-standards/SKILL.md +51 -28
- package/skills/refactor-planner/SKILL.md +3 -3
- package/skills/security-scanner/SKILL.md +4 -3
- package/skills/tech-stack/SKILL.md +1 -2
- package/dist/secret-vault-BJDY28ev.d.ts +0 -25
package/dist/defaults/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash }
|
|
|
3
3
|
import * as fsp2 from 'fs/promises';
|
|
4
4
|
import * as path3 from 'path';
|
|
5
5
|
import { isAbsolute, resolve } from 'path';
|
|
6
|
-
import * as
|
|
6
|
+
import * as fs4 from 'fs';
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import { hostname } from 'os';
|
|
9
9
|
import { execFile } from 'child_process';
|
|
@@ -234,7 +234,7 @@ var DefaultLogger = class _DefaultLogger {
|
|
|
234
234
|
this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
|
|
235
235
|
if (this.file) {
|
|
236
236
|
try {
|
|
237
|
-
|
|
237
|
+
fs4.mkdirSync(path3.dirname(this.file), { recursive: true });
|
|
238
238
|
} catch {
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -275,10 +275,10 @@ var DefaultLogger = class _DefaultLogger {
|
|
|
275
275
|
maybeRotate(file) {
|
|
276
276
|
if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
|
|
277
277
|
try {
|
|
278
|
-
const st =
|
|
278
|
+
const st = fs4.statSync(file);
|
|
279
279
|
if (st.size < this.maxFileBytes) return;
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
fs4.rmSync(`${file}.1`, { force: true });
|
|
281
|
+
fs4.renameSync(file, `${file}.1`);
|
|
282
282
|
} catch {
|
|
283
283
|
}
|
|
284
284
|
}
|
|
@@ -294,7 +294,7 @@ var DefaultLogger = class _DefaultLogger {
|
|
|
294
294
|
if (this.file) {
|
|
295
295
|
try {
|
|
296
296
|
this.maybeRotate(this.file);
|
|
297
|
-
|
|
297
|
+
fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
|
|
298
298
|
`);
|
|
299
299
|
} catch {
|
|
300
300
|
}
|
|
@@ -592,7 +592,8 @@ function resolveWstackPaths(opts) {
|
|
|
592
592
|
projectSddSession: path3.join(projectDir, "sdd-session.json"),
|
|
593
593
|
projectPlan: path3.join(projectDir, "plan.json"),
|
|
594
594
|
projectAutophase: path3.join(projectDir, "autophase"),
|
|
595
|
-
syncConfig: path3.join(globalRoot, "sync.json")
|
|
595
|
+
syncConfig: path3.join(globalRoot, "sync.json"),
|
|
596
|
+
projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
|
|
596
597
|
};
|
|
597
598
|
}
|
|
598
599
|
|
|
@@ -732,12 +733,9 @@ function getCachedEstimate(key, compute) {
|
|
|
732
733
|
const existing = ESTIMATE_CACHE.get(key);
|
|
733
734
|
if (existing !== void 0) return existing;
|
|
734
735
|
if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
|
|
735
|
-
let evicted = 0;
|
|
736
|
-
const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
|
|
737
736
|
for (const k of ESTIMATE_CACHE.keys()) {
|
|
738
|
-
if (
|
|
737
|
+
if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
|
|
739
738
|
ESTIMATE_CACHE.delete(k);
|
|
740
|
-
evicted++;
|
|
741
739
|
}
|
|
742
740
|
}
|
|
743
741
|
const estimate = compute(key);
|
|
@@ -1277,11 +1275,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
1277
1275
|
dir;
|
|
1278
1276
|
events;
|
|
1279
1277
|
secretScrubber;
|
|
1278
|
+
/**
|
|
1279
|
+
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
1280
|
+
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
1281
|
+
* file was written to). This eliminates redundant full-file reads and
|
|
1282
|
+
* JSON parses when the same session is loaded multiple times within the
|
|
1283
|
+
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
1284
|
+
*
|
|
1285
|
+
* Max size is capped to prevent unbounded memory growth in long-running
|
|
1286
|
+
* processes. When the limit is reached, the oldest entry is evicted.
|
|
1287
|
+
*/
|
|
1288
|
+
_loadCache = /* @__PURE__ */ new Map();
|
|
1289
|
+
static LOAD_CACHE_MAX_ENTRIES = 50;
|
|
1280
1290
|
constructor(opts) {
|
|
1281
1291
|
this.dir = opts.dir;
|
|
1282
1292
|
this.events = opts.events;
|
|
1283
1293
|
this.secretScrubber = opts.secretScrubber;
|
|
1284
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Clear the load() cache. Useful for testing or when the caller knows
|
|
1297
|
+
* the file has changed externally (e.g., another process wrote to it).
|
|
1298
|
+
*/
|
|
1299
|
+
clearLoadCache(sessionId) {
|
|
1300
|
+
if (sessionId !== void 0) {
|
|
1301
|
+
this._loadCache.delete(sessionId);
|
|
1302
|
+
} else {
|
|
1303
|
+
this._loadCache.clear();
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1285
1306
|
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
1286
1307
|
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
1287
1308
|
this.events?.emit("storage.read", {
|
|
@@ -1424,7 +1445,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
1424
1445
|
const t0 = Date.now();
|
|
1425
1446
|
let outcome = "success";
|
|
1426
1447
|
let errorMsg;
|
|
1448
|
+
let cacheHit = false;
|
|
1427
1449
|
try {
|
|
1450
|
+
let stat6;
|
|
1451
|
+
try {
|
|
1452
|
+
const s = await fsp2.stat(file);
|
|
1453
|
+
stat6 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
throw err;
|
|
1456
|
+
}
|
|
1457
|
+
const cached = this._loadCache.get(id);
|
|
1458
|
+
if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
|
|
1459
|
+
cacheHit = true;
|
|
1460
|
+
return cached.data;
|
|
1461
|
+
}
|
|
1428
1462
|
const raw = await fsp2.readFile(file, "utf8");
|
|
1429
1463
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
1430
1464
|
const events = [];
|
|
@@ -1440,13 +1474,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
1440
1474
|
const meta = this.metaFromEvents(id, events);
|
|
1441
1475
|
const { messages, usage } = this.replay(events, id);
|
|
1442
1476
|
const toolCallEnds = extractToolCallEnds(events);
|
|
1443
|
-
|
|
1477
|
+
const data = { metadata: meta, events, messages, usage, toolCallEnds };
|
|
1478
|
+
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
1479
|
+
const oldest = this._loadCache.keys().next().value;
|
|
1480
|
+
if (oldest !== void 0) {
|
|
1481
|
+
this._loadCache.delete(oldest);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
|
|
1485
|
+
return data;
|
|
1444
1486
|
} catch (err) {
|
|
1445
1487
|
outcome = "failure";
|
|
1446
1488
|
errorMsg = toErrorMessage(err);
|
|
1447
1489
|
throw err;
|
|
1448
1490
|
} finally {
|
|
1449
1491
|
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
1492
|
+
if (cacheHit) {
|
|
1493
|
+
this.events?.emit("storage.cache_hit", {
|
|
1494
|
+
sessionId: id,
|
|
1495
|
+
store: "session",
|
|
1496
|
+
filePath: file,
|
|
1497
|
+
operation: "load",
|
|
1498
|
+
durationMs: Date.now() - t0
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1450
1501
|
}
|
|
1451
1502
|
}
|
|
1452
1503
|
async list(limit = 20) {
|
|
@@ -3663,7 +3714,10 @@ function deepFreeze(obj) {
|
|
|
3663
3714
|
}
|
|
3664
3715
|
|
|
3665
3716
|
// src/types/secret-vault.ts
|
|
3666
|
-
var
|
|
3717
|
+
var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
|
|
3718
|
+
function encryptedPrefixForVersion(version) {
|
|
3719
|
+
return `enc:v${version}:`;
|
|
3720
|
+
}
|
|
3667
3721
|
|
|
3668
3722
|
// src/security/secret-vault.ts
|
|
3669
3723
|
init_atomic_write();
|
|
@@ -3672,10 +3726,12 @@ var IV_BYTES = 12;
|
|
|
3672
3726
|
var TAG_BYTES = 16;
|
|
3673
3727
|
var ALGO = "aes-256-gcm";
|
|
3674
3728
|
var KEY_FILE_MODE = 384;
|
|
3729
|
+
var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
|
|
3730
|
+
var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
|
|
3675
3731
|
function checkKeyFilePermissions(keyFile) {
|
|
3676
3732
|
if (process.platform === "win32") return;
|
|
3677
3733
|
try {
|
|
3678
|
-
const stat6 =
|
|
3734
|
+
const stat6 = fs4.statSync(keyFile);
|
|
3679
3735
|
const actualMode = stat6.mode & 511;
|
|
3680
3736
|
if (actualMode !== KEY_FILE_MODE) {
|
|
3681
3737
|
console.warn(JSON.stringify({
|
|
@@ -3694,11 +3750,17 @@ function checkKeyFilePermissions(keyFile) {
|
|
|
3694
3750
|
var DefaultSecretVault = class {
|
|
3695
3751
|
keyFile;
|
|
3696
3752
|
key;
|
|
3753
|
+
_keyVersion = 1;
|
|
3697
3754
|
constructor(opts) {
|
|
3698
3755
|
this.keyFile = opts.keyFile;
|
|
3699
3756
|
}
|
|
3757
|
+
/** Current key version. Starts at 1; incremented by rotateKey(). */
|
|
3758
|
+
get keyVersion() {
|
|
3759
|
+
if (!this.key) this.loadOrCreateKey();
|
|
3760
|
+
return this._keyVersion;
|
|
3761
|
+
}
|
|
3700
3762
|
isEncrypted(value) {
|
|
3701
|
-
return typeof value === "string" &&
|
|
3763
|
+
return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
|
|
3702
3764
|
}
|
|
3703
3765
|
encrypt(plaintext) {
|
|
3704
3766
|
if (this.isEncrypted(plaintext)) return plaintext;
|
|
@@ -3707,11 +3769,20 @@ var DefaultSecretVault = class {
|
|
|
3707
3769
|
const cipher = createCipheriv(ALGO, key, iv);
|
|
3708
3770
|
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
3709
3771
|
const tag = cipher.getAuthTag();
|
|
3710
|
-
|
|
3772
|
+
const prefix = encryptedPrefixForVersion(this._keyVersion);
|
|
3773
|
+
return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
3711
3774
|
}
|
|
3712
3775
|
decrypt(value) {
|
|
3713
3776
|
if (!this.isEncrypted(value)) return value;
|
|
3714
|
-
const
|
|
3777
|
+
const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
|
|
3778
|
+
if (!prefixMatch) {
|
|
3779
|
+
throw new ConfigError({
|
|
3780
|
+
message: "SecretVault: malformed encrypted value",
|
|
3781
|
+
code: ERROR_CODES.CONFIG_PARSE_FAILED,
|
|
3782
|
+
context: { field: "encrypted_value" }
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
const rest = value.slice(prefixMatch[0].length);
|
|
3715
3786
|
const parts = rest.split(":");
|
|
3716
3787
|
if (parts.length !== 3) {
|
|
3717
3788
|
throw new ConfigError({
|
|
@@ -3740,42 +3811,104 @@ var DefaultSecretVault = class {
|
|
|
3740
3811
|
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
3741
3812
|
return pt.toString("utf8");
|
|
3742
3813
|
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Generate a new encryption key, write it to disk, and increment the key version.
|
|
3816
|
+
* After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
|
|
3817
|
+
* The caller must re-encrypt existing config values (see rotateConfigKeys()).
|
|
3818
|
+
*/
|
|
3819
|
+
rotateKey() {
|
|
3820
|
+
const oldVersion = this._keyVersion;
|
|
3821
|
+
const newKey = randomBytes(KEY_BYTES);
|
|
3822
|
+
const newVersion = oldVersion + 1;
|
|
3823
|
+
const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
|
|
3824
|
+
KEY_FILE_MAGIC.copy(keyFileBuf, 0);
|
|
3825
|
+
keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
|
|
3826
|
+
newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
|
|
3827
|
+
fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
|
|
3828
|
+
fs4.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
|
|
3829
|
+
checkKeyFilePermissions(this.keyFile);
|
|
3830
|
+
this.key = newKey;
|
|
3831
|
+
this._keyVersion = newVersion;
|
|
3832
|
+
return { oldVersion, newVersion };
|
|
3833
|
+
}
|
|
3743
3834
|
loadOrCreateKey() {
|
|
3744
3835
|
if (this.key) return this.key;
|
|
3745
3836
|
try {
|
|
3746
|
-
const buf =
|
|
3747
|
-
if (buf.length
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3837
|
+
const buf = fs4.readFileSync(this.keyFile);
|
|
3838
|
+
if (buf.length === KEY_BYTES) {
|
|
3839
|
+
this.key = buf;
|
|
3840
|
+
this._keyVersion = 1;
|
|
3841
|
+
checkKeyFilePermissions(this.keyFile);
|
|
3842
|
+
return this.key;
|
|
3843
|
+
}
|
|
3844
|
+
if (buf.length === VERSIONED_KEY_FILE_SIZE) {
|
|
3845
|
+
const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
|
|
3846
|
+
if (!magic.equals(KEY_FILE_MAGIC)) {
|
|
3847
|
+
throw new ConfigError({
|
|
3848
|
+
message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
|
|
3849
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3850
|
+
context: { keyFile: this.keyFile }
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
const version = buf[KEY_FILE_MAGIC.length];
|
|
3854
|
+
const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
|
|
3855
|
+
if (key2.length !== KEY_BYTES) {
|
|
3856
|
+
throw new ConfigError({
|
|
3857
|
+
message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
|
|
3858
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3859
|
+
context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
this.key = Buffer.from(key2);
|
|
3863
|
+
this._keyVersion = version;
|
|
3864
|
+
checkKeyFilePermissions(this.keyFile);
|
|
3865
|
+
return this.key;
|
|
3753
3866
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3867
|
+
throw new ConfigError({
|
|
3868
|
+
message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
|
|
3869
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3870
|
+
context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
|
|
3871
|
+
});
|
|
3757
3872
|
} catch (err) {
|
|
3758
3873
|
if (err.code !== "ENOENT") throw err;
|
|
3759
3874
|
}
|
|
3760
|
-
|
|
3875
|
+
fs4.mkdirSync(path3.dirname(this.keyFile), { recursive: true });
|
|
3761
3876
|
const key = randomBytes(KEY_BYTES);
|
|
3762
3877
|
try {
|
|
3763
|
-
|
|
3878
|
+
fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
|
|
3764
3879
|
} catch (err) {
|
|
3765
3880
|
if (err.code !== "EEXIST") throw err;
|
|
3766
|
-
const buf =
|
|
3767
|
-
if (buf.length
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3881
|
+
const buf = fs4.readFileSync(this.keyFile);
|
|
3882
|
+
if (buf.length === KEY_BYTES) {
|
|
3883
|
+
this.key = buf;
|
|
3884
|
+
this._keyVersion = 1;
|
|
3885
|
+
checkKeyFilePermissions(this.keyFile);
|
|
3886
|
+
return this.key;
|
|
3887
|
+
}
|
|
3888
|
+
if (buf.length === VERSIONED_KEY_FILE_SIZE) {
|
|
3889
|
+
const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
|
|
3890
|
+
if (!magic.equals(KEY_FILE_MAGIC)) {
|
|
3891
|
+
throw new ConfigError({
|
|
3892
|
+
message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
|
|
3893
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3894
|
+
context: { keyFile: this.keyFile }
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
const version = buf[KEY_FILE_MAGIC.length];
|
|
3898
|
+
const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
|
|
3899
|
+
this.key = Buffer.from(winnerKey);
|
|
3900
|
+
this._keyVersion = version;
|
|
3901
|
+
checkKeyFilePermissions(this.keyFile);
|
|
3902
|
+
return this.key;
|
|
3773
3903
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3904
|
+
throw new ConfigError({
|
|
3905
|
+
message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
|
|
3906
|
+
code: ERROR_CODES.CONFIG_INVALID,
|
|
3907
|
+
context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
|
|
3908
|
+
});
|
|
3777
3909
|
}
|
|
3778
3910
|
this.key = key;
|
|
3911
|
+
this._keyVersion = 1;
|
|
3779
3912
|
return key;
|
|
3780
3913
|
}
|
|
3781
3914
|
};
|
|
@@ -3971,7 +4104,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
|
|
|
3971
4104
|
iterationTimeoutMs: 3e5,
|
|
3972
4105
|
sessionTimeoutMs: 18e5,
|
|
3973
4106
|
perIterationOutputCapBytes: 1e5,
|
|
3974
|
-
autoExtendLimit: true
|
|
4107
|
+
autoExtendLimit: true,
|
|
4108
|
+
restrictToProjectRoot: false
|
|
3975
4109
|
});
|
|
3976
4110
|
var DEFAULT_CONTEXT_CONFIG = Object.freeze({
|
|
3977
4111
|
preserveK: 10,
|
|
@@ -4014,7 +4148,8 @@ var BEHAVIOR_DEFAULTS = {
|
|
|
4014
4148
|
iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
|
|
4015
4149
|
sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
|
|
4016
4150
|
perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
|
|
4017
|
-
autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
|
|
4151
|
+
autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
|
|
4152
|
+
restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
|
|
4018
4153
|
},
|
|
4019
4154
|
log: { level: "info" },
|
|
4020
4155
|
features: {
|
|
@@ -5283,6 +5418,7 @@ async function savePlan(filePath, plan, events) {
|
|
|
5283
5418
|
outcome: "success",
|
|
5284
5419
|
durationMs: Date.now() - t0
|
|
5285
5420
|
});
|
|
5421
|
+
return true;
|
|
5286
5422
|
} catch (err) {
|
|
5287
5423
|
events?.emit("storage.error", {
|
|
5288
5424
|
sessionId: "~boot~",
|
|
@@ -5296,6 +5432,7 @@ async function savePlan(filePath, plan, events) {
|
|
|
5296
5432
|
"[plan-store] save failed:",
|
|
5297
5433
|
toErrorMessage(err)
|
|
5298
5434
|
);
|
|
5435
|
+
return false;
|
|
5299
5436
|
}
|
|
5300
5437
|
}
|
|
5301
5438
|
function emptyPlan(sessionId, title) {
|
|
@@ -5732,6 +5869,27 @@ var PATTERNS = [
|
|
|
5732
5869
|
{ type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
|
|
5733
5870
|
{ type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
|
|
5734
5871
|
{ type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
|
|
5872
|
+
// AI/ML provider keys — modern LLM services with well-known prefixes
|
|
5873
|
+
{
|
|
5874
|
+
type: "huggingface_token",
|
|
5875
|
+
// HuggingFace tokens: hf_ followed by 34 alphanumeric chars
|
|
5876
|
+
regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
|
|
5877
|
+
},
|
|
5878
|
+
{
|
|
5879
|
+
type: "replicate_token",
|
|
5880
|
+
// Replicate tokens: r8_ followed by 40+ alphanumeric chars
|
|
5881
|
+
regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
|
|
5882
|
+
},
|
|
5883
|
+
{
|
|
5884
|
+
type: "perplexity_key",
|
|
5885
|
+
// Perplexity API keys: pplx- followed by 40+ alphanumeric chars
|
|
5886
|
+
regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
|
|
5887
|
+
},
|
|
5888
|
+
{
|
|
5889
|
+
type: "groq_key",
|
|
5890
|
+
// Groq API keys: gsk_ followed by 40+ alphanumeric chars
|
|
5891
|
+
regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
|
|
5892
|
+
},
|
|
5735
5893
|
{
|
|
5736
5894
|
type: "bearer_token",
|
|
5737
5895
|
// Anchored with alternation instead of negative lookahead — avoids V8
|
|
@@ -5765,6 +5923,10 @@ function hasCredentialAnchors(text) {
|
|
|
5765
5923
|
text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
|
|
5766
5924
|
text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
|
|
5767
5925
|
text.includes("/bot") || // Telegram bot token (URL path pattern)
|
|
5926
|
+
text.includes("hf_") || // HuggingFace token
|
|
5927
|
+
text.includes("r8_") || // Replicate token
|
|
5928
|
+
text.includes("pplx-") || // Perplexity API key
|
|
5929
|
+
text.includes("gsk_") || // Groq API key
|
|
5768
5930
|
text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
|
|
5769
5931
|
text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
|
|
5770
5932
|
text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
|
|
@@ -6605,12 +6767,12 @@ var DefaultSkillLoader = class {
|
|
|
6605
6767
|
}
|
|
6606
6768
|
async find(name) {
|
|
6607
6769
|
const all = await this.list();
|
|
6608
|
-
|
|
6770
|
+
const lower = name.toLowerCase();
|
|
6771
|
+
return all.find((s) => s.name.toLowerCase() === lower);
|
|
6609
6772
|
}
|
|
6610
6773
|
async manifestText() {
|
|
6611
|
-
const skills = await this.list();
|
|
6612
|
-
if (skills.length === 0) return "";
|
|
6613
6774
|
const entries = await this.listEntries();
|
|
6775
|
+
if (entries.length === 0) return "";
|
|
6614
6776
|
const lines = ["## Available skills"];
|
|
6615
6777
|
for (const e of entries) {
|
|
6616
6778
|
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
|
|
@@ -6624,12 +6786,8 @@ var DefaultSkillLoader = class {
|
|
|
6624
6786
|
const skills = await this.list();
|
|
6625
6787
|
const entries = [];
|
|
6626
6788
|
for (const s of skills) {
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
const { trigger, scope } = parseDescription(raw);
|
|
6630
|
-
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
6631
|
-
} catch {
|
|
6632
|
-
}
|
|
6789
|
+
const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
|
|
6790
|
+
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
6633
6791
|
}
|
|
6634
6792
|
this.entriesCache = entries;
|
|
6635
6793
|
return entries;
|
|
@@ -6640,16 +6798,17 @@ var DefaultSkillLoader = class {
|
|
|
6640
6798
|
this.bodyCache.clear();
|
|
6641
6799
|
}
|
|
6642
6800
|
async readBody(name) {
|
|
6643
|
-
const
|
|
6801
|
+
const key = name.toLowerCase();
|
|
6802
|
+
const cached = this.bodyCache.get(key);
|
|
6644
6803
|
if (cached !== void 0) return cached;
|
|
6645
6804
|
const m = await this.find(name);
|
|
6646
6805
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
6647
6806
|
const body = await fsp2.readFile(m.path, "utf8");
|
|
6648
|
-
this.bodyCache.set(
|
|
6807
|
+
this.bodyCache.set(key, body);
|
|
6649
6808
|
return body;
|
|
6650
6809
|
}
|
|
6651
6810
|
async readSaveBody(name) {
|
|
6652
|
-
const key = `save:${name}`;
|
|
6811
|
+
const key = `save:${name.toLowerCase()}`;
|
|
6653
6812
|
const cached = this.bodyCache.get(key);
|
|
6654
6813
|
if (cached !== void 0) return cached;
|
|
6655
6814
|
const m = await this.find(name);
|
|
@@ -6710,9 +6869,7 @@ function parseFrontmatter(raw) {
|
|
|
6710
6869
|
flush();
|
|
6711
6870
|
return out;
|
|
6712
6871
|
}
|
|
6713
|
-
function
|
|
6714
|
-
const fm = parseFrontmatter(raw);
|
|
6715
|
-
const desc = fm.description ?? "";
|
|
6872
|
+
function parseDescriptionFromText(desc) {
|
|
6716
6873
|
const firstSentenceEnd = desc.indexOf(". ");
|
|
6717
6874
|
const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
|
|
6718
6875
|
const scope = [];
|
|
@@ -7116,7 +7273,11 @@ function isTextBlock(b) {
|
|
|
7116
7273
|
}
|
|
7117
7274
|
|
|
7118
7275
|
// src/execution/compaction-core.ts
|
|
7276
|
+
function compactionDebugEnabled() {
|
|
7277
|
+
return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
|
|
7278
|
+
}
|
|
7119
7279
|
function emitCompactionMetrics(event, metrics) {
|
|
7280
|
+
if (!compactionDebugEnabled()) return;
|
|
7120
7281
|
console.log(
|
|
7121
7282
|
JSON.stringify({
|
|
7122
7283
|
level: "debug",
|
|
@@ -7171,18 +7332,20 @@ function findPreserveStart(messages, preserveK) {
|
|
|
7171
7332
|
}
|
|
7172
7333
|
}
|
|
7173
7334
|
}
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7335
|
+
if (compactionDebugEnabled()) {
|
|
7336
|
+
console.log(
|
|
7337
|
+
JSON.stringify({
|
|
7338
|
+
level: "debug",
|
|
7339
|
+
event: "compaction.find_preserve_start.ended",
|
|
7340
|
+
messageCount: messages.length,
|
|
7341
|
+
preserveK,
|
|
7342
|
+
preserveStart,
|
|
7343
|
+
forwardWalkIterations,
|
|
7344
|
+
forwardWalkInnerIterations,
|
|
7345
|
+
forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
|
|
7346
|
+
})
|
|
7347
|
+
);
|
|
7348
|
+
}
|
|
7186
7349
|
return preserveStart;
|
|
7187
7350
|
}
|
|
7188
7351
|
function eliseOldToolResults(messages, opts) {
|
|
@@ -7249,7 +7412,7 @@ function eliseOldToolResults(messages, opts) {
|
|
|
7249
7412
|
changed = true;
|
|
7250
7413
|
}
|
|
7251
7414
|
fullPassInnerIterations += original.length;
|
|
7252
|
-
if (
|
|
7415
|
+
if (compactionDebugEnabled()) {
|
|
7253
7416
|
const ratio = fullPassInnerIterations / fullPassIterations;
|
|
7254
7417
|
if (ratio > 10) {
|
|
7255
7418
|
console.error(
|
|
@@ -7656,7 +7819,12 @@ var IntelligentCompactor = class {
|
|
|
7656
7819
|
};
|
|
7657
7820
|
const ac = ctx.signal ? void 0 : new AbortController();
|
|
7658
7821
|
const signal = ctx.signal ?? ac?.signal;
|
|
7659
|
-
|
|
7822
|
+
let res;
|
|
7823
|
+
try {
|
|
7824
|
+
res = await this.provider.complete(req, { signal });
|
|
7825
|
+
} finally {
|
|
7826
|
+
ac?.abort();
|
|
7827
|
+
}
|
|
7660
7828
|
const textBlocks = res.content.filter(isTextBlock);
|
|
7661
7829
|
return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
|
|
7662
7830
|
}
|
|
@@ -7700,9 +7868,9 @@ Rules:
|
|
|
7700
7868
|
- If unsure, keep rather than collapse (errors are more costly than waste)
|
|
7701
7869
|
|
|
7702
7870
|
Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
|
|
7703
|
-
function formatMessages(messages,
|
|
7871
|
+
function formatMessages(messages, maxTokens = 2048) {
|
|
7704
7872
|
const lines = [];
|
|
7705
|
-
let
|
|
7873
|
+
let usedTokens = 0;
|
|
7706
7874
|
for (let i = 0; i < messages.length; i++) {
|
|
7707
7875
|
const m = expectDefined(messages[i]);
|
|
7708
7876
|
const role = m.role.padEnd(10, " ");
|
|
@@ -7714,13 +7882,14 @@ function formatMessages(messages, maxChars = 8e3) {
|
|
|
7714
7882
|
text = content.filter(isTextBlock).map((b) => b.text).join(" ");
|
|
7715
7883
|
const toolUses = content.filter((b) => b.type === "tool_use");
|
|
7716
7884
|
if (toolUses.length > 0) {
|
|
7717
|
-
text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
|
|
7885
|
+
text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
|
|
7718
7886
|
}
|
|
7719
7887
|
}
|
|
7720
7888
|
const line = `[${i}][${role}]: ${text}`;
|
|
7721
|
-
|
|
7889
|
+
const lineTokens = estimateTextTokens(line);
|
|
7890
|
+
if (usedTokens + lineTokens > maxTokens) break;
|
|
7722
7891
|
lines.push(line);
|
|
7723
|
-
|
|
7892
|
+
usedTokens += lineTokens;
|
|
7724
7893
|
}
|
|
7725
7894
|
return lines.join("\n");
|
|
7726
7895
|
}
|
|
@@ -7729,20 +7898,29 @@ var LLMSelector = class {
|
|
|
7729
7898
|
model;
|
|
7730
7899
|
maxContextTokens;
|
|
7731
7900
|
systemPrompt;
|
|
7901
|
+
maxOutputTokens;
|
|
7732
7902
|
constructor(opts) {
|
|
7733
7903
|
this.provider = opts.provider;
|
|
7734
7904
|
this.model = opts.model ?? "unknown";
|
|
7905
|
+
if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
|
|
7906
|
+
console.warn(
|
|
7907
|
+
"[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
|
|
7908
|
+
);
|
|
7909
|
+
}
|
|
7735
7910
|
this.maxContextTokens = opts.maxContextTokens ?? 4e4;
|
|
7736
7911
|
this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
7912
|
+
this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
|
|
7737
7913
|
}
|
|
7738
7914
|
async select(messages, maxToKeep) {
|
|
7739
7915
|
const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
|
|
7740
|
-
const historyText = formatMessages(messages);
|
|
7741
7916
|
const totalTokens = estimateMessageTokens(messages);
|
|
7742
7917
|
const systemText = `${this.systemPrompt}
|
|
7743
7918
|
|
|
7744
7919
|
Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
|
|
7745
7920
|
`;
|
|
7921
|
+
const systemTokens = estimateTextTokens(systemText);
|
|
7922
|
+
const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
|
|
7923
|
+
const historyText = formatMessages(messages, historyBudget);
|
|
7746
7924
|
const budgetInstruction = totalTokens > effectiveBudget ? `
|
|
7747
7925
|
|
|
7748
7926
|
IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
|
|
@@ -7750,18 +7928,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
7750
7928
|
model: this.model,
|
|
7751
7929
|
system: [{ type: "text", text: systemText + budgetInstruction }],
|
|
7752
7930
|
messages: [{ role: "user", content: historyText }],
|
|
7753
|
-
maxTokens:
|
|
7931
|
+
maxTokens: this.maxOutputTokens
|
|
7754
7932
|
};
|
|
7755
7933
|
let raw;
|
|
7934
|
+
const ac = new AbortController();
|
|
7756
7935
|
try {
|
|
7757
|
-
const
|
|
7758
|
-
const res = await this.provider.complete(req, {
|
|
7936
|
+
const timeoutSignal = AbortSignal.timeout(3e4);
|
|
7937
|
+
const res = await this.provider.complete(req, {
|
|
7938
|
+
signal: AbortSignal.any([ac.signal, timeoutSignal])
|
|
7939
|
+
});
|
|
7759
7940
|
const textBlocks = res.content.filter(isTextBlock);
|
|
7760
7941
|
raw = textBlocks.map((b) => b.text).join("\n").trim();
|
|
7761
|
-
} catch (
|
|
7942
|
+
} catch (err) {
|
|
7943
|
+
if (err instanceof Error) {
|
|
7944
|
+
console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
|
|
7945
|
+
}
|
|
7762
7946
|
return this.fallbackSelect(messages, effectiveBudget);
|
|
7947
|
+
} finally {
|
|
7948
|
+
ac.abort();
|
|
7763
7949
|
}
|
|
7764
|
-
return this.parseSelectorOutput(raw, messages
|
|
7950
|
+
return this.parseSelectorOutput(raw, messages);
|
|
7765
7951
|
}
|
|
7766
7952
|
fallbackSelect(messages, budget) {
|
|
7767
7953
|
const toKeep = [];
|
|
@@ -7788,34 +7974,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
7788
7974
|
reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
|
|
7789
7975
|
};
|
|
7790
7976
|
}
|
|
7791
|
-
|
|
7977
|
+
/**
|
|
7978
|
+
* Parse and validate the raw LLM output into a SelectorResult.
|
|
7979
|
+
* Falls back to recency-based selection if the LLM output is malformed,
|
|
7980
|
+
* out-of-bounds, or internally inconsistent.
|
|
7981
|
+
*/
|
|
7982
|
+
parseSelectorOutput(raw, messages) {
|
|
7983
|
+
const messageCount = messages.length;
|
|
7984
|
+
if (messageCount === 0) {
|
|
7985
|
+
return { kept: [], collapsed: [], reasoning: "empty session" };
|
|
7986
|
+
}
|
|
7792
7987
|
const jsonStart = raw.indexOf("{");
|
|
7793
7988
|
const jsonEnd = raw.lastIndexOf("}");
|
|
7794
7989
|
if (jsonStart === -1 || jsonEnd === -1) {
|
|
7795
|
-
return this.fallbackSelect(
|
|
7796
|
-
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
7797
|
-
this.maxContextTokens
|
|
7798
|
-
);
|
|
7990
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
7799
7991
|
}
|
|
7800
7992
|
let parsed;
|
|
7801
7993
|
try {
|
|
7802
7994
|
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
7803
7995
|
} catch {
|
|
7804
|
-
return this.fallbackSelect(
|
|
7805
|
-
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
7806
|
-
this.maxContextTokens
|
|
7807
|
-
);
|
|
7996
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
7808
7997
|
}
|
|
7809
7998
|
const obj = parsed;
|
|
7810
|
-
const
|
|
7811
|
-
const
|
|
7812
|
-
|
|
7813
|
-
|
|
7999
|
+
const keptRaw = obj.kept ?? [];
|
|
8000
|
+
const collapsedRaw = obj.collapsed ?? [];
|
|
8001
|
+
const kept = [];
|
|
8002
|
+
for (const k of keptRaw) {
|
|
8003
|
+
if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
|
|
8004
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
8005
|
+
}
|
|
8006
|
+
kept.push({
|
|
7814
8007
|
from: k.from,
|
|
7815
8008
|
to: k.to,
|
|
7816
8009
|
importance: k.importance ?? "medium"
|
|
7817
|
-
})
|
|
7818
|
-
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
const collapsed = [];
|
|
8013
|
+
for (const c of collapsedRaw) {
|
|
8014
|
+
if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
|
|
8015
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
8016
|
+
}
|
|
8017
|
+
collapsed.push({ from: c.from, to: c.to, summary: c.summary });
|
|
8018
|
+
}
|
|
8019
|
+
const allRanges = [...kept, ...collapsed];
|
|
8020
|
+
for (let i = 0; i < allRanges.length; i++) {
|
|
8021
|
+
const a = allRanges[i];
|
|
8022
|
+
if (!a) continue;
|
|
8023
|
+
for (let j = i + 1; j < allRanges.length; j++) {
|
|
8024
|
+
const b = allRanges[j];
|
|
8025
|
+
if (!b) continue;
|
|
8026
|
+
if (a.from <= b.to && a.to >= b.from) {
|
|
8027
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
8028
|
+
}
|
|
8029
|
+
}
|
|
8030
|
+
}
|
|
8031
|
+
return {
|
|
8032
|
+
kept,
|
|
8033
|
+
collapsed,
|
|
7819
8034
|
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
|
|
7820
8035
|
};
|
|
7821
8036
|
}
|
|
@@ -7835,7 +8050,7 @@ var SelectiveCompactor = class {
|
|
|
7835
8050
|
summarizerPrompt;
|
|
7836
8051
|
constructor(opts) {
|
|
7837
8052
|
this.provider = opts.provider;
|
|
7838
|
-
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
|
|
8053
|
+
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
|
|
7839
8054
|
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
7840
8055
|
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
7841
8056
|
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
@@ -7843,6 +8058,11 @@ var SelectiveCompactor = class {
|
|
|
7843
8058
|
this.preserveK = opts.preserveK ?? 4;
|
|
7844
8059
|
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
7845
8060
|
this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
|
|
8061
|
+
if (this.summarizerModel === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
|
|
8062
|
+
console.warn(
|
|
8063
|
+
"[SelectiveCompactor] summarizerModel not set \u2014 will use provider default. Set `summarizerModel` explicitly to silence this warning."
|
|
8064
|
+
);
|
|
8065
|
+
}
|
|
7846
8066
|
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
|
|
7847
8067
|
}
|
|
7848
8068
|
async compact(ctx, opts = {}) {
|
|
@@ -7954,8 +8174,9 @@ Summarize the following message range:`;
|
|
|
7954
8174
|
maxTokens: 512
|
|
7955
8175
|
};
|
|
7956
8176
|
try {
|
|
8177
|
+
const timeoutSignal = AbortSignal.timeout(3e4);
|
|
7957
8178
|
const res = await this.provider.complete(req, {
|
|
7958
|
-
signal: ctx.signal
|
|
8179
|
+
signal: AbortSignal.any([ctx.signal, timeoutSignal])
|
|
7959
8180
|
});
|
|
7960
8181
|
return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
|
|
7961
8182
|
} catch {
|
|
@@ -8089,6 +8310,7 @@ var ProviderBackedCompactor = class {
|
|
|
8089
8310
|
return new SelectiveCompactor({
|
|
8090
8311
|
...common,
|
|
8091
8312
|
selectorModel: this.opts.summarizerModel,
|
|
8313
|
+
selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
|
|
8092
8314
|
summarizerModel: this.opts.summarizerModel
|
|
8093
8315
|
});
|
|
8094
8316
|
}
|
|
@@ -9818,6 +10040,7 @@ ${recentJournal}` : ""
|
|
|
9818
10040
|
|
|
9819
10041
|
// src/coordination/subagent-budget.ts
|
|
9820
10042
|
var TIMEOUT_PREEMPT_FRACTION = 0.85;
|
|
10043
|
+
var DECISION_TIMEOUT_MS = 6e4;
|
|
9821
10044
|
var BudgetExceededError = class extends Error {
|
|
9822
10045
|
kind;
|
|
9823
10046
|
limit;
|
|
@@ -9847,6 +10070,31 @@ var BudgetThresholdSignal = class extends Error {
|
|
|
9847
10070
|
};
|
|
9848
10071
|
var SubagentBudget = class _SubagentBudget {
|
|
9849
10072
|
limits;
|
|
10073
|
+
/** Patch one or more budget limits in-place after construction.
|
|
10074
|
+
* Used by the coordinator watchdog when granting an extension.
|
|
10075
|
+
* All fields are optional — only provided fields are updated.
|
|
10076
|
+
* This is the single write path for limit mutations so that future
|
|
10077
|
+
* validation or side-effects live in one place (M1). */
|
|
10078
|
+
patchLimits(ext) {
|
|
10079
|
+
if (ext.maxIterations !== void 0) {
|
|
10080
|
+
this.limits.maxIterations = ext.maxIterations;
|
|
10081
|
+
}
|
|
10082
|
+
if (ext.maxToolCalls !== void 0) {
|
|
10083
|
+
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
10084
|
+
}
|
|
10085
|
+
if (ext.maxTokens !== void 0) {
|
|
10086
|
+
this.limits.maxTokens = ext.maxTokens;
|
|
10087
|
+
}
|
|
10088
|
+
if (ext.maxCostUsd !== void 0) {
|
|
10089
|
+
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
10090
|
+
}
|
|
10091
|
+
if (ext.timeoutMs !== void 0) {
|
|
10092
|
+
this.limits.timeoutMs = ext.timeoutMs;
|
|
10093
|
+
}
|
|
10094
|
+
if (ext.idleTimeoutMs !== void 0) {
|
|
10095
|
+
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
9850
10098
|
iterations = 0;
|
|
9851
10099
|
toolCalls = 0;
|
|
9852
10100
|
tokenInput = 0;
|
|
@@ -9867,12 +10115,44 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
9867
10115
|
* or hung listener (Director not built / event filter detached mid-run)
|
|
9868
10116
|
* leaves the budget over-limit and never enforces anything.
|
|
9869
10117
|
*/
|
|
9870
|
-
static DECISION_TIMEOUT_MS =
|
|
10118
|
+
static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
|
|
9871
10119
|
/**
|
|
9872
10120
|
* Injected by the runner when wiring the budget to its EventBus.
|
|
9873
10121
|
* Used to emit `budget.threshold_reached` events in `'auto'` mode.
|
|
9874
10122
|
*/
|
|
9875
10123
|
_events;
|
|
10124
|
+
/**
|
|
10125
|
+
* Guard against dual-path races between the coordinator watchdog
|
|
10126
|
+
* (`executeWithTimeout`) and the budget's own `checkTimeout()`.
|
|
10127
|
+
* Both paths detect `elapsed >= timeoutMs` and can emit
|
|
10128
|
+
* `budget.threshold_reached` for kind `'timeout'` simultaneously.
|
|
10129
|
+
* Set to the current `timeoutMs` ceiling by the coordinator BEFORE
|
|
10130
|
+
* calling `onThreshold`, and cleared after the negotiation resolves.
|
|
10131
|
+
* `checkTimeout()` skips its wall-clock check while this is set so
|
|
10132
|
+
* the coordinator's watchdog is the sole source of wall-clock timeout
|
|
10133
|
+
* events — `checkTimeout()` focuses exclusively on `idle_timeout`.
|
|
10134
|
+
*/
|
|
10135
|
+
_watchdogActive;
|
|
10136
|
+
/** Returns the timeout ceiling currently being negotiated by the watchdog,
|
|
10137
|
+
* or `undefined` when no wall-clock negotiation is in flight.
|
|
10138
|
+
* Used by `executeWithTimeout` to detect a stale lock (M3). */
|
|
10139
|
+
get watchdogActive() {
|
|
10140
|
+
return this._watchdogActive;
|
|
10141
|
+
}
|
|
10142
|
+
/** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
|
|
10143
|
+
* `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
|
|
10144
|
+
* the budget's own `checkTimeout()` from emitting a second
|
|
10145
|
+
* `budget.threshold_reached` event while the watchdog is already
|
|
10146
|
+
* negotiating the same wall-clock deadline (C1). */
|
|
10147
|
+
setWatchdogNegotiation(timeoutMs) {
|
|
10148
|
+
this._watchdogActive = timeoutMs;
|
|
10149
|
+
}
|
|
10150
|
+
/** Clears the watchdog guard after negotiation resolves. Called in the
|
|
10151
|
+
* `finally` block of both the pre-empt and deadline branches so it fires
|
|
10152
|
+
* on every exit path: grant, deny, throw, or error. */
|
|
10153
|
+
clearWatchdogNegotiation() {
|
|
10154
|
+
this._watchdogActive = void 0;
|
|
10155
|
+
}
|
|
9876
10156
|
/**
|
|
9877
10157
|
* Negotiation mode — controls whether a threshold hit tries to emit
|
|
9878
10158
|
* `budget.threshold_reached` and wait for a coordinator decision, or
|
|
@@ -9973,7 +10253,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
9973
10253
|
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
9974
10254
|
exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
9975
10255
|
}
|
|
9976
|
-
|
|
10256
|
+
const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
10257
|
+
if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
|
|
9977
10258
|
exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
|
|
9978
10259
|
}
|
|
9979
10260
|
}
|
|
@@ -9987,19 +10268,99 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
9987
10268
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
9988
10269
|
}
|
|
9989
10270
|
const bus = this._events;
|
|
9990
|
-
if (!bus
|
|
10271
|
+
if (!bus) {
|
|
9991
10272
|
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
9992
10273
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
9993
10274
|
}
|
|
10275
|
+
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
10276
|
+
if (bus.hasListenerFor("budget.threshold_reached")) {
|
|
10277
|
+
for (const entry of exceeded) {
|
|
10278
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
10279
|
+
this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
|
|
10280
|
+
}
|
|
10281
|
+
const decision = this._pendingNegotiations.get(first.kind);
|
|
10282
|
+
if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
|
|
10283
|
+
throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
|
|
10284
|
+
}
|
|
10285
|
+
let hardStop = null;
|
|
9994
10286
|
for (const entry of exceeded) {
|
|
9995
10287
|
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
9996
|
-
const
|
|
9997
|
-
this._pendingNegotiations.set(entry.kind,
|
|
10288
|
+
const marker = Promise.resolve("stop");
|
|
10289
|
+
this._pendingNegotiations.set(entry.kind, marker);
|
|
10290
|
+
void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
|
|
10291
|
+
const sync = this._invokeHandlerSync(entry);
|
|
10292
|
+
if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
|
|
9998
10293
|
}
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10294
|
+
if (hardStop) throw hardStop;
|
|
10295
|
+
return exceeded;
|
|
10296
|
+
}
|
|
10297
|
+
/**
|
|
10298
|
+
* Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
|
|
10299
|
+
* whether it decided synchronously. Returns `true` when the handler returned
|
|
10300
|
+
* a synchronous decision (already honored — an `extend` patched the limits),
|
|
10301
|
+
* or `false` when it returned a Promise (async; the caller hard-stops, since
|
|
10302
|
+
* there is no listener to resolve the negotiation). The handler is given the
|
|
10303
|
+
* full info shape (`requestDecision` plus direct `extend`/`deny`) so both
|
|
10304
|
+
* recording handlers and policy handlers work without a wired listener.
|
|
10305
|
+
*/
|
|
10306
|
+
_invokeHandlerSync(entry) {
|
|
10307
|
+
const handler = this._onThreshold;
|
|
10308
|
+
if (!handler) return false;
|
|
10309
|
+
let extendArg;
|
|
10310
|
+
const result = handler({
|
|
10311
|
+
kind: entry.kind,
|
|
10312
|
+
used: entry.used,
|
|
10313
|
+
limit: entry.limit,
|
|
10314
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
10315
|
+
// Direct hooks for synchronous policy/recording handlers.
|
|
10316
|
+
extend: (extra) => {
|
|
10317
|
+
extendArg = extra;
|
|
10318
|
+
},
|
|
10319
|
+
deny: () => {
|
|
10320
|
+
}
|
|
10321
|
+
});
|
|
10322
|
+
if (result && typeof result.then === "function") return false;
|
|
10323
|
+
if (result === "throw") return false;
|
|
10324
|
+
if (result && typeof result === "object" && "extend" in result) {
|
|
10325
|
+
extendArg = result.extend;
|
|
10326
|
+
}
|
|
10327
|
+
if (extendArg) this.patchLimits(extendArg);
|
|
10328
|
+
return true;
|
|
10329
|
+
}
|
|
10330
|
+
/**
|
|
10331
|
+
* Emit `budget.threshold_reached` and resolve to the listener's verdict.
|
|
10332
|
+
* Resolves to `'stop'` immediately when there is no listener (or no bus) so
|
|
10333
|
+
* no negotiation can hang and no fallback timer leaks. Mirrors the
|
|
10334
|
+
* coordinator watchdog's own request path so both agree on the no-listener
|
|
10335
|
+
* default.
|
|
10336
|
+
*/
|
|
10337
|
+
_busRequestDecision(entry) {
|
|
10338
|
+
const bus = this._events;
|
|
10339
|
+
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
10340
|
+
return Promise.resolve("stop");
|
|
10341
|
+
}
|
|
10342
|
+
return new Promise((resolve5) => {
|
|
10343
|
+
let resolved = false;
|
|
10344
|
+
const respond = (d) => {
|
|
10345
|
+
if (resolved) return;
|
|
10346
|
+
resolved = true;
|
|
10347
|
+
clearTimeout(fallback);
|
|
10348
|
+
resolve5(d);
|
|
10349
|
+
};
|
|
10350
|
+
const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
|
|
10351
|
+
bus.emit("budget.threshold_reached", {
|
|
10352
|
+
kind: entry.kind,
|
|
10353
|
+
used: entry.used,
|
|
10354
|
+
limit: entry.limit,
|
|
10355
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
10356
|
+
// deny() wins over a same-dispatch extend(): a listener that both grants
|
|
10357
|
+
// and denies (or two listeners disagreeing) is resolved as a stop. The
|
|
10358
|
+
// grant is deferred a microtask so a synchronous deny in the same emit
|
|
10359
|
+
// pre-empts it; async grants still resolve normally.
|
|
10360
|
+
extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
|
|
10361
|
+
deny: () => respond("stop")
|
|
10362
|
+
});
|
|
10363
|
+
});
|
|
10003
10364
|
}
|
|
10004
10365
|
/**
|
|
10005
10366
|
* Per-kind in-flight negotiation Promises. Each budget kind can have its
|
|
@@ -10019,77 +10380,33 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
10019
10380
|
* `{ extend: {} }` — keep going without patching; next overrun fires
|
|
10020
10381
|
* a fresh signal.
|
|
10021
10382
|
*/
|
|
10022
|
-
async _negotiateExtension(
|
|
10383
|
+
async _negotiateExtension(entry) {
|
|
10023
10384
|
if (!this._onThreshold) {
|
|
10024
10385
|
return "stop";
|
|
10025
10386
|
}
|
|
10026
10387
|
try {
|
|
10027
|
-
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
10028
10388
|
const result = this._onThreshold({
|
|
10029
|
-
kind:
|
|
10030
|
-
used:
|
|
10031
|
-
limit:
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
if (resolved) return;
|
|
10041
|
-
resolved = true;
|
|
10042
|
-
resolve5(d);
|
|
10043
|
-
};
|
|
10044
|
-
const fallback = setTimeout(
|
|
10045
|
-
() => respond("stop"),
|
|
10046
|
-
_SubagentBudget.DECISION_TIMEOUT_MS
|
|
10047
|
-
);
|
|
10048
|
-
for (const { kind: kind2, used, limit } of exceeded) {
|
|
10049
|
-
bus.emit("budget.threshold_reached", {
|
|
10050
|
-
kind: kind2,
|
|
10051
|
-
used,
|
|
10052
|
-
limit,
|
|
10053
|
-
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
10054
|
-
extend: (extra) => {
|
|
10055
|
-
clearTimeout(fallback);
|
|
10056
|
-
respond({ extend: extra });
|
|
10057
|
-
},
|
|
10058
|
-
deny: () => {
|
|
10059
|
-
clearTimeout(fallback);
|
|
10060
|
-
respond("stop");
|
|
10061
|
-
}
|
|
10062
|
-
});
|
|
10063
|
-
}
|
|
10064
|
-
});
|
|
10389
|
+
kind: entry.kind,
|
|
10390
|
+
used: entry.used,
|
|
10391
|
+
limit: entry.limit,
|
|
10392
|
+
// One event for THIS kind only — each exceeded kind has its own
|
|
10393
|
+
// negotiation (and its own resolve), so there is no cross-kind
|
|
10394
|
+
// first-wins drop and no O(N^2) re-emission.
|
|
10395
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
10396
|
+
extend: (extra) => {
|
|
10397
|
+
this.patchLimits(extra);
|
|
10398
|
+
},
|
|
10399
|
+
deny: () => {
|
|
10065
10400
|
}
|
|
10066
10401
|
});
|
|
10067
10402
|
if (result === "throw") return "stop";
|
|
10068
10403
|
if (result === "continue") return { extend: {} };
|
|
10069
10404
|
const decision = await result;
|
|
10070
10405
|
if (decision === "stop") return "stop";
|
|
10071
|
-
|
|
10072
|
-
if (ext.maxIterations !== void 0) {
|
|
10073
|
-
this.limits.maxIterations = ext.maxIterations;
|
|
10074
|
-
}
|
|
10075
|
-
if (ext.maxToolCalls !== void 0) {
|
|
10076
|
-
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
10077
|
-
}
|
|
10078
|
-
if (ext.maxTokens !== void 0) {
|
|
10079
|
-
this.limits.maxTokens = ext.maxTokens;
|
|
10080
|
-
}
|
|
10081
|
-
if (ext.maxCostUsd !== void 0) {
|
|
10082
|
-
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
10083
|
-
}
|
|
10084
|
-
if (ext.timeoutMs !== void 0) {
|
|
10085
|
-
this.limits.timeoutMs = ext.timeoutMs;
|
|
10086
|
-
}
|
|
10087
|
-
if (ext.idleTimeoutMs !== void 0) {
|
|
10088
|
-
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
10089
|
-
}
|
|
10406
|
+
this.patchLimits(decision.extend);
|
|
10090
10407
|
return decision;
|
|
10091
10408
|
} finally {
|
|
10092
|
-
this._pendingNegotiations.delete(kind);
|
|
10409
|
+
this._pendingNegotiations.delete(entry.kind);
|
|
10093
10410
|
}
|
|
10094
10411
|
}
|
|
10095
10412
|
recordIteration() {
|
|
@@ -10132,7 +10449,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
10132
10449
|
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
10133
10450
|
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
10134
10451
|
const elapsed = Date.now() - this.startTime;
|
|
10135
|
-
const
|
|
10452
|
+
const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
|
|
10453
|
+
const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
10136
10454
|
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
10137
10455
|
if (!wallTripped && !idleTripped) return;
|
|
10138
10456
|
void this.checkLimits(elapsed);
|
|
@@ -13640,6 +13958,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
13640
13958
|
terminating = /* @__PURE__ */ new Set();
|
|
13641
13959
|
constructor(config, options = {}) {
|
|
13642
13960
|
super();
|
|
13961
|
+
this.setMaxListeners(0);
|
|
13643
13962
|
this.coordinatorId = config.coordinatorId;
|
|
13644
13963
|
this.config = config;
|
|
13645
13964
|
this.runner = options.runner;
|
|
@@ -14034,7 +14353,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14034
14353
|
let result;
|
|
14035
14354
|
budget.start();
|
|
14036
14355
|
try {
|
|
14037
|
-
const outcome = await this.executeWithTimeout(
|
|
14356
|
+
const outcome = await this.executeWithTimeout(
|
|
14357
|
+
this.runner,
|
|
14358
|
+
task,
|
|
14359
|
+
runCtx,
|
|
14360
|
+
budget,
|
|
14361
|
+
subagent.config.preemptFraction
|
|
14362
|
+
);
|
|
14038
14363
|
result = {
|
|
14039
14364
|
subagentId,
|
|
14040
14365
|
taskId: task.id,
|
|
@@ -14061,7 +14386,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14061
14386
|
}
|
|
14062
14387
|
this.recordCompletion(result);
|
|
14063
14388
|
}
|
|
14064
|
-
async executeWithTimeout(runner, task, ctx, budget) {
|
|
14389
|
+
async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
|
|
14065
14390
|
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
14066
14391
|
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
14067
14392
|
if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
|
|
@@ -14069,8 +14394,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14069
14394
|
}
|
|
14070
14395
|
const start = Date.now();
|
|
14071
14396
|
let timer = null;
|
|
14072
|
-
let
|
|
14397
|
+
let PreemptState;
|
|
14398
|
+
((PreemptState2) => {
|
|
14399
|
+
PreemptState2["ACTIVE"] = "active";
|
|
14400
|
+
PreemptState2["LOCKED"] = "locked";
|
|
14401
|
+
})(PreemptState || (PreemptState = {}));
|
|
14402
|
+
let preemptedCeiling = null;
|
|
14403
|
+
let preemptState = "active" /* ACTIVE */;
|
|
14404
|
+
let lastGrantActivityTs = -1;
|
|
14073
14405
|
const timeoutPromise = new Promise((_, reject) => {
|
|
14406
|
+
const terminate = (kind, limit, used) => {
|
|
14407
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
14408
|
+
reject(
|
|
14409
|
+
budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
|
|
14410
|
+
);
|
|
14411
|
+
};
|
|
14074
14412
|
const armFor = (ms) => {
|
|
14075
14413
|
if (timer) clearTimeout(timer);
|
|
14076
14414
|
timer = setTimeout(onTick, Math.max(0, ms));
|
|
@@ -14079,7 +14417,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14079
14417
|
const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
14080
14418
|
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
|
|
14081
14419
|
const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
14082
|
-
const preemptRemaining = initialTimeoutMs === void 0 ||
|
|
14420
|
+
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
14083
14421
|
armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
|
|
14084
14422
|
};
|
|
14085
14423
|
const negotiateTimeout = async (used, limit) => {
|
|
@@ -14089,16 +14427,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14089
14427
|
kind: "timeout",
|
|
14090
14428
|
used,
|
|
14091
14429
|
limit,
|
|
14092
|
-
requestDecision: () =>
|
|
14093
|
-
budget._events?.
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14430
|
+
requestDecision: () => {
|
|
14431
|
+
if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
|
|
14432
|
+
return Promise.resolve("stop");
|
|
14433
|
+
}
|
|
14434
|
+
return new Promise((resolveDecision) => {
|
|
14435
|
+
let settled = false;
|
|
14436
|
+
const resolve5 = (d) => {
|
|
14437
|
+
if (settled) return;
|
|
14438
|
+
settled = true;
|
|
14439
|
+
resolveDecision(d);
|
|
14440
|
+
};
|
|
14441
|
+
const fallback = setTimeout(() => resolve5("stop"), DECISION_TIMEOUT_MS);
|
|
14442
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
14443
|
+
kind: "timeout",
|
|
14444
|
+
used,
|
|
14445
|
+
limit,
|
|
14446
|
+
// Informational: the budget's own decision deadline. Listeners may use
|
|
14447
|
+
// this to display a countdown. The coordinator does NOT enforce it —
|
|
14448
|
+
// it is the budget's own `setTimeout(fallback)` that races against
|
|
14449
|
+
// the listener's `extend()`/`deny()` call to guarantee progress.
|
|
14450
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
14451
|
+
// deny() wins over a same-dispatch extend(): defer the grant a
|
|
14452
|
+
// microtask so a synchronous deny in the same emit pre-empts it
|
|
14453
|
+
// (a listener that both grants and denies, or two listeners
|
|
14454
|
+
// disagreeing, resolves as a stop). Async grants still resolve.
|
|
14455
|
+
extend: (extra) => {
|
|
14456
|
+
clearTimeout(fallback);
|
|
14457
|
+
queueMicrotask(() => resolve5({ extend: extra }));
|
|
14458
|
+
},
|
|
14459
|
+
deny: () => {
|
|
14460
|
+
clearTimeout(fallback);
|
|
14461
|
+
resolve5("stop");
|
|
14462
|
+
}
|
|
14463
|
+
});
|
|
14100
14464
|
});
|
|
14101
|
-
}
|
|
14465
|
+
}
|
|
14102
14466
|
});
|
|
14103
14467
|
return typeof result === "string" ? result : await result;
|
|
14104
14468
|
};
|
|
@@ -14109,21 +14473,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14109
14473
|
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
14110
14474
|
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
14111
14475
|
if (idleExceeded && !wallExceeded) {
|
|
14476
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
14477
|
+
kind: "idle_timeout",
|
|
14478
|
+
used: budget.idleMs(),
|
|
14479
|
+
limit: idleLimit ?? 0,
|
|
14480
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
14481
|
+
extend: () => {
|
|
14482
|
+
},
|
|
14483
|
+
deny: () => {
|
|
14484
|
+
}
|
|
14485
|
+
});
|
|
14112
14486
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
14113
|
-
reject(new BudgetExceededError("
|
|
14487
|
+
reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
|
|
14114
14488
|
return;
|
|
14115
14489
|
}
|
|
14116
|
-
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold &&
|
|
14490
|
+
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
14491
|
+
const activityTs = Date.now() - budget.idleMs();
|
|
14492
|
+
if (activityTs <= lastGrantActivityTs) {
|
|
14493
|
+
preemptState = "locked" /* LOCKED */;
|
|
14494
|
+
preemptedCeiling = wallLimit;
|
|
14495
|
+
scheduleNext();
|
|
14496
|
+
return;
|
|
14497
|
+
}
|
|
14498
|
+
budget.setWatchdogNegotiation(wallLimit);
|
|
14117
14499
|
try {
|
|
14118
14500
|
const decision = await negotiateTimeout(elapsed, wallLimit);
|
|
14119
14501
|
if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
|
|
14120
|
-
budget.
|
|
14121
|
-
|
|
14502
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
14503
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
14504
|
+
preemptState = "active" /* ACTIVE */;
|
|
14505
|
+
preemptedCeiling = null;
|
|
14122
14506
|
} else {
|
|
14123
|
-
|
|
14507
|
+
preemptState = "locked" /* LOCKED */;
|
|
14508
|
+
preemptedCeiling = wallLimit;
|
|
14124
14509
|
}
|
|
14125
14510
|
} catch {
|
|
14126
|
-
|
|
14511
|
+
preemptState = "locked" /* LOCKED */;
|
|
14512
|
+
preemptedCeiling = wallLimit;
|
|
14513
|
+
} finally {
|
|
14514
|
+
budget.clearWatchdogNegotiation();
|
|
14127
14515
|
}
|
|
14128
14516
|
scheduleNext();
|
|
14129
14517
|
return;
|
|
@@ -14138,26 +14526,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
14138
14526
|
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
14139
14527
|
return;
|
|
14140
14528
|
}
|
|
14529
|
+
budget.setWatchdogNegotiation(limit);
|
|
14141
14530
|
try {
|
|
14142
14531
|
const decision = await negotiateTimeout(elapsed, limit);
|
|
14143
|
-
if (decision === "
|
|
14144
|
-
|
|
14532
|
+
if (decision === "throw") {
|
|
14533
|
+
terminate("timeout", limit, elapsed);
|
|
14534
|
+
return;
|
|
14535
|
+
}
|
|
14536
|
+
if (decision === "continue") {
|
|
14537
|
+
preemptState = "locked" /* LOCKED */;
|
|
14538
|
+
preemptedCeiling = wallLimit;
|
|
14145
14539
|
armFor(Math.max(1e3, limit));
|
|
14146
14540
|
return;
|
|
14147
14541
|
}
|
|
14542
|
+
if (decision === "stop") {
|
|
14543
|
+
terminate("timeout", limit, elapsed);
|
|
14544
|
+
return;
|
|
14545
|
+
}
|
|
14148
14546
|
if (decision.extend.timeoutMs !== void 0) {
|
|
14149
|
-
budget.
|
|
14150
|
-
|
|
14547
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
14548
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
14549
|
+
preemptState = "active" /* ACTIVE */;
|
|
14550
|
+
preemptedCeiling = null;
|
|
14151
14551
|
scheduleNext();
|
|
14152
14552
|
return;
|
|
14153
14553
|
}
|
|
14154
|
-
|
|
14155
|
-
|
|
14554
|
+
terminate("timeout", limit, elapsed);
|
|
14555
|
+
return;
|
|
14156
14556
|
} catch (err) {
|
|
14157
14557
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
14158
14558
|
reject(
|
|
14159
14559
|
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
14160
14560
|
);
|
|
14561
|
+
return;
|
|
14562
|
+
} finally {
|
|
14563
|
+
budget.clearWatchdogNegotiation();
|
|
14161
14564
|
}
|
|
14162
14565
|
};
|
|
14163
14566
|
scheduleNext();
|
|
@@ -18120,6 +18523,7 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
18120
18523
|
const extendCounts = /* @__PURE__ */ new Map();
|
|
18121
18524
|
let progress = 0;
|
|
18122
18525
|
let lastTimeoutProgress = -1;
|
|
18526
|
+
let lastSeenKey = null;
|
|
18123
18527
|
const unsubs = [
|
|
18124
18528
|
events.on("tool.executed", () => {
|
|
18125
18529
|
progress++;
|
|
@@ -18129,6 +18533,9 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
18129
18533
|
}),
|
|
18130
18534
|
events.on("budget.threshold_reached", (e) => {
|
|
18131
18535
|
const { kind, limit, extend, deny } = e;
|
|
18536
|
+
const key = `${kind}:${limit}`;
|
|
18537
|
+
if (key === lastSeenKey) return;
|
|
18538
|
+
lastSeenKey = key;
|
|
18132
18539
|
if (kind === "timeout" || kind === "idle_timeout") {
|
|
18133
18540
|
if (progress > lastTimeoutProgress) {
|
|
18134
18541
|
lastTimeoutProgress = progress;
|
|
@@ -22343,11 +22750,12 @@ function createContextManagerTool(opts = {}) {
|
|
|
22343
22750
|
const applyMessages = (next) => {
|
|
22344
22751
|
const repaired = repairToolUseAdjacency(next);
|
|
22345
22752
|
const finalMessages = repaired.messages;
|
|
22753
|
+
if (finalMessages === messages) return repaired.report;
|
|
22346
22754
|
if (ctx.state) {
|
|
22347
22755
|
ctx.state.replaceMessages(finalMessages);
|
|
22348
22756
|
} else {
|
|
22349
22757
|
messages.length = 0;
|
|
22350
|
-
messages.
|
|
22758
|
+
messages.push(...finalMessages);
|
|
22351
22759
|
}
|
|
22352
22760
|
return repaired.report;
|
|
22353
22761
|
};
|
|
@@ -22422,9 +22830,15 @@ function createContextManagerTool(opts = {}) {
|
|
|
22422
22830
|
}
|
|
22423
22831
|
const report = await opts.compactor.compact(ctx);
|
|
22424
22832
|
ctx.clearFileTracking();
|
|
22425
|
-
|
|
22426
|
-
|
|
22427
|
-
|
|
22833
|
+
let repaired = report.repaired;
|
|
22834
|
+
let afterTokens;
|
|
22835
|
+
if (!ctx.state) {
|
|
22836
|
+
const repair = applyMessages([...ctx.messages]);
|
|
22837
|
+
repaired = report.repaired ?? (repair.changed ? repair : void 0);
|
|
22838
|
+
afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
|
|
22839
|
+
} else {
|
|
22840
|
+
afterTokens = report.after;
|
|
22841
|
+
}
|
|
22428
22842
|
const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
|
|
22429
22843
|
const repairedSomething = !!report.repaired;
|
|
22430
22844
|
if (reduced || repairedSomething) {
|