@wrongstack/core 0.1.1 → 0.1.3

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.
@@ -1,9 +1,10 @@
1
- import * as fs4 from 'fs';
1
+ import * as fs5 from 'fs';
2
2
  import * as path2 from 'path';
3
3
  import * as fsp from 'fs/promises';
4
+ import * as crypto2 from 'crypto';
4
5
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
5
- import * as os from 'os';
6
6
  import { EventEmitter } from 'events';
7
+ import * as os from 'os';
7
8
 
8
9
  // src/defaults/logger.ts
9
10
 
@@ -61,7 +62,7 @@ var DefaultLogger = class _DefaultLogger {
61
62
  this.pretty = opts.pretty ?? true;
62
63
  if (this.file) {
63
64
  try {
64
- fs4.mkdirSync(path2.dirname(this.file), { recursive: true });
65
+ fs5.mkdirSync(path2.dirname(this.file), { recursive: true });
65
66
  } catch {
66
67
  }
67
68
  }
@@ -100,7 +101,7 @@ var DefaultLogger = class _DefaultLogger {
100
101
  }
101
102
  if (this.file) {
102
103
  try {
103
- fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
104
+ fs5.appendFileSync(this.file, `${JSON.stringify(entry)}
104
105
  `);
105
106
  } catch {
106
107
  }
@@ -148,7 +149,7 @@ var DefaultPathResolver = class {
148
149
  while (dir !== root) {
149
150
  for (const marker of PROJECT_MARKERS) {
150
151
  try {
151
- fs4.accessSync(path2.join(dir, marker));
152
+ fs5.accessSync(path2.join(dir, marker));
152
153
  return dir;
153
154
  } catch {
154
155
  }
@@ -163,7 +164,7 @@ var DefaultPathResolver = class {
163
164
  const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
164
165
  let real;
165
166
  try {
166
- real = fs4.realpathSync(abs);
167
+ real = fs5.realpathSync(abs);
167
168
  } catch {
168
169
  real = path2.normalize(abs);
169
170
  }
@@ -187,248 +188,6 @@ var DefaultPathResolver = class {
187
188
  }
188
189
  };
189
190
 
190
- // src/defaults/secret-scrubber.ts
191
- var PATTERNS = [
192
- // Anchored at the start where possible so partial matches inside larger
193
- // strings don't trigger false positives.
194
- { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
195
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
196
- { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
197
- { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
198
- { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
199
- { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
200
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
201
- { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
202
- { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
203
- {
204
- type: "jwt",
205
- // Anchored: look for literal "eyJ" which is unambiguous for JWT header
206
- regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
207
- },
208
- {
209
- type: "private_key",
210
- // Anchored: start must be BEGIN, end must be END with no extra dashes after END
211
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
212
- },
213
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
214
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
215
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
216
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
217
- { type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
218
- {
219
- type: "high_entropy_env",
220
- // Value-side word boundary + length gate to avoid matching short random strings
221
- 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
222
- }
223
- ];
224
- var DefaultSecretScrubber = class {
225
- scrub(text) {
226
- if (!text) return text;
227
- let out = text;
228
- for (const p of PATTERNS) {
229
- out = out.replace(p.regex, (_match, group1, group2) => {
230
- if (p.type === "high_entropy_env" && group1 && group2) {
231
- return `${group1}=[REDACTED:${p.type}]`;
232
- }
233
- return `[REDACTED:${p.type}]`;
234
- });
235
- }
236
- return out;
237
- }
238
- scrubObject(obj) {
239
- const seen = /* @__PURE__ */ new WeakSet();
240
- const visit = (v) => {
241
- if (typeof v === "string") return this.scrub(v);
242
- if (v === null || typeof v !== "object") return v;
243
- if (seen.has(v)) return v;
244
- seen.add(v);
245
- if (Array.isArray(v)) return v.map(visit);
246
- const out = {};
247
- for (const [k, val] of Object.entries(v)) {
248
- out[k] = visit(val);
249
- }
250
- return out;
251
- };
252
- return visit(obj);
253
- }
254
- };
255
-
256
- // src/types/provider.ts
257
- var ProviderError = class extends Error {
258
- status;
259
- retryable;
260
- providerId;
261
- body;
262
- cause;
263
- constructor(message, status, retryable, providerId, opts = {}) {
264
- super(message);
265
- this.name = "ProviderError";
266
- this.status = status;
267
- this.retryable = retryable;
268
- this.providerId = providerId;
269
- this.body = opts.body;
270
- this.cause = opts.cause;
271
- }
272
- /**
273
- * Render a one-line, user-facing description. Designed for the CLI/TUI
274
- * status line and the agent's retry warning. Avoids dumping raw JSON
275
- * (which is what users see today when a 529 lands and the log message
276
- * includes the full `{"type":"error",...}` body).
277
- *
278
- * Examples:
279
- * "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
280
- * "openai rate limited (429): Retry after 12s"
281
- * "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
282
- * "groq HTTP 500 (server error)"
283
- */
284
- describe() {
285
- const kind = describeStatus(this.status, this.body?.type);
286
- const head = `${this.providerId} ${kind}`;
287
- const detail = this.body?.message?.trim();
288
- const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
289
- if (detail && detail.length > 0) {
290
- return `${head}: ${truncate(detail, 240)}${reqId}`;
291
- }
292
- return `${head}${reqId}`;
293
- }
294
- };
295
- function describeStatus(status, type) {
296
- if (status === 0) return "network error";
297
- if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
298
- if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
299
- if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
300
- if (type === "permission_error" || status === 403) return `forbidden (${status})`;
301
- if (type === "not_found_error" || status === 404) return `not found (${status})`;
302
- if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
303
- if (status === 408) return `timeout (${status})`;
304
- if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
305
- if (type) return `${type} (${status})`;
306
- return `HTTP ${status}`;
307
- }
308
- function truncate(s, n) {
309
- return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
310
- }
311
-
312
- // src/defaults/retry-policy.ts
313
- var DefaultRetryPolicy = class {
314
- shouldRetry(err, attempt) {
315
- if (err instanceof ProviderError) {
316
- if (!err.retryable) return false;
317
- return attempt < this.maxAttempts(err);
318
- }
319
- const msg = err.message ?? "";
320
- const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
321
- if (isNetwork) return attempt < 2;
322
- return false;
323
- }
324
- maxAttempts(err) {
325
- if (err instanceof ProviderError) {
326
- if (err.status === 429) return 5;
327
- if (err.status === 529) return 3;
328
- if (err.status >= 500) return 3;
329
- return 0;
330
- }
331
- return 2;
332
- }
333
- delayMs(attempt) {
334
- const base = 1e3;
335
- const exp = base * 2 ** attempt;
336
- const jitter = Math.random() * base;
337
- return Math.min(3e4, exp + jitter);
338
- }
339
- };
340
-
341
- // src/defaults/error-handler.ts
342
- function buildRecoveryStrategies(opts) {
343
- return [
344
- {
345
- label: "context_overflow_reduce",
346
- compactor: opts?.compactor,
347
- async attempt(err, ctx) {
348
- if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
349
- if (this.compactor) {
350
- try {
351
- const report = await this.compactor.compact(ctx, { aggressive: true });
352
- if (report.after < report.before) {
353
- return {
354
- content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
355
- stopReason: "end_turn",
356
- usage: { input: 0, output: 0 },
357
- model: ctx.model
358
- };
359
- }
360
- } catch {
361
- }
362
- }
363
- return null;
364
- }
365
- return null;
366
- }
367
- },
368
- {
369
- label: "rate_limit_backoff",
370
- async attempt(err, ctx) {
371
- if (err instanceof ProviderError && err.status === 429) {
372
- const delayMs = err.body?.retryAfterMs ?? 5e3;
373
- const delay = Math.max(1e3, Math.min(delayMs, 6e4));
374
- await new Promise((r) => setTimeout(r, delay));
375
- return {
376
- content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
377
- stopReason: "end_turn",
378
- usage: { input: 0, output: 0 },
379
- model: ctx.model
380
- };
381
- }
382
- return null;
383
- }
384
- },
385
- {
386
- label: "downgrade_model",
387
- async attempt(err, ctx) {
388
- if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
389
- return null;
390
- }
391
- return null;
392
- }
393
- }
394
- ];
395
- }
396
- var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
397
- var DefaultErrorHandler = class {
398
- strategies;
399
- constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
400
- this.strategies = strategies;
401
- }
402
- classify(err) {
403
- if (err instanceof DOMException && err.name === "AbortError") {
404
- return { kind: "abort", retryable: false };
405
- }
406
- if (err instanceof Error && err.name === "AbortError") {
407
- return { kind: "abort", retryable: false };
408
- }
409
- if (err instanceof ProviderError) {
410
- if (err.status === 429) return { kind: "rate_limit", retryable: true };
411
- if (err.status === 529) return { kind: "overloaded", retryable: true };
412
- if (err.status >= 500) return { kind: "server", retryable: true };
413
- if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
414
- return { kind: "context_overflow", retryable: false };
415
- }
416
- if (err.status >= 400) return { kind: "client", retryable: false };
417
- }
418
- if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
419
- return { kind: "network", retryable: true };
420
- }
421
- return { kind: "unknown", retryable: false };
422
- }
423
- async recover(err, ctx) {
424
- for (const strategy of this.strategies) {
425
- const result = await strategy.attempt(err, ctx);
426
- if (result !== null) return result;
427
- }
428
- return null;
429
- }
430
- };
431
-
432
191
  // src/defaults/token-counter.ts
433
192
  var DefaultTokenCounter = class {
434
193
  input = 0;
@@ -547,8 +306,11 @@ async function atomicWrite(targetPath, content, opts = {}) {
547
306
  }
548
307
  try {
549
308
  const fh = await fsp.open(tmp, "r+");
550
- await fh.sync();
551
- await fh.close();
309
+ try {
310
+ await fh.sync();
311
+ } finally {
312
+ await fh.close();
313
+ }
552
314
  } catch {
553
315
  }
554
316
  let mode;
@@ -863,130 +625,7 @@ function userInputTitle(content) {
863
625
  const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
864
626
  return (text || "(non-text input)").slice(0, 60);
865
627
  }
866
- var LOCK_FILE = "active.json";
867
- var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
868
- var RecoveryLock = class {
869
- file;
870
- pid;
871
- hostname;
872
- maxAgeMs;
873
- sessionStore;
874
- probe;
875
- constructor(opts) {
876
- this.file = path2.join(opts.dir, LOCK_FILE);
877
- this.pid = opts.pid ?? process.pid;
878
- this.hostname = opts.hostname ?? os.hostname();
879
- this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
880
- this.sessionStore = opts.sessionStore;
881
- this.probe = opts.isPidAlive ?? defaultIsPidAlive;
882
- }
883
- /**
884
- * Examine the lockfile and decide whether it represents an abandoned
885
- * session. Returns `null` if the file is missing, points to a live
886
- * instance, references a clean-closed session, is too old, or is
887
- * malformed. Otherwise returns enough detail to prompt the user.
888
- *
889
- * Important: this is a read-only check. We never delete an active
890
- * lock from here — if another wstack instance is alive, the caller
891
- * should bail or run with a fresh session instead.
892
- */
893
- async checkAbandoned() {
894
- const lock = await this.readLock();
895
- if (!lock) return null;
896
- const ageMs = Date.now() - new Date(lock.startedAt).getTime();
897
- if (Number.isNaN(ageMs) || ageMs < 0) {
898
- return null;
899
- }
900
- if (ageMs > this.maxAgeMs) return null;
901
- if (lock.hostname === this.hostname && this.probe(lock.pid)) {
902
- return null;
903
- }
904
- let messageCount = 0;
905
- if (this.sessionStore) {
906
- try {
907
- const data = await this.sessionStore.load(lock.sessionId);
908
- const closed = data.events.some((e) => e.type === "session_end");
909
- if (closed) return null;
910
- messageCount = data.messages.length;
911
- } catch {
912
- return null;
913
- }
914
- }
915
- return {
916
- sessionId: lock.sessionId,
917
- pid: lock.pid,
918
- startedAt: lock.startedAt,
919
- ageMs,
920
- messageCount
921
- };
922
- }
923
- /**
924
- * Claim the lock for the given session. Overwrites any existing lock
925
- * — the caller should have already handled abandonment (via
926
- * `checkAbandoned`) before calling this.
927
- */
928
- async write(sessionId) {
929
- await ensureDir(path2.dirname(this.file));
930
- const lock = {
931
- v: 1,
932
- sessionId,
933
- pid: this.pid,
934
- hostname: this.hostname,
935
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
936
- };
937
- const tmp = `${this.file}.tmp`;
938
- await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
939
- await fsp.rename(tmp, this.file);
940
- }
941
- /**
942
- * Release the lock. Idempotent — silently succeeds if the file is
943
- * already gone (e.g. someone else cleared it, or the directory was
944
- * wiped).
945
- */
946
- async clear() {
947
- try {
948
- await fsp.unlink(this.file);
949
- } catch (err) {
950
- const code = err.code;
951
- if (code === "ENOENT") return;
952
- throw err;
953
- }
954
- }
955
- async readLock() {
956
- let raw;
957
- try {
958
- raw = await fsp.readFile(this.file, "utf8");
959
- } catch (err) {
960
- const code = err.code;
961
- if (code === "ENOENT") return null;
962
- return null;
963
- }
964
- try {
965
- const parsed = JSON.parse(raw);
966
- if (!isLockFile(parsed)) return null;
967
- return parsed;
968
- } catch {
969
- return null;
970
- }
971
- }
972
- };
973
- function isLockFile(v) {
974
- if (typeof v !== "object" || v === null) return false;
975
- const o = v;
976
- return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
977
- }
978
- function defaultIsPidAlive(pid) {
979
- if (!Number.isInteger(pid) || pid <= 0) return false;
980
- try {
981
- process.kill(pid, 0);
982
- return true;
983
- } catch (err) {
984
- const code = err.code;
985
- if (code === "EPERM") return true;
986
- return false;
987
- }
988
- }
989
- var QueueStore = class {
628
+ var QueueStore = class {
990
629
  file;
991
630
  constructor(opts) {
992
631
  this.file = path2.join(opts.dir, "queue.json");
@@ -1148,92 +787,291 @@ function mergeAdjacentText(blocks) {
1148
787
  }
1149
788
  return out;
1150
789
  }
1151
-
1152
- // src/types/secret-vault.ts
1153
- var ENCRYPTED_PREFIX = "enc:v1:";
1154
-
1155
- // src/defaults/secret-vault.ts
1156
- var KEY_BYTES = 32;
1157
- var IV_BYTES = 12;
1158
- var TAG_BYTES = 16;
1159
- var ALGO = "aes-256-gcm";
1160
- var DefaultSecretVault = class {
1161
- keyFile;
1162
- key;
790
+ var MAX_BYTES_TOTAL = 32e3;
791
+ var DefaultMemoryStore = class {
792
+ files;
1163
793
  constructor(opts) {
1164
- this.keyFile = opts.keyFile;
1165
- }
1166
- isEncrypted(value) {
1167
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
794
+ this.files = {
795
+ "project-agents": opts.paths.inProjectAgentsFile,
796
+ "project-memory": opts.paths.projectMemory,
797
+ "user-memory": opts.paths.globalMemory
798
+ };
1168
799
  }
1169
- encrypt(plaintext) {
1170
- if (this.isEncrypted(plaintext)) return plaintext;
1171
- const key = this.loadOrCreateKey();
1172
- const iv = randomBytes(IV_BYTES);
1173
- const cipher = createCipheriv(ALGO, key, iv);
1174
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1175
- const tag = cipher.getAuthTag();
1176
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
800
+ async readAll() {
801
+ const parts = [];
802
+ for (const scope of ["project-agents", "project-memory", "user-memory"]) {
803
+ const body = await this.read(scope);
804
+ if (body.trim()) parts.push(`## ${labelOf(scope)}
805
+
806
+ ${body.trim()}`);
807
+ }
808
+ return parts.join("\n\n");
1177
809
  }
1178
- decrypt(value) {
1179
- if (!this.isEncrypted(value)) return value;
1180
- const rest = value.slice(ENCRYPTED_PREFIX.length);
1181
- const parts = rest.split(":");
1182
- if (parts.length !== 3) {
1183
- throw new Error("SecretVault: malformed encrypted value");
810
+ async read(scope) {
811
+ try {
812
+ return await fsp.readFile(this.files[scope], "utf8");
813
+ } catch {
814
+ return "";
1184
815
  }
1185
- const [ivB64, tagB64, ctB64] = parts;
1186
- const iv = Buffer.from(ivB64, "base64");
1187
- const tag = Buffer.from(tagB64, "base64");
1188
- const ct = Buffer.from(ctB64, "base64");
1189
- if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
1190
- if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
1191
- const key = this.loadOrCreateKey();
1192
- const decipher = createDecipheriv(ALGO, key, iv);
1193
- decipher.setAuthTag(tag);
1194
- const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
1195
- return pt.toString("utf8");
1196
816
  }
1197
- loadOrCreateKey() {
1198
- if (this.key) return this.key;
817
+ async remember(text, scope = "project-memory") {
818
+ const file = this.files[scope];
819
+ await ensureDir(path2.dirname(file));
820
+ let existing = "";
1199
821
  try {
1200
- const buf = fs4.readFileSync(this.keyFile);
1201
- if (buf.length !== KEY_BYTES) {
1202
- throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
1203
- }
1204
- this.key = buf;
1205
- return this.key;
1206
- } catch (err) {
1207
- if (err.code !== "ENOENT") throw err;
822
+ existing = await fsp.readFile(file, "utf8");
823
+ } catch {
1208
824
  }
1209
- fs4.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
1210
- const key = randomBytes(KEY_BYTES);
825
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
826
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
827
+ const entry = `
828
+ - [${ts}] ${id} ${text.replace(/\n/g, " ")}
829
+ `;
830
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
831
+ ${entry}`;
832
+ await atomicWrite(file, next);
833
+ const buf = Buffer.byteLength(next, "utf8");
834
+ if (buf > MAX_BYTES_TOTAL) {
835
+ await this.consolidate(scope);
836
+ }
837
+ }
838
+ async forget(query, scope = "project-memory") {
839
+ const file = this.files[scope];
840
+ let existing;
1211
841
  try {
1212
- fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
1213
- } catch (err) {
1214
- if (err.code !== "EEXIST") throw err;
1215
- const buf = fs4.readFileSync(this.keyFile);
1216
- if (buf.length !== KEY_BYTES) {
1217
- throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
842
+ existing = await fsp.readFile(file, "utf8");
843
+ } catch {
844
+ return 0;
845
+ }
846
+ const needle = query.toLowerCase();
847
+ const idMatcher = /mem_\d+_\w+/;
848
+ let removed = 0;
849
+ const lines = existing.split("\n").filter((line) => {
850
+ const trimmed = line.trim();
851
+ if (!trimmed.startsWith("- ")) return true;
852
+ if (idMatcher.test(query)) {
853
+ const afterBracket = trimmed.indexOf("] ");
854
+ if (afterBracket !== -1) {
855
+ const afterTs = trimmed.slice(afterBracket + 2);
856
+ const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
857
+ if (entryIdMatch && entryIdMatch[0] === query) {
858
+ removed++;
859
+ return false;
860
+ }
861
+ }
1218
862
  }
1219
- this.key = buf;
1220
- return this.key;
863
+ if (trimmed.toLowerCase().includes(needle)) {
864
+ removed++;
865
+ return false;
866
+ }
867
+ return true;
868
+ });
869
+ if (removed > 0) {
870
+ await atomicWrite(file, lines.join("\n"));
1221
871
  }
1222
- this.key = key;
1223
- return key;
872
+ return removed;
1224
873
  }
1225
- };
1226
- function decryptConfigSecrets(cfg, vault) {
1227
- return walk(cfg, vault, (v) => vault.decrypt(v));
1228
- }
1229
- function encryptConfigSecrets(cfg, vault) {
1230
- return walk(cfg, vault, (v) => vault.encrypt(v));
1231
- }
1232
- function walk(node, vault, transform) {
1233
- if (node === null || node === void 0) return node;
1234
- if (typeof node !== "object") return node;
1235
- if (Array.isArray(node)) {
1236
- return node.map((item) => walk(item, vault, transform));
874
+ async consolidate(scope) {
875
+ const file = this.files[scope];
876
+ let existing;
877
+ try {
878
+ existing = await fsp.readFile(file, "utf8");
879
+ } catch {
880
+ return;
881
+ }
882
+ const seen = /* @__PURE__ */ new Set();
883
+ const lines = existing.split("\n").filter((line) => {
884
+ const trimmed = line.trim();
885
+ if (!trimmed.startsWith("- ")) return true;
886
+ const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
887
+ if (seen.has(norm)) return false;
888
+ seen.add(norm);
889
+ return true;
890
+ });
891
+ const next = lines.join("\n");
892
+ try {
893
+ await atomicWrite(file, next);
894
+ } catch {
895
+ return;
896
+ }
897
+ const backup = `${file}.bak.${Date.now()}`;
898
+ try {
899
+ await fsp.copyFile(file, backup);
900
+ } catch {
901
+ }
902
+ }
903
+ async clear(scope) {
904
+ if (scope) {
905
+ await atomicWrite(this.files[scope], "");
906
+ } else {
907
+ for (const s of ["project-agents", "project-memory", "user-memory"]) {
908
+ await atomicWrite(this.files[s], "");
909
+ }
910
+ }
911
+ }
912
+ };
913
+ function labelOf(scope) {
914
+ switch (scope) {
915
+ case "project-agents":
916
+ return "Project AGENTS.md";
917
+ case "project-memory":
918
+ return "Project memory";
919
+ case "user-memory":
920
+ return "User memory";
921
+ }
922
+ }
923
+
924
+ // src/defaults/secret-scrubber.ts
925
+ var PATTERNS = [
926
+ // Anchored at the start where possible so partial matches inside larger
927
+ // strings don't trigger false positives.
928
+ { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
929
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
930
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
931
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
932
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
933
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
934
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
935
+ { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
936
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
937
+ {
938
+ type: "jwt",
939
+ // Anchored: look for literal "eyJ" which is unambiguous for JWT header
940
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
941
+ },
942
+ {
943
+ type: "private_key",
944
+ // Anchored: start must be BEGIN, end must be END with no extra dashes after END
945
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
946
+ },
947
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
948
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
949
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
950
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
951
+ { type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
952
+ {
953
+ type: "high_entropy_env",
954
+ // Value-side word boundary + length gate to avoid matching short random strings
955
+ 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
+ }
957
+ ];
958
+ var DefaultSecretScrubber = class {
959
+ scrub(text) {
960
+ if (!text) return text;
961
+ let out = text;
962
+ for (const p of PATTERNS) {
963
+ out = out.replace(p.regex, (_match, group1, group2) => {
964
+ if (p.type === "high_entropy_env" && group1 && group2) {
965
+ return `${group1}=[REDACTED:${p.type}]`;
966
+ }
967
+ return `[REDACTED:${p.type}]`;
968
+ });
969
+ }
970
+ return out;
971
+ }
972
+ scrubObject(obj) {
973
+ const seen = /* @__PURE__ */ new WeakSet();
974
+ const visit = (v) => {
975
+ if (typeof v === "string") return this.scrub(v);
976
+ if (v === null || typeof v !== "object") return v;
977
+ if (seen.has(v)) return v;
978
+ seen.add(v);
979
+ if (Array.isArray(v)) return v.map(visit);
980
+ const out = {};
981
+ for (const [k, val] of Object.entries(v)) {
982
+ out[k] = visit(val);
983
+ }
984
+ return out;
985
+ };
986
+ return visit(obj);
987
+ }
988
+ };
989
+
990
+ // src/types/secret-vault.ts
991
+ var ENCRYPTED_PREFIX = "enc:v1:";
992
+
993
+ // src/defaults/secret-vault.ts
994
+ var KEY_BYTES = 32;
995
+ var IV_BYTES = 12;
996
+ var TAG_BYTES = 16;
997
+ var ALGO = "aes-256-gcm";
998
+ var DefaultSecretVault = class {
999
+ keyFile;
1000
+ key;
1001
+ constructor(opts) {
1002
+ this.keyFile = opts.keyFile;
1003
+ }
1004
+ isEncrypted(value) {
1005
+ return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
1006
+ }
1007
+ encrypt(plaintext) {
1008
+ if (this.isEncrypted(plaintext)) return plaintext;
1009
+ const key = this.loadOrCreateKey();
1010
+ const iv = randomBytes(IV_BYTES);
1011
+ const cipher = createCipheriv(ALGO, key, iv);
1012
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1013
+ const tag = cipher.getAuthTag();
1014
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
1015
+ }
1016
+ decrypt(value) {
1017
+ if (!this.isEncrypted(value)) return value;
1018
+ const rest = value.slice(ENCRYPTED_PREFIX.length);
1019
+ const parts = rest.split(":");
1020
+ if (parts.length !== 3) {
1021
+ throw new Error("SecretVault: malformed encrypted value");
1022
+ }
1023
+ const [ivB64, tagB64, ctB64] = parts;
1024
+ const iv = Buffer.from(ivB64, "base64");
1025
+ const tag = Buffer.from(tagB64, "base64");
1026
+ const ct = Buffer.from(ctB64, "base64");
1027
+ if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
1028
+ if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
1029
+ const key = this.loadOrCreateKey();
1030
+ const decipher = createDecipheriv(ALGO, key, iv);
1031
+ decipher.setAuthTag(tag);
1032
+ const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
1033
+ return pt.toString("utf8");
1034
+ }
1035
+ loadOrCreateKey() {
1036
+ if (this.key) return this.key;
1037
+ try {
1038
+ const buf = fs5.readFileSync(this.keyFile);
1039
+ if (buf.length !== KEY_BYTES) {
1040
+ throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
1041
+ }
1042
+ this.key = buf;
1043
+ return this.key;
1044
+ } catch (err) {
1045
+ if (err.code !== "ENOENT") throw err;
1046
+ }
1047
+ fs5.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
1048
+ const key = randomBytes(KEY_BYTES);
1049
+ try {
1050
+ fs5.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
1051
+ } catch (err) {
1052
+ if (err.code !== "EEXIST") throw err;
1053
+ const buf = fs5.readFileSync(this.keyFile);
1054
+ if (buf.length !== KEY_BYTES) {
1055
+ throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
1056
+ }
1057
+ this.key = buf;
1058
+ return this.key;
1059
+ }
1060
+ this.key = key;
1061
+ return key;
1062
+ }
1063
+ };
1064
+ function decryptConfigSecrets(cfg, vault) {
1065
+ return walk(cfg, vault, (v) => vault.decrypt(v));
1066
+ }
1067
+ function encryptConfigSecrets(cfg, vault) {
1068
+ return walk(cfg, vault, (v) => vault.encrypt(v));
1069
+ }
1070
+ function walk(node, vault, transform) {
1071
+ if (node === null || node === void 0) return node;
1072
+ if (typeof node !== "object") return node;
1073
+ if (Array.isArray(node)) {
1074
+ return node.map((item) => walk(item, vault, transform));
1237
1075
  }
1238
1076
  const out = {};
1239
1077
  for (const [k, v] of Object.entries(node)) {
@@ -1324,139 +1162,6 @@ function deepMerge(a, b) {
1324
1162
  }
1325
1163
  return out;
1326
1164
  }
1327
- var MAX_BYTES_TOTAL = 32e3;
1328
- var DefaultMemoryStore = class {
1329
- files;
1330
- constructor(opts) {
1331
- this.files = {
1332
- "project-agents": opts.paths.inProjectAgentsFile,
1333
- "project-memory": opts.paths.projectMemory,
1334
- "user-memory": opts.paths.globalMemory
1335
- };
1336
- }
1337
- async readAll() {
1338
- const parts = [];
1339
- for (const scope of ["project-agents", "project-memory", "user-memory"]) {
1340
- const body = await this.read(scope);
1341
- if (body.trim()) parts.push(`## ${labelOf(scope)}
1342
-
1343
- ${body.trim()}`);
1344
- }
1345
- return parts.join("\n\n");
1346
- }
1347
- async read(scope) {
1348
- try {
1349
- return await fsp.readFile(this.files[scope], "utf8");
1350
- } catch {
1351
- return "";
1352
- }
1353
- }
1354
- async remember(text, scope = "project-memory") {
1355
- const file = this.files[scope];
1356
- await ensureDir(path2.dirname(file));
1357
- let existing = "";
1358
- try {
1359
- existing = await fsp.readFile(file, "utf8");
1360
- } catch {
1361
- }
1362
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1363
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
1364
- const entry = `
1365
- - [${ts}] ${id} ${text.replace(/\n/g, " ")}
1366
- `;
1367
- const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
1368
- ${entry}`;
1369
- await atomicWrite(file, next);
1370
- const buf = Buffer.byteLength(next, "utf8");
1371
- if (buf > MAX_BYTES_TOTAL) {
1372
- await this.consolidate(scope);
1373
- }
1374
- }
1375
- async forget(query, scope = "project-memory") {
1376
- const file = this.files[scope];
1377
- let existing;
1378
- try {
1379
- existing = await fsp.readFile(file, "utf8");
1380
- } catch {
1381
- return 0;
1382
- }
1383
- const needle = query.toLowerCase();
1384
- const idMatcher = /mem_\d+_\w+/;
1385
- let removed = 0;
1386
- const lines = existing.split("\n").filter((line) => {
1387
- const trimmed = line.trim();
1388
- if (!trimmed.startsWith("- ")) return true;
1389
- if (idMatcher.test(query)) {
1390
- const afterBracket = trimmed.indexOf("] ");
1391
- if (afterBracket !== -1) {
1392
- const afterTs = trimmed.slice(afterBracket + 2);
1393
- const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
1394
- if (entryIdMatch && entryIdMatch[0] === query) {
1395
- removed++;
1396
- return false;
1397
- }
1398
- }
1399
- }
1400
- if (trimmed.toLowerCase().includes(needle)) {
1401
- removed++;
1402
- return false;
1403
- }
1404
- return true;
1405
- });
1406
- if (removed > 0) {
1407
- await atomicWrite(file, lines.join("\n"));
1408
- }
1409
- return removed;
1410
- }
1411
- async consolidate(scope) {
1412
- const file = this.files[scope];
1413
- let existing;
1414
- try {
1415
- existing = await fsp.readFile(file, "utf8");
1416
- } catch {
1417
- return;
1418
- }
1419
- const seen = /* @__PURE__ */ new Set();
1420
- const lines = existing.split("\n").filter((line) => {
1421
- const trimmed = line.trim();
1422
- if (!trimmed.startsWith("- ")) return true;
1423
- const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
1424
- if (seen.has(norm)) return false;
1425
- seen.add(norm);
1426
- return true;
1427
- });
1428
- const next = lines.join("\n");
1429
- try {
1430
- await atomicWrite(file, next);
1431
- } catch {
1432
- return;
1433
- }
1434
- const backup = `${file}.bak.${Date.now()}`;
1435
- try {
1436
- await fsp.copyFile(file, backup);
1437
- } catch {
1438
- }
1439
- }
1440
- async clear(scope) {
1441
- if (scope) {
1442
- await atomicWrite(this.files[scope], "");
1443
- } else {
1444
- for (const s of ["project-agents", "project-memory", "user-memory"]) {
1445
- await atomicWrite(this.files[s], "");
1446
- }
1447
- }
1448
- }
1449
- };
1450
- function labelOf(scope) {
1451
- switch (scope) {
1452
- case "project-agents":
1453
- return "Project AGENTS.md";
1454
- case "project-memory":
1455
- return "Project memory";
1456
- case "user-memory":
1457
- return "User memory";
1458
- }
1459
- }
1460
1165
 
1461
1166
  // src/utils/glob-match.ts
1462
1167
  function escapeRegex(s) {
@@ -1632,118 +1337,363 @@ var DefaultPermissionPolicy = class {
1632
1337
  return void 0;
1633
1338
  }
1634
1339
  };
1635
- var DefaultSkillLoader = class {
1636
- dirs;
1637
- cache;
1340
+
1341
+ // src/types/errors.ts
1342
+ var WrongStackError = class extends Error {
1343
+ code;
1344
+ subsystem;
1345
+ severity;
1346
+ recoverable;
1347
+ context;
1638
1348
  constructor(opts) {
1639
- this.dirs = [
1640
- { dir: opts.paths.inProjectSkills, source: "project" },
1641
- { dir: opts.paths.globalSkills, source: "user" }
1642
- ];
1643
- if (opts.bundledDir) {
1644
- this.dirs.push({ dir: opts.bundledDir, source: "bundled" });
1645
- }
1349
+ super(opts.message, { cause: opts.cause });
1350
+ this.name = "WrongStackError";
1351
+ this.code = opts.code;
1352
+ this.subsystem = opts.subsystem;
1353
+ this.severity = opts.severity ?? "error";
1354
+ this.recoverable = opts.recoverable ?? false;
1355
+ this.context = opts.context;
1646
1356
  }
1647
- async list() {
1648
- if (this.cache) return this.cache;
1649
- const found = [];
1650
- const seen = /* @__PURE__ */ new Set();
1651
- for (const { dir, source } of this.dirs) {
1652
- try {
1653
- const entries = await fsp.readdir(dir, { withFileTypes: true });
1654
- for (const e of entries) {
1655
- if (!e.isDirectory()) continue;
1656
- const skillFile = path2.join(dir, e.name, "SKILL.md");
1657
- try {
1658
- const raw = await fsp.readFile(skillFile, "utf8");
1659
- const meta = parseFrontmatter(raw);
1660
- if (!meta.name || !meta.description) continue;
1661
- if (seen.has(meta.name)) continue;
1662
- seen.add(meta.name);
1663
- found.push({
1664
- name: meta.name,
1665
- description: meta.description,
1666
- version: meta.version,
1667
- path: skillFile,
1668
- source
1669
- });
1670
- } catch {
1671
- }
1672
- }
1673
- } catch {
1674
- }
1675
- }
1676
- this.cache = found;
1677
- return found;
1357
+ /**
1358
+ * Render a one-line user-facing description.
1359
+ * Subclasses should override for domain-specific formatting.
1360
+ */
1361
+ describe() {
1362
+ const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1363
+ return `${this.code}: ${this.message}${ctx}`;
1678
1364
  }
1679
- async find(name) {
1680
- const all = await this.list();
1681
- return all.find((s) => s.name === name);
1365
+ };
1366
+ function formatContext(ctx) {
1367
+ const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1368
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1369
+ }
1370
+ var AgentError = class extends WrongStackError {
1371
+ constructor(opts) {
1372
+ super({
1373
+ message: opts.message,
1374
+ code: opts.code,
1375
+ subsystem: "agent",
1376
+ severity: opts.code === "AGENT_ABORTED" ? "warning" : "error",
1377
+ recoverable: opts.recoverable ?? opts.code === "AGENT_ITERATION_LIMIT",
1378
+ context: opts.context,
1379
+ cause: opts.cause
1380
+ });
1381
+ this.name = "AgentError";
1682
1382
  }
1683
- async manifestText() {
1684
- const skills = await this.list();
1685
- if (skills.length === 0) return "";
1686
- const lines = ["## Available skills"];
1687
- for (const s of skills) {
1688
- lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
1689
- lines.push(` Path: ${s.path}`);
1690
- }
1691
- return lines.join("\n");
1383
+ };
1384
+ function toWrongStackError(err, code = "AGENT_RUN_FAILED") {
1385
+ if (err instanceof WrongStackError) return err;
1386
+ const message = err instanceof Error ? err.message : String(err);
1387
+ return new AgentError({
1388
+ message,
1389
+ code: code === "UNKNOWN" ? "AGENT_RUN_FAILED" : code,
1390
+ cause: err
1391
+ });
1392
+ }
1393
+
1394
+ // src/types/provider.ts
1395
+ var ProviderError = class extends WrongStackError {
1396
+ status;
1397
+ retryable;
1398
+ providerId;
1399
+ body;
1400
+ constructor(message, status, retryable, providerId, opts = {}) {
1401
+ super({
1402
+ message,
1403
+ code: providerStatusToCode(status, opts.body?.type),
1404
+ subsystem: "provider",
1405
+ severity: status >= 500 ? "error" : "warning",
1406
+ recoverable: retryable,
1407
+ context: { providerId, status },
1408
+ cause: opts.cause
1409
+ });
1410
+ this.name = "ProviderError";
1411
+ this.status = status;
1412
+ this.retryable = retryable;
1413
+ this.providerId = providerId;
1414
+ this.body = opts.body;
1692
1415
  }
1693
- async readBody(name) {
1694
- const m = await this.find(name);
1695
- if (!m) throw new Error(`Skill "${name}" not found`);
1696
- return fsp.readFile(m.path, "utf8");
1416
+ /**
1417
+ * Render a one-line, user-facing description. Designed for the CLI/TUI
1418
+ * status line and the agent's retry warning. Avoids dumping raw JSON
1419
+ * (which is what users see today when a 529 lands and the log message
1420
+ * includes the full `{"type":"error",...}` body).
1421
+ *
1422
+ * Examples:
1423
+ * "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
1424
+ * "openai rate limited (429): Retry after 12s"
1425
+ * "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
1426
+ * "groq HTTP 500 (server error)"
1427
+ */
1428
+ describe() {
1429
+ const kind = describeStatus(this.status, this.body?.type);
1430
+ const head = `${this.providerId} ${kind}`;
1431
+ const detail = this.body?.message?.trim();
1432
+ const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
1433
+ if (detail && detail.length > 0) {
1434
+ return `${head}: ${truncate(detail, 240)}${reqId}`;
1435
+ }
1436
+ return `${head}${reqId}`;
1697
1437
  }
1698
1438
  };
1699
- function parseFrontmatter(raw) {
1700
- if (!raw.startsWith("---")) return {};
1701
- const end = raw.indexOf("\n---", 4);
1702
- if (end === -1) return {};
1703
- const block = raw.slice(4, end);
1704
- const out = {};
1705
- let key = null;
1706
- let value = [];
1707
- const flush = () => {
1708
- if (key) {
1709
- out[key] = value.join("\n").trim();
1439
+ function describeStatus(status, type) {
1440
+ if (status === 0) return "network error";
1441
+ if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
1442
+ if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
1443
+ if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
1444
+ if (type === "permission_error" || status === 403) return `forbidden (${status})`;
1445
+ if (type === "not_found_error" || status === 404) return `not found (${status})`;
1446
+ if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
1447
+ if (status === 408) return `timeout (${status})`;
1448
+ if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
1449
+ if (type) return `${type} (${status})`;
1450
+ return `HTTP ${status}`;
1451
+ }
1452
+ function truncate(s, n) {
1453
+ return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
1454
+ }
1455
+ function providerStatusToCode(status, type) {
1456
+ if (status === 0) return "PROVIDER_NETWORK_ERROR";
1457
+ if (type === "rate_limit_error" || status === 429) return "PROVIDER_RATE_LIMITED";
1458
+ if (type === "authentication_error" || status === 401) return "PROVIDER_AUTH_FAILED";
1459
+ if (type === "overloaded_error" || status === 529) return "PROVIDER_OVERLOADED";
1460
+ if (type === "invalid_request_error" || status === 400) return "PROVIDER_INVALID_REQUEST";
1461
+ if (status === 408) return "PROVIDER_NETWORK_ERROR";
1462
+ if (status >= 500) return "PROVIDER_SERVER_ERROR";
1463
+ return "PROVIDER_INVALID_REQUEST";
1464
+ }
1465
+
1466
+ // src/defaults/retry-policy.ts
1467
+ var DefaultRetryPolicy = class {
1468
+ shouldRetry(err, attempt) {
1469
+ if (err instanceof ProviderError) {
1470
+ if (!err.retryable) return false;
1471
+ return attempt < this.maxAttempts(err);
1710
1472
  }
1711
- key = null;
1712
- value = [];
1713
- };
1714
- for (const line of block.split("\n")) {
1715
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
1716
- if (m) {
1717
- flush();
1718
- key = m[1] ?? "";
1719
- const pipe = m[2];
1720
- const rest = m[3] ?? "";
1721
- if (pipe === "|") {
1722
- value = [];
1723
- } else if (rest) {
1724
- value = [rest];
1725
- } else {
1726
- value = [];
1727
- }
1728
- } else if (key) {
1729
- value.push(line.replace(/^\s+/, ""));
1473
+ const msg = err.message ?? "";
1474
+ const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
1475
+ if (isNetwork) return attempt < 2;
1476
+ return false;
1477
+ }
1478
+ maxAttempts(err) {
1479
+ if (err instanceof ProviderError) {
1480
+ if (err.status === 429) return 5;
1481
+ if (err.status === 529) return 3;
1482
+ if (err.status >= 500) return 3;
1483
+ return 0;
1730
1484
  }
1485
+ return 2;
1731
1486
  }
1732
- flush();
1733
- return out;
1734
- }
1735
- var BEHAVIOR_DEFAULTS = {
1736
- version: 1,
1737
- context: {
1738
- warnThreshold: 0.6,
1739
- softThreshold: 0.75,
1740
- hardThreshold: 0.9,
1741
- autoCompact: true,
1742
- preserveK: 10,
1743
- eliseThreshold: 2e3
1744
- },
1745
- tools: {
1746
- defaultExecutionStrategy: "smart",
1487
+ delayMs(attempt) {
1488
+ const base = 1e3;
1489
+ const exp = base * 2 ** attempt;
1490
+ const jitter = Math.random() * base;
1491
+ return Math.min(3e4, exp + jitter);
1492
+ }
1493
+ };
1494
+
1495
+ // src/defaults/error-handler.ts
1496
+ function buildRecoveryStrategies(opts) {
1497
+ return [
1498
+ {
1499
+ label: "context_overflow_reduce",
1500
+ compactor: opts?.compactor,
1501
+ async attempt(err, ctx) {
1502
+ if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
1503
+ if (this.compactor) {
1504
+ try {
1505
+ const report = await this.compactor.compact(ctx, { aggressive: true });
1506
+ if (report.after < report.before) {
1507
+ return {
1508
+ content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
1509
+ stopReason: "end_turn",
1510
+ usage: { input: 0, output: 0 },
1511
+ model: ctx.model
1512
+ };
1513
+ }
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ return null;
1518
+ }
1519
+ return null;
1520
+ }
1521
+ },
1522
+ {
1523
+ label: "rate_limit_backoff",
1524
+ async attempt(err, ctx) {
1525
+ if (err instanceof ProviderError && err.status === 429) {
1526
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
1527
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1528
+ await new Promise((r) => setTimeout(r, delay));
1529
+ return {
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;
1537
+ }
1538
+ },
1539
+ {
1540
+ label: "downgrade_model",
1541
+ async attempt(err, ctx) {
1542
+ if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
1543
+ return null;
1544
+ }
1545
+ return null;
1546
+ }
1547
+ }
1548
+ ];
1549
+ }
1550
+ var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
1551
+ var DefaultErrorHandler = class {
1552
+ strategies;
1553
+ constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
1554
+ this.strategies = strategies;
1555
+ }
1556
+ classify(err) {
1557
+ if (err instanceof DOMException && err.name === "AbortError") {
1558
+ return { kind: "abort", retryable: false };
1559
+ }
1560
+ if (err instanceof Error && err.name === "AbortError") {
1561
+ return { kind: "abort", retryable: false };
1562
+ }
1563
+ if (err instanceof ProviderError) {
1564
+ if (err.status === 429) return { kind: "rate_limit", retryable: true };
1565
+ if (err.status === 529) return { kind: "overloaded", retryable: true };
1566
+ if (err.status >= 500) return { kind: "server", retryable: true };
1567
+ if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
1568
+ return { kind: "context_overflow", retryable: false };
1569
+ }
1570
+ if (err.status >= 400) return { kind: "client", retryable: false };
1571
+ }
1572
+ if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
1573
+ return { kind: "network", retryable: true };
1574
+ }
1575
+ return { kind: "unknown", retryable: false };
1576
+ }
1577
+ async recover(err, ctx) {
1578
+ for (const strategy of this.strategies) {
1579
+ const result = await strategy.attempt(err, ctx);
1580
+ if (result !== null) return result;
1581
+ }
1582
+ return null;
1583
+ }
1584
+ };
1585
+ var DefaultSkillLoader = class {
1586
+ dirs;
1587
+ cache;
1588
+ constructor(opts) {
1589
+ this.dirs = [
1590
+ { dir: opts.paths.inProjectSkills, source: "project" },
1591
+ { dir: opts.paths.globalSkills, source: "user" }
1592
+ ];
1593
+ if (opts.bundledDir) {
1594
+ this.dirs.push({ dir: opts.bundledDir, source: "bundled" });
1595
+ }
1596
+ }
1597
+ async list() {
1598
+ if (this.cache) return this.cache;
1599
+ const found = [];
1600
+ const seen = /* @__PURE__ */ new Set();
1601
+ for (const { dir, source } of this.dirs) {
1602
+ try {
1603
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
1604
+ for (const e of entries) {
1605
+ if (!e.isDirectory()) continue;
1606
+ const skillFile = path2.join(dir, e.name, "SKILL.md");
1607
+ try {
1608
+ const raw = await fsp.readFile(skillFile, "utf8");
1609
+ const meta = parseFrontmatter(raw);
1610
+ if (!meta.name || !meta.description) continue;
1611
+ if (seen.has(meta.name)) continue;
1612
+ seen.add(meta.name);
1613
+ found.push({
1614
+ name: meta.name,
1615
+ description: meta.description,
1616
+ version: meta.version,
1617
+ path: skillFile,
1618
+ source
1619
+ });
1620
+ } catch {
1621
+ }
1622
+ }
1623
+ } catch {
1624
+ }
1625
+ }
1626
+ this.cache = found;
1627
+ return found;
1628
+ }
1629
+ async find(name) {
1630
+ const all = await this.list();
1631
+ return all.find((s) => s.name === name);
1632
+ }
1633
+ async manifestText() {
1634
+ const skills = await this.list();
1635
+ if (skills.length === 0) return "";
1636
+ const lines = ["## Available skills"];
1637
+ for (const s of skills) {
1638
+ lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
1639
+ lines.push(` Path: ${s.path}`);
1640
+ }
1641
+ return lines.join("\n");
1642
+ }
1643
+ async readBody(name) {
1644
+ const m = await this.find(name);
1645
+ if (!m) throw new Error(`Skill "${name}" not found`);
1646
+ return fsp.readFile(m.path, "utf8");
1647
+ }
1648
+ };
1649
+ function parseFrontmatter(raw) {
1650
+ if (!raw.startsWith("---")) return {};
1651
+ const end = raw.indexOf("\n---", 4);
1652
+ if (end === -1) return {};
1653
+ const block = raw.slice(4, end);
1654
+ const out = {};
1655
+ let key = null;
1656
+ let value = [];
1657
+ const flush = () => {
1658
+ if (key) {
1659
+ out[key] = value.join("\n").trim();
1660
+ }
1661
+ key = null;
1662
+ value = [];
1663
+ };
1664
+ for (const line of block.split("\n")) {
1665
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
1666
+ if (m) {
1667
+ flush();
1668
+ key = m[1] ?? "";
1669
+ const pipe = m[2];
1670
+ const rest = m[3] ?? "";
1671
+ if (pipe === "|") {
1672
+ value = [];
1673
+ } else if (rest) {
1674
+ value = [rest];
1675
+ } else {
1676
+ value = [];
1677
+ }
1678
+ } else if (key) {
1679
+ value.push(line.replace(/^\s+/, ""));
1680
+ }
1681
+ }
1682
+ flush();
1683
+ return out;
1684
+ }
1685
+ var BEHAVIOR_DEFAULTS = {
1686
+ version: 1,
1687
+ context: {
1688
+ warnThreshold: 0.6,
1689
+ softThreshold: 0.75,
1690
+ hardThreshold: 0.9,
1691
+ autoCompact: true,
1692
+ preserveK: 10,
1693
+ eliseThreshold: 2e3
1694
+ },
1695
+ tools: {
1696
+ defaultExecutionStrategy: "smart",
1747
1697
  maxIterations: 100,
1748
1698
  iterationTimeoutMs: 3e5,
1749
1699
  sessionTimeoutMs: 18e5,
@@ -1849,6 +1799,20 @@ var DefaultConfigLoader = class {
1849
1799
  if (this.vault) {
1850
1800
  cfg = decryptConfigSecrets(cfg, this.vault);
1851
1801
  }
1802
+ if (cfg.providers) {
1803
+ for (const pcfg of Object.values(cfg.providers)) {
1804
+ if (!pcfg || typeof pcfg !== "object") continue;
1805
+ const keys = pcfg.apiKeys;
1806
+ if (!Array.isArray(keys) || keys.length === 0) continue;
1807
+ const existing = pcfg.apiKey;
1808
+ if (existing && existing.length > 0) continue;
1809
+ const activeLabel = pcfg.activeKey;
1810
+ const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
1811
+ if (chosen?.apiKey) {
1812
+ pcfg.apiKey = chosen.apiKey;
1813
+ }
1814
+ }
1815
+ }
1852
1816
  this.validateBehavior(cfg);
1853
1817
  if (this.strict) this.validateIdentity(cfg);
1854
1818
  return Object.freeze(cfg);
@@ -1885,6 +1849,132 @@ var DefaultConfigLoader = class {
1885
1849
  }
1886
1850
  };
1887
1851
 
1852
+ // src/defaults/config-store.ts
1853
+ var DefaultConfigStore = class {
1854
+ current;
1855
+ watchers = /* @__PURE__ */ new Set();
1856
+ constructor(initial) {
1857
+ this.current = deepFreeze(structuredClone(initial));
1858
+ }
1859
+ get() {
1860
+ return this.current;
1861
+ }
1862
+ getSection(key) {
1863
+ return this.current[key];
1864
+ }
1865
+ getExtension(pluginName) {
1866
+ const ext = this.current.extensions?.[pluginName];
1867
+ return ext ? ext : FROZEN_EMPTY;
1868
+ }
1869
+ update(partial) {
1870
+ const next = deepFreeze(
1871
+ structuredClone({ ...this.current, ...partial })
1872
+ );
1873
+ if (next.version !== 1) {
1874
+ throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
1875
+ }
1876
+ const prev = this.current;
1877
+ this.current = next;
1878
+ for (const w of this.watchers) {
1879
+ try {
1880
+ w(next, prev);
1881
+ } catch {
1882
+ }
1883
+ }
1884
+ return next;
1885
+ }
1886
+ watch(cb) {
1887
+ this.watchers.add(cb);
1888
+ return () => this.watchers.delete(cb);
1889
+ }
1890
+ };
1891
+ var FROZEN_EMPTY = Object.freeze({});
1892
+ function deepFreeze(obj) {
1893
+ if (obj === null || typeof obj !== "object") return obj;
1894
+ if (Object.isFrozen(obj)) return obj;
1895
+ for (const key of Object.keys(obj)) {
1896
+ const v = obj[key];
1897
+ if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
1898
+ deepFreeze(v);
1899
+ }
1900
+ }
1901
+ return Object.freeze(obj);
1902
+ }
1903
+
1904
+ // src/defaults/config-migration.ts
1905
+ var ConfigMigrationError = class extends Error {
1906
+ fromVersion;
1907
+ targetVersion;
1908
+ missingStep;
1909
+ constructor(opts) {
1910
+ super(opts.message);
1911
+ this.name = "ConfigMigrationError";
1912
+ this.fromVersion = opts.fromVersion;
1913
+ this.targetVersion = opts.targetVersion;
1914
+ this.missingStep = opts.missingStep;
1915
+ }
1916
+ };
1917
+ function runConfigMigrations(input, targetVersion, migrations) {
1918
+ const initial = typeof input["version"] === "number" ? input["version"] : 1;
1919
+ let current = { ...input };
1920
+ let currentVersion = initial;
1921
+ const applied = [];
1922
+ let shouldPersist = false;
1923
+ let guard = 0;
1924
+ while (currentVersion !== targetVersion) {
1925
+ if (++guard > 100) {
1926
+ throw new ConfigMigrationError({
1927
+ message: `Config migration looped past 100 steps (from v${initial} toward v${targetVersion})`,
1928
+ fromVersion: initial,
1929
+ targetVersion,
1930
+ missingStep: currentVersion
1931
+ });
1932
+ }
1933
+ const step = migrations.find((m) => m.from === currentVersion);
1934
+ if (!step) {
1935
+ throw new ConfigMigrationError({
1936
+ message: `No migration registered from config v${currentVersion} (target v${targetVersion}). Update the framework or revert the config file.`,
1937
+ fromVersion: initial,
1938
+ targetVersion,
1939
+ missingStep: currentVersion
1940
+ });
1941
+ }
1942
+ const ctx = { fromVersion: currentVersion, shouldPersist: false };
1943
+ const next = step.migrate(current, ctx);
1944
+ if (typeof next["version"] !== "number" || next["version"] !== step.to) {
1945
+ next["version"] = step.to;
1946
+ }
1947
+ current = next;
1948
+ currentVersion = step.to;
1949
+ applied.push(`v${step.from}\u2192v${step.to}`);
1950
+ shouldPersist = shouldPersist || ctx.shouldPersist || step.from < step.to;
1951
+ }
1952
+ return { config: current, applied, shouldPersist };
1953
+ }
1954
+ var DEFAULT_CONFIG_MIGRATIONS = [];
1955
+
1956
+ // src/utils/token-estimate.ts
1957
+ var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
1958
+ function estimateToolInputTokens(input) {
1959
+ if (typeof input === "string") return RoughTokenEstimate(input);
1960
+ if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
1961
+ return input.__tokenEstimate;
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;
1967
+ }
1968
+ return estimate;
1969
+ }
1970
+ function estimateToolResultTokens(content) {
1971
+ if (typeof content === "string") return RoughTokenEstimate(content);
1972
+ return RoughTokenEstimate(JSON.stringify(content));
1973
+ }
1974
+ function estimateTextTokens(text) {
1975
+ return RoughTokenEstimate(text);
1976
+ }
1977
+
1888
1978
  // src/defaults/compactor.ts
1889
1979
  var HybridCompactor = class {
1890
1980
  preserveK;
@@ -1893,7 +1983,7 @@ var HybridCompactor = class {
1893
1983
  constructor(opts = {}) {
1894
1984
  this.preserveK = opts.preserveK ?? 10;
1895
1985
  this.eliseThreshold = opts.eliseThreshold ?? 2e3;
1896
- this.estimator = opts.estimator ?? roughTokenEstimate;
1986
+ this.estimator = opts.estimator ?? estimateTextTokens;
1897
1987
  }
1898
1988
  async compact(ctx, opts = {}) {
1899
1989
  const beforeTokens = this.estimateMessages(ctx.messages);
@@ -1925,8 +2015,7 @@ var HybridCompactor = class {
1925
2015
  if (!msg || !Array.isArray(msg.content)) continue;
1926
2016
  const newContent = msg.content.map((b) => {
1927
2017
  if (b.type !== "tool_result") return b;
1928
- const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
1929
- const tokens = this.estimator(text);
2018
+ const tokens = estimateToolResultTokens(b.content);
1930
2019
  if (tokens < this.eliseThreshold) return b;
1931
2020
  saved += tokens;
1932
2021
  const elided = {
@@ -1964,23 +2053,20 @@ var HybridCompactor = class {
1964
2053
  },
1965
2054
  { role: "assistant", content: "Continuing from compacted context." }
1966
2055
  ];
1967
- ctx.messages.splice(0, boundary, ...summary);
2056
+ const tail = ctx.messages.slice(boundary);
2057
+ ctx.state.replaceMessages([...summary, ...tail]);
1968
2058
  return Math.max(0, removedTokens - this.estimateMessages(summary));
1969
2059
  }
1970
2060
  estimateMessages(messages) {
1971
2061
  let total = 0;
1972
2062
  for (const m of messages) {
1973
2063
  if (typeof m.content === "string") {
1974
- total += this.estimator(m.content);
2064
+ total += estimateTextTokens(m.content);
1975
2065
  } else {
1976
2066
  for (const b of m.content) {
1977
- if (b.type === "text") total += this.estimator(b.text);
1978
- else if (b.type === "tool_use") total += this.estimator(JSON.stringify(b.input));
1979
- else if (b.type === "tool_result") {
1980
- total += this.estimator(
1981
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
1982
- );
1983
- }
2067
+ if (b.type === "text") total += estimateTextTokens(b.text);
2068
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
2069
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1984
2070
  }
1985
2071
  }
1986
2072
  }
@@ -1991,9 +2077,6 @@ function hasTextContent(m) {
1991
2077
  if (typeof m.content === "string") return m.content.trim().length > 0;
1992
2078
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
1993
2079
  }
1994
- function roughTokenEstimate(text) {
1995
- return Math.max(1, Math.ceil(text.length / 4));
1996
- }
1997
2080
 
1998
2081
  // src/types/blocks.ts
1999
2082
  function isTextBlock(b) {
@@ -2058,7 +2141,8 @@ var IntelligentCompactor = class {
2058
2141
  content: `[prior_turns_summary: ${summaryText}]`
2059
2142
  };
2060
2143
  const summaryTokens = this.estimateTokens([summaryMsg]);
2061
- ctx.messages.splice(0, boundary, summaryMsg);
2144
+ const tail = ctx.messages.slice(boundary);
2145
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
2062
2146
  return Math.max(0, removedTokens - summaryTokens);
2063
2147
  }
2064
2148
  findSafeBoundary(messages, from, to) {
@@ -2139,8 +2223,7 @@ var IntelligentCompactor = class {
2139
2223
  if (!msg || !Array.isArray(msg.content)) continue;
2140
2224
  const newContent = msg.content.map((b) => {
2141
2225
  if (b.type !== "tool_result") return b;
2142
- const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
2143
- const tokens = this.roughTokenEstimate(text);
2226
+ const tokens = estimateToolResultTokens(b.content);
2144
2227
  if (tokens < this.eliseThreshold) return b;
2145
2228
  saved += tokens;
2146
2229
  return {
@@ -2162,24 +2245,17 @@ var IntelligentCompactor = class {
2162
2245
  let total = 0;
2163
2246
  for (const m of messages) {
2164
2247
  if (typeof m.content === "string") {
2165
- total += this.roughTokenEstimate(m.content);
2248
+ total += estimateTextTokens(m.content);
2166
2249
  } else {
2167
2250
  for (const b of m.content) {
2168
- if (b.type === "text") total += this.roughTokenEstimate(b.text);
2169
- else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
2170
- else if (b.type === "tool_result") {
2171
- total += this.roughTokenEstimate(
2172
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
2173
- );
2174
- }
2251
+ if (b.type === "text") total += estimateTextTokens(b.text);
2252
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
2253
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
2175
2254
  }
2176
2255
  }
2177
2256
  }
2178
2257
  return total;
2179
2258
  }
2180
- roughTokenEstimate(text) {
2181
- return Math.max(1, Math.ceil(text.length / 4));
2182
- }
2183
2259
  };
2184
2260
 
2185
2261
  // src/defaults/llm-selector.ts
@@ -2402,8 +2478,8 @@ var SelectiveCompactor = class {
2402
2478
  * insert summaries where the selector provided them.
2403
2479
  */
2404
2480
  async executePlan(ctx, plan) {
2405
- const messages = ctx.messages;
2406
- if (messages.length === 0) return;
2481
+ if (ctx.messages.length === 0) return;
2482
+ const messages = [...ctx.messages];
2407
2483
  const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
2408
2484
  for (const range of sortedCollapsed) {
2409
2485
  if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
@@ -2418,6 +2494,7 @@ var SelectiveCompactor = class {
2418
2494
  };
2419
2495
  messages.splice(range.from, range.to - range.from + 1, summaryMsg);
2420
2496
  }
2497
+ ctx.state.replaceMessages(messages);
2421
2498
  }
2422
2499
  async summarizeRange(messages, ctx) {
2423
2500
  const systemText = `${this.summarizerPrompt}
@@ -2464,7 +2541,8 @@ Summarize the following message range:`;
2464
2541
  role: "system",
2465
2542
  content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
2466
2543
  };
2467
- messages.splice(0, boundary, summaryMsg);
2544
+ const tail = messages.slice(boundary);
2545
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
2468
2546
  return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
2469
2547
  }
2470
2548
  computeTargetBudget(load, aggressive) {
@@ -2991,44 +3069,128 @@ async function loadUserModes(modesDir) {
2991
3069
  }
2992
3070
  return modes;
2993
3071
  }
3072
+
3073
+ // src/defaults/subagent-budget.ts
3074
+ var BudgetExceededError = class extends Error {
3075
+ kind;
3076
+ limit;
3077
+ observed;
3078
+ constructor(kind, limit, observed) {
3079
+ super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
3080
+ this.name = "BudgetExceededError";
3081
+ this.kind = kind;
3082
+ this.limit = limit;
3083
+ this.observed = observed;
3084
+ }
3085
+ };
3086
+ var SubagentBudget = class {
3087
+ limits;
3088
+ iterations = 0;
3089
+ toolCalls = 0;
3090
+ tokenInput = 0;
3091
+ tokenOutput = 0;
3092
+ costUsd = 0;
3093
+ startTime = null;
3094
+ constructor(limits = {}) {
3095
+ this.limits = Object.freeze({ ...limits });
3096
+ }
3097
+ start() {
3098
+ this.startTime = Date.now();
3099
+ }
3100
+ recordIteration() {
3101
+ this.iterations++;
3102
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
3103
+ throw new BudgetExceededError("iterations", this.limits.maxIterations, this.iterations);
3104
+ }
3105
+ }
3106
+ recordToolCall() {
3107
+ this.toolCalls++;
3108
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
3109
+ throw new BudgetExceededError("tool_calls", this.limits.maxToolCalls, this.toolCalls);
3110
+ }
3111
+ }
3112
+ recordUsage(usage, costUsd = 0) {
3113
+ this.tokenInput += usage.input;
3114
+ this.tokenOutput += usage.output;
3115
+ this.costUsd += costUsd;
3116
+ const totalTokens = this.tokenInput + this.tokenOutput;
3117
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
3118
+ throw new BudgetExceededError("tokens", this.limits.maxTokens, totalTokens);
3119
+ }
3120
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
3121
+ throw new BudgetExceededError("cost", this.limits.maxCostUsd, this.costUsd);
3122
+ }
3123
+ }
3124
+ /**
3125
+ * Throws if the wall-clock budget is exhausted. Call this from the iteration
3126
+ * loop so a hung tool can't keep a subagent running past its deadline.
3127
+ */
3128
+ checkTimeout() {
3129
+ if (this.startTime === null || this.limits.timeoutMs === void 0) return;
3130
+ const elapsed = Date.now() - this.startTime;
3131
+ if (elapsed > this.limits.timeoutMs) {
3132
+ throw new BudgetExceededError("timeout", this.limits.timeoutMs, elapsed);
3133
+ }
3134
+ }
3135
+ /** Returns true if a timeout has occurred without throwing. Useful for races. */
3136
+ isTimedOut() {
3137
+ if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
3138
+ return Date.now() - this.startTime > this.limits.timeoutMs;
3139
+ }
3140
+ usage() {
3141
+ return {
3142
+ iterations: this.iterations,
3143
+ toolCalls: this.toolCalls,
3144
+ tokens: {
3145
+ input: this.tokenInput,
3146
+ output: this.tokenOutput,
3147
+ total: this.tokenInput + this.tokenOutput
3148
+ },
3149
+ costUsd: this.costUsd,
3150
+ elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
3151
+ };
3152
+ }
3153
+ };
3154
+
3155
+ // src/defaults/multi-agent-coordinator.ts
2994
3156
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
2995
3157
  coordinatorId;
2996
3158
  config;
3159
+ runner;
2997
3160
  subagents = /* @__PURE__ */ new Map();
2998
3161
  pendingTasks = [];
2999
3162
  completedResults = [];
3000
3163
  totalIterations = 0;
3001
- constructor(config) {
3164
+ inFlight = 0;
3165
+ constructor(config, options = {}) {
3002
3166
  super();
3003
3167
  this.coordinatorId = config.coordinatorId;
3004
3168
  this.config = config;
3169
+ this.runner = options.runner;
3005
3170
  }
3006
3171
  async spawn(subagent) {
3007
3172
  const id = subagent.id || randomUUID();
3008
3173
  const context = {
3009
3174
  subagentId: id,
3010
3175
  tasks: [],
3176
+ // parentBridge: wired by the caller via setSubagentBridge() once the
3177
+ // bidirectional bridge is created. Reads gated by hasParentBridge().
3011
3178
  parentBridge: null,
3012
3179
  doneCondition: this.config.doneCondition,
3013
3180
  maxConcurrent: this.config.maxConcurrent ?? 4
3014
3181
  };
3015
3182
  this.subagents.set(id, {
3016
- config: subagent,
3183
+ config: { ...subagent, id },
3017
3184
  context,
3018
- status: "idle"
3185
+ status: "idle",
3186
+ abortController: new AbortController()
3019
3187
  });
3020
3188
  this.emit("subagent.started", { subagent: { ...subagent, id } });
3021
- return {
3022
- subagentId: id,
3023
- agentId: id
3024
- };
3189
+ return { subagentId: id, agentId: id };
3025
3190
  }
3026
3191
  async assign(task) {
3027
3192
  this.pendingTasks.push(task);
3028
- const available = this.getAvailableSubagent();
3029
- if (available) {
3030
- await this.dispatch(available, task);
3031
- }
3193
+ this.tryDispatchNext();
3032
3194
  }
3033
3195
  async delegate(to, msg) {
3034
3196
  const subagent = this.subagents.get(to);
@@ -3039,8 +3201,8 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3039
3201
  await subagent.context.parentBridge.send(msg);
3040
3202
  }
3041
3203
  /**
3042
- * Wire up the communication bridge for a subagent. Call this after `spawn()`
3043
- * once the caller has created the bidirectional bridge connection.
3204
+ * Wire up the communication bridge for a subagent. Call after spawn() once
3205
+ * the caller has created the bidirectional connection.
3044
3206
  */
3045
3207
  setSubagentBridge(subagentId, bridge) {
3046
3208
  const subagent = this.subagents.get(subagentId);
@@ -3050,6 +3212,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3050
3212
  async stop(subagentId) {
3051
3213
  const subagent = this.subagents.get(subagentId);
3052
3214
  if (!subagent) return;
3215
+ subagent.abortController.abort();
3053
3216
  subagent.status = "stopped";
3054
3217
  subagent.currentTask = void 0;
3055
3218
  subagent.context.parentBridge = null;
@@ -3075,71 +3238,234 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3075
3238
  done: this.isDone()
3076
3239
  };
3077
3240
  }
3078
- getAvailableSubagent() {
3241
+ /** Expose snapshot of completed results — useful for callers awaiting all done. */
3242
+ results() {
3243
+ return this.completedResults;
3244
+ }
3245
+ /**
3246
+ * Manual completion — for callers that drive subagents without a runner
3247
+ * (e.g. external orchestrators). When a runner is configured the coordinator
3248
+ * calls this itself.
3249
+ */
3250
+ completeTask(result) {
3251
+ this.recordCompletion(result);
3252
+ }
3253
+ // --- internal dispatching ---------------------------------------------
3254
+ tryDispatchNext() {
3255
+ while (this.canDispatch()) {
3256
+ const subagentId = this.findIdleSubagent();
3257
+ if (!subagentId) return;
3258
+ const task = this.pendingTasks.shift();
3259
+ if (!task) return;
3260
+ void this.runDispatched(subagentId, task);
3261
+ }
3262
+ }
3263
+ canDispatch() {
3264
+ const max = this.config.maxConcurrent ?? 4;
3265
+ return this.inFlight < max && this.pendingTasks.length > 0;
3266
+ }
3267
+ findIdleSubagent() {
3079
3268
  for (const [id, s] of this.subagents) {
3080
3269
  if (s.status === "idle") return id;
3081
3270
  }
3082
3271
  return null;
3083
3272
  }
3084
- async dispatch(subagentId, task) {
3273
+ async runDispatched(subagentId, task) {
3085
3274
  const subagent = this.subagents.get(subagentId);
3086
3275
  if (!subagent) return;
3087
3276
  subagent.status = "running";
3088
3277
  subagent.currentTask = task.id;
3089
3278
  task.subagentId = subagentId;
3090
3279
  subagent.context.tasks.push(task);
3091
- if (!subagent.context.parentBridge) {
3092
- this.emit("task.assigned", { task, subagentId });
3280
+ this.inFlight++;
3281
+ this.emit("task.assigned", { task, subagentId });
3282
+ const budget = new SubagentBudget({
3283
+ maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
3284
+ maxToolCalls: task.maxToolCalls ?? subagent.config.maxToolCalls ?? this.config.defaultBudget?.maxToolCalls,
3285
+ maxTokens: subagent.config.maxTokens ?? this.config.defaultBudget?.maxTokens,
3286
+ maxCostUsd: subagent.config.maxCostUsd ?? this.config.defaultBudget?.maxCostUsd,
3287
+ timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
3288
+ });
3289
+ subagent.activeBudget = budget;
3290
+ const startTime = Date.now();
3291
+ const runCtx = {
3292
+ subagentId,
3293
+ config: subagent.config,
3294
+ budget,
3295
+ signal: subagent.abortController.signal,
3296
+ bridge: subagent.context.parentBridge || null
3297
+ };
3298
+ let result;
3299
+ if (!this.runner) {
3093
3300
  return;
3094
3301
  }
3095
- await subagent.context.parentBridge.send({
3096
- id: randomUUID(),
3097
- type: "task",
3098
- from: this.coordinatorId,
3099
- to: subagentId,
3100
- payload: task,
3101
- timestamp: Date.now()
3102
- });
3103
- this.emit("task.assigned", { task, subagentId });
3104
- }
3105
- isDone() {
3106
- if (this.config.doneCondition.type === "all_tasks_done") {
3107
- return this.pendingTasks.length === 0 && this.completedResults.every((r) => r.status === "success");
3302
+ budget.start();
3303
+ try {
3304
+ const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
3305
+ result = {
3306
+ subagentId,
3307
+ taskId: task.id,
3308
+ status: "success",
3309
+ result: outcome.result,
3310
+ iterations: outcome.iterations,
3311
+ toolCalls: outcome.toolCalls,
3312
+ durationMs: Date.now() - startTime
3313
+ };
3314
+ } catch (err) {
3315
+ const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
3316
+ result = {
3317
+ subagentId,
3318
+ taskId: task.id,
3319
+ status,
3320
+ error: err instanceof Error ? err.message : String(err),
3321
+ iterations: budget.usage().iterations,
3322
+ toolCalls: budget.usage().toolCalls,
3323
+ durationMs: Date.now() - startTime
3324
+ };
3108
3325
  }
3109
- if (this.config.doneCondition.maxIterations && this.totalIterations >= this.config.doneCondition.maxIterations) {
3110
- return true;
3326
+ this.recordCompletion(result);
3327
+ }
3328
+ async executeWithTimeout(runner, task, ctx, budget) {
3329
+ const timeoutMs = budget.limits.timeoutMs;
3330
+ if (timeoutMs === void 0) return runner(task, ctx);
3331
+ let timer = null;
3332
+ const timeoutPromise = new Promise((_, reject) => {
3333
+ timer = setTimeout(() => {
3334
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
3335
+ reject(new BudgetExceededError("timeout", timeoutMs, Date.now()));
3336
+ }, timeoutMs);
3337
+ });
3338
+ try {
3339
+ return await Promise.race([runner(task, ctx), timeoutPromise]);
3340
+ } finally {
3341
+ if (timer) clearTimeout(timer);
3111
3342
  }
3112
- return false;
3113
3343
  }
3114
- completeTask(result) {
3344
+ recordCompletion(result) {
3115
3345
  this.completedResults.push(result);
3116
3346
  this.totalIterations += result.iterations;
3347
+ this.inFlight = Math.max(0, this.inFlight - 1);
3117
3348
  const subagent = this.subagents.get(result.subagentId);
3118
- if (subagent) {
3119
- subagent.status = "idle";
3349
+ if (subagent && subagent.status !== "stopped") {
3350
+ subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
3120
3351
  subagent.currentTask = void 0;
3352
+ if (subagent.status === "error") {
3353
+ queueMicrotask(() => {
3354
+ if (subagent.status === "error") subagent.status = "idle";
3355
+ this.tryDispatchNext();
3356
+ });
3357
+ }
3121
3358
  }
3122
3359
  this.emit("task.completed", {
3123
- task: this.pendingTasks.shift(),
3360
+ task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
3124
3361
  result
3125
3362
  });
3126
- if (this.pendingTasks.length > 0) {
3127
- const available = this.getAvailableSubagent();
3128
- if (available) {
3129
- const nextTask = this.pendingTasks.shift();
3130
- this.dispatch(available, nextTask);
3131
- }
3132
- } else if (this.isDone()) {
3363
+ this.tryDispatchNext();
3364
+ if (this.isDone()) {
3133
3365
  this.emit("done", {
3134
3366
  results: this.completedResults,
3135
3367
  totalIterations: this.totalIterations
3136
3368
  });
3137
3369
  }
3138
3370
  }
3371
+ isDone() {
3372
+ if (this.config.doneCondition.type === "all_tasks_done") {
3373
+ return this.pendingTasks.length === 0 && this.inFlight === 0;
3374
+ }
3375
+ if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
3376
+ return true;
3377
+ }
3378
+ return false;
3379
+ }
3139
3380
  };
3381
+
3382
+ // src/defaults/agent-subagent-runner.ts
3383
+ function makeAgentSubagentRunner(opts) {
3384
+ const format = opts.formatTaskInput ?? defaultFormatTaskInput;
3385
+ return async (task, ctx) => {
3386
+ const { agent, events } = await opts.factory(ctx.config);
3387
+ const aborter = new AbortController();
3388
+ const onBudgetError = (err) => {
3389
+ if (err instanceof BudgetExceededError) {
3390
+ aborter.abort();
3391
+ budgetError = err;
3392
+ } else {
3393
+ throw err;
3394
+ }
3395
+ };
3396
+ let budgetError = null;
3397
+ const unsub = [];
3398
+ unsub.push(
3399
+ events.on("tool.started", () => {
3400
+ try {
3401
+ ctx.budget.recordToolCall();
3402
+ } catch (e) {
3403
+ onBudgetError(e);
3404
+ }
3405
+ }),
3406
+ events.on("provider.response", (e) => {
3407
+ try {
3408
+ ctx.budget.recordUsage(e.usage);
3409
+ } catch (e2) {
3410
+ onBudgetError(e2);
3411
+ }
3412
+ }),
3413
+ events.on("iteration.started", () => {
3414
+ try {
3415
+ ctx.budget.recordIteration();
3416
+ ctx.budget.checkTimeout();
3417
+ } catch (e) {
3418
+ onBudgetError(e);
3419
+ }
3420
+ })
3421
+ );
3422
+ const onParentAbort = () => aborter.abort();
3423
+ ctx.signal.addEventListener("abort", onParentAbort);
3424
+ let result;
3425
+ try {
3426
+ result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
3427
+ } finally {
3428
+ ctx.signal.removeEventListener("abort", onParentAbort);
3429
+ for (const u of unsub) u();
3430
+ }
3431
+ if (budgetError) throw budgetError;
3432
+ if (result.status === "failed") {
3433
+ throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
3434
+ }
3435
+ if (result.status === "aborted") {
3436
+ throw new Error("agent aborted");
3437
+ }
3438
+ if (result.status === "max_iterations") {
3439
+ throw new Error("agent exhausted iteration limit");
3440
+ }
3441
+ const usage = ctx.budget.usage();
3442
+ return {
3443
+ result: result.finalText,
3444
+ iterations: result.iterations,
3445
+ toolCalls: usage.toolCalls
3446
+ };
3447
+ };
3448
+ }
3449
+ function defaultFormatTaskInput(task) {
3450
+ return task.description ?? "";
3451
+ }
3452
+
3453
+ // src/defaults/transport/in-memory-transport.ts
3140
3454
  var InMemoryBridgeTransport = class {
3141
3455
  subs = /* @__PURE__ */ new Map();
3142
3456
  send(msg, to) {
3457
+ if (to === "*") {
3458
+ for (const [id, handlers2] of this.subs) {
3459
+ if (id === msg.from) continue;
3460
+ for (const h of handlers2) {
3461
+ try {
3462
+ h(msg);
3463
+ } catch {
3464
+ }
3465
+ }
3466
+ }
3467
+ return Promise.resolve();
3468
+ }
3143
3469
  const handlers = this.subs.get(to);
3144
3470
  if (handlers) {
3145
3471
  for (const h of handlers) {
@@ -3161,6 +3487,8 @@ var InMemoryBridgeTransport = class {
3161
3487
  return Promise.resolve();
3162
3488
  }
3163
3489
  };
3490
+
3491
+ // src/defaults/agent-bridge.ts
3164
3492
  var InMemoryAgentBridge = class {
3165
3493
  agentId;
3166
3494
  coordinatorId;
@@ -3327,7 +3655,7 @@ var AutonomousRunner = class {
3327
3655
  if (e.message.includes("timeout")) {
3328
3656
  const timeoutResult = {
3329
3657
  status: "failed",
3330
- error: e,
3658
+ error: toWrongStackError(e),
3331
3659
  iterations: this.iterations,
3332
3660
  toolCalls: this.toolCalls,
3333
3661
  reason: "iteration timeout"
@@ -4133,251 +4461,1354 @@ var TaskFlow = class {
4133
4461
  });
4134
4462
  }
4135
4463
  };
4136
- var SpecDrivenDev = class {
4137
- store;
4138
- tracker;
4139
- events;
4140
- flows = /* @__PURE__ */ new Map();
4141
- constructor(opts) {
4142
- this.store = new DefaultTaskStore();
4143
- this.tracker = new TaskTracker({ store: this.store });
4144
- this.events = opts.events;
4464
+ var SpecDrivenDev = class {
4465
+ store;
4466
+ tracker;
4467
+ events;
4468
+ flows = /* @__PURE__ */ new Map();
4469
+ constructor(opts) {
4470
+ this.store = new DefaultTaskStore();
4471
+ this.tracker = new TaskTracker({ store: this.store });
4472
+ this.events = opts.events;
4473
+ }
4474
+ async createFlow(specContent, options) {
4475
+ const flow = new TaskFlow({
4476
+ tracker: this.tracker,
4477
+ events: this.events,
4478
+ ...options
4479
+ });
4480
+ const graph = await flow.fromSpec(specContent);
4481
+ this.flows.set(graph.id, flow);
4482
+ return flow;
4483
+ }
4484
+ getTracker() {
4485
+ return this.tracker;
4486
+ }
4487
+ getFlow(graphId) {
4488
+ return this.flows.get(graphId);
4489
+ }
4490
+ listFlows() {
4491
+ return Array.from(this.flows.entries()).map(([id, flow]) => ({
4492
+ id,
4493
+ title: flow.getGraph()?.title ?? "Untitled",
4494
+ phase: flow.getPhase()
4495
+ }));
4496
+ }
4497
+ };
4498
+ var LOCK_FILE = "active.json";
4499
+ var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4500
+ var RecoveryLock = class {
4501
+ file;
4502
+ pid;
4503
+ hostname;
4504
+ maxAgeMs;
4505
+ sessionStore;
4506
+ probe;
4507
+ constructor(opts) {
4508
+ this.file = path2.join(opts.dir, LOCK_FILE);
4509
+ this.pid = opts.pid ?? process.pid;
4510
+ this.hostname = opts.hostname ?? os.hostname();
4511
+ this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
4512
+ this.sessionStore = opts.sessionStore;
4513
+ this.probe = opts.isPidAlive ?? defaultIsPidAlive;
4514
+ }
4515
+ /**
4516
+ * Examine the lockfile and decide whether it represents an abandoned
4517
+ * session. Returns `null` if the file is missing, points to a live
4518
+ * instance, references a clean-closed session, is too old, or is
4519
+ * malformed. Otherwise returns enough detail to prompt the user.
4520
+ *
4521
+ * Important: this is a read-only check. We never delete an active
4522
+ * lock from here — if another wstack instance is alive, the caller
4523
+ * should bail or run with a fresh session instead.
4524
+ */
4525
+ async checkAbandoned() {
4526
+ const lock = await this.readLock();
4527
+ if (!lock) return null;
4528
+ const ageMs = Date.now() - new Date(lock.startedAt).getTime();
4529
+ if (Number.isNaN(ageMs) || ageMs < 0) {
4530
+ return null;
4531
+ }
4532
+ if (ageMs > this.maxAgeMs) return null;
4533
+ if (lock.hostname === this.hostname && this.probe(lock.pid)) {
4534
+ return null;
4535
+ }
4536
+ let messageCount = 0;
4537
+ if (this.sessionStore) {
4538
+ try {
4539
+ const data = await this.sessionStore.load(lock.sessionId);
4540
+ const closed = data.events.some((e) => e.type === "session_end");
4541
+ if (closed) return null;
4542
+ messageCount = data.messages.length;
4543
+ } catch {
4544
+ return null;
4545
+ }
4546
+ }
4547
+ return {
4548
+ sessionId: lock.sessionId,
4549
+ pid: lock.pid,
4550
+ startedAt: lock.startedAt,
4551
+ ageMs,
4552
+ messageCount
4553
+ };
4554
+ }
4555
+ /**
4556
+ * Claim the lock for the given session. Overwrites any existing lock
4557
+ * — the caller should have already handled abandonment (via
4558
+ * `checkAbandoned`) before calling this.
4559
+ */
4560
+ async write(sessionId) {
4561
+ await ensureDir(path2.dirname(this.file));
4562
+ const lock = {
4563
+ v: 1,
4564
+ sessionId,
4565
+ pid: this.pid,
4566
+ hostname: this.hostname,
4567
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4568
+ };
4569
+ const tmp = `${this.file}.tmp`;
4570
+ await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
4571
+ await fsp.rename(tmp, this.file);
4572
+ }
4573
+ /**
4574
+ * Release the lock. Idempotent — silently succeeds if the file is
4575
+ * already gone (e.g. someone else cleared it, or the directory was
4576
+ * wiped).
4577
+ */
4578
+ async clear() {
4579
+ try {
4580
+ await fsp.unlink(this.file);
4581
+ } catch (err) {
4582
+ const code = err.code;
4583
+ if (code === "ENOENT") return;
4584
+ throw err;
4585
+ }
4586
+ }
4587
+ async readLock() {
4588
+ let raw;
4589
+ try {
4590
+ raw = await fsp.readFile(this.file, "utf8");
4591
+ } catch (err) {
4592
+ const code = err.code;
4593
+ if (code === "ENOENT") return null;
4594
+ return null;
4595
+ }
4596
+ try {
4597
+ const parsed = JSON.parse(raw);
4598
+ if (!isLockFile(parsed)) return null;
4599
+ return parsed;
4600
+ } catch {
4601
+ return null;
4602
+ }
4603
+ }
4604
+ };
4605
+ function isLockFile(v) {
4606
+ if (typeof v !== "object" || v === null) return false;
4607
+ const o = v;
4608
+ return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
4609
+ }
4610
+ function defaultIsPidAlive(pid) {
4611
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4612
+ try {
4613
+ process.kill(pid, 0);
4614
+ return true;
4615
+ } catch (err) {
4616
+ const code = err.code;
4617
+ if (code === "EPERM") return true;
4618
+ return false;
4619
+ }
4620
+ }
4621
+
4622
+ // src/utils/tool-output-serializer.ts
4623
+ function createToolOutputSerializer(opts = {}) {
4624
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
4625
+ function serialize(value) {
4626
+ if (typeof value === "string") return value;
4627
+ if (value === null || value === void 0) return "";
4628
+ if (typeof value === "object") {
4629
+ if (Array.isArray(value)) return value.map(serialize).join("\n");
4630
+ if ("text" in value) {
4631
+ const t = value.text;
4632
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
4633
+ }
4634
+ try {
4635
+ return JSON.stringify(value, null, 2);
4636
+ } catch {
4637
+ return String(value);
4638
+ }
4639
+ }
4640
+ return String(value);
4641
+ }
4642
+ function enforceCap(text, remainingBudget) {
4643
+ if (remainingBudget <= 0) {
4644
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
4645
+ }
4646
+ const textBytes = Buffer.byteLength(text, "utf8");
4647
+ if (textBytes <= remainingBudget) {
4648
+ return { text, newBudget: remainingBudget - textBytes };
4649
+ }
4650
+ const marker = `
4651
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
4652
+ `;
4653
+ const markerBytes = Buffer.byteLength(marker, "utf8");
4654
+ const available = remainingBudget - markerBytes;
4655
+ if (available <= 0) {
4656
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
4657
+ }
4658
+ const half = Math.floor(available / 2);
4659
+ const first = text.slice(0, half);
4660
+ Buffer.byteLength(first, "utf8");
4661
+ const second = text.slice(text.length - half);
4662
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
4663
+ }
4664
+ return { serialize, enforceCap, capBytes };
4665
+ }
4666
+
4667
+ // src/defaults/tool-executor.ts
4668
+ var ToolExecutor = class {
4669
+ constructor(registry, opts) {
4670
+ this.registry = registry;
4671
+ this.opts = opts;
4672
+ this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
4673
+ this.serializer = createToolOutputSerializer({
4674
+ perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
4675
+ });
4676
+ }
4677
+ registry;
4678
+ opts;
4679
+ serializer;
4680
+ iterationTimeoutMs;
4681
+ /**
4682
+ * Execute a batch of tool uses using the configured strategy.
4683
+ * Returns the execution results and the remaining output budget.
4684
+ */
4685
+ async executeBatch(toolUses, ctx, strategy) {
4686
+ let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
4687
+ const runOne = async (use) => {
4688
+ const start = Date.now();
4689
+ const tool = this.registry.get(use.name);
4690
+ if (!tool) {
4691
+ const result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
4692
+ budget = this.decrementBudget(result, budget);
4693
+ return { result, tool, durationMs: Date.now() - start };
4694
+ }
4695
+ const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
4696
+ if (decision.permission === "deny") {
4697
+ const result = this.deniedResult(use, decision.reason);
4698
+ budget = this.decrementBudget(result, budget);
4699
+ return { result, tool, durationMs: Date.now() - start };
4700
+ }
4701
+ if (decision.permission === "confirm") {
4702
+ if (this.opts.confirmAwaiter) {
4703
+ const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
4704
+ if (choice !== "yes" && choice !== "always") {
4705
+ const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" denied by user.`, is_error: true };
4706
+ budget = this.decrementBudget(result, budget);
4707
+ return { result, tool, durationMs: Date.now() - start };
4708
+ }
4709
+ } else {
4710
+ const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
4711
+ const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
4712
+ return { result: pending, tool, durationMs: Date.now() - start };
4713
+ }
4714
+ }
4715
+ const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
4716
+ "tool.name": tool.name,
4717
+ "tool.mutating": tool.mutating,
4718
+ "tool.permission": tool.permission
4719
+ });
4720
+ try {
4721
+ const result = await this.executeTool(tool, use, ctx, budget);
4722
+ budget = this.decrementBudget(result, budget);
4723
+ span?.setAttribute("tool.is_error", !!result.is_error);
4724
+ span?.setAttribute(
4725
+ "tool.output_bytes",
4726
+ typeof result.content === "string" ? result.content.length : 0
4727
+ );
4728
+ return { result, tool, durationMs: Date.now() - start };
4729
+ } catch (err) {
4730
+ const msg = err instanceof Error ? err.message : String(err);
4731
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
4732
+ this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
4733
+ const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" threw: ${scrubbed}`, is_error: true };
4734
+ budget = this.decrementBudget(result, budget);
4735
+ if (err instanceof Error) span?.recordError(err);
4736
+ span?.setAttribute("tool.is_error", true);
4737
+ return { result, tool, durationMs: Date.now() - start };
4738
+ } finally {
4739
+ span?.end();
4740
+ }
4741
+ };
4742
+ if (strategy === "sequential") {
4743
+ const outputs = [];
4744
+ for (const use of toolUses) {
4745
+ if (use) outputs.push(await runOne(use));
4746
+ }
4747
+ return { outputs, remainingBudget: budget };
4748
+ }
4749
+ if (strategy === "parallel") {
4750
+ const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
4751
+ return { outputs, remainingBudget: budget };
4752
+ }
4753
+ const nonMutating = [];
4754
+ const mutating = [];
4755
+ for (const use of toolUses) {
4756
+ if (!use) continue;
4757
+ const tool = this.registry.get(use.name);
4758
+ if (tool?.mutating) mutating.push(use);
4759
+ else nonMutating.push(use);
4760
+ }
4761
+ const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
4762
+ const secondPass = [];
4763
+ for (const use of mutating) {
4764
+ secondPass.push(await runOne(use));
4765
+ }
4766
+ return {
4767
+ outputs: [...firstPass, ...secondPass],
4768
+ remainingBudget: budget
4769
+ };
4770
+ }
4771
+ /**
4772
+ * Execute a single tool with timeout, permission check, and output capping.
4773
+ * Emits `tool.started` via the injected EventBus (if any) right before
4774
+ * invoking the tool — closes the observability gap between "model decided
4775
+ * to call a tool" and "tool.executed".
4776
+ */
4777
+ async executeTool(tool, use, ctx, budget) {
4778
+ this.opts.events?.emit("tool.started", {
4779
+ name: tool.name,
4780
+ id: use.id,
4781
+ input: use.input
4782
+ });
4783
+ this.opts.renderer?.writeToolCall(tool.name, use.input);
4784
+ const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
4785
+ const text = this.serializer.serialize(output);
4786
+ const scrubbed = this.opts.secretScrubber.scrub(text);
4787
+ const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
4788
+ this.opts.renderer?.writeToolResult(tool.name, capped, false);
4789
+ return {
4790
+ type: "tool_result",
4791
+ tool_use_id: use.id,
4792
+ name: tool.name,
4793
+ content: capped,
4794
+ is_error: false
4795
+ };
4796
+ }
4797
+ async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
4798
+ if (parentSignal.aborted) {
4799
+ throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
4800
+ }
4801
+ const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
4802
+ const ctrl = new AbortController();
4803
+ const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
4804
+ const combined = AbortSignal.any([parentSignal, ctrl.signal]);
4805
+ try {
4806
+ if (typeof tool.executeStream === "function") {
4807
+ return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
4808
+ }
4809
+ return await tool.execute(input, ctx, { signal: combined });
4810
+ } catch (err) {
4811
+ if (combined.aborted && typeof tool.cleanup === "function") {
4812
+ try {
4813
+ await tool.cleanup(input, ctx);
4814
+ } catch {
4815
+ }
4816
+ }
4817
+ throw err;
4818
+ } finally {
4819
+ clearTimeout(timer);
4820
+ }
4821
+ }
4822
+ async runStreamedTool(tool, input, ctx, signal, toolUseId) {
4823
+ let finalOutput;
4824
+ let sawFinal = false;
4825
+ const stream = tool.executeStream(input, ctx, { signal });
4826
+ for await (const ev of stream) {
4827
+ if (ev.type === "final") {
4828
+ finalOutput = ev.output;
4829
+ sawFinal = true;
4830
+ break;
4831
+ }
4832
+ this.opts.events?.emit("tool.progress", {
4833
+ name: tool.name,
4834
+ id: toolUseId ?? "<unknown>",
4835
+ event: ev
4836
+ });
4837
+ }
4838
+ if (!sawFinal) {
4839
+ throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
4840
+ }
4841
+ return finalOutput;
4842
+ }
4843
+ unknownToolResult(use, listFns) {
4844
+ return {
4845
+ type: "tool_result",
4846
+ tool_use_id: use.id,
4847
+ content: `Tool "${use.name}" is not registered. Available tools: ${listFns().join(", ")}`,
4848
+ is_error: true
4849
+ };
4850
+ }
4851
+ deniedResult(use, reason) {
4852
+ return {
4853
+ type: "tool_result",
4854
+ tool_use_id: use.id,
4855
+ content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
4856
+ is_error: true
4857
+ };
4858
+ }
4859
+ decrementBudget(result, budget) {
4860
+ const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
4861
+ return Math.max(0, budget - contentBytes);
4862
+ }
4863
+ /**
4864
+ * Compute the suggestedPattern string for a tool+input pair.
4865
+ * Matches the logic in DefaultPermissionPolicy so the TUI shows the
4866
+ * same subject that the trust file would use.
4867
+ */
4868
+ subjectFor(toolName, input) {
4869
+ if (!input || typeof input !== "object") return void 0;
4870
+ const obj = input;
4871
+ const globChars = /[*?\[\]]/g;
4872
+ const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
4873
+ if (toolName === "bash" && typeof obj.command === "string") {
4874
+ return escapeGlob(obj.command);
4875
+ }
4876
+ if (typeof obj.path === "string") {
4877
+ return escapeGlob(obj.path.replace(/\\/g, "/"));
4878
+ }
4879
+ if (typeof obj.url === "string") {
4880
+ return escapeGlob(obj.url);
4881
+ }
4882
+ if (typeof obj.name === "string") {
4883
+ return escapeGlob(obj.name);
4884
+ }
4885
+ return void 0;
4886
+ }
4887
+ };
4888
+
4889
+ // src/defaults/session-reader.ts
4890
+ var DefaultSessionReader = class {
4891
+ store;
4892
+ constructor(opts) {
4893
+ this.store = opts.store;
4894
+ }
4895
+ async query(q = {}) {
4896
+ const raw = await this.store.list(q.limit ? Math.max(q.limit * 4, 100) : 1e3);
4897
+ const titleNeedle = q.titleContains?.toLowerCase();
4898
+ const filtered = raw.filter((s) => {
4899
+ if (q.since && s.startedAt < q.since) return false;
4900
+ if (q.until && s.startedAt > q.until) return false;
4901
+ if (q.provider && s.provider !== q.provider) return false;
4902
+ if (q.model && s.model !== q.model) return false;
4903
+ if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
4904
+ if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
4905
+ return true;
4906
+ });
4907
+ const out = filtered.map((s) => ({
4908
+ id: s.id,
4909
+ title: s.title,
4910
+ startedAt: s.startedAt,
4911
+ provider: s.provider,
4912
+ model: s.model,
4913
+ tokenTotal: s.tokenTotal
4914
+ }));
4915
+ return q.limit ? out.slice(0, q.limit) : out;
4916
+ }
4917
+ async *replay(sessionId) {
4918
+ const data = await this.store.load(sessionId);
4919
+ for (const e of data.events) yield e;
4920
+ }
4921
+ async search(q, sessionId) {
4922
+ const limit = q.limit ?? 100;
4923
+ const matcher = buildMatcher(q);
4924
+ const allowedTypes = q.types ? new Set(q.types) : null;
4925
+ const ids = sessionId ? [sessionId] : (await this.store.list(1e3)).map((s) => s.id);
4926
+ const hits = [];
4927
+ for (const id of ids) {
4928
+ let data;
4929
+ try {
4930
+ data = await this.store.load(id);
4931
+ } catch {
4932
+ continue;
4933
+ }
4934
+ for (let i = 0; i < data.events.length; i++) {
4935
+ const ev = data.events[i];
4936
+ if (allowedTypes && !allowedTypes.has(ev.type)) continue;
4937
+ const text = eventText(ev);
4938
+ if (text === null) continue;
4939
+ const hit = matcher(text);
4940
+ if (!hit) continue;
4941
+ hits.push({
4942
+ sessionId: id,
4943
+ eventIndex: i,
4944
+ ts: ev.ts,
4945
+ type: ev.type,
4946
+ snippet: snippetOf(text, hit.start, hit.end)
4947
+ });
4948
+ if (hits.length >= limit) return hits;
4949
+ }
4950
+ }
4951
+ return hits;
4952
+ }
4953
+ async export(sessionId, opts) {
4954
+ const data = await this.store.load(sessionId);
4955
+ const includeTools = opts.includeTools ?? true;
4956
+ const includeDiagnostics = opts.includeDiagnostics ?? true;
4957
+ const filtered = data.events.filter((e) => {
4958
+ if (!includeTools && (e.type === "tool_use" || e.type === "tool_result" || e.type === "tool_call_start" || e.type === "tool_call_end")) {
4959
+ return false;
4960
+ }
4961
+ if (!includeDiagnostics && (e.type === "error" || e.type === "compaction" || e.type === "message_truncated")) {
4962
+ return false;
4963
+ }
4964
+ return true;
4965
+ });
4966
+ if (opts.format === "json") {
4967
+ return JSON.stringify({ metadata: data.metadata, events: filtered }, null, 2);
4968
+ }
4969
+ if (opts.format === "text") {
4970
+ return renderPlainText(data.metadata, filtered);
4971
+ }
4972
+ return renderMarkdown(data.metadata, filtered);
4973
+ }
4974
+ async metadata(sessionId) {
4975
+ const data = await this.store.load(sessionId);
4976
+ return data.metadata;
4977
+ }
4978
+ };
4979
+ function buildMatcher(q) {
4980
+ const ci = q.caseInsensitive ?? true;
4981
+ if (q.regex) {
4982
+ const flags = ci ? "i" : "";
4983
+ const re = new RegExp(q.query, flags);
4984
+ return (text) => {
4985
+ const m = re.exec(text);
4986
+ return m ? { start: m.index, end: m.index + m[0].length } : null;
4987
+ };
4988
+ }
4989
+ const needle = ci ? q.query.toLowerCase() : q.query;
4990
+ return (text) => {
4991
+ const hay = ci ? text.toLowerCase() : text;
4992
+ const idx = hay.indexOf(needle);
4993
+ return idx === -1 ? null : { start: idx, end: idx + needle.length };
4994
+ };
4995
+ }
4996
+ function eventText(e) {
4997
+ switch (e.type) {
4998
+ case "user_input":
4999
+ return contentToString(e.content);
5000
+ case "llm_response":
5001
+ return contentToString(e.content);
5002
+ case "tool_use":
5003
+ return `${e.name} ${JSON.stringify(e.input)}`;
5004
+ case "tool_result":
5005
+ return typeof e.content === "string" ? e.content : JSON.stringify(e.content);
5006
+ case "error":
5007
+ return `${e.phase}: ${e.message}`;
5008
+ case "session_start":
5009
+ case "session_resumed":
5010
+ return `${e.model}/${e.provider}`;
5011
+ case "task_created":
5012
+ case "task_completed":
5013
+ return e.title;
5014
+ case "task_failed":
5015
+ return `${e.title}: ${e.error}`;
5016
+ case "skill_activated":
5017
+ case "skill_deactivated":
5018
+ return e.skillName;
5019
+ default:
5020
+ return null;
5021
+ }
5022
+ }
5023
+ function contentToString(content) {
5024
+ if (typeof content === "string") return content;
5025
+ return content.map((b) => {
5026
+ switch (b.type) {
5027
+ case "text":
5028
+ return b.text;
5029
+ case "tool_use":
5030
+ return `[tool_use:${b.name} ${JSON.stringify(b.input)}]`;
5031
+ case "tool_result":
5032
+ return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
5033
+ default:
5034
+ return "";
5035
+ }
5036
+ }).join("\n");
5037
+ }
5038
+ var SNIPPET_RADIUS = 60;
5039
+ function snippetOf(text, start, end) {
5040
+ const from = Math.max(0, start - SNIPPET_RADIUS);
5041
+ const to = Math.min(text.length, end + SNIPPET_RADIUS);
5042
+ const prefix = from > 0 ? "\u2026" : "";
5043
+ const suffix = to < text.length ? "\u2026" : "";
5044
+ return prefix + text.slice(from, to).replace(/\s+/g, " ").trim() + suffix;
5045
+ }
5046
+ function renderMarkdown(meta, events) {
5047
+ const lines = [];
5048
+ lines.push(`# Session ${meta.id}`);
5049
+ lines.push("");
5050
+ if (meta.model || meta.provider) {
5051
+ lines.push(`- **Model:** ${meta.provider ?? "?"}/${meta.model ?? "?"}`);
5052
+ }
5053
+ lines.push(`- **Started:** ${meta.startedAt}`);
5054
+ if (meta.endedAt) lines.push(`- **Ended:** ${meta.endedAt}`);
5055
+ lines.push("");
5056
+ lines.push("---");
5057
+ lines.push("");
5058
+ for (const e of events) {
5059
+ switch (e.type) {
5060
+ case "user_input": {
5061
+ lines.push(`## User \u2014 ${e.ts}`);
5062
+ lines.push("");
5063
+ lines.push(contentToString(e.content));
5064
+ lines.push("");
5065
+ break;
5066
+ }
5067
+ case "llm_response": {
5068
+ lines.push(`## Assistant \u2014 ${e.ts}`);
5069
+ lines.push("");
5070
+ lines.push(contentToString(e.content));
5071
+ if (e.stopReason && e.stopReason !== "end_turn") {
5072
+ lines.push("");
5073
+ lines.push(`*stop: ${e.stopReason}*`);
5074
+ }
5075
+ lines.push("");
5076
+ break;
5077
+ }
5078
+ case "tool_use": {
5079
+ lines.push(`### Tool call: \`${e.name}\``);
5080
+ lines.push("");
5081
+ lines.push("```json");
5082
+ lines.push(JSON.stringify(e.input, null, 2));
5083
+ lines.push("```");
5084
+ lines.push("");
5085
+ break;
5086
+ }
5087
+ case "tool_result": {
5088
+ const body = typeof e.content === "string" ? e.content : JSON.stringify(e.content, null, 2);
5089
+ lines.push(`### Tool result${e.isError ? " (error)" : ""}`);
5090
+ lines.push("");
5091
+ lines.push("```");
5092
+ lines.push(body);
5093
+ lines.push("```");
5094
+ lines.push("");
5095
+ break;
5096
+ }
5097
+ case "error": {
5098
+ lines.push(`> **Error** (${e.phase}): ${e.message}`);
5099
+ lines.push("");
5100
+ break;
5101
+ }
5102
+ case "compaction": {
5103
+ lines.push(`> **Compaction**: ${e.before} \u2192 ${e.after} tokens`);
5104
+ lines.push("");
5105
+ break;
5106
+ }
5107
+ }
5108
+ }
5109
+ return lines.join("\n");
5110
+ }
5111
+ function renderPlainText(meta, events) {
5112
+ const lines = [];
5113
+ lines.push(`Session ${meta.id} \u2014 ${meta.provider ?? "?"}/${meta.model ?? "?"} \u2014 started ${meta.startedAt}`);
5114
+ lines.push("".padEnd(72, "-"));
5115
+ for (const e of events) {
5116
+ switch (e.type) {
5117
+ case "user_input":
5118
+ lines.push(`[${e.ts}] USER`);
5119
+ lines.push(contentToString(e.content));
5120
+ lines.push("");
5121
+ break;
5122
+ case "llm_response":
5123
+ lines.push(`[${e.ts}] ASSISTANT`);
5124
+ lines.push(contentToString(e.content));
5125
+ lines.push("");
5126
+ break;
5127
+ case "tool_use":
5128
+ lines.push(`[${e.ts}] TOOL_USE ${e.name} ${JSON.stringify(e.input)}`);
5129
+ break;
5130
+ case "tool_result":
5131
+ lines.push(
5132
+ `[${e.ts}] TOOL_RESULT${e.isError ? " (error)" : ""} ${typeof e.content === "string" ? e.content : JSON.stringify(e.content)}`
5133
+ );
5134
+ break;
5135
+ case "error":
5136
+ lines.push(`[${e.ts}] ERROR (${e.phase}): ${e.message}`);
5137
+ break;
5138
+ }
5139
+ }
5140
+ return lines.join("\n");
5141
+ }
5142
+
5143
+ // src/defaults/observability/metrics.ts
5144
+ var RESERVOIR_SIZE = 1024;
5145
+ function labelKey(labels) {
5146
+ if (!labels) return "";
5147
+ const keys = Object.keys(labels).sort();
5148
+ return keys.map((k) => `${k}=${labels[k]}`).join(",");
5149
+ }
5150
+ function quantile(sorted, q) {
5151
+ if (sorted.length === 0) return 0;
5152
+ const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
5153
+ return sorted[idx] ?? 0;
5154
+ }
5155
+ var InMemoryMetricsSink = class {
5156
+ counters = /* @__PURE__ */ new Map();
5157
+ gauges = /* @__PURE__ */ new Map();
5158
+ histograms = /* @__PURE__ */ new Map();
5159
+ counter(name, value = 1, labels) {
5160
+ const series = this.getOrCreate(this.counters, name);
5161
+ const key = labelKey(labels);
5162
+ const state = series.get(key) ?? { value: 0 };
5163
+ state.value += value;
5164
+ series.set(key, state);
5165
+ }
5166
+ gauge(name, value, labels) {
5167
+ const series = this.getOrCreate(this.gauges, name);
5168
+ series.set(labelKey(labels), { value });
5169
+ }
5170
+ histogram(name, value, labels) {
5171
+ const series = this.getOrCreate(this.histograms, name);
5172
+ const key = labelKey(labels);
5173
+ let state = series.get(key);
5174
+ if (!state) {
5175
+ state = { count: 0, sum: 0, min: value, max: value, samples: [] };
5176
+ series.set(key, state);
5177
+ }
5178
+ state.count++;
5179
+ state.sum += value;
5180
+ if (value < state.min) state.min = value;
5181
+ if (value > state.max) state.max = value;
5182
+ if (state.samples.length < RESERVOIR_SIZE) {
5183
+ state.samples.push(value);
5184
+ } else {
5185
+ const r = Math.floor(Math.random() * state.count);
5186
+ if (r < RESERVOIR_SIZE) state.samples[r] = value;
5187
+ }
5188
+ }
5189
+ snapshot() {
5190
+ const series = [];
5191
+ for (const [name, byLabel] of this.counters) {
5192
+ for (const [key, state] of byLabel) {
5193
+ series.push({
5194
+ name,
5195
+ type: "counter",
5196
+ labels: parseLabelKey(key),
5197
+ values: { value: state.value }
5198
+ });
5199
+ }
5200
+ }
5201
+ for (const [name, byLabel] of this.gauges) {
5202
+ for (const [key, state] of byLabel) {
5203
+ series.push({
5204
+ name,
5205
+ type: "gauge",
5206
+ labels: parseLabelKey(key),
5207
+ values: { value: state.value }
5208
+ });
5209
+ }
5210
+ }
5211
+ for (const [name, byLabel] of this.histograms) {
5212
+ for (const [key, state] of byLabel) {
5213
+ const sorted = [...state.samples].sort((a, b) => a - b);
5214
+ series.push({
5215
+ name,
5216
+ type: "histogram",
5217
+ labels: parseLabelKey(key),
5218
+ values: {
5219
+ count: state.count,
5220
+ sum: state.sum,
5221
+ min: state.min,
5222
+ max: state.max,
5223
+ p50: quantile(sorted, 0.5),
5224
+ p95: quantile(sorted, 0.95),
5225
+ p99: quantile(sorted, 0.99)
5226
+ }
5227
+ });
5228
+ }
5229
+ }
5230
+ return { timestamp: Date.now(), series };
5231
+ }
5232
+ reset() {
5233
+ this.counters.clear();
5234
+ this.gauges.clear();
5235
+ this.histograms.clear();
5236
+ }
5237
+ getOrCreate(bag, name) {
5238
+ let series = bag.get(name);
5239
+ if (!series) {
5240
+ series = /* @__PURE__ */ new Map();
5241
+ bag.set(name, series);
5242
+ }
5243
+ return series;
5244
+ }
5245
+ };
5246
+ function parseLabelKey(key) {
5247
+ if (!key) return {};
5248
+ const labels = {};
5249
+ for (const pair of key.split(",")) {
5250
+ const eq = pair.indexOf("=");
5251
+ if (eq > 0) labels[pair.slice(0, eq)] = pair.slice(eq + 1);
5252
+ }
5253
+ return labels;
5254
+ }
5255
+ var NoopMetricsSink = class {
5256
+ counter() {
5257
+ }
5258
+ gauge() {
5259
+ }
5260
+ histogram() {
5261
+ }
5262
+ snapshot() {
5263
+ return { timestamp: Date.now(), series: [] };
5264
+ }
5265
+ reset() {
5266
+ }
5267
+ };
5268
+
5269
+ // src/defaults/observability/health.ts
5270
+ var SEVERITY = {
5271
+ healthy: 0,
5272
+ degraded: 1,
5273
+ unhealthy: 2
5274
+ };
5275
+ var DefaultHealthRegistry = class {
5276
+ checks = /* @__PURE__ */ new Map();
5277
+ timeoutMs;
5278
+ constructor(opts = {}) {
5279
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
5280
+ }
5281
+ register(check) {
5282
+ this.checks.set(check.name, check);
5283
+ }
5284
+ unregister(name) {
5285
+ this.checks.delete(name);
5286
+ }
5287
+ async run() {
5288
+ const results = await Promise.all(
5289
+ Array.from(this.checks.values()).map(async (c) => {
5290
+ const result = await this.runOne(c);
5291
+ return { name: c.name, ...result };
5292
+ })
5293
+ );
5294
+ let status = "healthy";
5295
+ for (const r of results) {
5296
+ if (SEVERITY[r.status] > SEVERITY[status]) status = r.status;
5297
+ }
5298
+ return { status, timestamp: Date.now(), checks: results };
5299
+ }
5300
+ async runOne(check) {
5301
+ const timeout = new Promise((resolve3) => {
5302
+ setTimeout(
5303
+ () => resolve3({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
5304
+ this.timeoutMs
5305
+ );
5306
+ });
5307
+ try {
5308
+ return await Promise.race([check.check(), timeout]);
5309
+ } catch (err) {
5310
+ return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
5311
+ }
5312
+ }
5313
+ };
5314
+
5315
+ // src/defaults/observability/tracer.ts
5316
+ var NoopTracer = class {
5317
+ startSpan() {
5318
+ return NOOP_SPAN;
4145
5319
  }
4146
- async createFlow(specContent, options) {
4147
- const flow = new TaskFlow({
4148
- tracker: this.tracker,
4149
- events: this.events,
4150
- ...options
4151
- });
4152
- const graph = await flow.fromSpec(specContent);
4153
- this.flows.set(graph.id, flow);
4154
- return flow;
5320
+ };
5321
+ var NOOP_SPAN = {
5322
+ setAttribute() {
5323
+ },
5324
+ recordError() {
5325
+ },
5326
+ end() {
4155
5327
  }
4156
- getTracker() {
4157
- return this.tracker;
5328
+ };
5329
+
5330
+ // src/defaults/observability/otel-tracer.ts
5331
+ var OTEL_STATUS_ERROR = 2;
5332
+ var OTelTracer = class {
5333
+ constructor(upstream) {
5334
+ this.upstream = upstream;
4158
5335
  }
4159
- getFlow(graphId) {
4160
- return this.flows.get(graphId);
5336
+ upstream;
5337
+ startSpan(name, attrs) {
5338
+ const otelSpan = this.upstream.startSpan(name, attrs ? { attributes: attrs } : void 0);
5339
+ return new OTelSpan(otelSpan);
4161
5340
  }
4162
- listFlows() {
4163
- return Array.from(this.flows.entries()).map(([id, flow]) => ({
4164
- id,
4165
- title: flow.getGraph()?.title ?? "Untitled",
4166
- phase: flow.getPhase()
4167
- }));
5341
+ };
5342
+ var OTelSpan = class {
5343
+ constructor(span) {
5344
+ this.span = span;
5345
+ }
5346
+ span;
5347
+ setAttribute(key, value) {
5348
+ this.span.setAttribute(key, value);
5349
+ }
5350
+ recordError(err) {
5351
+ this.span.recordException(err);
5352
+ this.span.setStatus?.({ code: OTEL_STATUS_ERROR, message: err.message });
5353
+ }
5354
+ end() {
5355
+ this.span.end();
4168
5356
  }
4169
5357
  };
4170
5358
 
4171
- // src/utils/tool-output-serializer.ts
4172
- function createToolOutputSerializer(opts = {}) {
4173
- const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
4174
- function serialize(value) {
4175
- if (typeof value === "string") return value;
4176
- if (value === null || value === void 0) return "";
4177
- if (typeof value === "object") {
4178
- if (Array.isArray(value)) return value.map(serialize).join("\n");
4179
- if ("text" in value) {
4180
- const t = value.text;
4181
- return typeof t === "string" ? t : JSON.stringify(value, null, 2);
5359
+ // src/defaults/observability/event-bridge.ts
5360
+ function wireMetricsToEvents(events, sink) {
5361
+ const unsubs = [];
5362
+ unsubs.push(
5363
+ events.on("session.started", () => sink.counter("agent.sessions.started")),
5364
+ events.on("session.ended", (e) => {
5365
+ sink.counter("agent.sessions.ended");
5366
+ sink.histogram("agent.session.tokens.input", e.usage.input);
5367
+ sink.histogram("agent.session.tokens.output", e.usage.output);
5368
+ }),
5369
+ events.on("session.damaged", () => sink.counter("agent.sessions.damaged")),
5370
+ events.on("iteration.completed", () => sink.counter("agent.iterations.total")),
5371
+ events.on("iteration.limit_reached", () => sink.counter("agent.iteration_limit.hit")),
5372
+ events.on("provider.response", (e) => {
5373
+ sink.counter("provider.responses.total", 1, { stop_reason: e.stopReason });
5374
+ sink.counter("provider.tokens.input", e.usage.input);
5375
+ sink.counter("provider.tokens.output", e.usage.output);
5376
+ if (e.usage.cacheRead) sink.counter("provider.tokens.cache_read", e.usage.cacheRead);
5377
+ if (e.usage.cacheWrite) sink.counter("provider.tokens.cache_write", e.usage.cacheWrite);
5378
+ }),
5379
+ events.on(
5380
+ "provider.retry",
5381
+ (e) => sink.counter("provider.retries.total", 1, {
5382
+ provider: e.providerId,
5383
+ status: String(e.status)
5384
+ })
5385
+ ),
5386
+ events.on(
5387
+ "provider.error",
5388
+ (e) => sink.counter("provider.errors.total", 1, {
5389
+ provider: e.providerId,
5390
+ status: String(e.status),
5391
+ retryable: String(e.retryable)
5392
+ })
5393
+ ),
5394
+ events.on("tool.started", (e) => sink.counter("tool.starts.total", 1, { tool: e.name })),
5395
+ events.on("tool.executed", (e) => {
5396
+ sink.counter("tool.executions.total", 1, { tool: e.name, ok: String(e.ok) });
5397
+ sink.histogram("tool.duration_ms", e.durationMs, { tool: e.name });
5398
+ }),
5399
+ events.on("token.threshold", (e) => sink.gauge("agent.tokens.used", e.used)),
5400
+ events.on("compaction.fired", (e) => {
5401
+ sink.counter("compaction.fired.total");
5402
+ sink.histogram("compaction.reduction_tokens", e.before - e.after);
5403
+ }),
5404
+ events.on(
5405
+ "mcp.server.connected",
5406
+ (e) => sink.counter("mcp.connects.total", 1, { server: e.name })
5407
+ ),
5408
+ events.on(
5409
+ "mcp.server.reconnected",
5410
+ (e) => sink.counter("mcp.reconnects.total", 1, { server: e.name })
5411
+ ),
5412
+ events.on(
5413
+ "mcp.server.disconnected",
5414
+ (e) => sink.counter("mcp.disconnects.total", 1, { server: e.name })
5415
+ ),
5416
+ events.on("error", (e) => sink.counter("agent.errors.total", 1, { phase: e.phase }))
5417
+ );
5418
+ return () => {
5419
+ for (const u of unsubs) u();
5420
+ };
5421
+ }
5422
+
5423
+ // src/defaults/observability/prometheus.ts
5424
+ var NUMBER_FORMAT_INFINITY = "NaN";
5425
+ function escapeLabelValue(v) {
5426
+ return v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
5427
+ }
5428
+ function formatLabels(labels) {
5429
+ const keys = Object.keys(labels);
5430
+ if (keys.length === 0) return "";
5431
+ const parts = keys.map((k) => `${k}="${escapeLabelValue(labels[k] ?? "")}"`);
5432
+ return `{${parts.join(",")}}`;
5433
+ }
5434
+ function formatNumber(n) {
5435
+ if (!Number.isFinite(n)) return NUMBER_FORMAT_INFINITY;
5436
+ return Number.isInteger(n) ? n.toString() : n.toString();
5437
+ }
5438
+ function joinLabels(base, extra) {
5439
+ return { ...base, ...extra };
5440
+ }
5441
+ function renderPrometheus(snapshot) {
5442
+ const groups = /* @__PURE__ */ new Map();
5443
+ for (const s of snapshot.series) {
5444
+ let g = groups.get(s.name);
5445
+ if (!g) {
5446
+ g = { type: s.type, rows: [] };
5447
+ groups.set(s.name, g);
5448
+ }
5449
+ g.rows.push({ labels: s.labels, values: s.values });
5450
+ }
5451
+ const lines = [];
5452
+ for (const [name, g] of groups) {
5453
+ const promType = g.type === "histogram" ? "summary" : g.type;
5454
+ lines.push(`# HELP ${name} ${name}`);
5455
+ lines.push(`# TYPE ${name} ${promType}`);
5456
+ if (g.type === "counter" || g.type === "gauge") {
5457
+ for (const row of g.rows) {
5458
+ lines.push(`${name}${formatLabels(row.labels)} ${formatNumber(row.values.value ?? 0)}`);
4182
5459
  }
4183
- try {
4184
- return JSON.stringify(value, null, 2);
4185
- } catch {
4186
- return String(value);
5460
+ } else {
5461
+ for (const row of g.rows) {
5462
+ const { count = 0, sum = 0, p50 = 0, p95 = 0, p99 = 0 } = row.values;
5463
+ lines.push(
5464
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.5" }))} ${formatNumber(p50)}`
5465
+ );
5466
+ lines.push(
5467
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.95" }))} ${formatNumber(p95)}`
5468
+ );
5469
+ lines.push(
5470
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.99" }))} ${formatNumber(p99)}`
5471
+ );
5472
+ lines.push(`${name}_sum${formatLabels(row.labels)} ${formatNumber(sum)}`);
5473
+ lines.push(`${name}_count${formatLabels(row.labels)} ${formatNumber(count)}`);
4187
5474
  }
4188
5475
  }
4189
- return String(value);
4190
5476
  }
4191
- function enforceCap(text, remainingBudget) {
4192
- if (remainingBudget <= 0) {
4193
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
5477
+ return lines.join("\n") + "\n";
5478
+ }
5479
+ var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
5480
+ async function startMetricsServer(opts) {
5481
+ const { createServer } = await import('http');
5482
+ const host = opts.host ?? "127.0.0.1";
5483
+ const path13 = opts.path ?? "/metrics";
5484
+ const healthPath = opts.healthPath ?? "/healthz";
5485
+ const healthRegistry = opts.healthRegistry;
5486
+ const server = createServer((req, res) => {
5487
+ if (!req.url || req.method !== "GET") {
5488
+ res.statusCode = req.url ? 405 : 400;
5489
+ res.end();
5490
+ return;
4194
5491
  }
4195
- const textBytes = Buffer.byteLength(text, "utf8");
4196
- if (textBytes <= remainingBudget) {
4197
- return { text, newBudget: remainingBudget - textBytes };
5492
+ const url = req.url.split("?")[0];
5493
+ if (url === path13) {
5494
+ let body;
5495
+ try {
5496
+ body = renderPrometheus(opts.sink.snapshot());
5497
+ } catch (err) {
5498
+ res.statusCode = 500;
5499
+ res.setHeader("content-type", "text/plain; charset=utf-8");
5500
+ res.end(`metrics render failed: ${err instanceof Error ? err.message : String(err)}`);
5501
+ return;
5502
+ }
5503
+ res.statusCode = 200;
5504
+ res.setHeader("content-type", PROMETHEUS_CONTENT_TYPE);
5505
+ res.end(body);
5506
+ return;
4198
5507
  }
4199
- const marker = `
4200
- \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
4201
- `;
4202
- const markerBytes = Buffer.byteLength(marker, "utf8");
4203
- const available = remainingBudget - markerBytes;
4204
- if (available <= 0) {
4205
- return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
5508
+ if (healthRegistry && url === healthPath) {
5509
+ healthRegistry.run().then(
5510
+ (agg) => {
5511
+ res.statusCode = agg.status === "unhealthy" ? 503 : 200;
5512
+ res.setHeader("content-type", "application/json; charset=utf-8");
5513
+ res.end(JSON.stringify(agg, null, 2));
5514
+ },
5515
+ (err) => {
5516
+ res.statusCode = 500;
5517
+ res.setHeader("content-type", "text/plain; charset=utf-8");
5518
+ res.end(`health run failed: ${err instanceof Error ? err.message : String(err)}`);
5519
+ }
5520
+ );
5521
+ return;
4206
5522
  }
4207
- const half = Math.floor(available / 2);
4208
- const first = text.slice(0, half);
4209
- Buffer.byteLength(first, "utf8");
4210
- const second = text.slice(text.length - half);
4211
- return { text: `${first}${marker}${second}`, newBudget: 0 };
4212
- }
4213
- return { serialize, enforceCap, capBytes };
5523
+ res.statusCode = 404;
5524
+ res.setHeader("content-type", "text/plain; charset=utf-8");
5525
+ res.end("Not Found");
5526
+ });
5527
+ await new Promise((resolve3, reject) => {
5528
+ const onError = (err) => {
5529
+ server.off("listening", onListening);
5530
+ reject(err);
5531
+ };
5532
+ const onListening = () => {
5533
+ server.off("error", onError);
5534
+ resolve3();
5535
+ };
5536
+ server.once("error", onError);
5537
+ server.once("listening", onListening);
5538
+ server.listen(opts.port, host);
5539
+ });
5540
+ const addr = server.address();
5541
+ const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
5542
+ return {
5543
+ port: boundPort,
5544
+ url: `http://${host}:${boundPort}${path13}`,
5545
+ close: () => new Promise((resolve3, reject) => {
5546
+ server.close((err) => err ? reject(err) : resolve3());
5547
+ })
5548
+ };
4214
5549
  }
4215
5550
 
4216
- // src/defaults/tool-executor.ts
4217
- var ToolExecutor = class {
4218
- constructor(registry, opts) {
4219
- this.registry = registry;
4220
- this.opts = opts;
4221
- this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
4222
- this.serializer = createToolOutputSerializer({
4223
- perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
4224
- });
4225
- }
4226
- registry;
4227
- opts;
4228
- serializer;
4229
- iterationTimeoutMs;
4230
- /**
4231
- * Execute a batch of tool uses using the configured strategy.
4232
- * Returns the execution results and the remaining output budget.
4233
- */
4234
- async executeBatch(toolUses, ctx, strategy) {
4235
- let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
4236
- const runOne = async (use, index) => {
4237
- const start = Date.now();
4238
- const tool = this.registry.get(use.name);
4239
- let result;
4240
- if (!tool) {
4241
- result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
4242
- } else {
4243
- const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
4244
- if (decision.permission === "deny") {
4245
- result = this.deniedResult(use, decision.reason);
4246
- } else if (decision.permission === "confirm") {
4247
- result = this.confirmResult(use);
4248
- } else {
4249
- try {
4250
- result = await this.executeTool(tool, use, ctx, budget);
4251
- } catch (err) {
4252
- const msg = err instanceof Error ? err.message : String(err);
4253
- const scrubbed = this.opts.secretScrubber.scrub(msg);
4254
- this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
4255
- result = {
4256
- type: "tool_result",
4257
- tool_use_id: use.id,
4258
- content: `Tool "${use.name}" threw: ${scrubbed}`,
4259
- is_error: true
4260
- };
4261
- }
4262
- }
4263
- }
4264
- const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
4265
- budget = Math.max(0, budget - contentBytes);
4266
- return { result, tool, durationMs: Date.now() - start };
5551
+ // src/defaults/observability/otlp-metrics.ts
5552
+ var DEFAULT_INTERVAL_MS = 3e4;
5553
+ var DEFAULT_TIMEOUT_MS = 1e4;
5554
+ function joinEndpoint(base) {
5555
+ if (/\/v1\/metrics\/?$/.test(base)) return base;
5556
+ return base.replace(/\/$/, "") + "/v1/metrics";
5557
+ }
5558
+ function attributesFor(labels) {
5559
+ return Object.entries(labels).map(([key, value]) => ({
5560
+ key,
5561
+ value: { stringValue: value }
5562
+ }));
5563
+ }
5564
+ function buildExportBody(opts) {
5565
+ const metrics = [];
5566
+ for (const s of opts.series) {
5567
+ const dp = {
5568
+ attributes: attributesFor(s.labels),
5569
+ timeUnixNano: opts.timeUnixNano
4267
5570
  };
4268
- if (strategy === "sequential") {
4269
- const outputs = [];
4270
- for (let i = 0; i < toolUses.length; i++) {
4271
- const use = toolUses[i];
4272
- if (use) outputs.push(await runOne(use));
4273
- }
4274
- return { outputs, remainingBudget: budget };
4275
- }
4276
- if (strategy === "parallel") {
4277
- const outputs = await Promise.all(toolUses.map((use, i) => runOne(use)));
4278
- return { outputs, remainingBudget: budget };
4279
- }
4280
- const nonMutating = [];
4281
- const mutating = [];
4282
- for (let i = 0; i < toolUses.length; i++) {
4283
- const use = toolUses[i];
4284
- if (!use) continue;
4285
- const tool = this.registry.get(use.name);
4286
- if (tool?.mutating) mutating.push({ use, index: i });
4287
- else nonMutating.push({ use, index: i });
4288
- }
4289
- const firstPass = await Promise.all(nonMutating.map(({ use, index }) => runOne(use)));
4290
- const secondPass = [];
4291
- for (const { use, index } of mutating) {
4292
- secondPass.push(await runOne(use));
5571
+ if (s.type === "counter") {
5572
+ dp.asDouble = s.values.value ?? 0;
5573
+ metrics.push({
5574
+ name: s.name,
5575
+ sum: { dataPoints: [dp], aggregationTemporality: 2, isMonotonic: true }
5576
+ });
5577
+ } else if (s.type === "gauge") {
5578
+ dp.asDouble = s.values.value ?? 0;
5579
+ metrics.push({ name: s.name, gauge: { dataPoints: [dp] } });
5580
+ } else {
5581
+ dp.count = String(s.values.count ?? 0);
5582
+ dp.sum = s.values.sum ?? 0;
5583
+ dp.quantileValues = [
5584
+ { quantile: 0.5, value: s.values.p50 ?? 0 },
5585
+ { quantile: 0.95, value: s.values.p95 ?? 0 },
5586
+ { quantile: 0.99, value: s.values.p99 ?? 0 }
5587
+ ];
5588
+ metrics.push({ name: s.name, summary: { dataPoints: [dp] } });
4293
5589
  }
4294
- return {
4295
- outputs: [...firstPass, ...secondPass],
4296
- remainingBudget: budget
4297
- };
4298
5590
  }
4299
- /**
4300
- * Execute a single tool with timeout, permission check, and output capping.
4301
- * Emits `tool.started` via the injected EventBus (if any) right before
4302
- * invoking the tool — closes the observability gap between "model decided
4303
- * to call a tool" and "tool.executed".
4304
- */
4305
- async executeTool(tool, use, ctx, budget) {
4306
- this.opts.events?.emit("tool.started", {
4307
- name: tool.name,
4308
- id: use.id,
4309
- input: use.input
5591
+ return {
5592
+ resourceMetrics: [
5593
+ {
5594
+ resource: { attributes: attributesFor(opts.resourceAttributes) },
5595
+ scopeMetrics: [
5596
+ {
5597
+ scope: { name: opts.scopeName },
5598
+ metrics
5599
+ }
5600
+ ]
5601
+ }
5602
+ ]
5603
+ };
5604
+ }
5605
+ function buildOtlpMetricsRequest(sink, opts = {}) {
5606
+ return buildExportBody({
5607
+ series: sink.snapshot().series,
5608
+ resourceAttributes: opts.resourceAttributes ?? { "service.name": "wrongstack" },
5609
+ scopeName: opts.scopeName ?? "wrongstack",
5610
+ timeUnixNano: String(BigInt(Date.now()) * 1000000n)
5611
+ });
5612
+ }
5613
+ function startOtlpMetricsExporter(opts) {
5614
+ const url = joinEndpoint(opts.endpoint);
5615
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
5616
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5617
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
5618
+ const onError = opts.onError ?? (() => {
5619
+ });
5620
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
5621
+ const scopeName = opts.scopeName ?? "wrongstack";
5622
+ let stopped = false;
5623
+ const headers = {
5624
+ "content-type": "application/json",
5625
+ ...opts.headers ?? {}
5626
+ };
5627
+ if (opts.authorization) headers.authorization = opts.authorization;
5628
+ async function pushOnce() {
5629
+ if (stopped) return;
5630
+ const body = buildExportBody({
5631
+ series: opts.sink.snapshot().series,
5632
+ resourceAttributes,
5633
+ scopeName,
5634
+ timeUnixNano: String(BigInt(Date.now()) * 1000000n)
4310
5635
  });
4311
- this.opts.renderer?.writeToolCall(tool.name, use.input);
4312
- const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx);
4313
- const text = this.serializer.serialize(output);
4314
- const scrubbed = this.opts.secretScrubber.scrub(text);
4315
- const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
4316
- this.opts.renderer?.writeToolResult(tool.name, capped, false);
4317
- return {
4318
- type: "tool_result",
4319
- tool_use_id: use.id,
4320
- name: tool.name,
4321
- content: capped,
4322
- is_error: false
4323
- };
4324
- }
4325
- async runWithTimeout(tool, input, parentSignal, ctx) {
4326
- if (parentSignal.aborted) {
4327
- throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
4328
- }
4329
- const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
4330
- const ctrl = new AbortController();
4331
- const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
4332
- const combined = anySignal([parentSignal, ctrl.signal]);
5636
+ const controller = new AbortController();
5637
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4333
5638
  try {
4334
- return await tool.execute(input, ctx, { signal: combined });
5639
+ const res = await fetchImpl(url, {
5640
+ method: "POST",
5641
+ headers,
5642
+ body: JSON.stringify(body),
5643
+ signal: controller.signal
5644
+ });
5645
+ if (!res.ok) {
5646
+ const text = await res.text().catch(() => "");
5647
+ onError(new Error(`OTLP push failed: ${res.status} ${res.statusText} ${text}`));
5648
+ }
5649
+ } catch (err) {
5650
+ onError(err);
4335
5651
  } finally {
4336
5652
  clearTimeout(timer);
4337
5653
  }
4338
5654
  }
4339
- unknownToolResult(use, listFns) {
4340
- return {
4341
- type: "tool_result",
4342
- tool_use_id: use.id,
4343
- content: `Tool "${use.name}" is not registered. Available tools: ${listFns().join(", ")}`,
4344
- is_error: true
4345
- };
5655
+ const handle = setInterval(() => {
5656
+ void pushOnce();
5657
+ }, intervalMs);
5658
+ handle.unref?.();
5659
+ return {
5660
+ flush: pushOnce,
5661
+ async stop() {
5662
+ stopped = true;
5663
+ clearInterval(handle);
5664
+ await pushOnce().catch(onError);
5665
+ }
5666
+ };
5667
+ }
5668
+ var SPAN_STATUS_CODE_UNSET = 0;
5669
+ var SPAN_STATUS_CODE_OK = 1;
5670
+ var SPAN_STATUS_CODE_ERROR = 2;
5671
+ function hex(bytes) {
5672
+ return crypto2.randomBytes(bytes).toString("hex");
5673
+ }
5674
+ function nowNs() {
5675
+ return BigInt(Date.now()) * 1000000n;
5676
+ }
5677
+ var CapturingSpan = class {
5678
+ constructor(state, onEnd) {
5679
+ this.state = state;
5680
+ this.onEnd = onEnd;
4346
5681
  }
4347
- deniedResult(use, reason) {
4348
- return {
4349
- type: "tool_result",
4350
- tool_use_id: use.id,
4351
- content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
4352
- is_error: true
4353
- };
5682
+ state;
5683
+ onEnd;
5684
+ setAttribute(key, value) {
5685
+ this.state.attributes[key] = value;
4354
5686
  }
4355
- confirmResult(use) {
4356
- return {
4357
- type: "tool_result",
4358
- tool_use_id: use.id,
4359
- content: `Tool "${use.name}" requires user confirmation but no prompt handler was available.`,
4360
- is_error: true
4361
- };
5687
+ recordError(err) {
5688
+ this.state.status = { code: SPAN_STATUS_CODE_ERROR, message: err.message };
5689
+ this.state.attributes["exception.message"] = err.message;
5690
+ if (err.name) this.state.attributes["exception.type"] = err.name;
5691
+ }
5692
+ end() {
5693
+ if (this.state.endTimeUnixNano !== void 0) return;
5694
+ this.state.endTimeUnixNano = nowNs();
5695
+ if (this.state.status.code === SPAN_STATUS_CODE_UNSET) {
5696
+ this.state.status.code = SPAN_STATUS_CODE_OK;
5697
+ }
5698
+ this.onEnd(this.state);
4362
5699
  }
4363
5700
  };
4364
- function anySignal(signals) {
4365
- if (typeof AbortSignal.any === "function") {
4366
- return AbortSignal.any(signals);
5701
+ var DEFAULT_INTERVAL_MS2 = 5e3;
5702
+ var DEFAULT_BUFFER_CAP = 2048;
5703
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
5704
+ function joinEndpoint2(base) {
5705
+ if (/\/v1\/traces\/?$/.test(base)) return base;
5706
+ return base.replace(/\/$/, "") + "/v1/traces";
5707
+ }
5708
+ function encodeAttr(key, value) {
5709
+ if (typeof value === "boolean") return { key, value: { boolValue: value } };
5710
+ if (typeof value === "number") {
5711
+ return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
4367
5712
  }
4368
- const ctrl = new AbortController();
4369
- const abortSources = [];
4370
- for (const s of signals) {
4371
- if (s.aborted) {
4372
- ctrl.abort(s.reason);
4373
- return ctrl.signal;
5713
+ return { key, value: { stringValue: value } };
5714
+ }
5715
+ function buildOtlpTracesRequest(spans, opts = {}) {
5716
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
5717
+ const scopeName = opts.scopeName ?? "wrongstack";
5718
+ const otlpSpans = spans.map((s) => ({
5719
+ traceId: s.traceId,
5720
+ spanId: s.spanId,
5721
+ name: s.name,
5722
+ kind: 1,
5723
+ // SPAN_KIND_INTERNAL
5724
+ startTimeUnixNano: s.startTimeUnixNano.toString(),
5725
+ endTimeUnixNano: (s.endTimeUnixNano ?? s.startTimeUnixNano).toString(),
5726
+ attributes: Object.entries(s.attributes).map(([k, v]) => encodeAttr(k, v)),
5727
+ status: s.status
5728
+ }));
5729
+ return {
5730
+ resourceSpans: [
5731
+ {
5732
+ resource: {
5733
+ attributes: Object.entries(resourceAttributes).map(
5734
+ ([k, v]) => encodeAttr(k, v)
5735
+ )
5736
+ },
5737
+ scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }]
5738
+ }
5739
+ ]
5740
+ };
5741
+ }
5742
+ function startOtlpTraceExporter(opts) {
5743
+ const url = joinEndpoint2(opts.endpoint);
5744
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
5745
+ const maxBuffered = opts.maxBufferedSpans ?? DEFAULT_BUFFER_CAP;
5746
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5747
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
5748
+ const onError = opts.onError ?? (() => {
5749
+ });
5750
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
5751
+ const scopeName = opts.scopeName ?? "wrongstack";
5752
+ let stopped = false;
5753
+ const buffer = [];
5754
+ const headers = {
5755
+ "content-type": "application/json",
5756
+ ...opts.headers ?? {}
5757
+ };
5758
+ if (opts.authorization) headers.authorization = opts.authorization;
5759
+ const tracer = {
5760
+ startSpan(name, attrs) {
5761
+ const state = {
5762
+ traceId: hex(16),
5763
+ spanId: hex(8),
5764
+ name,
5765
+ startTimeUnixNano: nowNs(),
5766
+ attributes: { ...attrs ?? {} },
5767
+ status: { code: SPAN_STATUS_CODE_UNSET }
5768
+ };
5769
+ return new CapturingSpan(state, (ended) => {
5770
+ if (buffer.length >= maxBuffered) buffer.shift();
5771
+ buffer.push(ended);
5772
+ });
5773
+ }
5774
+ };
5775
+ async function pushOnce() {
5776
+ if (buffer.length === 0) return;
5777
+ const batch = buffer.splice(0, buffer.length);
5778
+ const body = buildOtlpTracesRequest(batch, { resourceAttributes, scopeName });
5779
+ const controller = new AbortController();
5780
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5781
+ try {
5782
+ const res = await fetchImpl(url, {
5783
+ method: "POST",
5784
+ headers,
5785
+ body: JSON.stringify(body),
5786
+ signal: controller.signal
5787
+ });
5788
+ if (!res.ok) {
5789
+ const text = await res.text().catch(() => "");
5790
+ onError(new Error(`OTLP traces push failed: ${res.status} ${res.statusText} ${text}`));
5791
+ }
5792
+ } catch (err) {
5793
+ onError(err);
5794
+ } finally {
5795
+ clearTimeout(timer);
4374
5796
  }
4375
- abortSources.push(s);
4376
- }
4377
- for (const s of abortSources) {
4378
- s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
4379
5797
  }
4380
- return ctrl.signal;
5798
+ const handle = setInterval(() => {
5799
+ if (!stopped) void pushOnce();
5800
+ }, intervalMs);
5801
+ handle.unref?.();
5802
+ return {
5803
+ tracer,
5804
+ flush: pushOnce,
5805
+ async stop() {
5806
+ stopped = true;
5807
+ clearInterval(handle);
5808
+ await pushOnce().catch(onError);
5809
+ },
5810
+ buffered: () => [...buffer]
5811
+ };
4381
5812
  }
4382
5813
 
4383
5814
  // src/defaults/context-manager.ts
@@ -4544,6 +5975,107 @@ function createContextManagerTool(opts = {}) {
4544
5975
  }
4545
5976
  var contextManagerTool = createContextManagerTool();
4546
5977
 
4547
- export { AutoCompactionMiddleware, AutonomousRunner, DefaultAttachmentStore, DefaultConfigLoader, DefaultErrorHandler, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, IntelligentCompactor, LLMSelector, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, classifyFamily, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, loadProjectModes, loadUserModes, migratePlaintextSecrets, rewriteConfigEncrypted };
5978
+ // src/defaults/mcp-servers.ts
5979
+ var filesystemServer = () => ({
5980
+ name: "filesystem",
5981
+ description: "Read, write, and navigate the local filesystem (read-heavy tools)",
5982
+ transport: "stdio",
5983
+ command: "npx",
5984
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
5985
+ permission: "confirm"
5986
+ });
5987
+ var githubServer = () => ({
5988
+ name: "github",
5989
+ description: "GitHub API \u2014 issues, PRs, repos, search, file ops (requires GITHUB_PERSONAL_ACCESS_TOKEN)",
5990
+ transport: "stdio",
5991
+ command: "npx",
5992
+ args: ["-y", "@modelcontextprotocol/server-github"],
5993
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN ?? "" },
5994
+ permission: "confirm"
5995
+ });
5996
+ var context7Server = () => ({
5997
+ name: "context7",
5998
+ description: "Codebase-aware documentation and Q&A (context7.ai)",
5999
+ transport: "streamable-http",
6000
+ url: "https://server.context7.ai/mcp",
6001
+ permission: "confirm"
6002
+ });
6003
+ var braveSearchServer = () => ({
6004
+ name: "brave-search",
6005
+ description: "Web search (Brave). Requires BRAVE_SEARCH_API_KEY \u2014 free tier 2k queries/month",
6006
+ transport: "stdio",
6007
+ command: "npx",
6008
+ args: ["-y", "@modelcontextprotocol/server-brave-search"],
6009
+ env: { BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY ?? "" },
6010
+ permission: "confirm"
6011
+ });
6012
+ var blockServer = () => ({
6013
+ name: "block",
6014
+ description: "Postgres database access via SQL (Block MCP server)",
6015
+ transport: "stdio",
6016
+ command: "npx",
6017
+ args: ["-y", "@modelcontextprotocol/server-block"],
6018
+ permission: "confirm"
6019
+ });
6020
+ var everArtServer = () => ({
6021
+ name: "everart",
6022
+ description: "AI image generation (EverArt). Requires EVERART_API_KEY",
6023
+ transport: "stdio",
6024
+ command: "npx",
6025
+ args: ["-y", "@modelcontextprotocol/server-everart"],
6026
+ env: { EVERART_API_KEY: process.env.EVERART_API_KEY ?? "" },
6027
+ permission: "confirm"
6028
+ });
6029
+ var slackServer = () => ({
6030
+ name: "slack",
6031
+ description: "Slack \u2014 messaging, channels, search. Requires SLACK_BOT_TOKEN + SLACK_TEAM_ID",
6032
+ transport: "stdio",
6033
+ command: "npx",
6034
+ args: ["-y", "@modelcontextprotocol/server-slack"],
6035
+ env: {
6036
+ SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN ?? "",
6037
+ SLACK_TEAM_ID: process.env.SLACK_TEAM_ID ?? ""
6038
+ },
6039
+ permission: "confirm"
6040
+ });
6041
+ var awsServer = () => ({
6042
+ name: "aws",
6043
+ description: "AWS \u2014 EC2, S3, Lambda, IAM, CloudFormation, costs. Requires AWS credentials",
6044
+ transport: "stdio",
6045
+ command: "npx",
6046
+ args: ["-y", "@modelcontextprotocol/server-aws"],
6047
+ permission: "confirm"
6048
+ });
6049
+ var googleMapsServer = () => ({
6050
+ name: "google-maps",
6051
+ description: "Google Maps \u2014 directions, geocoding, places. Requires GOOGLE_MAPS_API_KEY",
6052
+ transport: "stdio",
6053
+ command: "npx",
6054
+ args: ["-y", "@modelcontextprotocol/server-google-maps"],
6055
+ env: { GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY ?? "" },
6056
+ permission: "confirm"
6057
+ });
6058
+ var sentinelServer = () => ({
6059
+ name: "sentinel",
6060
+ description: "Security vulnerability scanning (Sentinel)",
6061
+ transport: "streamable-http",
6062
+ url: "https://mcp.sentinel.ai",
6063
+ permission: "deny"
6064
+ // security tool — require explicit confirmation
6065
+ });
6066
+ var allServers = () => ({
6067
+ filesystem: { ...filesystemServer(), enabled: false },
6068
+ github: { ...githubServer(), enabled: false },
6069
+ context7: { ...context7Server(), enabled: false },
6070
+ "brave-search": { ...braveSearchServer(), enabled: false },
6071
+ block: { ...blockServer(), enabled: false },
6072
+ everart: { ...everArtServer(), enabled: false },
6073
+ slack: { ...slackServer(), enabled: false },
6074
+ aws: { ...awsServer(), enabled: false },
6075
+ "google-maps": { ...googleMapsServer(), enabled: false },
6076
+ sentinel: { ...sentinelServer(), enabled: false }
6077
+ });
6078
+
6079
+ export { AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, allServers, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, context7Server, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, everArtServer, filesystemServer, githubServer, googleMapsServer, loadProjectModes, loadUserModes, makeAgentSubagentRunner, migratePlaintextSecrets, renderPrometheus, rewriteConfigEncrypted, runConfigMigrations, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
4548
6080
  //# sourceMappingURL=index.js.map
4549
6081
  //# sourceMappingURL=index.js.map