@theokit/sdk 2.27.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/a2a/index.cjs +373 -17
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js +373 -17
  5. package/dist/a2a/index.js.map +1 -1
  6. package/dist/{cron-BR1NCSk1.d.cts → cron-COSAehOL.d.ts} +110 -3
  7. package/dist/{cron-DgEQCJ2i.d.ts → cron-yJoNUZxe.d.cts} +110 -3
  8. package/dist/cron.cjs +914 -508
  9. package/dist/cron.cjs.map +1 -1
  10. package/dist/cron.d.cts +2 -2
  11. package/dist/cron.d.ts +2 -2
  12. package/dist/cron.js +915 -509
  13. package/dist/cron.js.map +1 -1
  14. package/dist/{errors-CbY3pxY7.d.ts → errors-BxMIlgLP.d.ts} +1 -1
  15. package/dist/{errors-DLMNb4Ka.d.cts → errors-tP-8O-hR.d.cts} +1 -1
  16. package/dist/errors.d.cts +2 -2
  17. package/dist/eval.cjs +914 -508
  18. package/dist/eval.cjs.map +1 -1
  19. package/dist/eval.js +915 -509
  20. package/dist/eval.js.map +1 -1
  21. package/dist/filesystem/index.cjs +304 -0
  22. package/dist/filesystem/index.cjs.map +1 -0
  23. package/dist/filesystem/index.d.cts +12 -0
  24. package/dist/filesystem/index.d.ts +12 -0
  25. package/dist/filesystem/index.js +295 -0
  26. package/dist/filesystem/index.js.map +1 -0
  27. package/dist/filesystem/local-filesystem.d.cts +36 -0
  28. package/dist/filesystem/local-filesystem.d.ts +36 -0
  29. package/dist/filesystem/types.d.cts +118 -0
  30. package/dist/filesystem/types.d.ts +118 -0
  31. package/dist/index.cjs +973 -570
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +13 -7
  34. package/dist/index.d.ts +13 -7
  35. package/dist/index.js +972 -568
  36. package/dist/index.js.map +1 -1
  37. package/dist/internal/persistence/conversation-storage-fs.d.cts +4 -0
  38. package/dist/internal/persistence/conversation-storage-fs.d.ts +4 -0
  39. package/dist/internal/persistence/conversation-storage-memory.d.cts +4 -0
  40. package/dist/internal/persistence/conversation-storage-memory.d.ts +4 -0
  41. package/dist/internal/persistence/objective-coerce.d.cts +9 -0
  42. package/dist/internal/persistence/objective-coerce.d.ts +9 -0
  43. package/dist/internal/runtime/lifecycle/wrap-completion-check-run.d.ts +30 -0
  44. package/dist/internal/runtime/local-agent/local-agent-goal-extensions.d.ts +80 -0
  45. package/dist/internal/runtime/objective/objective-store.d.ts +33 -0
  46. package/dist/{run-CdWiihyU.d.cts → run-q_P0vHlY.d.cts} +71 -2
  47. package/dist/{run-CdWiihyU.d.ts → run-q_P0vHlY.d.ts} +71 -2
  48. package/dist/types/agent.d.ts +32 -1
  49. package/dist/types/conversation-storage.d.ts +23 -0
  50. package/dist/types/goal-events.d.ts +7 -0
  51. package/dist/types/index.d.ts +1 -0
  52. package/dist/types/objective.d.ts +45 -0
  53. package/dist/types/run-events.d.ts +12 -1
  54. package/dist/types/run.d.ts +58 -0
  55. package/package.json +11 -1
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { z, toJSONSchema } from 'zod';
2
2
  import { createRequire } from 'module';
3
3
  import { createHash, randomUUID, randomBytes } from 'crypto';
4
4
  import { existsSync, rmSync, mkdirSync, renameSync, readFileSync, realpathSync, lstatSync, readlinkSync, readdirSync } from 'fs';
5
- import { join, dirname, relative, resolve, sep, isAbsolute } from 'path';
6
- import { readFile, stat, rm, readdir, mkdir, appendFile, rename, open, unlink, statfs, access, writeFile } from 'fs/promises';
5
+ import { join, dirname, resolve, sep, relative, isAbsolute } from 'path';
6
+ import { rm, readdir, readFile, mkdir, stat, appendFile, open, rename, unlink, statfs, access, writeFile } from 'fs/promises';
7
7
  import { AsyncLocalStorage } from 'async_hooks';
8
8
  import { homedir } from 'os';
9
9
  import { spawn } from 'child_process';
@@ -913,7 +913,7 @@ function safeFilenameForId(id, options) {
913
913
  code: "invalid_filename_id"
914
914
  });
915
915
  }
916
- const maxLen = options?.maxLen;
916
+ const maxLen = options?.maxLen ?? 128;
917
917
  const lower = id.toLowerCase();
918
918
  if (lower.length <= maxLen && IDENTIFIER_PATTERN.test(lower)) {
919
919
  return lower;
@@ -1110,6 +1110,72 @@ var init_markdown_store = __esm({
1110
1110
  }
1111
1111
  });
1112
1112
 
1113
+ // src/internal/persistence/file-lock.ts
1114
+ async function getProperLockfile() {
1115
+ if (cached !== void 0) return cached;
1116
+ try {
1117
+ const mod = await import('proper-lockfile');
1118
+ if (!validateLockModule(mod)) {
1119
+ if (!warnedStructural) {
1120
+ warnedStructural = true;
1121
+ process.stderr.write(
1122
+ "[theokit-sdk] proper-lockfile: imported module does NOT expose the expected `lock`/`unlock` API surface. This may indicate a supply-chain compromise or an incompatible major version. Falling back to in-process mutex (no cross-process safety). Reinstall with: pnpm add proper-lockfile@^11\n"
1123
+ );
1124
+ }
1125
+ cached = null;
1126
+ return cached;
1127
+ }
1128
+ cached = mod;
1129
+ } catch {
1130
+ cached = null;
1131
+ }
1132
+ return cached;
1133
+ }
1134
+ function validateLockModule(mod) {
1135
+ if (mod === null || mod === void 0 || typeof mod !== "object") return false;
1136
+ const m = mod;
1137
+ return typeof m.lock === "function" && typeof m.unlock === "function";
1138
+ }
1139
+ async function withFileLock(path, fn, options) {
1140
+ const lib = await getProperLockfile();
1141
+ if (lib === null) {
1142
+ if (!warnedMissing) {
1143
+ warnedMissing = true;
1144
+ process.stderr.write(
1145
+ "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
1146
+ );
1147
+ }
1148
+ return withCwdMutex(`file-lock:${path}`, fn);
1149
+ }
1150
+ return withCwdMutex(`file-lock:${path}`, async () => {
1151
+ const release = await lib.lock(path, {
1152
+ // EC-1: companion lockfile, target path may not exist yet.
1153
+ lockfilePath: `${path}.lock`,
1154
+ realpath: false,
1155
+ stale: 3e4,
1156
+ retries: {
1157
+ retries: 5,
1158
+ factor: 1.5,
1159
+ minTimeout: 100,
1160
+ maxTimeout: 5e3
1161
+ }
1162
+ });
1163
+ try {
1164
+ return await fn();
1165
+ } finally {
1166
+ await release();
1167
+ }
1168
+ });
1169
+ }
1170
+ var cached, warnedMissing, warnedStructural;
1171
+ var init_file_lock = __esm({
1172
+ "src/internal/persistence/file-lock.ts"() {
1173
+ init_cwd_mutex();
1174
+ warnedMissing = false;
1175
+ warnedStructural = false;
1176
+ }
1177
+ });
1178
+
1113
1179
  // src/internal/runtime/context/yaml-frontmatter.ts
1114
1180
  function parseSimpleYaml(text) {
1115
1181
  const fields = {};
@@ -1364,94 +1430,459 @@ var init_session_summary_writer = __esm({
1364
1430
  MAX_TURN_CHARS = 2e3;
1365
1431
  }
1366
1432
  });
1367
- async function withToolWhitelist(whitelist, fn) {
1368
- return toolWhitelistStore.run(whitelist, fn);
1369
- }
1370
- function currentToolWhitelist() {
1371
- return toolWhitelistStore.getStore();
1433
+ function sessionFilePath(cwd, agentId) {
1434
+ const safe2 = sanitizeIdentifier(agentId, { maxLen: 128 });
1435
+ return safePathJoin(cwd, ".theokit", "agents", safe2, "messages.jsonl");
1372
1436
  }
1373
- function checkToolWhitelist(toolName) {
1374
- const whitelist = currentToolWhitelist();
1375
- if (whitelist === void 0) return { allowed: true };
1376
- if (!whitelist.has(toolName)) {
1377
- return {
1378
- allowed: false,
1379
- reason: `Tool "${toolName}" not available in this fork context`
1380
- };
1437
+ async function readJsonlLines(cwd, agentId) {
1438
+ const path = sessionFilePath(cwd, agentId);
1439
+ try {
1440
+ const raw = await readFile(path, "utf8");
1441
+ return raw.split("\n").filter((line) => line.length > 0);
1442
+ } catch {
1443
+ return [];
1381
1444
  }
1382
- return { allowed: true };
1383
1445
  }
1384
- var toolWhitelistStore;
1385
- var init_async_local_storage = __esm({
1386
- "src/internal/runtime/concurrency/async-local-storage.ts"() {
1387
- toolWhitelistStore = new AsyncLocalStorage();
1446
+ function warnMalformed(agentId, line) {
1447
+ process.stderr.write(
1448
+ `[theokit-sdk] skipping malformed line in messages.jsonl (${agentId}): ${line.slice(0, 80)}...
1449
+ `
1450
+ );
1451
+ }
1452
+ function hydrateSessionLine(parsed) {
1453
+ if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
1454
+ if (parsed.role === "user" || parsed.role === "assistant") {
1455
+ return { role: parsed.role, text: parsed.text };
1388
1456
  }
1389
- });
1390
-
1391
- // src/internal/runtime/concurrency/async-semaphore.ts
1392
- function createSemaphore(permits) {
1393
- if (!Number.isInteger(permits) || permits < 1) {
1394
- throw new ConfigurationError(
1395
- `async-semaphore: permits must be a positive integer, got ${permits}`,
1396
- { code: "invalid_concurrency" }
1397
- );
1457
+ if (parsed.role === "tool_call" || parsed.role === "tool_result") {
1458
+ const label = parsed.role === "tool_call" ? "tool call" : "tool result";
1459
+ return { role: "assistant", text: `[${label}] ${parsed.text}` };
1398
1460
  }
1399
- let active = 0;
1400
- const queue = [];
1401
- function tryGrant() {
1402
- if (active < permits && queue.length > 0) {
1403
- const resolve3 = queue.shift();
1404
- if (resolve3 !== void 0) {
1405
- active += 1;
1406
- resolve3();
1407
- }
1461
+ return void 0;
1462
+ }
1463
+ async function readSessionFile(cwd, agentId) {
1464
+ const lines = await readJsonlLines(cwd, agentId);
1465
+ const messages = [];
1466
+ for (const line of lines) {
1467
+ try {
1468
+ const msg = hydrateSessionLine(JSON.parse(line));
1469
+ if (msg !== void 0) messages.push(msg);
1470
+ } catch {
1471
+ warnMalformed(agentId, line);
1408
1472
  }
1409
1473
  }
1410
- return {
1411
- inFlight: () => active,
1412
- pending: () => queue.length + active,
1413
- async acquire() {
1414
- await new Promise((resolve3) => {
1415
- queue.push(resolve3);
1416
- tryGrant();
1417
- });
1418
- let released = false;
1419
- return () => {
1420
- if (released) return;
1421
- released = true;
1422
- active -= 1;
1423
- tryGrant();
1424
- };
1474
+ return messages;
1475
+ }
1476
+ async function readAllPersistedMessages(cwd, agentId) {
1477
+ const lines = await readJsonlLines(cwd, agentId);
1478
+ const messages = [];
1479
+ for (const line of lines) {
1480
+ try {
1481
+ const parsed = JSON.parse(line);
1482
+ if (parsed.role !== void 0 && VALID_ROLES.has(parsed.role) && typeof parsed.text === "string") {
1483
+ messages.push({
1484
+ role: parsed.role,
1485
+ text: parsed.text,
1486
+ at: typeof parsed.at === "number" ? parsed.at : Date.now()
1487
+ });
1488
+ }
1489
+ } catch {
1490
+ warnMalformed(agentId, line);
1425
1491
  }
1492
+ }
1493
+ return messages;
1494
+ }
1495
+ async function appendAnyPersistedMessage(cwd, agentId, record) {
1496
+ await appendPersistedMessages(cwd, agentId, [record]);
1497
+ }
1498
+ async function appendPersistedMessages(cwd, agentId, records) {
1499
+ if (records.length === 0) return;
1500
+ const path = sessionFilePath(cwd, agentId);
1501
+ const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
1502
+ `).join("");
1503
+ const dir = dirname(path);
1504
+ let written = false;
1505
+ const attempt = async () => {
1506
+ await mkdir(dir, { recursive: true });
1507
+ await withFileLock(path, async () => {
1508
+ await appendFile(path, payload, "utf8");
1509
+ written = true;
1510
+ });
1426
1511
  };
1512
+ try {
1513
+ await attempt();
1514
+ } catch (cause) {
1515
+ if (written || cause.code !== "ENOENT") throw cause;
1516
+ await attempt();
1517
+ }
1427
1518
  }
1428
- var init_async_semaphore = __esm({
1429
- "src/internal/runtime/concurrency/async-semaphore.ts"() {
1430
- init_errors();
1519
+ async function rewriteLockedSession(path, transform) {
1520
+ await withFileLock(path, async () => {
1521
+ let raw;
1522
+ try {
1523
+ raw = await readFile(path, "utf8");
1524
+ } catch {
1525
+ return;
1526
+ }
1527
+ const lines = raw.split("\n").filter((line) => line.length > 0);
1528
+ const next = transform(lines);
1529
+ if (next === void 0) return;
1530
+ await replaceFileAtomic(path, next);
1531
+ });
1532
+ }
1533
+ async function compactSessionFile(cwd, agentId, maxTurns) {
1534
+ const path = sessionFilePath(cwd, agentId);
1535
+ if (!existsSync(path)) return;
1536
+ await rewriteLockedSession(
1537
+ path,
1538
+ (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
1539
+ `
1540
+ );
1541
+ }
1542
+ async function truncateSessionTo(cwd, agentId, keepCount) {
1543
+ const path = sessionFilePath(cwd, agentId);
1544
+ if (!existsSync(path)) return 0;
1545
+ let kept = 0;
1546
+ await rewriteLockedSession(path, (lines) => {
1547
+ const keep = Math.max(0, Math.min(keepCount, lines.length));
1548
+ kept = keep;
1549
+ if (keep === lines.length) return void 0;
1550
+ return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
1551
+ `;
1552
+ });
1553
+ return kept;
1554
+ }
1555
+ var VALID_ROLES;
1556
+ var init_agent_session_store = __esm({
1557
+ "src/internal/runtime/session/agent-session-store.ts"() {
1558
+ init_atomic_write();
1559
+ init_file_lock();
1560
+ init_security();
1561
+ VALID_ROLES = /* @__PURE__ */ new Set([
1562
+ "user",
1563
+ "assistant",
1564
+ "system",
1565
+ "tool_call",
1566
+ "tool_result"
1567
+ ]);
1431
1568
  }
1432
1569
  });
1433
1570
 
1434
- // src/internal/llm/credential-pool-types.ts
1435
- var COOLDOWN_MS, DEFAULT_COOLDOWN_MS;
1436
- var init_credential_pool_types = __esm({
1437
- "src/internal/llm/credential-pool-types.ts"() {
1438
- COOLDOWN_MS = {
1439
- 401: 5 * 60 * 1e3,
1440
- // 5 minutes OAuth refresh can recover quickly
1441
- 402: 60 * 60 * 1e3,
1442
- // 1 hour billing quota typically hourly+
1443
- 429: 60 * 60 * 1e3
1444
- // 1 hour — daily-rate-limit windows
1445
- };
1446
- DEFAULT_COOLDOWN_MS = 60 * 60 * 1e3;
1571
+ // src/internal/persistence/objective-coerce.ts
1572
+ function coerceOptions(raw) {
1573
+ if (typeof raw !== "object" || raw === null) return void 0;
1574
+ const o = raw;
1575
+ const out = {};
1576
+ if (typeof o.maxRuns === "number") out.maxRuns = o.maxRuns;
1577
+ if (typeof o.judgeModel === "string") out.judgeModel = o.judgeModel;
1578
+ if (typeof o.prompt === "string") out.prompt = o.prompt;
1579
+ return Object.keys(out).length > 0 ? out : void 0;
1580
+ }
1581
+ function coerceObjectiveRecord(raw) {
1582
+ if (typeof raw !== "object" || raw === null) return void 0;
1583
+ const o = raw;
1584
+ if (o._schemaVersion !== 1) return void 0;
1585
+ if (typeof o.objective !== "string") return void 0;
1586
+ if (typeof o.runsUsed !== "number") return void 0;
1587
+ if (typeof o.status !== "string" || !STATUSES.includes(o.status))
1588
+ return void 0;
1589
+ const options = coerceOptions(o.options);
1590
+ return {
1591
+ _schemaVersion: 1,
1592
+ objective: o.objective,
1593
+ ...options !== void 0 ? { options } : {},
1594
+ status: o.status,
1595
+ runsUsed: o.runsUsed
1596
+ };
1597
+ }
1598
+ var STATUSES;
1599
+ var init_objective_coerce = __esm({
1600
+ "src/internal/persistence/objective-coerce.ts"() {
1601
+ STATUSES = ["active", "done", "paused"];
1447
1602
  }
1448
1603
  });
1449
1604
 
1450
- // src/internal/llm/retry.ts
1451
- function computeBackoffMs(opts) {
1452
- const base = opts.baseMs ?? DEFAULT_BASE_MS;
1453
- const cap2 = opts.capMs ?? DEFAULT_CAP_MS;
1454
- const rng = opts.rng ?? Math.random;
1605
+ // src/internal/persistence/pagination.ts
1606
+ function paginate(items, opts) {
1607
+ if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
1608
+ const start = Math.max(0, opts.offset ?? 0);
1609
+ const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
1610
+ return items.slice(start, end);
1611
+ }
1612
+ var init_pagination = __esm({
1613
+ "src/internal/persistence/pagination.ts"() {
1614
+ }
1615
+ });
1616
+
1617
+ // src/internal/persistence/session-meta.ts
1618
+ function applyMetaPatch(current, patch) {
1619
+ const next = {};
1620
+ if (current.title !== void 0) next.title = current.title;
1621
+ if (current.tag !== void 0) next.tag = current.tag;
1622
+ if (patch.title === null) delete next.title;
1623
+ else if (patch.title !== void 0) next.title = patch.title;
1624
+ if (patch.tag === null) delete next.tag;
1625
+ else if (patch.tag !== void 0) next.tag = patch.tag;
1626
+ return next;
1627
+ }
1628
+ function coerceSessionMeta(raw) {
1629
+ if (typeof raw !== "object" || raw === null) return void 0;
1630
+ const obj = raw;
1631
+ const meta = {};
1632
+ if (typeof obj.title === "string") meta.title = obj.title;
1633
+ if (typeof obj.tag === "string") meta.tag = obj.tag;
1634
+ return meta.title === void 0 && meta.tag === void 0 ? void 0 : meta;
1635
+ }
1636
+ var init_session_meta = __esm({
1637
+ "src/internal/persistence/session-meta.ts"() {
1638
+ }
1639
+ });
1640
+ function toStoredMessage(record) {
1641
+ return {
1642
+ role: record.role,
1643
+ content: record.text,
1644
+ at: record.at
1645
+ };
1646
+ }
1647
+ function toRecord(message) {
1648
+ return { role: message.role, text: message.content, at: message.at ?? Date.now() };
1649
+ }
1650
+ var FileSystemConversationStorage;
1651
+ var init_conversation_storage_fs = __esm({
1652
+ "src/internal/persistence/conversation-storage-fs.ts"() {
1653
+ init_agent_session_store();
1654
+ init_security();
1655
+ init_path_guard();
1656
+ init_file_lock();
1657
+ init_objective_coerce();
1658
+ init_pagination();
1659
+ init_session_meta();
1660
+ FileSystemConversationStorage = class {
1661
+ #root;
1662
+ constructor(opts = {}) {
1663
+ this.#root = opts.root ?? process.cwd();
1664
+ }
1665
+ /** Exposed for tests + diagnostics. The path is sanitized at use sites. */
1666
+ get root() {
1667
+ return this.#root;
1668
+ }
1669
+ async getMessages(conversationId, opts) {
1670
+ const records = await readAllPersistedMessages(this.#root, conversationId);
1671
+ const all = records.map(toStoredMessage);
1672
+ return paginate(all, opts);
1673
+ }
1674
+ async appendMessage(conversationId, message) {
1675
+ await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
1676
+ }
1677
+ async appendMessages(conversationId, messages) {
1678
+ await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
1679
+ }
1680
+ async truncateConversation(conversationId, keepCount) {
1681
+ return truncateSessionTo(this.#root, conversationId, keepCount);
1682
+ }
1683
+ async deleteConversation(conversationId) {
1684
+ const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
1685
+ const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
1686
+ await rm(dirPath, { recursive: true, force: true });
1687
+ }
1688
+ async deleteScope(prefix) {
1689
+ const ids = await this.listConversationIds();
1690
+ const matching = ids.filter((id) => id.startsWith(prefix));
1691
+ for (const id of matching) await this.deleteConversation(id);
1692
+ return matching.length;
1693
+ }
1694
+ async listConversationIds(opts = {}) {
1695
+ const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
1696
+ let entries;
1697
+ try {
1698
+ entries = await readdir(agentsRoot);
1699
+ } catch (cause) {
1700
+ if (cause.code === "ENOENT") return [];
1701
+ throw cause;
1702
+ }
1703
+ if (opts.limit !== void 0) return entries.slice(0, opts.limit);
1704
+ return entries;
1705
+ }
1706
+ async compact(conversationId, maxTurns) {
1707
+ await compactSessionFile(this.#root, conversationId, maxTurns);
1708
+ }
1709
+ // SE4 — session metadata persisted as a per-conversation sidecar
1710
+ // `<root>/.theokit/agents/<safeId>/session.json` (same sanitized perimeter as
1711
+ // the transcript). Kept separate from messages.jsonl so a title/tag write does
1712
+ // not rewrite the append-only log.
1713
+ #metaPath(conversationId) {
1714
+ const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
1715
+ return safePathJoin(this.#root, ".theokit", "agents", safe2, "session.json");
1716
+ }
1717
+ async getSessionMeta(conversationId) {
1718
+ try {
1719
+ const raw = await readFile(this.#metaPath(conversationId), "utf8");
1720
+ return coerceSessionMeta(JSON.parse(raw));
1721
+ } catch (cause) {
1722
+ if (cause.code === "ENOENT") return void 0;
1723
+ throw cause;
1724
+ }
1725
+ }
1726
+ async setSessionMeta(conversationId, patch) {
1727
+ const metaPath = this.#metaPath(conversationId);
1728
+ await mkdir(dirname(metaPath), { recursive: true });
1729
+ await withFileLock(metaPath, async () => {
1730
+ let current = {};
1731
+ try {
1732
+ current = coerceSessionMeta(JSON.parse(await readFile(metaPath, "utf8"))) ?? {};
1733
+ } catch (cause) {
1734
+ if (cause.code !== "ENOENT") throw cause;
1735
+ }
1736
+ const next = applyMetaPatch(current, patch);
1737
+ await writeFile(metaPath, redactSecrets(JSON.stringify(next)), "utf8");
1738
+ });
1739
+ }
1740
+ // SE33 — the durable objective is kept in `objective.json` beside the
1741
+ // transcript (separate from messages.jsonl + session.json). Uses the TOTAL
1742
+ // `safeFilenameForId` (not `sanitizeIdentifier`) so a caller-supplied
1743
+ // `threadId` with exotic characters (e.g. "user@example.com") hashes to a
1744
+ // deterministic dir instead of throwing — honoring the objective methods'
1745
+ // never-throw contract (ADR D6). Conforming ids pass through unchanged, so
1746
+ // the objective still sits beside the transcript for the normal case.
1747
+ #objectivePath(conversationId) {
1748
+ const safe2 = safeFilenameForId(conversationId, { maxLen: 128 });
1749
+ return safePathJoin(this.#root, ".theokit", "agents", safe2, "objective.json");
1750
+ }
1751
+ async getObjectiveRecord(conversationId) {
1752
+ try {
1753
+ const raw = await readFile(this.#objectivePath(conversationId), "utf8");
1754
+ return coerceObjectiveRecord(JSON.parse(raw));
1755
+ } catch (cause) {
1756
+ if (cause.code === "ENOENT") return void 0;
1757
+ throw cause;
1758
+ }
1759
+ }
1760
+ async setObjectiveRecord(conversationId, record) {
1761
+ const path = this.#objectivePath(conversationId);
1762
+ if (record === null) {
1763
+ await rm(path, { force: true });
1764
+ return;
1765
+ }
1766
+ await mkdir(dirname(path), { recursive: true });
1767
+ await withFileLock(path, async () => {
1768
+ await writeFile(path, redactSecrets(JSON.stringify(record)), "utf8");
1769
+ });
1770
+ }
1771
+ // SE33 (HIGH-1 fix) — atomic read-modify-write: the read that feeds `mutate`
1772
+ // happens INSIDE the same file lock as the write, so two concurrent progress
1773
+ // write-backs on one thread cannot both read a stale `runsUsed` and drop turns.
1774
+ async updateObjectiveRecord(conversationId, mutate) {
1775
+ const path = this.#objectivePath(conversationId);
1776
+ await mkdir(dirname(path), { recursive: true });
1777
+ await withFileLock(path, async () => {
1778
+ let current;
1779
+ try {
1780
+ current = coerceObjectiveRecord(JSON.parse(await readFile(path, "utf8")));
1781
+ } catch (cause) {
1782
+ if (cause.code !== "ENOENT") throw cause;
1783
+ }
1784
+ const next = mutate(current);
1785
+ if (next === void 0) return;
1786
+ if (next === null) {
1787
+ await rm(path, { force: true });
1788
+ return;
1789
+ }
1790
+ await writeFile(path, redactSecrets(JSON.stringify(next)), "utf8");
1791
+ });
1792
+ }
1793
+ async dispose() {
1794
+ }
1795
+ };
1796
+ }
1797
+ });
1798
+ async function withToolWhitelist(whitelist, fn) {
1799
+ return toolWhitelistStore.run(whitelist, fn);
1800
+ }
1801
+ function currentToolWhitelist() {
1802
+ return toolWhitelistStore.getStore();
1803
+ }
1804
+ function checkToolWhitelist(toolName) {
1805
+ const whitelist = currentToolWhitelist();
1806
+ if (whitelist === void 0) return { allowed: true };
1807
+ if (!whitelist.has(toolName)) {
1808
+ return {
1809
+ allowed: false,
1810
+ reason: `Tool "${toolName}" not available in this fork context`
1811
+ };
1812
+ }
1813
+ return { allowed: true };
1814
+ }
1815
+ var toolWhitelistStore;
1816
+ var init_async_local_storage = __esm({
1817
+ "src/internal/runtime/concurrency/async-local-storage.ts"() {
1818
+ toolWhitelistStore = new AsyncLocalStorage();
1819
+ }
1820
+ });
1821
+
1822
+ // src/internal/runtime/concurrency/async-semaphore.ts
1823
+ function createSemaphore(permits) {
1824
+ if (!Number.isInteger(permits) || permits < 1) {
1825
+ throw new ConfigurationError(
1826
+ `async-semaphore: permits must be a positive integer, got ${permits}`,
1827
+ { code: "invalid_concurrency" }
1828
+ );
1829
+ }
1830
+ let active = 0;
1831
+ const queue = [];
1832
+ function tryGrant() {
1833
+ if (active < permits && queue.length > 0) {
1834
+ const resolve3 = queue.shift();
1835
+ if (resolve3 !== void 0) {
1836
+ active += 1;
1837
+ resolve3();
1838
+ }
1839
+ }
1840
+ }
1841
+ return {
1842
+ inFlight: () => active,
1843
+ pending: () => queue.length + active,
1844
+ async acquire() {
1845
+ await new Promise((resolve3) => {
1846
+ queue.push(resolve3);
1847
+ tryGrant();
1848
+ });
1849
+ let released = false;
1850
+ return () => {
1851
+ if (released) return;
1852
+ released = true;
1853
+ active -= 1;
1854
+ tryGrant();
1855
+ };
1856
+ }
1857
+ };
1858
+ }
1859
+ var init_async_semaphore = __esm({
1860
+ "src/internal/runtime/concurrency/async-semaphore.ts"() {
1861
+ init_errors();
1862
+ }
1863
+ });
1864
+
1865
+ // src/internal/llm/credential-pool-types.ts
1866
+ var COOLDOWN_MS, DEFAULT_COOLDOWN_MS;
1867
+ var init_credential_pool_types = __esm({
1868
+ "src/internal/llm/credential-pool-types.ts"() {
1869
+ COOLDOWN_MS = {
1870
+ 401: 5 * 60 * 1e3,
1871
+ // 5 minutes — OAuth refresh can recover quickly
1872
+ 402: 60 * 60 * 1e3,
1873
+ // 1 hour — billing quota typically hourly+
1874
+ 429: 60 * 60 * 1e3
1875
+ // 1 hour — daily-rate-limit windows
1876
+ };
1877
+ DEFAULT_COOLDOWN_MS = 60 * 60 * 1e3;
1878
+ }
1879
+ });
1880
+
1881
+ // src/internal/llm/retry.ts
1882
+ function computeBackoffMs(opts) {
1883
+ const base = opts.baseMs ?? DEFAULT_BASE_MS;
1884
+ const cap2 = opts.capMs ?? DEFAULT_CAP_MS;
1885
+ const rng = opts.rng ?? Math.random;
1455
1886
  if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
1456
1887
  return Math.max(base, Math.min(cap2, opts.retryAfterMs));
1457
1888
  }
@@ -1737,6 +2168,151 @@ var init_credential_pool_context = __esm({
1737
2168
  }
1738
2169
  });
1739
2170
 
2171
+ // src/internal/runtime/objective/objective-store.ts
2172
+ function canPersist(a) {
2173
+ return typeof a.getObjectiveRecord === "function" && typeof a.setObjectiveRecord === "function";
2174
+ }
2175
+ async function mutateObjective(adapter, conversationId, mutate) {
2176
+ if (typeof adapter.updateObjectiveRecord === "function") {
2177
+ await adapter.updateObjectiveRecord(conversationId, mutate);
2178
+ return;
2179
+ }
2180
+ const current = await adapter.getObjectiveRecord(conversationId);
2181
+ const next = mutate(current);
2182
+ if (next === void 0) return;
2183
+ await adapter.setObjectiveRecord(conversationId, next);
2184
+ }
2185
+ async function getObjective(adapter, conversationId) {
2186
+ if (!canPersist(adapter)) return void 0;
2187
+ return adapter.getObjectiveRecord(conversationId);
2188
+ }
2189
+ async function setObjective(adapter, conversationId, objective, options) {
2190
+ if (!canPersist(adapter)) return;
2191
+ const record = {
2192
+ _schemaVersion: 1,
2193
+ objective,
2194
+ ...options !== void 0 ? { options } : {},
2195
+ status: "active",
2196
+ runsUsed: 0
2197
+ };
2198
+ await adapter.setObjectiveRecord(conversationId, record);
2199
+ }
2200
+ async function updateObjectiveOptions(adapter, conversationId, patch) {
2201
+ if (!canPersist(adapter)) return;
2202
+ await mutateObjective(
2203
+ adapter,
2204
+ conversationId,
2205
+ (current) => current === void 0 ? void 0 : { ...current, options: { ...current.options, ...patch } }
2206
+ );
2207
+ }
2208
+ async function writeObjectiveProgress(adapter, conversationId, progress) {
2209
+ if (!canPersist(adapter)) return;
2210
+ await mutateObjective(
2211
+ adapter,
2212
+ conversationId,
2213
+ (current) => current === void 0 ? void 0 : { ...current, runsUsed: progress.runsUsed, status: progress.status }
2214
+ );
2215
+ }
2216
+ async function clearObjective(adapter, conversationId) {
2217
+ if (!canPersist(adapter)) return;
2218
+ await adapter.setObjectiveRecord(conversationId, null);
2219
+ }
2220
+ var init_objective_store = __esm({
2221
+ "src/internal/runtime/objective/objective-store.ts"() {
2222
+ }
2223
+ });
2224
+
2225
+ // src/internal/runtime/local-agent/local-agent-goal-extensions.ts
2226
+ var local_agent_goal_extensions_exports = {};
2227
+ __export(local_agent_goal_extensions_exports, {
2228
+ formatObjectiveProjection: () => formatObjectiveProjection,
2229
+ localAgentClearObjective: () => localAgentClearObjective,
2230
+ localAgentGetObjective: () => localAgentGetObjective,
2231
+ localAgentSetObjective: () => localAgentSetObjective,
2232
+ localAgentUpdateObjectiveOptions: () => localAgentUpdateObjectiveOptions,
2233
+ persistDurableProgress: () => persistDurableProgress,
2234
+ resolveCurrentObjectiveText: () => resolveCurrentObjectiveText,
2235
+ resolveDurableRun: () => resolveDurableRun
2236
+ });
2237
+ function resolveAdapter(handle) {
2238
+ return typeof handle === "string" ? new FileSystemConversationStorage({ root: handle }) : handle;
2239
+ }
2240
+ function assertValidGoalOptions(options) {
2241
+ if (options.maxRuns !== void 0 && (!Number.isInteger(options.maxRuns) || options.maxRuns <= 0)) {
2242
+ throw new ConfigurationError(`maxRuns must be a positive integer, got ${options.maxRuns}`, {
2243
+ code: "invalid_objective_max_runs"
2244
+ });
2245
+ }
2246
+ }
2247
+ async function localAgentSetObjective(handle, objective, opts) {
2248
+ const { threadId, ...options } = opts;
2249
+ assertValidGoalOptions(options);
2250
+ await setObjective(
2251
+ resolveAdapter(handle),
2252
+ threadId,
2253
+ objective,
2254
+ Object.keys(options).length > 0 ? options : void 0
2255
+ );
2256
+ }
2257
+ async function localAgentGetObjective(handle, opts) {
2258
+ return getObjective(resolveAdapter(handle), opts.threadId);
2259
+ }
2260
+ async function localAgentUpdateObjectiveOptions(handle, opts) {
2261
+ const { threadId, ...patch } = opts;
2262
+ assertValidGoalOptions(patch);
2263
+ await updateObjectiveOptions(resolveAdapter(handle), threadId, patch);
2264
+ }
2265
+ async function localAgentClearObjective(handle, opts) {
2266
+ await clearObjective(resolveAdapter(handle), opts.threadId);
2267
+ }
2268
+ async function resolveCurrentObjectiveText(handle, threadId) {
2269
+ const record = await getObjective(resolveAdapter(handle), threadId);
2270
+ return record?.status === "active" ? record.objective : void 0;
2271
+ }
2272
+ function formatObjectiveProjection(objective, assembled) {
2273
+ const signal = `<current-objective>
2274
+ ${objective}
2275
+ </current-objective>`;
2276
+ return assembled === void 0 || assembled.length === 0 ? signal : `${signal}
2277
+
2278
+ ${assembled}`;
2279
+ }
2280
+ async function resolveDurableRun(handle, goalConfig, threadId, callerOptions) {
2281
+ const record = await getObjective(resolveAdapter(handle), threadId);
2282
+ if (record === void 0) return { kind: "none" };
2283
+ const judgeModel = callerOptions?.judgeModel ?? record.options?.judgeModel ?? goalConfig?.judgeModel;
2284
+ if (judgeModel === void 0) return { kind: "inert" };
2285
+ const totalBudget = record.options?.maxRuns ?? goalConfig?.maxRuns ?? DEFAULT_MAX_RUNS;
2286
+ const remaining = Math.max(0, totalBudget - record.runsUsed);
2287
+ if (remaining === 0) return { kind: "exhausted" };
2288
+ const perCall = callerOptions?.maxTurns;
2289
+ const maxTurns = perCall !== void 0 ? Math.min(perCall, remaining) : remaining;
2290
+ return {
2291
+ kind: "run",
2292
+ goal: record.objective,
2293
+ options: { ...callerOptions, maxTurns, judgeModel }
2294
+ };
2295
+ }
2296
+ async function persistDurableProgress(handle, threadId, result) {
2297
+ const adapter = resolveAdapter(handle);
2298
+ const record = await getObjective(adapter, threadId);
2299
+ if (record === void 0) return;
2300
+ const status = result.status === "completed" ? "done" : result.status === "paused" ? "paused" : "active";
2301
+ await writeObjectiveProgress(adapter, threadId, {
2302
+ runsUsed: record.runsUsed + result.turnsUsed,
2303
+ status
2304
+ });
2305
+ }
2306
+ var DEFAULT_MAX_RUNS;
2307
+ var init_local_agent_goal_extensions = __esm({
2308
+ "src/internal/runtime/local-agent/local-agent-goal-extensions.ts"() {
2309
+ init_errors();
2310
+ init_conversation_storage_fs();
2311
+ init_objective_store();
2312
+ DEFAULT_MAX_RUNS = 20;
2313
+ }
2314
+ });
2315
+
1740
2316
  // src/internal/memory/embedding-cache.ts
1741
2317
  var LruEmbeddingCache, globalEmbeddingCache;
1742
2318
  var init_embedding_cache = __esm({
@@ -7920,68 +8496,7 @@ init_cwd_mutex();
7920
8496
 
7921
8497
  // src/internal/personality/store.ts
7922
8498
  init_atomic_write();
7923
-
7924
- // src/internal/persistence/file-lock.ts
7925
- init_cwd_mutex();
7926
- var cached;
7927
- var warnedMissing = false;
7928
- var warnedStructural = false;
7929
- async function getProperLockfile() {
7930
- if (cached !== void 0) return cached;
7931
- try {
7932
- const mod = await import('proper-lockfile');
7933
- if (!validateLockModule(mod)) {
7934
- if (!warnedStructural) {
7935
- warnedStructural = true;
7936
- process.stderr.write(
7937
- "[theokit-sdk] proper-lockfile: imported module does NOT expose the expected `lock`/`unlock` API surface. This may indicate a supply-chain compromise or an incompatible major version. Falling back to in-process mutex (no cross-process safety). Reinstall with: pnpm add proper-lockfile@^11\n"
7938
- );
7939
- }
7940
- cached = null;
7941
- return cached;
7942
- }
7943
- cached = mod;
7944
- } catch {
7945
- cached = null;
7946
- }
7947
- return cached;
7948
- }
7949
- function validateLockModule(mod) {
7950
- if (mod === null || mod === void 0 || typeof mod !== "object") return false;
7951
- const m = mod;
7952
- return typeof m.lock === "function" && typeof m.unlock === "function";
7953
- }
7954
- async function withFileLock(path, fn, options) {
7955
- const lib = await getProperLockfile();
7956
- if (lib === null) {
7957
- if (!warnedMissing) {
7958
- warnedMissing = true;
7959
- process.stderr.write(
7960
- "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
7961
- );
7962
- }
7963
- return withCwdMutex(`file-lock:${path}`, fn);
7964
- }
7965
- return withCwdMutex(`file-lock:${path}`, async () => {
7966
- const release = await lib.lock(path, {
7967
- // EC-1: companion lockfile, target path may not exist yet.
7968
- lockfilePath: `${path}.lock`,
7969
- realpath: false,
7970
- stale: 3e4,
7971
- retries: {
7972
- retries: 5,
7973
- factor: 1.5,
7974
- minTimeout: 100,
7975
- maxTimeout: 5e3
7976
- }
7977
- });
7978
- try {
7979
- return await fn();
7980
- } finally {
7981
- await release();
7982
- }
7983
- });
7984
- }
8499
+ init_file_lock();
7985
8500
  var THEOKIT_DIR_NAME = ".theokit";
7986
8501
  function getTheokitHome(cwd) {
7987
8502
  const override = process.env.THEOKIT_HOME?.trim();
@@ -8856,431 +9371,174 @@ function resolveChildEnv(options = {}) {
8856
9371
  if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
8857
9372
  }
8858
9373
  for (const [name, value] of Object.entries(options.overrides ?? {})) {
8859
- base[name] = value;
8860
- }
8861
- return base;
8862
- }
8863
-
8864
- // src/internal/runtime/lifecycle/spawn-collect.ts
8865
- function spawnAndCollect(options) {
8866
- return new Promise((resolve3) => {
8867
- const timeoutMs = options.timeoutMs ?? 3e4;
8868
- const spawnOptions = {
8869
- cwd: options.cwd,
8870
- // #54 — scrub secret-like parent env by default; `options.env` still wins.
8871
- env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
8872
- };
8873
- const child = spawn(options.command, options.args ?? [], spawnOptions);
8874
- let stdout = "";
8875
- let stderr = "";
8876
- let timedOut = false;
8877
- let settled = false;
8878
- const settle = (result) => {
8879
- if (settled) return;
8880
- settled = true;
8881
- resolve3(result);
8882
- };
8883
- const timer = setTimeout(() => {
8884
- timedOut = true;
8885
- try {
8886
- child.kill("SIGKILL");
8887
- } catch {
8888
- }
8889
- settle({ stdout, stderr, exitCode: null, timedOut: true });
8890
- }, timeoutMs);
8891
- child.stdout?.on("data", (chunk) => {
8892
- stdout += chunk.toString("utf8");
8893
- });
8894
- child.stderr?.on("data", (chunk) => {
8895
- stderr += chunk.toString("utf8");
8896
- });
8897
- child.on("error", (cause) => {
8898
- clearTimeout(timer);
8899
- settle({ stdout, stderr, exitCode: -1, timedOut, spawnError: cause });
8900
- });
8901
- child.on("close", (code) => {
8902
- clearTimeout(timer);
8903
- settle({ stdout, stderr, exitCode: code, timedOut });
8904
- });
8905
- if (options.stdin !== void 0 && child.stdin !== null) {
8906
- child.stdin.end(options.stdin);
8907
- }
8908
- });
8909
- }
8910
-
8911
- // src/internal/runtime/hooks/hooks-executor.ts
8912
- init_hooks_source();
8913
- var HooksExecutor = class {
8914
- constructor(cwd) {
8915
- this.cwd = cwd;
8916
- }
8917
- cwd;
8918
- config = {};
8919
- async initialize(settingSourcesIncludeProject) {
8920
- if (!settingSourcesIncludeProject) {
8921
- this.config = {};
8922
- return;
8923
- }
8924
- this.config = await loadHookConfig(this.cwd);
8925
- }
8926
- /** Fire every hook registered for `event` and aggregate the decisions. */
8927
- async run(payload) {
8928
- const commands = this.commandsFor(payload.event, payload.tool);
8929
- if (commands.length === 0) return { decisions: [], blocked: false };
8930
- const decisions = [];
8931
- for (const command of commands) {
8932
- const decision = await this.executeOne(command, payload);
8933
- decisions.push(decision);
8934
- if (decision.decision === "deny") {
8935
- const result = {
8936
- decisions,
8937
- blocked: true
8938
- };
8939
- if (decision.reason !== void 0) result.reason = decision.reason;
8940
- return result;
8941
- }
8942
- }
8943
- return { decisions, blocked: false };
8944
- }
8945
- commandsFor(event, tool) {
8946
- const list2 = this.config.hooks?.[event] ?? [];
8947
- if (tool === void 0) return list2;
8948
- return list2.filter((entry) => {
8949
- if (entry.matcher === void 0) return true;
8950
- try {
8951
- return new RegExp(entry.matcher).test(tool);
8952
- } catch {
8953
- return entry.matcher === tool;
8954
- }
8955
- });
8956
- }
8957
- async executeOne(command, payload) {
8958
- const timeoutMs = command.timeoutMs ?? 3e4;
8959
- const result = await spawnAndCollect({
8960
- command: "sh",
8961
- args: ["-c", command.command],
8962
- cwd: this.cwd,
8963
- timeoutMs,
8964
- stdin: JSON.stringify(payload)
8965
- });
8966
- if (result.timedOut) {
8967
- return { decision: "deny", reason: `Hook timed out after ${timeoutMs}ms` };
8968
- }
8969
- if (result.spawnError !== void 0) {
8970
- return { decision: "deny", reason: `Hook spawn failed: ${result.spawnError.message}` };
8971
- }
8972
- if (result.exitCode !== 0) {
8973
- return {
8974
- decision: "deny",
8975
- reason: result.stderr.trim().length > 0 ? result.stderr.trim() : `Hook exited with code ${result.exitCode}`
8976
- };
8977
- }
8978
- return parseDecisionFromStdout(result.stdout);
8979
- }
8980
- };
8981
- function parseDecisionFromStdout(stdout) {
8982
- const trimmed = stdout.trim();
8983
- if (trimmed.length === 0) return { decision: "allow" };
8984
- try {
8985
- const parsed = JSON.parse(trimmed);
8986
- if (parsed.decision === "deny" || parsed.decision === "feedback") {
8987
- const result = { decision: parsed.decision };
8988
- if (parsed.reason !== void 0) result.reason = parsed.reason;
8989
- if (parsed.feedback !== void 0) result.feedback = parsed.feedback;
8990
- return result;
8991
- }
8992
- if (parsed.decision === "allow") return { decision: "allow" };
8993
- } catch {
8994
- return { decision: "feedback", feedback: trimmed };
8995
- }
8996
- return { decision: "allow" };
8997
- }
8998
-
8999
- // src/internal/runtime/lifecycle/post-run-lifecycle.ts
9000
- init_session_summary_writer();
9001
-
9002
- // src/internal/runtime/memory/memory-path-selector.ts
9003
- var PORT_MEMORY_PATH_ENV_VAR = "THEOKIT_PORT_MEMORY_PATH";
9004
- function shouldUsePortMemoryPath() {
9005
- const env = globalThis.process?.env;
9006
- if (env === void 0) return false;
9007
- const val = env[PORT_MEMORY_PATH_ENV_VAR];
9008
- return val === "1" || val === "true";
9009
- }
9010
- function resolveMemoryProviderForLoop(consumerSupplied, defaultAdapter, portPathEnabled) {
9011
- if (consumerSupplied !== void 0) return consumerSupplied;
9012
- if (portPathEnabled) return defaultAdapter;
9013
- return void 0;
9014
- }
9015
- function resolveMemoryToolsForLoop(legacyTools, portPathEnabled) {
9016
- if (portPathEnabled) return void 0;
9017
- return legacyTools;
9018
- }
9019
- function resolveActiveMemorySummaryForSend(legacySummary, portPathEnabled) {
9020
- if (portPathEnabled) return void 0;
9021
- return legacySummary;
9022
- }
9023
-
9024
- // src/internal/runtime/session/agent-session-store.ts
9025
- init_atomic_write();
9026
- init_security();
9027
- var VALID_ROLES = /* @__PURE__ */ new Set([
9028
- "user",
9029
- "assistant",
9030
- "system",
9031
- "tool_call",
9032
- "tool_result"
9033
- ]);
9034
- function sessionFilePath(cwd, agentId) {
9035
- const safe2 = sanitizeIdentifier(agentId, { maxLen: 128 });
9036
- return safePathJoin(cwd, ".theokit", "agents", safe2, "messages.jsonl");
9037
- }
9038
- async function readJsonlLines(cwd, agentId) {
9039
- const path = sessionFilePath(cwd, agentId);
9040
- try {
9041
- const raw = await readFile(path, "utf8");
9042
- return raw.split("\n").filter((line) => line.length > 0);
9043
- } catch {
9044
- return [];
9045
- }
9046
- }
9047
- function warnMalformed(agentId, line) {
9048
- process.stderr.write(
9049
- `[theokit-sdk] skipping malformed line in messages.jsonl (${agentId}): ${line.slice(0, 80)}...
9050
- `
9051
- );
9052
- }
9053
- function hydrateSessionLine(parsed) {
9054
- if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
9055
- if (parsed.role === "user" || parsed.role === "assistant") {
9056
- return { role: parsed.role, text: parsed.text };
9057
- }
9058
- if (parsed.role === "tool_call" || parsed.role === "tool_result") {
9059
- const label = parsed.role === "tool_call" ? "tool call" : "tool result";
9060
- return { role: "assistant", text: `[${label}] ${parsed.text}` };
9061
- }
9062
- return void 0;
9063
- }
9064
- async function readSessionFile(cwd, agentId) {
9065
- const lines = await readJsonlLines(cwd, agentId);
9066
- const messages = [];
9067
- for (const line of lines) {
9068
- try {
9069
- const msg = hydrateSessionLine(JSON.parse(line));
9070
- if (msg !== void 0) messages.push(msg);
9071
- } catch {
9072
- warnMalformed(agentId, line);
9073
- }
9074
- }
9075
- return messages;
9076
- }
9077
- async function readAllPersistedMessages(cwd, agentId) {
9078
- const lines = await readJsonlLines(cwd, agentId);
9079
- const messages = [];
9080
- for (const line of lines) {
9081
- try {
9082
- const parsed = JSON.parse(line);
9083
- if (parsed.role !== void 0 && VALID_ROLES.has(parsed.role) && typeof parsed.text === "string") {
9084
- messages.push({
9085
- role: parsed.role,
9086
- text: parsed.text,
9087
- at: typeof parsed.at === "number" ? parsed.at : Date.now()
9088
- });
9089
- }
9090
- } catch {
9091
- warnMalformed(agentId, line);
9092
- }
9093
- }
9094
- return messages;
9095
- }
9096
- async function appendAnyPersistedMessage(cwd, agentId, record) {
9097
- await appendPersistedMessages(cwd, agentId, [record]);
9098
- }
9099
- async function appendPersistedMessages(cwd, agentId, records) {
9100
- if (records.length === 0) return;
9101
- const path = sessionFilePath(cwd, agentId);
9102
- const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
9103
- `).join("");
9104
- const dir = dirname(path);
9105
- let written = false;
9106
- const attempt = async () => {
9107
- await mkdir(dir, { recursive: true });
9108
- await withFileLock(path, async () => {
9109
- await appendFile(path, payload, "utf8");
9110
- written = true;
9111
- });
9112
- };
9113
- try {
9114
- await attempt();
9115
- } catch (cause) {
9116
- if (written || cause.code !== "ENOENT") throw cause;
9117
- await attempt();
9118
- }
9119
- }
9120
- async function rewriteLockedSession(path, transform) {
9121
- await withFileLock(path, async () => {
9122
- let raw;
9123
- try {
9124
- raw = await readFile(path, "utf8");
9125
- } catch {
9126
- return;
9127
- }
9128
- const lines = raw.split("\n").filter((line) => line.length > 0);
9129
- const next = transform(lines);
9130
- if (next === void 0) return;
9131
- await replaceFileAtomic(path, next);
9132
- });
9133
- }
9134
- async function compactSessionFile(cwd, agentId, maxTurns) {
9135
- const path = sessionFilePath(cwd, agentId);
9136
- if (!existsSync(path)) return;
9137
- await rewriteLockedSession(
9138
- path,
9139
- (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
9140
- `
9141
- );
9142
- }
9143
- async function truncateSessionTo(cwd, agentId, keepCount) {
9144
- const path = sessionFilePath(cwd, agentId);
9145
- if (!existsSync(path)) return 0;
9146
- let kept = 0;
9147
- await rewriteLockedSession(path, (lines) => {
9148
- const keep = Math.max(0, Math.min(keepCount, lines.length));
9149
- kept = keep;
9150
- if (keep === lines.length) return void 0;
9151
- return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
9152
- `;
9153
- });
9154
- return kept;
9155
- }
9156
-
9157
- // src/internal/persistence/conversation-storage-fs.ts
9158
- init_security();
9159
-
9160
- // src/internal/persistence/pagination.ts
9161
- function paginate(items, opts) {
9162
- if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
9163
- const start = Math.max(0, opts.offset ?? 0);
9164
- const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
9165
- return items.slice(start, end);
9374
+ base[name] = value;
9375
+ }
9376
+ return base;
9166
9377
  }
9167
9378
 
9168
- // src/internal/persistence/session-meta.ts
9169
- function applyMetaPatch(current, patch) {
9170
- const next = {};
9171
- if (current.title !== void 0) next.title = current.title;
9172
- if (current.tag !== void 0) next.tag = current.tag;
9173
- if (patch.title === null) delete next.title;
9174
- else if (patch.title !== void 0) next.title = patch.title;
9175
- if (patch.tag === null) delete next.tag;
9176
- else if (patch.tag !== void 0) next.tag = patch.tag;
9177
- return next;
9178
- }
9179
- function coerceSessionMeta(raw) {
9180
- if (typeof raw !== "object" || raw === null) return void 0;
9181
- const obj = raw;
9182
- const meta = {};
9183
- if (typeof obj.title === "string") meta.title = obj.title;
9184
- if (typeof obj.tag === "string") meta.tag = obj.tag;
9185
- return meta.title === void 0 && meta.tag === void 0 ? void 0 : meta;
9379
+ // src/internal/runtime/lifecycle/spawn-collect.ts
9380
+ function spawnAndCollect(options) {
9381
+ return new Promise((resolve3) => {
9382
+ const timeoutMs = options.timeoutMs ?? 3e4;
9383
+ const spawnOptions = {
9384
+ cwd: options.cwd,
9385
+ // #54 scrub secret-like parent env by default; `options.env` still wins.
9386
+ env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
9387
+ };
9388
+ const child = spawn(options.command, options.args ?? [], spawnOptions);
9389
+ let stdout = "";
9390
+ let stderr = "";
9391
+ let timedOut = false;
9392
+ let settled = false;
9393
+ const settle = (result) => {
9394
+ if (settled) return;
9395
+ settled = true;
9396
+ resolve3(result);
9397
+ };
9398
+ const timer = setTimeout(() => {
9399
+ timedOut = true;
9400
+ try {
9401
+ child.kill("SIGKILL");
9402
+ } catch {
9403
+ }
9404
+ settle({ stdout, stderr, exitCode: null, timedOut: true });
9405
+ }, timeoutMs);
9406
+ child.stdout?.on("data", (chunk) => {
9407
+ stdout += chunk.toString("utf8");
9408
+ });
9409
+ child.stderr?.on("data", (chunk) => {
9410
+ stderr += chunk.toString("utf8");
9411
+ });
9412
+ child.on("error", (cause) => {
9413
+ clearTimeout(timer);
9414
+ settle({ stdout, stderr, exitCode: -1, timedOut, spawnError: cause });
9415
+ });
9416
+ child.on("close", (code) => {
9417
+ clearTimeout(timer);
9418
+ settle({ stdout, stderr, exitCode: code, timedOut });
9419
+ });
9420
+ if (options.stdin !== void 0 && child.stdin !== null) {
9421
+ child.stdin.end(options.stdin);
9422
+ }
9423
+ });
9186
9424
  }
9187
9425
 
9188
- // src/internal/persistence/conversation-storage-fs.ts
9189
- var FileSystemConversationStorage = class {
9190
- #root;
9191
- constructor(opts = {}) {
9192
- this.#root = opts.root ?? process.cwd();
9193
- }
9194
- /** Exposed for tests + diagnostics. The path is sanitized at use sites. */
9195
- get root() {
9196
- return this.#root;
9197
- }
9198
- async getMessages(conversationId, opts) {
9199
- const records = await readAllPersistedMessages(this.#root, conversationId);
9200
- const all = records.map(toStoredMessage);
9201
- return paginate(all, opts);
9202
- }
9203
- async appendMessage(conversationId, message) {
9204
- await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
9205
- }
9206
- async appendMessages(conversationId, messages) {
9207
- await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
9208
- }
9209
- async truncateConversation(conversationId, keepCount) {
9210
- return truncateSessionTo(this.#root, conversationId, keepCount);
9211
- }
9212
- async deleteConversation(conversationId) {
9213
- const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
9214
- const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
9215
- await rm(dirPath, { recursive: true, force: true });
9426
+ // src/internal/runtime/hooks/hooks-executor.ts
9427
+ init_hooks_source();
9428
+ var HooksExecutor = class {
9429
+ constructor(cwd) {
9430
+ this.cwd = cwd;
9216
9431
  }
9217
- async deleteScope(prefix) {
9218
- const ids = await this.listConversationIds();
9219
- const matching = ids.filter((id) => id.startsWith(prefix));
9220
- for (const id of matching) await this.deleteConversation(id);
9221
- return matching.length;
9222
- }
9223
- async listConversationIds(opts = {}) {
9224
- const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
9225
- let entries;
9226
- try {
9227
- entries = await readdir(agentsRoot);
9228
- } catch (cause) {
9229
- if (cause.code === "ENOENT") return [];
9230
- throw cause;
9432
+ cwd;
9433
+ config = {};
9434
+ async initialize(settingSourcesIncludeProject) {
9435
+ if (!settingSourcesIncludeProject) {
9436
+ this.config = {};
9437
+ return;
9231
9438
  }
9232
- if (opts.limit !== void 0) return entries.slice(0, opts.limit);
9233
- return entries;
9234
- }
9235
- async compact(conversationId, maxTurns) {
9236
- await compactSessionFile(this.#root, conversationId, maxTurns);
9237
- }
9238
- // SE4 — session metadata persisted as a per-conversation sidecar
9239
- // `<root>/.theokit/agents/<safeId>/session.json` (same sanitized perimeter as
9240
- // the transcript). Kept separate from messages.jsonl so a title/tag write does
9241
- // not rewrite the append-only log.
9242
- #metaPath(conversationId) {
9243
- const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
9244
- return safePathJoin(this.#root, ".theokit", "agents", safe2, "session.json");
9439
+ this.config = await loadHookConfig(this.cwd);
9245
9440
  }
9246
- async getSessionMeta(conversationId) {
9247
- try {
9248
- const raw = await readFile(this.#metaPath(conversationId), "utf8");
9249
- return coerceSessionMeta(JSON.parse(raw));
9250
- } catch (cause) {
9251
- if (cause.code === "ENOENT") return void 0;
9252
- throw cause;
9441
+ /** Fire every hook registered for `event` and aggregate the decisions. */
9442
+ async run(payload) {
9443
+ const commands = this.commandsFor(payload.event, payload.tool);
9444
+ if (commands.length === 0) return { decisions: [], blocked: false };
9445
+ const decisions = [];
9446
+ for (const command of commands) {
9447
+ const decision = await this.executeOne(command, payload);
9448
+ decisions.push(decision);
9449
+ if (decision.decision === "deny") {
9450
+ const result = {
9451
+ decisions,
9452
+ blocked: true
9453
+ };
9454
+ if (decision.reason !== void 0) result.reason = decision.reason;
9455
+ return result;
9456
+ }
9253
9457
  }
9458
+ return { decisions, blocked: false };
9254
9459
  }
9255
- async setSessionMeta(conversationId, patch) {
9256
- const metaPath = this.#metaPath(conversationId);
9257
- await mkdir(dirname(metaPath), { recursive: true });
9258
- await withFileLock(metaPath, async () => {
9259
- let current = {};
9460
+ commandsFor(event, tool) {
9461
+ const list2 = this.config.hooks?.[event] ?? [];
9462
+ if (tool === void 0) return list2;
9463
+ return list2.filter((entry) => {
9464
+ if (entry.matcher === void 0) return true;
9260
9465
  try {
9261
- current = coerceSessionMeta(JSON.parse(await readFile(metaPath, "utf8"))) ?? {};
9262
- } catch (cause) {
9263
- if (cause.code !== "ENOENT") throw cause;
9466
+ return new RegExp(entry.matcher).test(tool);
9467
+ } catch {
9468
+ return entry.matcher === tool;
9264
9469
  }
9265
- const next = applyMetaPatch(current, patch);
9266
- await writeFile(metaPath, redactSecrets(JSON.stringify(next)), "utf8");
9267
9470
  });
9268
9471
  }
9269
- async dispose() {
9472
+ async executeOne(command, payload) {
9473
+ const timeoutMs = command.timeoutMs ?? 3e4;
9474
+ const result = await spawnAndCollect({
9475
+ command: "sh",
9476
+ args: ["-c", command.command],
9477
+ cwd: this.cwd,
9478
+ timeoutMs,
9479
+ stdin: JSON.stringify(payload)
9480
+ });
9481
+ if (result.timedOut) {
9482
+ return { decision: "deny", reason: `Hook timed out after ${timeoutMs}ms` };
9483
+ }
9484
+ if (result.spawnError !== void 0) {
9485
+ return { decision: "deny", reason: `Hook spawn failed: ${result.spawnError.message}` };
9486
+ }
9487
+ if (result.exitCode !== 0) {
9488
+ return {
9489
+ decision: "deny",
9490
+ reason: result.stderr.trim().length > 0 ? result.stderr.trim() : `Hook exited with code ${result.exitCode}`
9491
+ };
9492
+ }
9493
+ return parseDecisionFromStdout(result.stdout);
9270
9494
  }
9271
9495
  };
9272
- function toStoredMessage(record) {
9273
- return {
9274
- role: record.role,
9275
- content: record.text,
9276
- at: record.at
9277
- };
9496
+ function parseDecisionFromStdout(stdout) {
9497
+ const trimmed = stdout.trim();
9498
+ if (trimmed.length === 0) return { decision: "allow" };
9499
+ try {
9500
+ const parsed = JSON.parse(trimmed);
9501
+ if (parsed.decision === "deny" || parsed.decision === "feedback") {
9502
+ const result = { decision: parsed.decision };
9503
+ if (parsed.reason !== void 0) result.reason = parsed.reason;
9504
+ if (parsed.feedback !== void 0) result.feedback = parsed.feedback;
9505
+ return result;
9506
+ }
9507
+ if (parsed.decision === "allow") return { decision: "allow" };
9508
+ } catch {
9509
+ return { decision: "feedback", feedback: trimmed };
9510
+ }
9511
+ return { decision: "allow" };
9278
9512
  }
9279
- function toRecord(message) {
9280
- return { role: message.role, text: message.content, at: message.at ?? Date.now() };
9513
+
9514
+ // src/internal/runtime/lifecycle/post-run-lifecycle.ts
9515
+ init_session_summary_writer();
9516
+
9517
+ // src/internal/runtime/memory/memory-path-selector.ts
9518
+ var PORT_MEMORY_PATH_ENV_VAR = "THEOKIT_PORT_MEMORY_PATH";
9519
+ function shouldUsePortMemoryPath() {
9520
+ const env = globalThis.process?.env;
9521
+ if (env === void 0) return false;
9522
+ const val = env[PORT_MEMORY_PATH_ENV_VAR];
9523
+ return val === "1" || val === "true";
9524
+ }
9525
+ function resolveMemoryProviderForLoop(consumerSupplied, defaultAdapter, portPathEnabled) {
9526
+ if (consumerSupplied !== void 0) return consumerSupplied;
9527
+ if (portPathEnabled) return defaultAdapter;
9528
+ return void 0;
9529
+ }
9530
+ function resolveMemoryToolsForLoop(legacyTools, portPathEnabled) {
9531
+ if (portPathEnabled) return void 0;
9532
+ return legacyTools;
9533
+ }
9534
+ function resolveActiveMemorySummaryForSend(legacySummary, portPathEnabled) {
9535
+ if (portPathEnabled) return void 0;
9536
+ return legacySummary;
9281
9537
  }
9282
9538
 
9283
9539
  // src/internal/runtime/session/agent-session.ts
9540
+ init_conversation_storage_fs();
9541
+ init_agent_session_store();
9284
9542
  var DEFAULT_MAX_TURNS = 200;
9285
9543
  var COMPACTION_CHECK_INTERVAL = 50;
9286
9544
  var sessions = /* @__PURE__ */ new Map();
@@ -16357,6 +16615,9 @@ async function readProjectMcpServers(cwd) {
16357
16615
  }
16358
16616
  }
16359
16617
 
16618
+ // src/internal/runtime/local-agent/local-agent.ts
16619
+ init_local_agent_goal_extensions();
16620
+
16360
16621
  // src/internal/runtime/local-agent/local-agent-invalidate.ts
16361
16622
  async function applyDeferredInvalidation(agentId, pending, refresh) {
16362
16623
  process.stderr.write(
@@ -17739,16 +18000,49 @@ async function localAgentUsePersonality(args) {
17739
18000
  init_local_agent_plugins();
17740
18001
 
17741
18002
  // src/internal/runtime/local-agent/local-agent-runtime-extensions.ts
17742
- function localAgentRunUntil(agent, goal, options) {
18003
+ function pausedReason(kind, threadId) {
18004
+ switch (kind) {
18005
+ case "none":
18006
+ return `no durable objective set for thread "${threadId}" \u2014 call setObjective() first`;
18007
+ case "inert":
18008
+ return `durable objective for thread "${threadId}" is inert \u2014 no judge resolved (set a judge to activate)`;
18009
+ case "exhausted":
18010
+ return `durable objective for thread "${threadId}" exhausted its run budget \u2014 raise maxRuns to resume`;
18011
+ }
18012
+ }
18013
+ function localAgentRunUntil(agent, goal, options, durable) {
17743
18014
  async function* wrap() {
17744
18015
  const { runUntilImpl: runUntilImpl2 } = await Promise.resolve().then(() => (init_run_until(), run_until_exports));
17745
- const { judgeCallImpl: judgeCallImpl2 } = await Promise.resolve().then(() => (init_judge_call(), judge_call_exports));
17746
- const { getAgentFacade: getAgentFacade2 } = await Promise.resolve().then(() => (init_agent_factory_registry(), agent_factory_registry_exports));
17747
- const create = getAgentFacade2().create;
17748
- const deps = {
17749
- judge: async (ctx, opts) => judgeCallImpl2(ctx, opts, { create })
18016
+ const buildDeps = async () => {
18017
+ const { judgeCallImpl: judgeCallImpl2 } = await Promise.resolve().then(() => (init_judge_call(), judge_call_exports));
18018
+ const { getAgentFacade: getAgentFacade2 } = await Promise.resolve().then(() => (init_agent_factory_registry(), agent_factory_registry_exports));
18019
+ const create = getAgentFacade2().create;
18020
+ return {
18021
+ judge: (ctx, opts) => judgeCallImpl2(ctx, opts, { create })
18022
+ };
17750
18023
  };
17751
- return yield* runUntilImpl2(agent, goal, options, deps);
18024
+ if (goal !== void 0) {
18025
+ return yield* runUntilImpl2(agent, goal, options, await buildDeps());
18026
+ }
18027
+ const threadId = options?.threadId;
18028
+ if (durable === void 0 || threadId === void 0) {
18029
+ const reason = "runUntil() called with no goal and no threadId \u2014 nothing to resolve a durable objective from";
18030
+ yield { type: "status_change", status: "paused", reason };
18031
+ return { status: "paused", turnsUsed: 0, finalResponse: void 0 };
18032
+ }
18033
+ const { resolveDurableRun: resolveDurableRun2, persistDurableProgress: persistDurableProgress2 } = await Promise.resolve().then(() => (init_local_agent_goal_extensions(), local_agent_goal_extensions_exports));
18034
+ const resolved = await resolveDurableRun2(durable.handle, durable.goalConfig, threadId, options);
18035
+ if (resolved.kind !== "run") {
18036
+ yield {
18037
+ type: "status_change",
18038
+ status: "paused",
18039
+ reason: pausedReason(resolved.kind, threadId)
18040
+ };
18041
+ return { status: "paused", turnsUsed: 0, finalResponse: void 0 };
18042
+ }
18043
+ const result = yield* runUntilImpl2(agent, resolved.goal, resolved.options, await buildDeps());
18044
+ await persistDurableProgress2(durable.handle, threadId, result);
18045
+ return result;
17752
18046
  }
17753
18047
  return wrap();
17754
18048
  }
@@ -17814,6 +18108,44 @@ function ponyfillAny(signals) {
17814
18108
  return ctrl.signal;
17815
18109
  }
17816
18110
 
18111
+ // src/internal/runtime/lifecycle/wrap-completion-check-run.ts
18112
+ function wrapRunWithCompletionCheck(args) {
18113
+ const check = args.completionCheck;
18114
+ if (check === void 0) return args.run;
18115
+ const compute = async () => {
18116
+ const result = await args.run.wait();
18117
+ if (result.status !== "finished" || result.result === void 0) return result;
18118
+ const judgeOpts = {};
18119
+ if (check.judgeModel !== void 0) judgeOpts.judgeModel = check.judgeModel;
18120
+ if (check.apiKey !== void 0) judgeOpts.apiKey = check.apiKey;
18121
+ const verdict = await args.deps.judge(
18122
+ { goal: check.criteria, lastResponse: result.result },
18123
+ judgeOpts
18124
+ );
18125
+ const complete = !verdict.parseFailed && verdict.verdict === "done";
18126
+ emitRunEvent(args.onRunEvent, {
18127
+ type: "completion_check",
18128
+ complete,
18129
+ reason: verdict.reason
18130
+ });
18131
+ return {
18132
+ ...result,
18133
+ completionCheck: { complete, reason: verdict.reason, parseFailed: verdict.parseFailed }
18134
+ };
18135
+ };
18136
+ let judged;
18137
+ const wrappedWait = () => {
18138
+ judged ??= compute();
18139
+ return judged;
18140
+ };
18141
+ return new Proxy(args.run, {
18142
+ get(target, prop, receiver) {
18143
+ if (prop === "wait") return wrappedWait;
18144
+ return Reflect.get(target, prop, receiver);
18145
+ }
18146
+ });
18147
+ }
18148
+
17817
18149
  // src/internal/runtime/processors/run-processors.ts
17818
18150
  var ProcessorAbort = class {
17819
18151
  constructor(processorId, reason) {
@@ -17953,6 +18285,9 @@ function wrapRunWithOutputProcessors(args) {
17953
18285
  });
17954
18286
  }
17955
18287
 
18288
+ // src/internal/runtime/local-agent/local-agent-send.ts
18289
+ init_local_agent_goal_extensions();
18290
+
17956
18291
  // src/internal/runtime/local-agent/local-agent-memory-hooks.ts
17957
18292
  var DEFAULT_MAX_RECALL_BYTES = 16e3;
17958
18293
  async function applyPreUserSendHook(args) {
@@ -18068,6 +18403,11 @@ async function executeSendLocked(inputs, message, options) {
18068
18403
  memoryFacts,
18069
18404
  activeMemorySummary
18070
18405
  );
18406
+ const projectedSystemPrompt = await projectCurrentObjective(
18407
+ inputs.storageHandle,
18408
+ options.objectiveThreadId,
18409
+ assembledSystemPrompt
18410
+ );
18071
18411
  const composedOptions = {
18072
18412
  ...options,
18073
18413
  signal: anySignal([options.signal, inputs.lifecycleAbortController.signal])
@@ -18075,7 +18415,7 @@ async function executeSendLocked(inputs, message, options) {
18075
18415
  const run = await inputs.dispatchRun(
18076
18416
  adaptedMessage,
18077
18417
  composedOptions,
18078
- assembledSystemPrompt,
18418
+ projectedSystemPrompt,
18079
18419
  memoryFacts,
18080
18420
  priorMessages,
18081
18421
  memoryTools,
@@ -18088,18 +18428,43 @@ async function executeSendLocked(inputs, message, options) {
18088
18428
  agentId: inputs.agentId,
18089
18429
  onRunEvent: options.onRunEvent
18090
18430
  }) : run;
18091
- return wrapRunWithPostReplyHook({
18431
+ const hookedRun = wrapRunWithPostReplyHook({
18092
18432
  pluginManager: inputs.pluginManagerCode,
18093
18433
  agentId: inputs.agentId,
18094
18434
  options: inputs.options,
18095
18435
  run: processedRun,
18096
18436
  userText
18097
18437
  });
18438
+ return wrapRunWithCompletionCheck({
18439
+ run: hookedRun,
18440
+ completionCheck: options.completionCheck,
18441
+ onRunEvent: options.onRunEvent,
18442
+ deps: buildCompletionCheckDeps()
18443
+ });
18444
+ }
18445
+ function buildCompletionCheckDeps() {
18446
+ return {
18447
+ judge: async (ctx, opts) => {
18448
+ const { judgeCallImpl: judgeCallImpl2 } = await Promise.resolve().then(() => (init_judge_call(), judge_call_exports));
18449
+ const { getAgentFacade: getAgentFacade2 } = await Promise.resolve().then(() => (init_agent_factory_registry(), agent_factory_registry_exports));
18450
+ return judgeCallImpl2(ctx, opts, { create: getAgentFacade2().create });
18451
+ }
18452
+ };
18098
18453
  }
18099
18454
  function readMemoryForSend(workspaceCwd, memoryConfig) {
18100
18455
  if (memoryConfig?.enabled !== true) return Promise.resolve([]);
18101
18456
  return safeCall(() => readMemoryFacts(workspaceCwd, memoryConfig), [], "memory read");
18102
18457
  }
18458
+ async function projectCurrentObjective(storageHandle, objectiveThreadId, assembled) {
18459
+ if (objectiveThreadId === void 0) return assembled;
18460
+ const objective = await safeCall(
18461
+ () => resolveCurrentObjectiveText(storageHandle, objectiveThreadId),
18462
+ void 0,
18463
+ "objective projection"
18464
+ );
18465
+ if (objective === void 0) return assembled;
18466
+ return formatObjectiveProjection(objective, assembled);
18467
+ }
18103
18468
 
18104
18469
  // src/internal/runtime/local-agent/local-agent-task-wrap.ts
18105
18470
  init_registry();
@@ -18462,20 +18827,33 @@ var LocalAgent = class {
18462
18827
  ...opts !== void 0 ? { opts } : {}
18463
18828
  });
18464
18829
  }
18830
+ // biome-ignore format: G8 budget — artifact stubs (local agents have no artifacts); kept 1-line each.
18465
18831
  listArtifacts() {
18466
18832
  return Promise.resolve([]);
18467
18833
  }
18834
+ // biome-ignore format: G8 budget — see listArtifacts.
18468
18835
  downloadArtifact(_path) {
18469
- return Promise.reject(
18470
- new UnsupportedRunOperationError(
18471
- "Artifacts are not supported for local agents",
18472
- "downloadArtifact"
18473
- )
18474
- );
18836
+ return Promise.reject(new UnsupportedRunOperationError("Artifacts are not supported for local agents", "downloadArtifact"));
18475
18837
  }
18476
18838
  // biome-ignore format: G8 budget — both methods delegate to `local-agent-runtime-extensions.ts`; signatures kept as 1-line each.
18477
18839
  runUntil(goal, options) {
18478
- return localAgentRunUntil(this, goal, options);
18840
+ return localAgentRunUntil(this, goal, options, { handle: this.storageHandle(), goalConfig: this.options.goal });
18841
+ }
18842
+ // biome-ignore format: SE33 G8 budget — objective methods delegate to `local-agent-goal-extensions.ts`.
18843
+ setObjective(objective, opts) {
18844
+ return localAgentSetObjective(this.storageHandle(), objective, opts);
18845
+ }
18846
+ // biome-ignore format: SE33 G8 budget — see setObjective above.
18847
+ getObjective(opts) {
18848
+ return localAgentGetObjective(this.storageHandle(), opts);
18849
+ }
18850
+ // biome-ignore format: SE33 G8 budget — see setObjective above.
18851
+ updateObjectiveOptions(opts) {
18852
+ return localAgentUpdateObjectiveOptions(this.storageHandle(), opts);
18853
+ }
18854
+ // biome-ignore format: SE33 G8 budget — see setObjective above.
18855
+ clearObjective(opts) {
18856
+ return localAgentClearObjective(this.storageHandle(), opts);
18479
18857
  }
18480
18858
  // biome-ignore format: G8 budget — see runUntil comment above.
18481
18859
  fork(options) {
@@ -20059,12 +20437,17 @@ var EventBus = class {
20059
20437
 
20060
20438
  // src/index.ts
20061
20439
  init_generate_object();
20440
+ init_conversation_storage_fs();
20062
20441
 
20063
20442
  // src/internal/persistence/conversation-storage-memory.ts
20443
+ init_pagination();
20444
+ init_session_meta();
20064
20445
  var InMemoryConversationStorage = class {
20065
20446
  #store = /* @__PURE__ */ new Map();
20066
20447
  // SE4 — session-level metadata (title/tag), kept beside the transcript.
20067
20448
  #meta = /* @__PURE__ */ new Map();
20449
+ // SE33 — durable thread-scoped objective, kept beside the transcript.
20450
+ #objectives = /* @__PURE__ */ new Map();
20068
20451
  async getMessages(conversationId, opts) {
20069
20452
  const existing = this.#store.get(conversationId);
20070
20453
  return existing === void 0 ? [] : paginate(existing.slice(), opts);
@@ -20094,18 +20477,21 @@ var InMemoryConversationStorage = class {
20094
20477
  async deleteConversation(conversationId) {
20095
20478
  this.#store.delete(conversationId);
20096
20479
  this.#meta.delete(conversationId);
20480
+ this.#objectives.delete(conversationId);
20097
20481
  }
20098
20482
  async deleteScope(prefix) {
20099
20483
  let n = 0;
20100
- for (const id of [...this.#store.keys()]) {
20101
- if (id.startsWith(prefix)) {
20102
- this.#store.delete(id);
20103
- this.#meta.delete(id);
20104
- n += 1;
20105
- }
20106
- }
20107
- for (const id of [...this.#meta.keys()]) {
20108
- if (id.startsWith(prefix)) this.#meta.delete(id);
20484
+ const ids = /* @__PURE__ */ new Set([
20485
+ ...this.#store.keys(),
20486
+ ...this.#meta.keys(),
20487
+ ...this.#objectives.keys()
20488
+ ]);
20489
+ for (const id of ids) {
20490
+ if (!id.startsWith(prefix)) continue;
20491
+ this.#store.delete(id);
20492
+ this.#meta.delete(id);
20493
+ this.#objectives.delete(id);
20494
+ n += 1;
20109
20495
  }
20110
20496
  return n;
20111
20497
  }
@@ -20122,9 +20508,27 @@ var InMemoryConversationStorage = class {
20122
20508
  const current = this.#meta.get(conversationId) ?? {};
20123
20509
  this.#meta.set(conversationId, applyMetaPatch(current, patch));
20124
20510
  }
20511
+ async getObjectiveRecord(conversationId) {
20512
+ const rec = this.#objectives.get(conversationId);
20513
+ return rec === void 0 ? void 0 : { ...rec };
20514
+ }
20515
+ async setObjectiveRecord(conversationId, record) {
20516
+ if (record === null) this.#objectives.delete(conversationId);
20517
+ else this.#objectives.set(conversationId, { ...record });
20518
+ }
20519
+ // SE33 (HIGH-1 fix) — atomic in the single-threaded runtime: the get→mutate→set
20520
+ // runs synchronously, so there is no interleaving window for a concurrent frame.
20521
+ async updateObjectiveRecord(conversationId, mutate) {
20522
+ const current = this.#objectives.get(conversationId);
20523
+ const next = mutate(current === void 0 ? void 0 : { ...current });
20524
+ if (next === void 0) return;
20525
+ if (next === null) this.#objectives.delete(conversationId);
20526
+ else this.#objectives.set(conversationId, { ...next });
20527
+ }
20125
20528
  async dispose() {
20126
20529
  this.#store.clear();
20127
20530
  this.#meta.clear();
20531
+ this.#objectives.clear();
20128
20532
  }
20129
20533
  };
20130
20534