cogpit 3.5.1 → 3.5.2

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 (89) hide show
  1. package/dist/cli-runtime.js +39453 -35647
  2. package/dist/instance-worker.js +1187 -151
  3. package/dist/web/assets/AutomationsPanel-B3zT5G5K.js +1 -0
  4. package/dist/web/assets/{CogpitBlock-ZgOubpsS.js → CogpitBlock-d05YgSdQ.js} +12 -12
  5. package/dist/web/assets/CommandPaletteHost-D8fk5Qkq.js +1 -0
  6. package/dist/web/assets/ConfigBrowser-BRsvua8m.js +1 -0
  7. package/dist/web/assets/CrewPanel-C1Yq8jMT.js +1 -0
  8. package/dist/web/assets/HighlightedEditor-clIIwmWB.js +2 -0
  9. package/dist/web/assets/KeyboardShortcutsDialog-0VokzXOy.js +1 -0
  10. package/dist/web/assets/MissionHome-CNjhPZva.js +2 -0
  11. package/dist/web/assets/MobileFileChanges-iLhOAdgx.js +1 -0
  12. package/dist/web/assets/PluginsDialog-xM9uR8bB.js +1 -0
  13. package/dist/web/assets/PreviewPanel-DF4kUYpX.js +1 -0
  14. package/dist/web/assets/ProjectFilesPanel-D9_OzQi2.js +4 -0
  15. package/dist/web/assets/ProjectSwitcherModal-DMEEWm7Q.js +1 -0
  16. package/dist/web/assets/{TerminalOutput-DOX29EmA.js → TerminalOutput-CrGF3VWW.js} +3 -3
  17. package/dist/web/assets/ThemeSelectorModal-BLBet6sQ.js +1 -0
  18. package/dist/web/assets/WorkflowsPanel-DkXoYVsK.js +1 -0
  19. package/dist/web/assets/WorktreePanel-DvFGKfAn.js +1 -0
  20. package/dist/web/assets/{bash-Yzrsuije.js → bash-DgJyDXW6.js} +1 -1
  21. package/dist/web/assets/{c-BIGW1oBm.js → c-BG-tZi7o.js} +1 -1
  22. package/dist/web/assets/cpp-BzPMphJ4.js +1 -0
  23. package/dist/web/assets/{css-DPfMkruS.js → css-PZBBrkI0.js} +1 -1
  24. package/dist/web/assets/edition-ui-CvFxsVfJ.js +2 -0
  25. package/dist/web/assets/github-dark-BQV5ugrc.js +1 -0
  26. package/dist/web/assets/github-light-Rcb2q3L5.js +1 -0
  27. package/dist/web/assets/{go-CxLEBnE3.js → go-BBr2UQCn.js} +1 -1
  28. package/dist/web/assets/{html-GMplVEZG.js → html-DOf0XnR2.js} +1 -1
  29. package/dist/web/assets/index-BvNyEUVG.js +506 -0
  30. package/dist/web/assets/index-CAupRX51.js +1 -0
  31. package/dist/web/assets/index-D6HYPdiw.css +1 -0
  32. package/dist/web/assets/{index-By5-_P50.js → index-DueGSx7L.js} +4 -4
  33. package/dist/web/assets/index-DxZSG7e3.js +2 -0
  34. package/dist/web/assets/{java-CylS5w8V.js → java-BTDmaRNO.js} +1 -1
  35. package/dist/web/assets/{javascript-wDzz0qaB.js → javascript-WyGyM1wV.js} +1 -1
  36. package/dist/web/assets/{json-Cp-IABpG.js → json-B8JryHcM.js} +1 -1
  37. package/dist/web/assets/{jsx-g9-lgVsj.js → jsx-8cLv2Six.js} +1 -1
  38. package/dist/web/assets/{kotlin-BdnUsdx6.js → kotlin-BQy537ds.js} +1 -1
  39. package/dist/web/assets/{markdown-Cvjx9yec.js → markdown-DX-TNHnc.js} +1 -1
  40. package/dist/web/assets/{php-aq0itWAE.js → php-47JHu45N.js} +1 -1
  41. package/dist/web/assets/{python-B6aJPvgy.js → python-B6Ihcp-E.js} +1 -1
  42. package/dist/web/assets/render.worker-DW0Lo4fc.js +154 -0
  43. package/dist/web/assets/ruby-BrGyNOch.js +1 -0
  44. package/dist/web/assets/{rust-B1yitclQ.js → rust-CxuBE0J8.js} +1 -1
  45. package/dist/web/assets/{scss-OYdSNvt2.js → scss-DxBkwadj.js} +1 -1
  46. package/dist/web/assets/session-parser.worker-Dv5Kch9n.js +41 -0
  47. package/dist/web/assets/{sql-BLtJtn59.js → sql-BbONfI8K.js} +1 -1
  48. package/dist/web/assets/svelte-H3swLp47.js +1 -0
  49. package/dist/web/assets/{swift-D82vCrfD.js → swift-tEesUtni.js} +1 -1
  50. package/dist/web/assets/switch-BHG6oFbM.js +1 -0
  51. package/dist/web/assets/{toml-vGWfd6FD.js → toml-Ms459DBw.js} +1 -1
  52. package/dist/web/assets/{tsx-COt5Ahok.js → tsx-D6116Ycy.js} +1 -1
  53. package/dist/web/assets/{typescript-BPQ3VLAy.js → typescript-BqvgUolh.js} +1 -1
  54. package/dist/web/assets/vendor-markdown-BEJ8yVzA.js +29 -0
  55. package/dist/web/assets/{vendor-ui-DQeMoSiV.js → vendor-ui-keo4oB2X.js} +221 -191
  56. package/dist/web/assets/vue-CWSgrUPf.js +1 -0
  57. package/dist/web/assets/{xml-D2mA2LmW.js → xml-CdYS3kUe.js} +1 -1
  58. package/dist/web/assets/{yaml-Buea-lGh.js → yaml-Bopit2L3.js} +1 -1
  59. package/dist/web/index.html +4 -5
  60. package/package.json +1 -1
  61. package/dist/web/assets/CommandPaletteHost-Djn2jqYR.js +0 -1
  62. package/dist/web/assets/ConfigBrowser-CRwh8BXq.js +0 -1
  63. package/dist/web/assets/CrewPanel-BxPl5D7h.js +0 -1
  64. package/dist/web/assets/HighlightedEditor-D9A-dW2Z.js +0 -2
  65. package/dist/web/assets/KeyboardShortcutsDialog-L7rRhApI.js +0 -1
  66. package/dist/web/assets/MissionHome-zp7VebSO.js +0 -2
  67. package/dist/web/assets/MobileFileChanges-O-SvMq9F.js +0 -1
  68. package/dist/web/assets/PluginsDialog-CBOyfoUX.js +0 -1
  69. package/dist/web/assets/PreviewPanel-_pKuUaps.js +0 -1
  70. package/dist/web/assets/ProjectFilesPanel-BsbpTgWK.js +0 -4
  71. package/dist/web/assets/ProjectSwitcherModal-D4MzdPVH.js +0 -1
  72. package/dist/web/assets/ThemeSelectorModal-7CWXZB-z.js +0 -1
  73. package/dist/web/assets/WorkflowsPanel-aOz1o3kA.js +0 -1
  74. package/dist/web/assets/WorktreePanel-BFve9bpC.js +0 -1
  75. package/dist/web/assets/cpp-BRuaLJcg.js +0 -1
  76. package/dist/web/assets/edition-ui-CFxEM3WF.js +0 -2
  77. package/dist/web/assets/github-dark-DHJKELXO.js +0 -1
  78. package/dist/web/assets/github-light-DAi9KRSo.js +0 -1
  79. package/dist/web/assets/index-CPTFl4Qh.js +0 -498
  80. package/dist/web/assets/index-DjpYbGVZ.js +0 -2
  81. package/dist/web/assets/index-DmMg-zf3.js +0 -1
  82. package/dist/web/assets/index-hGMtuUNI.css +0 -1
  83. package/dist/web/assets/ruby-B0Tyh1qT.js +0 -1
  84. package/dist/web/assets/session-parser.worker-3tFlB6dy.js +0 -31
  85. package/dist/web/assets/svelte-DZMnMd3x.js +0 -1
  86. package/dist/web/assets/switch-DS7UyNS2.js +0 -1
  87. package/dist/web/assets/vendor-markdown-BE1gYnO9.js +0 -37
  88. package/dist/web/assets/vendor-shiki-DOn6sF-i.js +0 -144
  89. package/dist/web/assets/vue-DVfSmUF7.js +0 -1
@@ -953,6 +953,65 @@ function refreshDirs() {
953
953
  return true;
954
954
  }
955
955
 
956
+ // ../../shared/objects.ts
957
+ function isRecord(value) {
958
+ return typeof value === "object" && value !== null && !Array.isArray(value);
959
+ }
960
+ function asRecord(value) {
961
+ return isRecord(value) ? value : null;
962
+ }
963
+
964
+ // ../../server/agents/runtimeTypes.ts
965
+ function resolvedApproval({ requestId, toolUseId, toolName }) {
966
+ return { requestId, toolUseId, toolName };
967
+ }
968
+ var AgentRuntimeError = class extends Error {
969
+ constructor(status, code, message, details) {
970
+ super(message);
971
+ this.status = status;
972
+ this.code = code;
973
+ this.details = details;
974
+ this.name = "AgentRuntimeError";
975
+ }
976
+ };
977
+ var NO_ACTIVITY = Object.freeze({ live: false, running: false, agentsRunning: false });
978
+ var startingSessions = /* @__PURE__ */ new Set();
979
+ function isSessionStarting(sessionId) {
980
+ return startingSessions.has(sessionId);
981
+ }
982
+ function finishSessionStartup(sessionId) {
983
+ startingSessions.delete(sessionId);
984
+ }
985
+ function reportSessionId(req, sessionId) {
986
+ if (req.beforeFirstTurn) startingSessions.add(sessionId);
987
+ try {
988
+ req.onSessionId?.(sessionId);
989
+ } catch (error2) {
990
+ console.error(`[agents] onSessionId failed for session ${sessionId}:`, error2);
991
+ }
992
+ }
993
+ function selectAvailableDecision(available, requested) {
994
+ if (available.includes(requested)) return requested;
995
+ if (requested === "allow_always" && available.includes("allow")) return "allow";
996
+ return null;
997
+ }
998
+
999
+ // ../../server/agents/startup.ts
1000
+ var failures = /* @__PURE__ */ new WeakSet();
1001
+ var isStartupFailure = (error2) => typeof error2 === "object" && error2 !== null && failures.has(error2);
1002
+ async function beforeFirstTurn(request, identity) {
1003
+ try {
1004
+ request.signal?.throwIfAborted();
1005
+ await request.beforeFirstTurn?.(identity);
1006
+ request.signal?.throwIfAborted();
1007
+ if (identity.sessionId) finishSessionStartup(identity.sessionId);
1008
+ } catch (error2) {
1009
+ if (identity.sessionId) finishSessionStartup(identity.sessionId);
1010
+ if (typeof error2 === "object" && error2 !== null) failures.add(error2);
1011
+ throw error2;
1012
+ }
1013
+ }
1014
+
956
1015
  // ../../server/agents/acpRuntime.ts
957
1016
  import { spawn } from "node:child_process";
958
1017
  import { appendFileSync, chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -1204,10 +1263,17 @@ function publishCompacting(sessionId, active) {
1204
1263
  emit(state, { type: "compacting", active });
1205
1264
  if (!active) maybeGc(sessionId, state);
1206
1265
  }
1266
+ var rateLimitListeners = /* @__PURE__ */ new Set();
1207
1267
  function publishRateLimit(sessionId, block) {
1208
1268
  const state = block ? getOrCreate(sessionId) : sessions.get(sessionId);
1209
1269
  if (!state || sameBlock(state.rateLimit, block)) return;
1210
1270
  state.rateLimit = block;
1271
+ for (const listener of rateLimitListeners) {
1272
+ try {
1273
+ listener(sessionId, block);
1274
+ } catch {
1275
+ }
1276
+ }
1211
1277
  emit(state, { type: "rate_limit", block });
1212
1278
  if (!block) maybeGc(sessionId, state);
1213
1279
  }
@@ -1294,14 +1360,6 @@ function isWithinDir(parent, child2) {
1294
1360
  // ../../server/agents/claudeMetadata.ts
1295
1361
  import { open as open3 } from "node:fs/promises";
1296
1362
 
1297
- // ../../shared/objects.ts
1298
- function isRecord(value) {
1299
- return typeof value === "object" && value !== null && !Array.isArray(value);
1300
- }
1301
- function asRecord(value) {
1302
- return isRecord(value) ? value : null;
1303
- }
1304
-
1305
1363
  // ../../shared/session/backgroundAgents.ts
1306
1364
  function awaitingAgentsStatus(launches, pendingQueue = 0) {
1307
1365
  if (!launches.some((launch) => launch.endedAt === void 0)) return null;
@@ -2208,6 +2266,29 @@ function buildTurns(messages) {
2208
2266
  return buildTurnsWithStarts(messages).turns;
2209
2267
  }
2210
2268
 
2269
+ // ../../shared/session/resultProjection.ts
2270
+ var ELISION_THRESHOLD_BYTES = 16 * 1024;
2271
+ function isPlainObject(value) {
2272
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2273
+ }
2274
+ function visitField(holder, key, visit, kind2 = "text") {
2275
+ const value = holder[key];
2276
+ if (typeof value !== "string") return false;
2277
+ const next = visit(value, kind2);
2278
+ if (next === value) return false;
2279
+ holder[key] = next;
2280
+ return true;
2281
+ }
2282
+ function visitJoinedText(parts, textOf, separator, withText, visit) {
2283
+ const texts = parts.map(textOf);
2284
+ const first = texts.findIndex((text2) => text2 !== void 0);
2285
+ if (first < 0) return null;
2286
+ const joined = texts.filter((text2) => Boolean(text2)).join(separator);
2287
+ const replacement = visit(joined, "text");
2288
+ if (replacement === joined) return null;
2289
+ return parts.flatMap((part, index) => index === first ? [withText(part, replacement)] : texts[index] === void 0 ? [part] : []);
2290
+ }
2291
+
2211
2292
  // ../../shared/session/claude.ts
2212
2293
  function isClaudeSessionText() {
2213
2294
  return true;
@@ -2332,17 +2413,17 @@ function collectClaudeBackgroundWork(rawMessages) {
2332
2413
  const resolvedAt = /* @__PURE__ */ new Map();
2333
2414
  const queuedIds = /* @__PURE__ */ new Set();
2334
2415
  const deliveredIds = /* @__PURE__ */ new Set();
2335
- function resolve9(id, index) {
2416
+ function resolve10(id, index) {
2336
2417
  if (!resolvedAt.has(id)) resolvedAt.set(id, index);
2337
2418
  }
2338
2419
  function noteNotifications(text2, index, sink) {
2339
2420
  if (!text2.includes(TASK_NOTIFICATION_MARKER)) return;
2340
2421
  for (const match of text2.matchAll(TASK_ID_RE)) {
2341
- resolve9(match[1], index);
2422
+ resolve10(match[1], index);
2342
2423
  sink?.add(match[1]);
2343
2424
  }
2344
2425
  for (const match of text2.matchAll(TOOL_USE_ID_RE)) {
2345
- resolve9(match[1], index);
2426
+ resolve10(match[1], index);
2346
2427
  sink?.add(match[1]);
2347
2428
  }
2348
2429
  }
@@ -2369,7 +2450,7 @@ function collectClaudeBackgroundWork(rawMessages) {
2369
2450
  if (!isObject(block) || block.type !== "tool_use" || block.name !== "TaskStop") continue;
2370
2451
  const input = isObject(block.input) ? block.input : null;
2371
2452
  const taskId = input?.task_id ?? input?.shell_id;
2372
- if (typeof taskId === "string") resolve9(taskId, index);
2453
+ if (typeof taskId === "string") resolve10(taskId, index);
2373
2454
  }
2374
2455
  continue;
2375
2456
  }
@@ -2482,6 +2563,44 @@ function brandClaudeBranch(firstRecord, sessionId, turnIndex) {
2482
2563
  function claudeTurnBoundaries(records) {
2483
2564
  return buildTurnsWithStarts(records).turnStartIndices;
2484
2565
  }
2566
+ var KEPT_TOOL_USE_RESULT_KEYS = /* @__PURE__ */ new Set(["bashEditDiff", "structuredPatch", "prompt", "summary", "description", "status"]);
2567
+ function visitStrings(holder, visit, depth) {
2568
+ let changed = false;
2569
+ for (const key of Object.keys(holder)) {
2570
+ if (KEPT_TOOL_USE_RESULT_KEYS.has(key)) continue;
2571
+ const value = holder[key];
2572
+ if (typeof value === "string") changed = visitField(holder, key, visit) || changed;
2573
+ else if (isPlainObject(value) && depth < 3) changed = visitStrings(value, visit, depth + 1) || changed;
2574
+ }
2575
+ return changed;
2576
+ }
2577
+ function projectClaudeResults(record3, visit) {
2578
+ if (record3.type !== "user") return false;
2579
+ const message = isPlainObject(record3.message) ? record3.message : null;
2580
+ let changed = false;
2581
+ if (message && Array.isArray(message.content)) {
2582
+ for (const block of message.content) {
2583
+ if (!isPlainObject(block) || block.type !== "tool_result") continue;
2584
+ if (typeof block.content === "string") changed = visitField(block, "content", visit) || changed;
2585
+ else if (Array.isArray(block.content)) {
2586
+ const joined = visitJoinedText(block.content, (item) => isPlainObject(item) && item.type === "text" && typeof item.text === "string" ? item.text : void 0, "\n", (item, text2) => ({ ...item, text: text2 }), visit);
2587
+ if (joined) {
2588
+ block.content = joined;
2589
+ changed = true;
2590
+ }
2591
+ for (const item of block.content) {
2592
+ if (isPlainObject(item) && item.type === "image" && isPlainObject(item.source)) changed = visitField(item.source, "data", visit, "image") || changed;
2593
+ }
2594
+ }
2595
+ }
2596
+ }
2597
+ const result = record3.toolUseResult;
2598
+ if (typeof result === "string") changed = visitField(record3, "toolUseResult", visit) || changed;
2599
+ else if (isPlainObject(result) && !result.agentId) {
2600
+ changed = visitStrings(result, visit, 0) || changed;
2601
+ }
2602
+ return changed;
2603
+ }
2485
2604
 
2486
2605
  // ../../server/agents/transcriptHead.ts
2487
2606
  import { readFile as readFile4, stat as stat3 } from "node:fs/promises";
@@ -3092,33 +3211,6 @@ function pluginSkillContext(workspacePath) {
3092
3211
  return current2 ? Promise.resolve().then(() => current2(workspacePath)).catch(() => "") : null;
3093
3212
  }
3094
3213
 
3095
- // ../../server/agents/runtimeTypes.ts
3096
- function resolvedApproval({ requestId, toolUseId, toolName }) {
3097
- return { requestId, toolUseId, toolName };
3098
- }
3099
- var AgentRuntimeError = class extends Error {
3100
- constructor(status, code, message, details) {
3101
- super(message);
3102
- this.status = status;
3103
- this.code = code;
3104
- this.details = details;
3105
- this.name = "AgentRuntimeError";
3106
- }
3107
- };
3108
- var NO_ACTIVITY = Object.freeze({ live: false, running: false, agentsRunning: false });
3109
- function reportSessionId(req, sessionId) {
3110
- try {
3111
- req.onSessionId?.(sessionId);
3112
- } catch (error2) {
3113
- console.error(`[agents] onSessionId failed for session ${sessionId}:`, error2);
3114
- }
3115
- }
3116
- function selectAvailableDecision(available, requested) {
3117
- if (available.includes(requested)) return requested;
3118
- if (requested === "allow_always" && available.includes("allow")) return "allow";
3119
- return null;
3120
- }
3121
-
3122
3214
  // ../../server/agents/acpRuntime.ts
3123
3215
  var descriptor = descriptorFor("acp");
3124
3216
  var sessions2 = /* @__PURE__ */ new Map();
@@ -3209,20 +3301,20 @@ async function connect() {
3209
3301
  if (!entry || generation !== currentGeneration || permissions.size >= 64) return { outcome: { outcome: "cancelled" } };
3210
3302
  const key = `${currentGeneration}:${JSON.stringify(requestId)}`;
3211
3303
  const decisions = params.options.flatMap((option) => option.kind === "allow_once" ? ["allow"] : option.kind === "allow_always" ? ["allow_always"] : ["deny"]);
3212
- return new Promise((resolve9) => {
3304
+ return new Promise((resolve10) => {
3213
3305
  const cancel = () => {
3214
3306
  const pending = permissions.get(key);
3215
3307
  if (!pending) return;
3216
3308
  clearTimeout(pending.timer);
3217
3309
  permissions.delete(key);
3218
- resolve9({ outcome: { outcome: "cancelled" } });
3310
+ resolve10({ outcome: { outcome: "cancelled" } });
3219
3311
  };
3220
3312
  const timer = setTimeout(cancel, 15 * 60 * 1e3);
3221
3313
  timer.unref();
3222
3314
  signal.addEventListener("abort", cancel, { once: true });
3223
3315
  permissions.set(key, { approval: { timestamp: Date.now(), sessionId: entry[0], requestId: key, toolUseId: params.toolCall.toolCallId, toolName: params.toolCall.title || "Tool", input: { value: params.toolCall.rawInput ?? {} }, availableDecisions: [...new Set(decisions)] }, options: params.options, resolve: (value) => {
3224
3316
  signal.removeEventListener("abort", cancel);
3225
- resolve9(value);
3317
+ resolve10(value);
3226
3318
  }, timer });
3227
3319
  });
3228
3320
  });
@@ -3264,6 +3356,7 @@ async function load(id, request) {
3264
3356
  return session;
3265
3357
  }
3266
3358
  async function send(id, request) {
3359
+ if (isSessionStarting(id)) return { delivery: "busy" };
3267
3360
  if (request.onlyWhileRunning) return { delivery: "busy" };
3268
3361
  if (sending.has(id)) return { delivery: "busy" };
3269
3362
  sending.add(id);
@@ -3342,6 +3435,7 @@ var acpRuntime = {
3342
3435
  kind: "acp",
3343
3436
  descriptor,
3344
3437
  async start(request) {
3438
+ request.signal?.throwIfAborted();
3345
3439
  const connected = await connect();
3346
3440
  const native = await bounded(connected.agent.request("session/new", { cwd: request.cwd, mcpServers: [] }, { cancellationSignal: AbortSignal.timeout(3e4) }), 3e4);
3347
3441
  const id = randomUUID();
@@ -3352,12 +3446,18 @@ var acpRuntime = {
3352
3446
  writeFileSync(path.filePath, JSON.stringify({ type: "acp_session", sessionId: id, nativeSessionId: native.sessionId, cwd: request.cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n", { mode: 384 });
3353
3447
  sessions2.set(id, { nativeId: native.sessionId, cwd: request.cwd, filePath: path.filePath, running: false, configOptions: native.configOptions ?? void 0 });
3354
3448
  reportSessionId(request, id);
3449
+ try {
3450
+ await beforeFirstTurn(request, { sessionId: id, cwd: request.cwd });
3451
+ } catch (error2) {
3452
+ sessions2.delete(id);
3453
+ throw error2;
3454
+ }
3355
3455
  if (request.message || request.images?.length) {
3356
3456
  const outcome = await send(id, request);
3357
3457
  void outcome.completion?.catch(() => {
3358
3458
  });
3359
3459
  }
3360
- return { sessionId: id, dirName, ...path, initialContent: readFileSync(path.filePath, "utf8") };
3460
+ return { sessionId: id, dirName, cwd: request.cwd, ...path, initialContent: readFileSync(path.filePath, "utf8") };
3361
3461
  },
3362
3462
  send,
3363
3463
  async interrupt(id) {
@@ -3525,6 +3625,164 @@ function linkAsyncAnswers(turns) {
3525
3625
  });
3526
3626
  }
3527
3627
 
3628
+ // ../../shared/session/composerContext.ts
3629
+ var EMPTY_DRAFT = Object.freeze({ v: 1, text: "", context: [], attachments: [] });
3630
+ var CONTEXT_LIMITS = Object.freeze({ items: 12, itemChars: 2e4, metadataChars: 2e3, totalChars: 1e5, noteChars: 2e3 });
3631
+ var ID_RE = /^ctx-[a-z0-9]{8,32}$/;
3632
+ function itemChars(item) {
3633
+ return item.kind === "browser" ? item.note.length : item.kind === "file" ? (item.text?.length ?? 0) + (item.comment?.length ?? 0) : item.text.length;
3634
+ }
3635
+ function itemRefusal(item) {
3636
+ if (!ID_RE.test(item.id)) return "This context has no valid id";
3637
+ if (itemChars(item) > CONTEXT_LIMITS.itemChars) return `This context is longer than ${CONTEXT_LIMITS.itemChars.toLocaleString("en-US")} characters`;
3638
+ if (item.kind === "browser" && item.note.length > CONTEXT_LIMITS.noteChars) return `An annotation note is limited to ${CONTEXT_LIMITS.noteChars.toLocaleString("en-US")} characters`;
3639
+ const metadata = attributes(item).filter(([key]) => key !== "comment");
3640
+ if (metadata.some(([, value]) => value.length > CONTEXT_LIMITS.metadataChars)) return `This context's ${metadata.find(([, value]) => value.length > CONTEXT_LIMITS.metadataChars)[0]} is longer than ${CONTEXT_LIMITS.metadataChars.toLocaleString("en-US")} characters`;
3641
+ return null;
3642
+ }
3643
+ function contextRefusal(context, item) {
3644
+ if (context.some((existing) => existing.id === item.id)) return null;
3645
+ if (context.length >= CONTEXT_LIMITS.items) return `A message holds at most ${CONTEXT_LIMITS.items} context items`;
3646
+ const refusal = itemRefusal(item);
3647
+ if (refusal) return refusal;
3648
+ const total = [...context, item].reduce((sum, existing) => sum + renderContextItem(existing).length, 0);
3649
+ if (total > CONTEXT_LIMITS.totalChars) return `The message's context would exceed ${CONTEXT_LIMITS.totalChars.toLocaleString("en-US")} characters`;
3650
+ return null;
3651
+ }
3652
+ function validateContext(context) {
3653
+ const accepted = [];
3654
+ for (const item of context) {
3655
+ if (accepted.some((existing) => existing.id === item.id)) return "The same context appears twice";
3656
+ const refusal = contextRefusal(accepted, item);
3657
+ if (refusal) return refusal;
3658
+ accepted.push(item);
3659
+ }
3660
+ return null;
3661
+ }
3662
+ function escapeAttribute(value) {
3663
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/\r/g, "&#13;").replace(/\n/g, "&#10;");
3664
+ }
3665
+ function unescapeAttribute(value) {
3666
+ return value.replace(/&#10;/g, "\n").replace(/&#13;/g, "\r").replace(/&lt;/g, "<").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
3667
+ }
3668
+ var CLOSE = "</context>";
3669
+ function escapeBody(text2) {
3670
+ return text2.split("\n").map((line) => /^ *<\/context>/.test(line) ? ` ${line}` : line).join("\n");
3671
+ }
3672
+ function unescapeBody(text2) {
3673
+ return text2.split("\n").map((line) => /^ +<\/context>/.test(line) ? line.slice(1) : line).join("\n");
3674
+ }
3675
+ function attributes(item) {
3676
+ switch (item.kind) {
3677
+ case "file":
3678
+ return [
3679
+ ["path", item.path],
3680
+ ...item.startLine !== void 0 ? [["lines", item.endLine !== void 0 && item.endLine !== item.startLine ? `${item.startLine}-${item.endLine}` : String(item.startLine)]] : [],
3681
+ ...item.comment ? [["comment", item.comment]] : []
3682
+ ];
3683
+ case "terminal":
3684
+ return item.label ? [["label", item.label]] : [];
3685
+ case "quote":
3686
+ return [["session", item.source.sessionId], ["turn", item.source.turnId], ["dir", item.source.dirName], ["file", item.source.fileName]];
3687
+ case "browser":
3688
+ return [
3689
+ ["url", item.url],
3690
+ ["title", item.title],
3691
+ ["viewport", `${item.viewport.width}x${item.viewport.height}`],
3692
+ ["region", `${item.region.x},${item.region.y},${item.region.width},${item.region.height}`],
3693
+ ["captured", item.capturedAt],
3694
+ ["browser", item.browser],
3695
+ ...item.tab ? [["tab", item.tab]] : [],
3696
+ ["image", String(item.image)]
3697
+ ];
3698
+ }
3699
+ }
3700
+ function body(item) {
3701
+ return item.kind === "browser" ? item.note : item.kind === "file" ? item.text ?? "" : item.text;
3702
+ }
3703
+ function renderContextItem(item) {
3704
+ const attrs = [["id", item.id], ["kind", item.kind], ...attributes(item)].map(([key, value]) => `${key}="${escapeAttribute(value)}"`).join(" ");
3705
+ return `<context ${attrs}>
3706
+ ${escapeBody(body(item))}
3707
+ ${CLOSE}`;
3708
+ }
3709
+ var OPEN_RE = /^<context ((?:[a-z]+="[^"]*"\s*)+)>\n/;
3710
+ function parseAttributes(source) {
3711
+ const found = /* @__PURE__ */ new Map();
3712
+ for (const match of source.matchAll(/([a-z]+)="([^"]*)"/g)) found.set(match[1], unescapeAttribute(match[2]));
3713
+ return found;
3714
+ }
3715
+ function positive(value) {
3716
+ if (value === void 0 || !/^\d+$/.test(value)) return void 0;
3717
+ return Number(value);
3718
+ }
3719
+ function itemFrom(attrs, text2) {
3720
+ const id = attrs.get("id");
3721
+ if (!id || !ID_RE.test(id)) return null;
3722
+ switch (attrs.get("kind")) {
3723
+ case "file": {
3724
+ const path = attrs.get("path");
3725
+ if (!path) return null;
3726
+ const [start, end] = (attrs.get("lines") ?? "").split("-");
3727
+ const startLine = positive(start);
3728
+ const endLine = positive(end) ?? startLine;
3729
+ const comment = attrs.get("comment");
3730
+ return { id, kind: "file", path, ...startLine !== void 0 ? { startLine, endLine } : {}, ...text2 ? { text: text2 } : {}, ...comment ? { comment } : {} };
3731
+ }
3732
+ case "terminal": {
3733
+ const label = attrs.get("label");
3734
+ return { id, kind: "terminal", text: text2, ...label ? { label } : {} };
3735
+ }
3736
+ case "quote": {
3737
+ const [sessionId, turnId, dirName, fileName] = ["session", "turn", "dir", "file"].map((key) => attrs.get(key));
3738
+ if (!sessionId || !turnId || !dirName || !fileName) return null;
3739
+ return { id, kind: "quote", text: text2, source: { sessionId, turnId, dirName, fileName } };
3740
+ }
3741
+ case "browser": {
3742
+ const viewport = (attrs.get("viewport") ?? "").split("x").map(Number);
3743
+ const region = (attrs.get("region") ?? "").split(",").map(Number);
3744
+ const image = positive(attrs.get("image"));
3745
+ if (viewport.length !== 2 || region.length !== 4 || ![...viewport, ...region].every(Number.isFinite) || !image) return null;
3746
+ const tab = attrs.get("tab");
3747
+ return {
3748
+ id,
3749
+ kind: "browser",
3750
+ note: text2,
3751
+ url: attrs.get("url") ?? "",
3752
+ title: attrs.get("title") ?? "",
3753
+ viewport: { width: viewport[0], height: viewport[1] },
3754
+ region: { x: region[0], y: region[1], width: region[2], height: region[3] },
3755
+ capturedAt: attrs.get("captured") ?? "",
3756
+ browser: attrs.get("browser") ?? "",
3757
+ ...tab ? { tab } : {},
3758
+ image
3759
+ };
3760
+ }
3761
+ default:
3762
+ return null;
3763
+ }
3764
+ }
3765
+ function parseComposerMessage(message) {
3766
+ const context = [];
3767
+ let rest = message;
3768
+ for (; ; ) {
3769
+ const open6 = OPEN_RE.exec(rest);
3770
+ if (!open6) break;
3771
+ const end = rest.indexOf(`
3772
+ ${CLOSE}`, open6[0].length - 1);
3773
+ if (end < 0) break;
3774
+ const item = itemFrom(parseAttributes(open6[1]), unescapeBody(rest.slice(open6[0].length, end)));
3775
+ if (!item) break;
3776
+ context.push(item);
3777
+ rest = rest.slice(end + CLOSE.length + 1).replace(/^\n{1,2}/, "");
3778
+ }
3779
+ if (context.length === 0 || validateContext(context)) return null;
3780
+ return { context, text: rest.trim() };
3781
+ }
3782
+ function composerPromptText(message) {
3783
+ return parseComposerMessage(message)?.text ?? message;
3784
+ }
3785
+
3528
3786
  // ../../shared/session/codex-exec.ts
3529
3787
  var JS_SIMPLE_ESCAPES = {
3530
3788
  "0": "\0",
@@ -3646,11 +3904,11 @@ function parseApplyPatch(patchText, callId, timestamp, cwd = "") {
3646
3904
  for (let i = 0; i < sections.length; i++) {
3647
3905
  const section = sections[i];
3648
3906
  const endIdx = i + 1 < sections.length ? sections[i + 1].headerIdx : patchText.length;
3649
- const body = patchText.slice(section.startIdx, endIdx);
3650
- const movedTo = body.match(/^\*\*\*\s+Move to:\s*(.+)$/m)?.[1]?.trim();
3907
+ const body2 = patchText.slice(section.startIdx, endIdx);
3908
+ const movedTo = body2.match(/^\*\*\*\s+Move to:\s*(.+)$/m)?.[1]?.trim();
3651
3909
  const sourcePath = resolveCodexPatchPath(section.filePath, cwd);
3652
3910
  const filePath = resolveCodexPatchPath(movedTo || section.filePath, cwd);
3653
- const hunkBodies = body.split(/^@@.*$/m).filter((s) => s.trim());
3911
+ const hunkBodies = body2.split(/^@@.*$/m).filter((s) => s.trim());
3654
3912
  const oldLines = [];
3655
3913
  const newLines = [];
3656
3914
  for (const hunk of hunkBodies) {
@@ -4255,7 +4513,7 @@ function extractMetadataFromRecords(records) {
4255
4513
  model ||= typeof record3.payload.model === "string" ? record3.payload.model : "";
4256
4514
  cwd ||= typeof record3.payload.cwd === "string" ? record3.payload.cwd : "";
4257
4515
  }
4258
- const prompt = readPrompt(record3);
4516
+ const prompt = composerPromptText(readPrompt(record3));
4259
4517
  if (!prompt) continue;
4260
4518
  if (!firstUserMessage) firstUserMessage = prompt;
4261
4519
  lastUserMessage = prompt;
@@ -5368,7 +5626,7 @@ function extractMetadata(events) {
5368
5626
  if (event.type !== "user.message") continue;
5369
5627
  metadata.turnCount++;
5370
5628
  metadata.timestamp ||= timestamp;
5371
- const content = readString(data.content).trim();
5629
+ const content = composerPromptText(readString(data.content)).trim();
5372
5630
  if (!content) continue;
5373
5631
  metadata.firstUserMessage ||= content;
5374
5632
  metadata.lastUserMessage = content;
@@ -6131,6 +6389,24 @@ function copilotTurnBoundaries(records) {
6131
6389
  }
6132
6390
  return boundaries;
6133
6391
  }
6392
+ function projectCopilotResults(record3, visit) {
6393
+ if (record3.type !== "tool.execution_complete" || !isObject4(record3.data)) return false;
6394
+ const data = record3.data;
6395
+ if (typeof data.result === "string") return visitField(data, "result", visit);
6396
+ if (!isObject4(data.result)) return false;
6397
+ const result = data.result;
6398
+ let changed = visitField(result, "detailedContent", visit);
6399
+ changed = visitField(result, "content", visit) || changed;
6400
+ if (Array.isArray(result.contents)) {
6401
+ const textOf = (part) => (typeof part === "string" ? part : isObject4(part) ? readString(part.text, part.content, part.output) : "") || void 0;
6402
+ const joined = visitJoinedText(result.contents, textOf, "\n", (part, text2) => typeof part === "string" ? text2 : { ...part, text: text2 }, visit);
6403
+ if (joined) {
6404
+ result.contents = joined;
6405
+ changed = true;
6406
+ }
6407
+ }
6408
+ return changed;
6409
+ }
6134
6410
 
6135
6411
  // ../../server/agents/copilotMetadata.ts
6136
6412
  function parseSimpleYamlValue(raw) {
@@ -6286,7 +6562,22 @@ function openDatabase(path) {
6286
6562
  return new Database(path);
6287
6563
  }
6288
6564
 
6565
+ // ../../shared/contracts/triggers.ts
6566
+ var TERMINAL_OCCURRENCE_STATES = ["completed", "failed", "cancelled", "skipped"];
6567
+ function occurrenceCommandId(occurrenceId) {
6568
+ return `trg-${occurrenceId}`;
6569
+ }
6570
+ function occurrenceCreationId(occurrenceId) {
6571
+ return `trgc-${occurrenceId}`;
6572
+ }
6573
+ function isTerminalOccurrence(state) {
6574
+ return TERMINAL_OCCURRENCE_STATES.includes(state);
6575
+ }
6576
+
6289
6577
  // ../../server/orchestration/store.ts
6578
+ var OCCURRENCE_REQUEST_LIMIT = 256 * 1024;
6579
+ var OCCURRENCE_INPUTS_LIMIT = 1024 * 1024;
6580
+ var ACTIVE_OCCURRENCE = "('admitting','creating')";
6290
6581
  var OrchestrationError = class extends Error {
6291
6582
  constructor(status, message) {
6292
6583
  super(message);
@@ -6323,7 +6614,7 @@ var OrchestrationStore = class {
6323
6614
  try {
6324
6615
  this.db.exec("PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;");
6325
6616
  const version = Number(this.db.prepare("PRAGMA user_version").get().user_version);
6326
- if (version > 4) throw new Error("Orchestration database was created by a newer Cogpit; upgrade before opening it");
6617
+ if (version > 5) throw new Error("Orchestration database was created by a newer Cogpit; upgrade before opening it");
6327
6618
  this.transaction(() => {
6328
6619
  this.db.exec(`
6329
6620
  CREATE TABLE IF NOT EXISTS conversations(id TEXT PRIMARY KEY, native_key TEXT UNIQUE NOT NULL, data TEXT NOT NULL);
@@ -6345,6 +6636,15 @@ var OrchestrationStore = class {
6345
6636
  CREATE TABLE IF NOT EXISTS crew_chat_listeners(root_id TEXT NOT NULL,session_id TEXT NOT NULL,listening INTEGER NOT NULL DEFAULT 1,cursor INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(root_id,session_id));
6346
6637
  CREATE TABLE IF NOT EXISTS crew_chat_deliveries(sequence INTEGER NOT NULL REFERENCES crew_chat_messages(sequence) ON DELETE CASCADE,session_id TEXT NOT NULL,scope TEXT NOT NULL,kind TEXT NOT NULL,state TEXT NOT NULL DEFAULT 'pending',error TEXT,PRIMARY KEY(sequence,session_id));
6347
6638
  CREATE INDEX IF NOT EXISTS crew_chat_pending ON crew_chat_deliveries(scope,state,sequence);
6639
+ CREATE TABLE IF NOT EXISTS execution_grants(id TEXT PRIMARY KEY,scope TEXT NOT NULL,revision INTEGER NOT NULL,data TEXT NOT NULL,revoked_at INTEGER);
6640
+ CREATE TABLE IF NOT EXISTS triggers(scope TEXT NOT NULL,id TEXT NOT NULL,kind TEXT NOT NULL,revision INTEGER NOT NULL,state TEXT NOT NULL,next_due_at INTEGER,data TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,PRIMARY KEY(scope,id));
6641
+ CREATE INDEX IF NOT EXISTS trigger_due ON triggers(kind,state,next_due_at);
6642
+ CREATE TABLE IF NOT EXISTS trigger_secrets(scope TEXT NOT NULL,trigger_id TEXT NOT NULL,id TEXT NOT NULL,data TEXT NOT NULL,created_at INTEGER NOT NULL,retired_at INTEGER,PRIMARY KEY(scope,trigger_id,id));
6643
+ CREATE TABLE IF NOT EXISTS trigger_occurrences(scope TEXT NOT NULL,id TEXT NOT NULL,trigger_id TEXT NOT NULL,kind TEXT NOT NULL,state TEXT NOT NULL,due_at INTEGER NOT NULL,attempt INTEGER NOT NULL DEFAULT 0,epoch INTEGER NOT NULL DEFAULT 0,fence_conversation TEXT,fence_position INTEGER,data TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,PRIMARY KEY(scope,id));
6644
+ CREATE INDEX IF NOT EXISTS occurrence_due ON trigger_occurrences(state,due_at);
6645
+ CREATE INDEX IF NOT EXISTS occurrence_trigger ON trigger_occurrences(scope,trigger_id,created_at);
6646
+ CREATE INDEX IF NOT EXISTS occurrence_fence ON trigger_occurrences(fence_conversation,state);
6647
+ CREATE TABLE IF NOT EXISTS trigger_keys(scope TEXT NOT NULL,trigger_id TEXT NOT NULL,key TEXT NOT NULL,occurrence_id TEXT NOT NULL,fingerprint TEXT NOT NULL,created_at INTEGER NOT NULL,PRIMARY KEY(scope,trigger_id,key));
6348
6648
  `);
6349
6649
  if (!this.db.prepare("PRAGMA table_info(events)").all().some((row) => row.name === "scope")) this.db.exec("ALTER TABLE events ADD COLUMN scope TEXT");
6350
6650
  if (!this.db.prepare("PRAGMA table_info(operations)").all().some((row) => row.name === "updated_at")) this.db.exec("ALTER TABLE operations ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0");
@@ -6353,7 +6653,7 @@ var OrchestrationStore = class {
6353
6653
  this.db.exec("ALTER TABLE transitions ADD COLUMN scope TEXT");
6354
6654
  this.db.exec("UPDATE transitions SET scope=(SELECT scope FROM operations WHERE id=transitions.operation_id) WHERE (SELECT COUNT(*) FROM operations WHERE id=transitions.operation_id)=1");
6355
6655
  }
6356
- this.db.exec("PRAGMA user_version=4");
6656
+ this.db.exec("PRAGMA user_version=5");
6357
6657
  });
6358
6658
  this.protectFiles();
6359
6659
  } catch (error2) {
@@ -6363,7 +6663,9 @@ var OrchestrationStore = class {
6363
6663
  }
6364
6664
  db;
6365
6665
  listeners = /* @__PURE__ */ new Set();
6666
+ triggerListeners = /* @__PURE__ */ new Set();
6366
6667
  pendingEvents = null;
6668
+ pendingTriggerEvents = [];
6367
6669
  protectFiles() {
6368
6670
  if (this.path === ":memory:" || process.platform === "win32") return;
6369
6671
  for (const file of [this.path, `${this.path}-wal`, `${this.path}-shm`]) if (existsSync(file)) chmodSync2(file, 384);
@@ -6380,6 +6682,7 @@ var OrchestrationStore = class {
6380
6682
  events = this.pendingEvents;
6381
6683
  } catch (error2) {
6382
6684
  this.db.exec("ROLLBACK");
6685
+ this.pendingTriggerEvents = [];
6383
6686
  throw error2;
6384
6687
  } finally {
6385
6688
  this.pendingEvents = null;
@@ -6391,10 +6694,17 @@ var OrchestrationStore = class {
6391
6694
  } catch {
6392
6695
  }
6393
6696
  }
6697
+ for (const change of this.pendingTriggerEvents.splice(0)) for (const listener of this.triggerListeners) {
6698
+ try {
6699
+ listener(change);
6700
+ } catch {
6701
+ }
6702
+ }
6394
6703
  return result;
6395
6704
  }
6396
6705
  close() {
6397
6706
  this.listeners.clear();
6707
+ this.triggerListeners.clear();
6398
6708
  this.db.close();
6399
6709
  }
6400
6710
  chatListener(rootId, sessionId) {
@@ -6560,6 +6870,7 @@ var OrchestrationStore = class {
6560
6870
  const accepted = { id, nativeMessageId, conversationId, sessionId: conversation.binding.sessionId, bindingRevision: conversation.revision, state: "queued", intent, message: typeof payload.message === "string" ? payload.message : void 0, questionId: typeof payload.questionId === "string" ? payload.questionId : void 0, createdAt: this.now(), updatedAt: this.now(), position, replaces };
6561
6871
  this.db.prepare("INSERT INTO commands(scope,id,conversation_id,fingerprint,payload,receipt,state,position,updated_at) VALUES(?,?,?,?,?,?,?,?,?)").run(scope, id, conversationId, hash, JSON.stringify(payload), JSON.stringify(accepted), accepted.state, position, accepted.updatedAt);
6562
6872
  this.event(conversationId, "command", accepted, scope);
6873
+ if (!payload.trigger && payload.notification !== true) this.cancelFenced(conversationId, "Someone messaged the session after this was scheduled");
6563
6874
  return accepted;
6564
6875
  });
6565
6876
  }
@@ -6591,17 +6902,23 @@ var OrchestrationStore = class {
6591
6902
  for (const command of this.pending()) {
6592
6903
  if (["delivered", "unknown"].includes(command.receipt.state) && settlesOnDelivery(command.receipt.delivery, command.payload)) this.update(command, "completed", { error: void 0 });
6593
6904
  else if (command.receipt.state === "dispatching" || command.receipt.state === "delivered") this.update(command, "unknown", { error: "Host stopped during delivery. Inspect native history before resolving or resending." });
6594
- else if (command.receipt.state === "queued") this.update(command, "held", { error: "Host restarted; resume this queue after reviewing it." });
6905
+ else if (command.receipt.state === "queued" && !command.payload.executionGrant) this.update(command, "held", { error: "Host restarted; resume this queue after reviewing it." });
6595
6906
  }
6907
+ this.reconcileOccurrences(epoch);
6596
6908
  return { owner, epoch };
6597
6909
  });
6598
6910
  }
6911
+ /** Gives the lease up for good: nothing the released owner still has in flight can write under it. */
6599
6912
  release(lease) {
6600
- this.db.prepare("UPDATE dispatcher SET expires_at=0 WHERE id=1 AND owner=? AND epoch=?").run(lease.owner, lease.epoch);
6913
+ this.db.prepare("UPDATE dispatcher SET owner='',expires_at=0 WHERE id=1 AND owner=? AND epoch=?").run(lease.owner, lease.epoch);
6601
6914
  }
6602
6915
  owns(lease) {
6603
6916
  return Boolean(this.db.prepare("SELECT 1 FROM dispatcher WHERE id=1 AND owner=? AND epoch=?").get(lease.owner, lease.epoch));
6604
6917
  }
6918
+ /** Whether `lease` is still the one held: released and taken-over leases are not. */
6919
+ ownsLease(lease) {
6920
+ return this.owns(lease);
6921
+ }
6605
6922
  currentAttempt(command, lease) {
6606
6923
  const current2 = this.command(command.scope, command.receipt.id);
6607
6924
  return this.owns(lease) && Boolean(current2 && current2.attempt === command.attempt && current2.epoch === lease.epoch && ["dispatching", "delivered"].includes(current2.receipt.state));
@@ -6676,6 +6993,7 @@ var OrchestrationStore = class {
6676
6993
  return this.transaction(() => {
6677
6994
  const waiting = this.db.prepare("SELECT scope,id FROM commands WHERE conversation_id=? AND state IN ('queued','held')").all(conversationId).map((row) => this.command(String(row.scope), String(row.id)));
6678
6995
  for (const command of waiting) this.update(command, "cancelled", { error: "The session was stopped" });
6996
+ this.cancelFenced(conversationId, "The session was stopped");
6679
6997
  return waiting.length;
6680
6998
  });
6681
6999
  }
@@ -6815,6 +7133,7 @@ var OrchestrationStore = class {
6815
7133
  this.db.prepare("DELETE FROM crew_chat_messages WHERE root_id=?").run(sessionId);
6816
7134
  this.db.prepare("DELETE FROM crew_chat_listeners WHERE root_id=? OR session_id=?").run(sessionId, sessionId);
6817
7135
  this.db.prepare("DELETE FROM crew_chat_deliveries WHERE session_id=?").run(sessionId);
7136
+ for (const row of this.db.prepare("SELECT scope,id FROM trigger_occurrences WHERE json_extract(data,'$.sessionId')=? AND state NOT IN ('completed','failed','cancelled','skipped')").all(sessionId)) this.settleOccurrence(String(row.scope), String(row.id), "cancelled", { reason: "The session was deleted" });
6818
7137
  this.db.prepare("DELETE FROM commands WHERE json_extract(receipt,'$.sessionId')=?").run(sessionId);
6819
7138
  for (const row of this.db.prepare("SELECT native_key,conversation_id,data FROM bindings").all()) {
6820
7139
  if (decode(row.data).sessionId !== sessionId) continue;
@@ -6855,6 +7174,10 @@ var OrchestrationStore = class {
6855
7174
  const task = decode(row.data);
6856
7175
  if (task.acknowledgedAt && task.acknowledgedAt < cutoff) this.db.prepare("UPDATE tasks SET data=? WHERE id=?").run(JSON.stringify({ ...task, result: void 0 }), String(row.id));
6857
7176
  }
7177
+ this.db.prepare("DELETE FROM trigger_occurrences WHERE updated_at<? AND state IN ('completed','failed','cancelled','skipped')").run(cutoff);
7178
+ this.db.prepare("DELETE FROM trigger_keys WHERE created_at<? AND occurrence_id NOT IN (SELECT id FROM trigger_occurrences)").run(cutoff);
7179
+ this.db.prepare("DELETE FROM trigger_secrets WHERE retired_at IS NOT NULL AND retired_at<?").run(cutoff);
7180
+ this.db.prepare("DELETE FROM execution_grants WHERE revoked_at IS NOT NULL AND revoked_at<?").run(cutoff);
6858
7181
  });
6859
7182
  }
6860
7183
  instances() {
@@ -6894,11 +7217,410 @@ var OrchestrationStore = class {
6894
7217
  return { fresh: importedResult === void 0, ...importedResult === void 0 ? {} : { result: importedResult } };
6895
7218
  });
6896
7219
  }
7220
+ /** Forgets a creation claim that provably started nothing, so the same identity can start it later. */
7221
+ abandonOperation(scope, id) {
7222
+ this.transaction(() => {
7223
+ this.db.prepare("DELETE FROM operations WHERE scope=? AND id=? AND result IS NULL").run(scope, id);
7224
+ });
7225
+ }
6897
7226
  finishOperation(scope, id, result) {
6898
7227
  this.transaction(() => {
6899
7228
  this.db.prepare("UPDATE operations SET result=?,updated_at=? WHERE scope=? AND id=? AND result IS NULL").run(JSON.stringify(result), this.now(), scope, id);
6900
7229
  });
6901
7230
  }
7231
+ // ── Unattended execution grants ──────────────────────────────────
7232
+ putGrant(grant) {
7233
+ return this.transaction(() => {
7234
+ this.db.prepare("INSERT INTO execution_grants(id,scope,revision,data,revoked_at) VALUES(?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET revision=excluded.revision,data=excluded.data,revoked_at=excluded.revoked_at").run(grant.id, grant.scope, grant.revision, JSON.stringify(grant), grant.revokedAt ?? null);
7235
+ return grant;
7236
+ });
7237
+ }
7238
+ grant(id) {
7239
+ const row = this.db.prepare("SELECT data FROM execution_grants WHERE id=?").get(id);
7240
+ return row ? decode(row.data) : null;
7241
+ }
7242
+ grants(scope) {
7243
+ return this.db.prepare("SELECT data FROM execution_grants WHERE scope=? ORDER BY id").all(scope).map((row) => decode(row.data));
7244
+ }
7245
+ /** Revokes a grant for good; work it admitted is held for reauthorization as soon as it is next claimed. */
7246
+ revokeGrant(id, reason) {
7247
+ return this.transaction(() => {
7248
+ const grant = this.grant(id);
7249
+ if (!grant || grant.revokedAt) return grant;
7250
+ return this.putGrant({ ...grant, revision: grant.revision + 1, revokedAt: this.now(), revokedReason: reason, updatedAt: this.now() });
7251
+ });
7252
+ }
7253
+ // ── Trigger definitions ──────────────────────────────────────────
7254
+ /** Whether any definition is active or any occurrence is still open, so the scheduler must run. */
7255
+ hasTriggerWork() {
7256
+ return Boolean(this.db.prepare("SELECT 1 FROM triggers WHERE state='active' UNION ALL SELECT 1 FROM trigger_occurrences WHERE state NOT IN ('completed','failed','cancelled','skipped') LIMIT 1").get());
7257
+ }
7258
+ subscribeTriggers(listener) {
7259
+ this.triggerListeners.add(listener);
7260
+ return () => this.triggerListeners.delete(listener);
7261
+ }
7262
+ triggerEvent(type, scope, triggerId, id) {
7263
+ this.pendingTriggerEvents.push({ type, scope, triggerId, id });
7264
+ }
7265
+ triggerRecord(row) {
7266
+ const data = decode(row.data);
7267
+ return { id: String(row.id), scope: String(row.scope), kind: String(row.kind), revision: Number(row.revision), state: row.state, grant: data.grant, nextDueAt: row.next_due_at === null ? null : Number(row.next_due_at), data: data.data, createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) };
7268
+ }
7269
+ trigger(scope, id) {
7270
+ const row = this.db.prepare("SELECT * FROM triggers WHERE scope=? AND id=?").get(scope, id);
7271
+ return row ? this.triggerRecord(row) : null;
7272
+ }
7273
+ triggers(scope, kind2) {
7274
+ const rows = scope === null ? this.db.prepare("SELECT * FROM triggers WHERE kind=? ORDER BY created_at,id").all(kind2) : this.db.prepare("SELECT * FROM triggers WHERE scope=? AND kind=? ORDER BY created_at,id").all(scope, kind2);
7275
+ return rows.map((row) => this.triggerRecord(row));
7276
+ }
7277
+ /** Active definitions of `kind` whose consumer asked to be woken by now. */
7278
+ dueTriggers(kind2) {
7279
+ return this.db.prepare("SELECT * FROM triggers WHERE kind=? AND state='active' AND next_due_at IS NOT NULL AND next_due_at<=? ORDER BY next_due_at").all(kind2, this.now()).map((row) => this.triggerRecord(row));
7280
+ }
7281
+ /** Creates a definition, or replaces it when `expectedRevision` is the stored one. */
7282
+ putTrigger(scope, input, expectedRevision) {
7283
+ if (!/^[a-zA-Z0-9_-]{8,128}$/.test(input.id)) throw new OrchestrationError(400, "Trigger ids must contain 8\u2013128 letters, digits, underscores or hyphens");
7284
+ if (JSON.stringify(input.data).length > OCCURRENCE_REQUEST_LIMIT) throw new OrchestrationError(413, "Trigger definition is too large");
7285
+ return this.transaction(() => {
7286
+ const old = this.trigger(scope, input.id);
7287
+ if (old && old.kind !== input.kind) throw new OrchestrationError(409, "Trigger id already belongs to another kind");
7288
+ if (old && expectedRevision === void 0) throw new OrchestrationError(409, "Trigger already exists");
7289
+ if (expectedRevision !== void 0 && old?.revision !== expectedRevision) throw new OrchestrationError(409, "Trigger changed; refresh before saving");
7290
+ const revision = (old?.revision ?? 0) + 1;
7291
+ const data = JSON.stringify({ grant: input.grant, data: input.data });
7292
+ this.db.prepare("INSERT INTO triggers(scope,id,kind,revision,state,next_due_at,data,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(scope,id) DO UPDATE SET revision=excluded.revision,state=excluded.state,next_due_at=excluded.next_due_at,data=excluded.data,updated_at=excluded.updated_at").run(scope, input.id, input.kind, revision, input.state, input.nextDueAt, data, old?.createdAt ?? this.now(), this.now());
7293
+ this.triggerEvent("trigger", scope, input.id, input.id);
7294
+ return this.trigger(scope, input.id);
7295
+ });
7296
+ }
7297
+ /** Moves a definition's next wake-up without counting as an edit, unless it changed since `revision`. */
7298
+ setTriggerDue(scope, id, revision, nextDueAt) {
7299
+ return this.transaction(() => {
7300
+ const changed = Number(this.db.prepare("UPDATE triggers SET next_due_at=? WHERE scope=? AND id=? AND revision=?").run(nextDueAt, scope, id, revision).changes) > 0;
7301
+ if (changed) this.triggerEvent("trigger", scope, id, id);
7302
+ return changed;
7303
+ });
7304
+ }
7305
+ /** Deletes a definition with its secrets, cancels what it still has waiting, and revokes a grant nothing else uses. */
7306
+ deleteTrigger(scope, id) {
7307
+ this.transaction(() => {
7308
+ const trigger = this.trigger(scope, id);
7309
+ if (!trigger) return;
7310
+ for (const row of this.db.prepare(`SELECT id FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND state IN ('scheduled','held')`).all(scope, id)) this.cancelOccurrence(scope, String(row.id), "The task was deleted");
7311
+ this.db.prepare("DELETE FROM trigger_secrets WHERE scope=? AND trigger_id=?").run(scope, id);
7312
+ this.db.prepare("DELETE FROM triggers WHERE scope=? AND id=?").run(scope, id);
7313
+ const grantId = trigger.grant?.grantId;
7314
+ if (grantId && !this.db.prepare("SELECT 1 FROM triggers WHERE json_extract(data,'$.grant.grantId')=? LIMIT 1").get(grantId)) this.revokeGrant(grantId, "Its task was deleted");
7315
+ this.triggerEvent("trigger", scope, id, id);
7316
+ });
7317
+ }
7318
+ /** Key material a consumer verifies deliveries with. Never part of a definition, a list or a log. */
7319
+ putTriggerSecret(scope, triggerId, id, data) {
7320
+ this.transaction(() => {
7321
+ this.db.prepare("INSERT INTO trigger_secrets(scope,trigger_id,id,data,created_at) VALUES(?,?,?,?,?)").run(scope, triggerId, id, JSON.stringify(data), this.now());
7322
+ });
7323
+ }
7324
+ /** Retires every secret but `keep`, which stay valid for `graceMs` so a rotation does not drop deliveries already in flight. */
7325
+ retireTriggerSecrets(scope, triggerId, keep, graceMs) {
7326
+ this.transaction(() => {
7327
+ this.db.prepare("UPDATE trigger_secrets SET retired_at=? WHERE scope=? AND trigger_id=? AND id<>? AND retired_at IS NULL").run(this.now() + graceMs, scope, triggerId, keep);
7328
+ });
7329
+ }
7330
+ triggerSecrets(scope, triggerId) {
7331
+ return this.db.prepare("SELECT id,data,created_at FROM trigger_secrets WHERE scope=? AND trigger_id=? AND (retired_at IS NULL OR retired_at>?) ORDER BY created_at DESC").all(scope, triggerId, this.now()).map((row) => ({ id: String(row.id), data: decode(row.data), createdAt: Number(row.created_at) }));
7332
+ }
7333
+ // ── Trigger occurrences ──────────────────────────────────────────
7334
+ occurrenceRow(row) {
7335
+ const data = decode(row.data);
7336
+ const command = this.command(String(row.scope), data.commandId)?.receipt;
7337
+ return { ...data, id: String(row.id), scope: String(row.scope), triggerId: String(row.trigger_id), kind: String(row.kind), state: row.state, dueAt: Number(row.due_at), attempt: Number(row.attempt), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at), ...command ? { command } : {} };
7338
+ }
7339
+ occurrence(scope, id) {
7340
+ const row = this.db.prepare("SELECT * FROM trigger_occurrences WHERE scope=? AND id=?").get(scope, id);
7341
+ return row ? this.occurrenceRow(row) : null;
7342
+ }
7343
+ /** A trigger's occurrences, newest first. */
7344
+ occurrences(scope, triggerId, options = {}) {
7345
+ const limit = Math.min(200, Math.max(1, options.limit ?? 50));
7346
+ return this.db.prepare("SELECT * FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND created_at<? ORDER BY created_at DESC,id DESC LIMIT ?").all(scope, triggerId, options.before ?? Number.MAX_SAFE_INTEGER, limit).map((row) => this.occurrenceRow(row));
7347
+ }
7348
+ /** Every session a trigger's occurrences started, except `exceptId`'s. */
7349
+ triggerSessionIds(scope, triggerId, exceptId) {
7350
+ return this.db.prepare("SELECT DISTINCT json_extract(data,'$.sessionId') AS session FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND id<>? AND json_extract(data,'$.sessionId') IS NOT NULL").all(scope, triggerId, exceptId ?? "").map((row) => String(row.session));
7351
+ }
7352
+ /** Occurrences of a trigger not yet settled: waiting, running, held or awaiting reconciliation. */
7353
+ openOccurrences(scope, triggerId) {
7354
+ return this.db.prepare("SELECT * FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND state NOT IN ('completed','failed','cancelled','skipped') ORDER BY created_at").all(scope, triggerId).map((row) => this.occurrenceRow(row));
7355
+ }
7356
+ writeOccurrence(occurrence, patch, state = occurrence.state, dueAt = occurrence.dueAt) {
7357
+ const old = decode(this.db.prepare("SELECT data FROM trigger_occurrences WHERE scope=? AND id=?").get(occurrence.scope, occurrence.id).data);
7358
+ const terminal = isTerminalOccurrence(state);
7359
+ const data = { ...old, ...patch, ...terminal && !old.finishedAt ? { finishedAt: this.now() } : {} };
7360
+ this.db.prepare("UPDATE trigger_occurrences SET state=?,due_at=?,data=?,updated_at=? WHERE scope=? AND id=?").run(state, dueAt, JSON.stringify(data), this.now(), occurrence.scope, occurrence.id);
7361
+ this.triggerEvent("occurrence", occurrence.scope, occurrence.triggerId, occurrence.id);
7362
+ }
7363
+ /**
7364
+ * Records one due event. A key already recorded returns the occurrence it
7365
+ * made (a replayed delivery or a slot computed twice); different content
7366
+ * under the same key is refused. A burst can fold into the newest
7367
+ * occurrence still waiting instead of starting another run.
7368
+ */
7369
+ recordOccurrence(scope, input) {
7370
+ if (!input.key || input.key.length > 256) throw new OrchestrationError(400, "Occurrence keys must contain 1\u2013256 characters");
7371
+ if (JSON.stringify(input.request).length > OCCURRENCE_REQUEST_LIMIT) throw new OrchestrationError(413, "Occurrence request is too large");
7372
+ const hash = fingerprint({ request: input.request, data: input.input?.data ?? null, action: input.action });
7373
+ return this.transaction(() => {
7374
+ const seen = this.db.prepare("SELECT occurrence_id,fingerprint FROM trigger_keys WHERE scope=? AND trigger_id=? AND key=?").get(scope, input.triggerId, input.key);
7375
+ if (seen) {
7376
+ if (seen.fingerprint !== hash) throw new OrchestrationError(409, "This delivery id was already recorded with different content");
7377
+ const existing = this.occurrence(scope, String(seen.occurrence_id));
7378
+ if (existing) return { occurrence: existing, outcome: "duplicate" };
7379
+ throw new OrchestrationError(409, "This delivery id was already recorded");
7380
+ }
7381
+ const received = input.input ? { key: input.key, receivedAt: this.now(), data: input.input.data } : void 0;
7382
+ if (input.coalesce && received) {
7383
+ const row = this.db.prepare("SELECT * FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND state='scheduled' AND created_at>=? AND json_extract(data,'$.coalesceGroup')=? ORDER BY created_at DESC LIMIT 1").get(scope, input.triggerId, this.now() - input.coalesce.windowMs, input.coalesce.group);
7384
+ const target = row && this.occurrenceRow(row);
7385
+ if (target && target.inputs.length < input.coalesce.maxInputs && JSON.stringify([...target.inputs, received]).length <= OCCURRENCE_INPUTS_LIMIT) {
7386
+ this.writeOccurrence(target, { inputs: [...target.inputs, received] });
7387
+ this.db.prepare("INSERT INTO trigger_keys VALUES(?,?,?,?,?,?)").run(scope, input.triggerId, input.key, target.id, hash, this.now());
7388
+ return { occurrence: this.occurrence(scope, target.id), outcome: "coalesced" };
7389
+ }
7390
+ }
7391
+ if (received && JSON.stringify(received).length > OCCURRENCE_INPUTS_LIMIT) throw new OrchestrationError(413, "Occurrence input is too large");
7392
+ const waiting = Number(this.db.prepare("SELECT COUNT(*) AS n FROM trigger_occurrences WHERE scope=? AND trigger_id=? AND state IN ('scheduled','held')").get(scope, input.triggerId).n);
7393
+ if (waiting >= (input.backlog ?? 100)) throw new OrchestrationError(429, "Too many runs of this task are already waiting");
7394
+ const id = `occ-${fingerprint([scope, input.triggerId, input.key]).slice(0, 32)}`;
7395
+ const fencePosition = input.fence ? Number(this.db.prepare("SELECT COALESCE(MAX(position),0) AS n FROM commands WHERE conversation_id=?").get(input.fence.conversationId).n) : null;
7396
+ const data = { key: input.key, action: input.action, grant: input.grant, request: input.request, inputs: received ? [received] : [], commandId: occurrenceCommandId(id), creationId: occurrenceCreationId(id), ...input.guarded ? { guarded: true } : {}, ...input.coalesce ? { coalesceGroup: input.coalesce.group } : {} };
7397
+ this.db.prepare("INSERT INTO trigger_occurrences(scope,id,trigger_id,kind,state,due_at,fence_conversation,fence_position,data,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(scope, id, input.triggerId, input.kind, "scheduled", input.dueAt, input.fence?.conversationId ?? null, fencePosition, JSON.stringify(data), this.now(), this.now());
7398
+ this.db.prepare("INSERT INTO trigger_keys VALUES(?,?,?,?,?,?)").run(scope, input.triggerId, input.key, id, hash, this.now());
7399
+ this.triggerEvent("occurrence", scope, input.triggerId, id);
7400
+ return { occurrence: this.occurrence(scope, id), outcome: "created" };
7401
+ });
7402
+ }
7403
+ /**
7404
+ * Claims due occurrences of `kinds` for this lease, one at a time per
7405
+ * trigger: a trigger with an occurrence still being admitted or created
7406
+ * waits for it.
7407
+ */
7408
+ claimOccurrences(lease, kinds, limit = 20) {
7409
+ if (!kinds.length) return [];
7410
+ return this.transaction(() => {
7411
+ if (!this.owns(lease)) return [];
7412
+ const rows = this.db.prepare(`SELECT * FROM trigger_occurrences o WHERE state='scheduled' AND due_at<=? AND kind IN (${kinds.map(() => "?").join(",")}) AND NOT EXISTS (SELECT 1 FROM trigger_occurrences a WHERE a.scope=o.scope AND a.trigger_id=o.trigger_id AND a.state IN ${ACTIVE_OCCURRENCE}) ORDER BY due_at,created_at LIMIT ?`).all(this.now(), ...kinds, limit);
7413
+ const claimed = [];
7414
+ const triggers = /* @__PURE__ */ new Set();
7415
+ for (const row of rows) {
7416
+ const key = JSON.stringify([row.scope, row.trigger_id]);
7417
+ if (triggers.has(key)) continue;
7418
+ triggers.add(key);
7419
+ this.db.prepare("UPDATE trigger_occurrences SET state='admitting',attempt=attempt+1,epoch=?,updated_at=? WHERE scope=? AND id=?").run(lease.epoch, this.now(), String(row.scope), String(row.id));
7420
+ this.triggerEvent("occurrence", String(row.scope), String(row.trigger_id), String(row.id));
7421
+ const occurrence = this.occurrence(String(row.scope), String(row.id));
7422
+ claimed.push({ occurrence, attempt: occurrence.attempt, epoch: lease.epoch });
7423
+ }
7424
+ return claimed;
7425
+ });
7426
+ }
7427
+ claimCurrent(claim, lease, states) {
7428
+ if (!this.owns(lease) || lease.epoch !== claim.epoch) return null;
7429
+ const row = this.db.prepare("SELECT * FROM trigger_occurrences WHERE scope=? AND id=? AND attempt=? AND epoch=?").get(claim.occurrence.scope, claim.occurrence.id, claim.attempt, claim.epoch);
7430
+ const occurrence = row && this.occurrenceRow(row);
7431
+ return occurrence && states.includes(occurrence.state) ? occurrence : null;
7432
+ }
7433
+ /** Whether a claim still owns its occurrence; every write a claimed execution makes checks it first. */
7434
+ occurrenceCurrent(claim, lease) {
7435
+ return this.claimCurrent(claim, lease, ["admitting", "creating"]) !== null;
7436
+ }
7437
+ /** Settles a claimed occurrence; false when another attempt or host owns it now. */
7438
+ finishOccurrence(claim, lease, state, details = {}) {
7439
+ return this.transaction(() => {
7440
+ const current2 = this.claimCurrent(claim, lease, ["admitting", "creating"]);
7441
+ if (!current2) return false;
7442
+ const { dueAt, ...patch } = details;
7443
+ this.writeOccurrence(current2, { hold: void 0, reason: void 0, ...patch }, state, dueAt ?? current2.dueAt);
7444
+ return true;
7445
+ });
7446
+ }
7447
+ /**
7448
+ * Marks a claimed occurrence as creating a session. From here a crash
7449
+ * leaves its creation operation to say what happened: no operation means
7450
+ * nothing started, one without a result means the outcome is unknown.
7451
+ */
7452
+ beginOccurrenceCreation(claim, lease) {
7453
+ return this.transaction(() => {
7454
+ const current2 = this.claimCurrent(claim, lease, ["admitting"]);
7455
+ if (!current2) return false;
7456
+ this.writeOccurrence(current2, {}, "creating");
7457
+ return true;
7458
+ });
7459
+ }
7460
+ /**
7461
+ * Admits a claimed occurrence's durable command in the same transaction that
7462
+ * records it, so a crash leaves either both or neither. Refuses, cancelling
7463
+ * the occurrence, when someone else messaged its fenced conversation since
7464
+ * it was recorded.
7465
+ */
7466
+ admitForOccurrence(claim, lease, conversationId, payload, intent, expectedRevision) {
7467
+ return this.transaction(() => {
7468
+ const current2 = this.claimCurrent(claim, lease, ["admitting"]);
7469
+ if (!current2) return null;
7470
+ const row = this.db.prepare("SELECT fence_conversation,fence_position FROM trigger_occurrences WHERE scope=? AND id=?").get(current2.scope, current2.id);
7471
+ if (row.fence_conversation && this.intervened(String(row.fence_conversation), Number(row.fence_position))) {
7472
+ this.writeOccurrence(current2, { reason: "Someone messaged the session after this was scheduled" }, "cancelled");
7473
+ return null;
7474
+ }
7475
+ const conversation = this.conversation(conversationId);
7476
+ if (expectedRevision !== void 0 && conversation?.revision !== expectedRevision) {
7477
+ this.writeOccurrence(current2, { hold: "binding", reason: "The session moved to another provider binding; review this run before resuming it" }, "held");
7478
+ return null;
7479
+ }
7480
+ const receipt2 = this.admit(current2.scope, current2.commandId, conversationId, { ...payload, trigger: { occurrenceId: current2.id } }, intent);
7481
+ this.writeOccurrence(current2, { sessionId: receipt2.sessionId, hold: void 0, reason: void 0 }, "admitted");
7482
+ return receipt2;
7483
+ });
7484
+ }
7485
+ intervened(conversationId, position) {
7486
+ return this.db.prepare("SELECT payload FROM commands WHERE conversation_id=? AND position>?").all(conversationId, position).some((row) => {
7487
+ const payload = decode(row.payload);
7488
+ return !payload.trigger && payload.notification !== true;
7489
+ });
7490
+ }
7491
+ cancelFenced(conversationId, reason) {
7492
+ for (const row of this.db.prepare("SELECT scope,id FROM trigger_occurrences WHERE fence_conversation=? AND state IN ('scheduled','held','admitting')").all(conversationId)) this.settleOccurrence(String(row.scope), String(row.id), "cancelled", { reason });
7493
+ }
7494
+ settleOccurrence(scope, id, state, patch) {
7495
+ const occurrence = this.occurrence(scope, id);
7496
+ if (occurrence) this.writeOccurrence(occurrence, patch, state);
7497
+ }
7498
+ /**
7499
+ * Mirrors admitted occurrences' commands: a settled command settles its
7500
+ * occurrence, a held or uncertain one holds it or marks it unknown with the
7501
+ * command's reason, and one resumed from the queue makes it admitted again.
7502
+ */
7503
+ settleAdmittedOccurrences() {
7504
+ return this.transaction(() => {
7505
+ let changed = 0;
7506
+ for (const row of this.db.prepare("SELECT * FROM trigger_occurrences WHERE state IN ('admitted','held','unknown')").all()) {
7507
+ const occurrence = this.occurrenceRow(row);
7508
+ const command = occurrence.command;
7509
+ if (!command) {
7510
+ if (occurrence.state !== "admitted") continue;
7511
+ this.writeOccurrence(occurrence, { reason: "Its command no longer exists" }, "cancelled");
7512
+ changed++;
7513
+ continue;
7514
+ }
7515
+ const next = command.state === "completed" || command.state === "failed" || command.state === "cancelled" || command.state === "unknown" ? command.state : command.state === "held" ? "held" : "admitted";
7516
+ if (next === occurrence.state) continue;
7517
+ const hold = command.errorCode === "REAUTHORIZE" ? "reauthorize" : "review";
7518
+ this.writeOccurrence(occurrence, next === "admitted" ? { hold: void 0, reason: void 0 } : { ...next === "held" ? { hold } : {}, ...command.error ? { reason: command.error } : {} }, next);
7519
+ changed++;
7520
+ }
7521
+ return changed;
7522
+ });
7523
+ }
7524
+ /**
7525
+ * After another host's lease lapsed, reads what its claims left behind:
7526
+ * an admission either committed its command or never happened, and a
7527
+ * creation's operation says whether a session may exist.
7528
+ */
7529
+ reconcileOccurrences(epoch) {
7530
+ for (const row of this.db.prepare(`SELECT * FROM trigger_occurrences WHERE state IN ${ACTIVE_OCCURRENCE} AND epoch<?`).all(epoch)) {
7531
+ const occurrence = this.occurrenceRow(row);
7532
+ if (occurrence.state === "admitting") {
7533
+ if (occurrence.command) this.writeOccurrence(occurrence, { sessionId: occurrence.command.sessionId }, "admitted");
7534
+ else this.writeOccurrence(occurrence, {}, "scheduled");
7535
+ continue;
7536
+ }
7537
+ const operation = this.operation(occurrence.scope, occurrence.creationId);
7538
+ const sessionId = operation?.result?.sessionId;
7539
+ if (!operation) this.writeOccurrence(occurrence, {}, "scheduled");
7540
+ else if (typeof sessionId === "string") this.writeOccurrence(occurrence, { sessionId }, "completed");
7541
+ else this.writeOccurrence(occurrence, { reason: "The host stopped while this run was starting a session. Check the project's sessions before marking it." }, "unknown");
7542
+ }
7543
+ }
7544
+ /** Cancels an occurrence that has not reached an agent yet, with its waiting command. */
7545
+ cancelOccurrence(scope, id, reason = "Cancelled") {
7546
+ return this.transaction(() => {
7547
+ const occurrence = this.occurrence(scope, id);
7548
+ if (!occurrence) throw new OrchestrationError(404, "Run not found");
7549
+ if (isTerminalOccurrence(occurrence.state)) return occurrence;
7550
+ if (occurrence.state === "admitting" || occurrence.state === "creating") throw new OrchestrationError(409, "This run is being started; wait for it to settle");
7551
+ if (occurrence.state === "unknown") throw new OrchestrationError(409, "Resolve this run's unknown outcome instead");
7552
+ const command = occurrence.command;
7553
+ if (command && ["dispatching", "delivered", "unknown"].includes(command.state)) throw new OrchestrationError(409, "Delivery has begun; stop the session's turn instead");
7554
+ if (command && ["queued", "held"].includes(command.state)) this.cancel(scope, command.id);
7555
+ this.writeOccurrence(occurrence, { hold: void 0, reason }, "cancelled");
7556
+ return this.occurrence(scope, id);
7557
+ });
7558
+ }
7559
+ /**
7560
+ * Returns a held occurrence to the schedule, due now, optionally under a
7561
+ * renewed grant. One whose message was already admitted releases that same
7562
+ * message — its payload and fingerprint untouched — with the grant replacing
7563
+ * the occurrence's own, once the grant is checked to be the owner's, live,
7564
+ * and for exactly what the old one allowed.
7565
+ */
7566
+ requeueOccurrence(scope, id, grant) {
7567
+ return this.transaction(() => {
7568
+ const occurrence = this.occurrence(scope, id);
7569
+ if (!occurrence) throw new OrchestrationError(404, "Run not found");
7570
+ if (occurrence.state !== "held") throw new OrchestrationError(409, "Only a held run can be resumed");
7571
+ if (grant) this.checkReplacementGrant(scope, occurrence, grant);
7572
+ const command = occurrence.command && this.command(scope, occurrence.command.id);
7573
+ if (command) {
7574
+ if (command.receipt.state !== "held") throw new OrchestrationError(409, "This run's message is not held");
7575
+ if (this.conversation(command.receipt.conversationId)?.revision !== command.receipt.bindingRevision) throw new OrchestrationError(409, "The session moved to another provider binding; cancel this run instead");
7576
+ this.update(command, "queued", { error: void 0, errorCode: void 0 });
7577
+ this.writeOccurrence(occurrence, { hold: void 0, reason: void 0, ...grant ? { grant } : {} }, "admitted");
7578
+ return this.occurrence(scope, id);
7579
+ }
7580
+ this.writeOccurrence(occurrence, { hold: void 0, reason: void 0, ...grant ? { grant } : {} }, "scheduled", Math.min(occurrence.dueAt, this.now()));
7581
+ return this.occurrence(scope, id);
7582
+ });
7583
+ }
7584
+ checkReplacementGrant(scope, occurrence, ref) {
7585
+ const grant = this.grant(ref.grantId);
7586
+ if (!grant || grant.revision !== ref.revision) throw new OrchestrationError(404, "Authorization not found");
7587
+ if (grant.scope !== scope) throw new OrchestrationError(403, "That authorization belongs to another account");
7588
+ if (grant.revokedAt || grant.expiresAt !== void 0 && grant.expiresAt <= this.now()) throw new OrchestrationError(409, "That authorization is no longer valid");
7589
+ if (!grant.actions.includes(occurrence.action)) throw new OrchestrationError(409, "That authorization does not allow this run");
7590
+ const previous = occurrence.grant && this.grant(occurrence.grant.grantId);
7591
+ const command = occurrence.command;
7592
+ const sameTarget = previous ? fingerprint(previous.target) === fingerprint(grant.target) : !command || grant.target.sessionId === command.sessionId && (grant.target.bindingRevision === void 0 || grant.target.bindingRevision === command.bindingRevision);
7593
+ if (!sameTarget) throw new OrchestrationError(409, "That authorization is for a different target");
7594
+ }
7595
+ /** Holds a scheduled occurrence for review, such as one a missed-run policy will not start by itself. */
7596
+ holdOccurrence(scope, id, hold, reason) {
7597
+ return this.transaction(() => {
7598
+ const occurrence = this.occurrence(scope, id);
7599
+ if (!occurrence) throw new OrchestrationError(404, "Run not found");
7600
+ if (occurrence.state !== "scheduled") throw new OrchestrationError(409, "Only a waiting run can be held");
7601
+ this.writeOccurrence(occurrence, { hold, reason }, "held");
7602
+ return this.occurrence(scope, id);
7603
+ });
7604
+ }
7605
+ /** Marks a scheduled occurrence skipped, such as a missed slot or an overlapping run. */
7606
+ skipOccurrence(scope, id, reason) {
7607
+ return this.transaction(() => {
7608
+ const occurrence = this.occurrence(scope, id);
7609
+ if (!occurrence) throw new OrchestrationError(404, "Run not found");
7610
+ if (occurrence.state !== "scheduled") throw new OrchestrationError(409, "Only a waiting run can be skipped");
7611
+ this.writeOccurrence(occurrence, { reason }, "skipped");
7612
+ return this.occurrence(scope, id);
7613
+ });
7614
+ }
7615
+ /** The user's answer to an occurrence whose session creation outcome is unknown. */
7616
+ resolveOccurrence(scope, id, disposition, sessionId) {
7617
+ return this.transaction(() => {
7618
+ const occurrence = this.occurrence(scope, id);
7619
+ if (!occurrence || occurrence.state !== "unknown") throw new OrchestrationError(409, "Run is not awaiting reconciliation");
7620
+ this.writeOccurrence(occurrence, { reason: disposition === "failed" ? "Marked as not started" : "Marked as started", ...sessionId ? { sessionId } : {} }, disposition);
7621
+ return this.occurrence(scope, id);
7622
+ });
7623
+ }
6902
7624
  };
6903
7625
 
6904
7626
  // ../../server/orchestration/storage.ts
@@ -7034,6 +7756,68 @@ function allStores() {
7034
7756
  return [...registry.allStores(), ...instanceStores()];
7035
7757
  }
7036
7758
 
7759
+ // ../../shared/session/codex-results.ts
7760
+ var OUTPUT_ITEMS = /* @__PURE__ */ new Set(["function_call_output", "custom_tool_call_output"]);
7761
+ var EXEC_OUTPUT_KEYS = ["aggregated_output", "formatted_output", "stdout", "stderr"];
7762
+ function semantics(output) {
7763
+ const parsed = parseCustomToolOutput(output);
7764
+ return JSON.stringify([parsed.isError, parsed.images?.length ?? 0]);
7765
+ }
7766
+ function visitImage(block, visit) {
7767
+ const source = isPlainObject(block.source) ? { ...block.source } : null;
7768
+ if (source) return visitField(source, "data", visit, "image") ? { ...block, source } : block;
7769
+ if (typeof block.image_url !== "string") return block;
7770
+ const match = /^(data:image\/[^;,]+;base64,)(.+)$/is.exec(block.image_url);
7771
+ if (!match) return block;
7772
+ const next = visit(match[2], "image");
7773
+ return next === match[2] ? block : { ...block, image_url: match[1] + next };
7774
+ }
7775
+ function projectBlocks(blocks, visit) {
7776
+ const joined = visitJoinedText(blocks, (block) => isPlainObject(block) && typeof block.text === "string" ? block.text : void 0, "", (block, text2) => ({ ...block, text: text2 }), visit) ?? blocks;
7777
+ let changed = joined !== blocks;
7778
+ const next = joined.map((block) => {
7779
+ if (!isPlainObject(block) || typeof block.text === "string") return block;
7780
+ const projected = visitImage(block, visit);
7781
+ if (projected !== block) changed = true;
7782
+ return projected;
7783
+ });
7784
+ return changed ? next : blocks;
7785
+ }
7786
+ function projectValue(output, visit) {
7787
+ if (Array.isArray(output)) return projectBlocks(output, visit);
7788
+ if (typeof output !== "string") return output;
7789
+ let envelope = null;
7790
+ try {
7791
+ envelope = JSON.parse(output);
7792
+ } catch {
7793
+ }
7794
+ if (isPlainObject(envelope) && ("output" in envelope || "content" in envelope)) {
7795
+ const key = envelope.output !== void 0 && envelope.output !== null ? "output" : "content";
7796
+ const inner = envelope[key];
7797
+ const projected = typeof inner === "string" ? visit(inner, "text") : Array.isArray(inner) ? projectValue(inner, visit) : inner;
7798
+ return projected === inner ? output : JSON.stringify({ ...envelope, [key]: projected });
7799
+ }
7800
+ return visit(output, "text");
7801
+ }
7802
+ function projectOutput(payload, visit) {
7803
+ const output = payload.output;
7804
+ const next = projectValue(output, visit);
7805
+ if (next === output || semantics(next) !== semantics(output)) return false;
7806
+ payload.output = next;
7807
+ return true;
7808
+ }
7809
+ function projectCodexResults(record3, visit) {
7810
+ const payload = isPlainObject(record3.payload) ? record3.payload : null;
7811
+ if (!payload) return false;
7812
+ if (record3.type === "response_item" && OUTPUT_ITEMS.has(String(payload.type))) return projectOutput(payload, visit);
7813
+ if (record3.type === "event_msg" && payload.type === "exec_command_end") {
7814
+ let changed = false;
7815
+ for (const key of EXEC_OUTPUT_KEYS) changed = visitField(payload, key, visit) || changed;
7816
+ return changed;
7817
+ }
7818
+ return false;
7819
+ }
7820
+
7037
7821
  // ../../shared/session/agents.ts
7038
7822
  var TERMINAL_AGENT_KIND = AGENT_KINDS[AGENT_KINDS.length - 1];
7039
7823
  var hasNoNoiseLines = () => false;
@@ -7049,7 +7833,8 @@ var codexFormat = {
7049
7833
  isNoiseLine: hasNoNoiseLines,
7050
7834
  pageHeaderLine: codexPageHeaderLine,
7051
7835
  brandBranch: brandCodexBranch,
7052
- turnBoundaries: codexTurnBoundaries
7836
+ turnBoundaries: codexTurnBoundaries,
7837
+ projectResults: projectCodexResults
7053
7838
  };
7054
7839
  var copilotFormat = {
7055
7840
  kind: "copilot",
@@ -7062,7 +7847,8 @@ var copilotFormat = {
7062
7847
  status: deriveCopilotSessionStatus,
7063
7848
  isNoiseLine: hasNoNoiseLines,
7064
7849
  brandBranch: brandCopilotBranch,
7065
- turnBoundaries: copilotTurnBoundaries
7850
+ turnBoundaries: copilotTurnBoundaries,
7851
+ projectResults: projectCopilotResults
7066
7852
  };
7067
7853
  var claudeFormat = {
7068
7854
  kind: "claude",
@@ -7076,7 +7862,8 @@ var claudeFormat = {
7076
7862
  status: deriveClaudeSessionStatus,
7077
7863
  isNoiseLine: isClaudeNoiseLine,
7078
7864
  brandBranch: brandClaudeBranch,
7079
- turnBoundaries: claudeTurnBoundaries
7865
+ turnBoundaries: claudeTurnBoundaries,
7866
+ projectResults: projectClaudeResults
7080
7867
  };
7081
7868
  var FORMATS = Object.freeze({
7082
7869
  acp: {
@@ -7611,7 +8398,7 @@ function extractVersion(output) {
7611
8398
  var DEFAULT_PROBE_TIMEOUT_MS = 5e3;
7612
8399
  var CLI_OUTPUT_MAX_CHARS = 1e4;
7613
8400
  function runCli(executable, args, timeoutMs) {
7614
- return new Promise((resolve9) => {
8401
+ return new Promise((resolve10) => {
7615
8402
  const resolved = resolveAgentCommand(executable, [...args]);
7616
8403
  const child2 = spawn3(resolved.command, resolved.args, {
7617
8404
  stdio: ["ignore", "pipe", "pipe"],
@@ -7633,11 +8420,11 @@ function runCli(executable, args, timeoutMs) {
7633
8420
  });
7634
8421
  child2.on("error", () => {
7635
8422
  clearTimeout(timer);
7636
- resolve9({ code: null, stdout, stderr, timedOut });
8423
+ resolve10({ code: null, stdout, stderr, timedOut });
7637
8424
  });
7638
8425
  child2.on("close", (code) => {
7639
8426
  clearTimeout(timer);
7640
- resolve9({ code, stdout, stderr, timedOut });
8427
+ resolve10({ code, stdout, stderr, timedOut });
7641
8428
  });
7642
8429
  });
7643
8430
  }
@@ -7848,12 +8635,12 @@ function effortLabel(effort) {
7848
8635
 
7849
8636
  // ../../server/agents/timeout.ts
7850
8637
  function withTimeout(promise, ms, label) {
7851
- return new Promise((resolve9, reject) => {
8638
+ return new Promise((resolve10, reject) => {
7852
8639
  const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
7853
8640
  promise.then(
7854
8641
  (value) => {
7855
8642
  clearTimeout(timer);
7856
- resolve9(value);
8643
+ resolve10(value);
7857
8644
  },
7858
8645
  (error2) => {
7859
8646
  clearTimeout(timer);
@@ -8774,8 +9561,8 @@ var resultObservers = /* @__PURE__ */ new WeakMap();
8774
9561
  function observeSDKResult(state) {
8775
9562
  let observer;
8776
9563
  const turn = (inputTurns.get(state)?.sent.length ?? Number(state.running)) + 1;
8777
- const completion = new Promise((resolve9) => {
8778
- observer = { turn, resolve: resolve9 };
9564
+ const completion = new Promise((resolve10) => {
9565
+ observer = { turn, resolve: resolve10 };
8779
9566
  });
8780
9567
  const observers = resultObservers.get(state) ?? /* @__PURE__ */ new Set();
8781
9568
  observers.add(observer);
@@ -8810,16 +9597,16 @@ function makeCanUseTool(state) {
8810
9597
  return (toolName, input, options) => {
8811
9598
  if (toolName === "AskUserQuestion") {
8812
9599
  const toolUseId = options.toolUseID;
8813
- return new Promise((resolve9) => {
9600
+ return new Promise((resolve10) => {
8814
9601
  const pending = {
8815
9602
  input,
8816
9603
  askedAt: Date.now(),
8817
- resolve: resolve9
9604
+ resolve: resolve10
8818
9605
  };
8819
9606
  state.pendingUserQuestions.set(toolUseId, pending);
8820
9607
  const onAbort = () => {
8821
9608
  if (state.pendingUserQuestions.delete(toolUseId)) {
8822
- resolve9({ behavior: "deny", message: "Session aborted", interrupt: true });
9609
+ resolve10({ behavior: "deny", message: "Session aborted", interrupt: true });
8823
9610
  }
8824
9611
  };
8825
9612
  options.signal?.addEventListener("abort", onAbort, { once: true });
@@ -8833,7 +9620,7 @@ function makeCanUseTool(state) {
8833
9620
  return Promise.resolve({ behavior: "allow", updatedInput: input });
8834
9621
  }
8835
9622
  const requestId = options.toolUseID;
8836
- return new Promise((resolve9) => {
9623
+ return new Promise((resolve10) => {
8837
9624
  const pending = {
8838
9625
  requestId,
8839
9626
  toolName,
@@ -8848,12 +9635,12 @@ function makeCanUseTool(state) {
8848
9635
  defaultToNo: options.defaultToNo,
8849
9636
  suppressAlwaysAllowRule: options.suppressAlwaysAllowRule,
8850
9637
  timestamp: Date.now(),
8851
- resolve: resolve9
9638
+ resolve: resolve10
8852
9639
  };
8853
9640
  state.pendingPermissions.set(requestId, pending);
8854
9641
  const onAbort = () => {
8855
9642
  if (state.pendingPermissions.delete(requestId)) {
8856
- resolve9({ behavior: "deny", message: "Session aborted", interrupt: true });
9643
+ resolve10({ behavior: "deny", message: "Session aborted", interrupt: true });
8857
9644
  }
8858
9645
  };
8859
9646
  options.signal?.addEventListener("abort", onAbort, { once: true });
@@ -8870,16 +9657,16 @@ function makeOnElicitation(state) {
8870
9657
  );
8871
9658
  return Promise.resolve({ action: "decline" });
8872
9659
  }
8873
- return new Promise((resolve9) => {
9660
+ return new Promise((resolve10) => {
8874
9661
  const requestId = options.requestId;
8875
9662
  state.pendingElicitations.set(requestId, {
8876
9663
  request,
8877
9664
  fields: projection.fields,
8878
9665
  askedAt: Date.now(),
8879
- resolve: resolve9
9666
+ resolve: resolve10
8880
9667
  });
8881
9668
  const onAbort = () => {
8882
- if (state.pendingElicitations.delete(requestId)) resolve9({ action: "decline" });
9669
+ if (state.pendingElicitations.delete(requestId)) resolve10({ action: "decline" });
8883
9670
  };
8884
9671
  options.signal?.addEventListener("abort", onAbort, { once: true });
8885
9672
  });
@@ -8908,14 +9695,14 @@ function makeOnUserDialog(state) {
8908
9695
  if (!(request.dialogKind in DIALOG_CHOICES)) return Promise.resolve(null);
8909
9696
  const projected = projectUserDialog(request);
8910
9697
  if (!projected) return Promise.resolve({ behavior: "cancelled" });
8911
- return new Promise((resolve9) => {
9698
+ return new Promise((resolve10) => {
8912
9699
  const requestId = options.requestId;
8913
9700
  state.pendingUserDialogs.set(requestId, {
8914
9701
  dialog: { ...projected, requestId, askedAt: Date.now() },
8915
- resolve: resolve9
9702
+ resolve: resolve10
8916
9703
  });
8917
9704
  const onAbort = () => {
8918
- if (state.pendingUserDialogs.delete(requestId)) resolve9({ behavior: "cancelled" });
9705
+ if (state.pendingUserDialogs.delete(requestId)) resolve10({ behavior: "cancelled" });
8919
9706
  };
8920
9707
  options.signal?.addEventListener("abort", onAbort, { once: true });
8921
9708
  });
@@ -9115,8 +9902,8 @@ var SDKMessageStream = class {
9115
9902
  closed = false;
9116
9903
  enqueue(message) {
9117
9904
  if (this.closed) return false;
9118
- const resolve9 = this.waiting.shift();
9119
- if (resolve9) resolve9({ value: message, done: false });
9905
+ const resolve10 = this.waiting.shift();
9906
+ if (resolve10) resolve10({ value: message, done: false });
9120
9907
  else this.queued.push(message);
9121
9908
  return true;
9122
9909
  }
@@ -9124,15 +9911,15 @@ var SDKMessageStream = class {
9124
9911
  if (this.closed) return;
9125
9912
  this.closed = true;
9126
9913
  this.queued.length = 0;
9127
- for (const resolve9 of this.waiting.splice(0)) {
9128
- resolve9({ value: void 0, done: true });
9914
+ for (const resolve10 of this.waiting.splice(0)) {
9915
+ resolve10({ value: void 0, done: true });
9129
9916
  }
9130
9917
  }
9131
9918
  next() {
9132
9919
  const message = this.queued.shift();
9133
9920
  if (message) return Promise.resolve({ value: message, done: false });
9134
9921
  if (this.closed) return Promise.resolve({ value: void 0, done: true });
9135
- return new Promise((resolve9) => this.waiting.push(resolve9));
9922
+ return new Promise((resolve10) => this.waiting.push(resolve10));
9136
9923
  }
9137
9924
  return() {
9138
9925
  this.close();
@@ -9181,7 +9968,28 @@ function runQuery(state, prompt, opts, pluginInstructions, preparation) {
9181
9968
  const messageStream = new SDKMessageStream();
9182
9969
  const first = buildUserMessage(prompt, opts.images);
9183
9970
  inputTurns.set(state, { sent: [first.uuid], completed: 0, taken: null });
9184
- messageStream.enqueue(first);
9971
+ if (opts.beforeFirstTurn) {
9972
+ let readiness2;
9973
+ queryOpts.hooks = {
9974
+ ...queryOpts.hooks,
9975
+ SessionStart: [{ timeout: 86400, hooks: [async (input) => {
9976
+ try {
9977
+ await (readiness2 ??= (async () => {
9978
+ state.cwd = input.cwd;
9979
+ await opts.beforeFirstTurn({ sessionId: state.sessionId, cwd: input.cwd });
9980
+ state.abort?.signal.throwIfAborted();
9981
+ messageStream.enqueue(first);
9982
+ })());
9983
+ return {};
9984
+ } catch (error2) {
9985
+ opts.onStartupError?.(error2);
9986
+ messageStream.close();
9987
+ state.abort?.abort();
9988
+ return { continue: false, decision: "block", reason: "Worktree startup did not complete" };
9989
+ }
9990
+ }] }]
9991
+ };
9992
+ } else messageStream.enqueue(first);
9185
9993
  state.messageStream = messageStream;
9186
9994
  const q = query2({ prompt: messageStream, options: queryOpts });
9187
9995
  state.activeQuery = q;
@@ -9310,6 +10118,8 @@ function createSDKSession(opts) {
9310
10118
  runQuery(state, opts.message, {
9311
10119
  name: opts.name,
9312
10120
  worktreeName: opts.worktreeName,
10121
+ beforeFirstTurn: opts.beforeFirstTurn,
10122
+ onStartupError: opts.onStartupError,
9313
10123
  mcpConfig: opts.mcpConfig,
9314
10124
  images: opts.images
9315
10125
  });
@@ -9693,8 +10503,13 @@ function sdkResumeOptions(req) {
9693
10503
  };
9694
10504
  }
9695
10505
  function startInteractive(req) {
10506
+ req.signal?.throwIfAborted();
9696
10507
  const sessionId = randomUUID3();
9697
10508
  reportSessionId(req, sessionId);
10509
+ let readinessComplete = !req.beforeFirstTurn;
10510
+ let initialized = false;
10511
+ let succeeded;
10512
+ let startupError;
9698
10513
  const state = createSDKSession({
9699
10514
  sessionId,
9700
10515
  // An images-only send has no prompt; the SDK substitutes its own
@@ -9705,13 +10520,20 @@ function startInteractive(req) {
9705
10520
  ...sdkResumeOptions(req),
9706
10521
  ultracode: !!req.ultracode,
9707
10522
  name: req.name,
9708
- worktreeName: req.worktreeName
10523
+ worktreeName: req.worktreeName,
10524
+ beforeFirstTurn: req.beforeFirstTurn ? async (identity) => {
10525
+ await beforeFirstTurn(req, { ...identity, nativeWorktree: !!req.worktreeName });
10526
+ readinessComplete = true;
10527
+ if (initialized) succeeded?.();
10528
+ } : void 0,
10529
+ onStartupError: (error2) => startupError?.(error2)
9709
10530
  });
9710
- return new Promise((resolve9, reject) => {
10531
+ return new Promise((resolve10, reject) => {
9711
10532
  let settled = false;
9712
10533
  const succeed = async () => {
9713
- if (settled) return;
10534
+ if (settled || !readinessComplete) return;
9714
10535
  settled = true;
10536
+ req.signal?.removeEventListener("abort", abort);
9715
10537
  state.onInit = null;
9716
10538
  state.onResult = null;
9717
10539
  const dirName = descriptor2.dirName.encode(state.cwd);
@@ -9722,18 +10544,39 @@ function startInteractive(req) {
9722
10544
  initialContent = await readFile6(filePath, "utf-8");
9723
10545
  } catch {
9724
10546
  }
9725
- resolve9({ sessionId, dirName, cwd: state.cwd, fileName, filePath, initialContent });
10547
+ resolve10({ sessionId, dirName, cwd: state.cwd, fileName, filePath, initialContent });
9726
10548
  };
9727
10549
  const fail = (message) => {
9728
10550
  if (settled) return;
9729
10551
  settled = true;
10552
+ req.signal?.removeEventListener("abort", abort);
9730
10553
  state.onInit = null;
9731
10554
  state.onResult = null;
10555
+ stopSDKSession(sessionId);
9732
10556
  reject(new AgentRuntimeError(500, "SPAWN_FAILED", message));
9733
10557
  };
10558
+ const abort = () => {
10559
+ stopSDKSession(sessionId);
10560
+ startupError?.(req.signal?.reason ?? new Error("Session startup cancelled"));
10561
+ };
10562
+ startupError = (error2) => {
10563
+ if (settled) return;
10564
+ settled = true;
10565
+ req.signal?.removeEventListener("abort", abort);
10566
+ state.onInit = null;
10567
+ state.onResult = null;
10568
+ stopSDKSession(sessionId);
10569
+ reject(error2);
10570
+ };
10571
+ succeeded = () => {
10572
+ void succeed();
10573
+ };
9734
10574
  state.onInit = () => {
10575
+ initialized = true;
9735
10576
  void succeed();
9736
10577
  };
10578
+ req.signal?.addEventListener("abort", abort, { once: true });
10579
+ if (req.signal?.aborted) abort();
9737
10580
  state.onResult = (result) => {
9738
10581
  state.onResult = null;
9739
10582
  const outcome = turnResultFrom(result);
@@ -9790,6 +10633,7 @@ var claudeRuntime = {
9790
10633
  return startInteractive(req);
9791
10634
  },
9792
10635
  async send(sessionId, req) {
10636
+ if (isSessionStarting(sessionId)) return { delivery: "busy" };
9793
10637
  const live = sdkSessions.get(sessionId);
9794
10638
  if (live?.pluginSkillLoading) return { delivery: "busy" };
9795
10639
  if (req.onlyWhileRunning && (!isSDKQueryLive(live) || !live.running)) return { delivery: "busy" };
@@ -9822,10 +10666,10 @@ var claudeRuntime = {
9822
10666
  ...sdkResumeOptions(req)
9823
10667
  });
9824
10668
  watchSubagentsFor(state, req.filePath ?? null);
9825
- const completion = new Promise((resolve9) => {
10669
+ const completion = new Promise((resolve10) => {
9826
10670
  state.onResult = (result) => {
9827
10671
  state.onResult = null;
9828
- resolve9(turnResultFrom(result));
10672
+ resolve10(turnResultFrom(result));
9829
10673
  };
9830
10674
  });
9831
10675
  return { delivery: "started", completion };
@@ -9901,6 +10745,112 @@ var claudeRuntime = {
9901
10745
  }
9902
10746
  };
9903
10747
 
10748
+ // ../../server/lib/managedCommand.ts
10749
+ import { spawn as spawn4, execFile } from "node:child_process";
10750
+ import { StringDecoder } from "node:string_decoder";
10751
+ async function runManagedCommand(options) {
10752
+ options.signal.throwIfAborted();
10753
+ const windows = process.platform === "win32";
10754
+ const child2 = spawn4(options.file, options.args, {
10755
+ cwd: options.cwd,
10756
+ stdio: ["ignore", "pipe", "pipe"],
10757
+ detached: !windows,
10758
+ windowsHide: true,
10759
+ env: { ...process.env, CI: "1", GIT_TERMINAL_PROMPT: "0", ...options.env }
10760
+ });
10761
+ const killTree = () => {
10762
+ if (!child2.pid) return;
10763
+ if (windows) {
10764
+ execFile("taskkill", ["/pid", String(child2.pid), "/T", "/F"], { windowsHide: true }, () => {
10765
+ });
10766
+ } else {
10767
+ try {
10768
+ process.kill(-child2.pid, "SIGKILL");
10769
+ } catch {
10770
+ }
10771
+ }
10772
+ };
10773
+ let timedOut = false;
10774
+ const timer = setTimeout(() => {
10775
+ timedOut = true;
10776
+ killTree();
10777
+ }, options.timeoutMs);
10778
+ const abort = () => killTree();
10779
+ options.signal.addEventListener("abort", abort, { once: true });
10780
+ if (options.signal.aborted) killTree();
10781
+ const stdout = new StringDecoder("utf8");
10782
+ const stderr = new StringDecoder("utf8");
10783
+ child2.stdout.on("data", (chunk) => options.output(stdout.write(chunk)));
10784
+ child2.stderr.on("data", (chunk) => options.output(stderr.write(chunk)));
10785
+ try {
10786
+ const code = await new Promise((resolve10, reject) => {
10787
+ child2.once("error", reject);
10788
+ child2.once("close", resolve10);
10789
+ });
10790
+ options.output(stdout.end() + stderr.end());
10791
+ options.signal.throwIfAborted();
10792
+ if (timedOut) throw new Error("Command timed out");
10793
+ return code;
10794
+ } finally {
10795
+ clearTimeout(timer);
10796
+ options.signal.removeEventListener("abort", abort);
10797
+ killTree();
10798
+ }
10799
+ }
10800
+
10801
+ // ../../server/agents/managedWorktree.ts
10802
+ import { randomUUID as randomUUID6 } from "node:crypto";
10803
+ import { mkdir as mkdir3 } from "node:fs/promises";
10804
+ import { join as join16 } from "node:path";
10805
+
10806
+ // ../../server/worktreeSetup/project.ts
10807
+ import { execFile as execFile2 } from "node:child_process";
10808
+ import { promisify as promisify2 } from "node:util";
10809
+ import { realpath as realpath4, stat as stat9 } from "node:fs/promises";
10810
+ import { isAbsolute as isAbsolute2, resolve as resolve8 } from "node:path";
10811
+ var execute = promisify2(execFile2);
10812
+ async function gitOutput(cwd, args, signal) {
10813
+ const result = await execute("git", args, { cwd, signal, encoding: "utf8", timeout: 15e3, maxBuffer: 1024 * 1024, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
10814
+ return result.stdout.trim();
10815
+ }
10816
+ async function physicalDirectory(cwd) {
10817
+ if (!isAbsolute2(cwd) || cwd.includes("\0")) throw new RouteError(400, ErrorCodes.INVALID_REQUEST, "An absolute project directory is required");
10818
+ const path = await realpath4(cwd);
10819
+ if (!(await stat9(path)).isDirectory()) throw new RouteError(400, ErrorCodes.INVALID_REQUEST, "Project directory does not exist");
10820
+ return path;
10821
+ }
10822
+ async function projectIdentity(cwd) {
10823
+ const path = await physicalDirectory(cwd);
10824
+ const common = await gitOutput(path, ["rev-parse", "--git-common-dir"]);
10825
+ return realpath4(resolve8(path, common));
10826
+ }
10827
+ async function validateInitializedDirectory(projectCwd, cwd) {
10828
+ const path = await physicalDirectory(cwd);
10829
+ if (await projectIdentity(projectCwd) !== await projectIdentity(path)) {
10830
+ throw new RouteError(403, ErrorCodes.FORBIDDEN, "Initialized worktree belongs to a different project");
10831
+ }
10832
+ return path;
10833
+ }
10834
+
10835
+ // ../../server/agents/managedWorktree.ts
10836
+ async function prepareManagedWorktree(request) {
10837
+ if (!request.worktreeName || !request.beforeFirstTurn) return request;
10838
+ if (!/^[a-zA-Z0-9._-]{1,80}$/.test(request.worktreeName) || request.worktreeName === "." || request.worktreeName === "..") throw new AgentRuntimeError(400, "INVALID_REQUEST", "Invalid worktree name");
10839
+ request.signal?.throwIfAborted();
10840
+ const suffix = randomUUID6().slice(0, 8);
10841
+ const name = `${request.worktreeName.slice(0, 28)}-${suffix}`;
10842
+ const parent = join16(request.cwd, ".worktrees");
10843
+ await mkdir3(parent, { recursive: true });
10844
+ const cwd = join16(parent, name);
10845
+ let output = "";
10846
+ const code = await runManagedCommand({ cwd: request.cwd, file: "git", args: ["worktree", "add", "-b", `worktree-${name}`, cwd, "HEAD"], signal: request.signal ?? new AbortController().signal, timeoutMs: 12e4, output: (text2) => {
10847
+ output = (output + text2).slice(-8e3);
10848
+ } });
10849
+ if (code !== 0) throw new AgentRuntimeError(500, "SPAWN_FAILED", output.trim() || "Could not initialize the managed worktree");
10850
+ await validateInitializedDirectory(request.cwd, cwd);
10851
+ return { ...request, worktreeName: void 0, cwd, dirName: descriptorFor("codex").dirName.encode(cwd) };
10852
+ }
10853
+
9904
10854
  // ../../shared/session/contextWindowSettings.ts
9905
10855
  function isValidContextWindowTokens(value) {
9906
10856
  return value === void 0 || value === null || typeof value === "number" && Number.isSafeInteger(value) && value > 0;
@@ -15945,7 +16895,7 @@ import { createInterface as createInterface2 } from "node:readline";
15945
16895
  var package_default = {
15946
16896
  name: "cogpit",
15947
16897
  private: true,
15948
- version: "3.5.1",
16898
+ version: "3.5.2",
15949
16899
  description: "Control center for Claude Code, OpenAI Codex, and GitHub Copilot CLI sessions",
15950
16900
  license: "MIT",
15951
16901
  author: {
@@ -16849,9 +17799,9 @@ var CodexAppServer = class {
16849
17799
  const completed = this.completedTurns.get(key);
16850
17800
  if (completed) return Promise.resolve(completed);
16851
17801
  if (!this.child) return Promise.reject(new CodexAppServerError("Codex disconnected before completion"));
16852
- return new Promise((resolve9, reject) => {
17802
+ return new Promise((resolve10, reject) => {
16853
17803
  const waiting = this.completionWaiters.get(key) ?? [];
16854
- waiting.push({ resolve: resolve9, reject });
17804
+ waiting.push({ resolve: resolve10, reject });
16855
17805
  this.completionWaiters.set(key, waiting);
16856
17806
  });
16857
17807
  }
@@ -17168,7 +18118,7 @@ var CodexAppServer = class {
17168
18118
  );
17169
18119
  }
17170
18120
  const id = this.nextRequestId++;
17171
- return new Promise((resolve9, reject) => {
18121
+ return new Promise((resolve10, reject) => {
17172
18122
  const timer = timeoutMs > 0 ? this.setTimer(() => {
17173
18123
  const pending = this.pendingRequests.get(id);
17174
18124
  if (!pending) return;
@@ -17181,7 +18131,7 @@ var CodexAppServer = class {
17181
18131
  }, timeoutMs) : null;
17182
18132
  this.pendingRequests.set(id, {
17183
18133
  method,
17184
- resolve: (result) => resolve9(result),
18134
+ resolve: (result) => resolve10(result),
17185
18135
  reject,
17186
18136
  timer
17187
18137
  });
@@ -17207,12 +18157,12 @@ var CodexAppServer = class {
17207
18157
  new CodexAppServerError("Codex app-server stdin is not writable")
17208
18158
  );
17209
18159
  }
17210
- return new Promise((resolve9, reject) => {
18160
+ return new Promise((resolve10, reject) => {
17211
18161
  try {
17212
18162
  child2.stdin.write(`${JSON.stringify(message)}
17213
18163
  `, (error2) => {
17214
18164
  if (error2) reject(error2);
17215
- else resolve9();
18165
+ else resolve10();
17216
18166
  });
17217
18167
  } catch (error2) {
17218
18168
  reject(error2);
@@ -17259,13 +18209,13 @@ var CodexAppServer = class {
17259
18209
  }
17260
18210
  }
17261
18211
  waitForChildClose(closed, timeoutMs) {
17262
- return new Promise((resolve9) => {
18212
+ return new Promise((resolve10) => {
17263
18213
  let settled = false;
17264
18214
  const finish = (didClose) => {
17265
18215
  if (settled) return;
17266
18216
  settled = true;
17267
18217
  this.clearTimer(timer);
17268
- resolve9(didClose);
18218
+ resolve10(didClose);
17269
18219
  };
17270
18220
  const timer = this.setTimer(() => finish(false), timeoutMs);
17271
18221
  void closed.then(() => finish(true));
@@ -17274,8 +18224,8 @@ var CodexAppServer = class {
17274
18224
  async terminateChild(child2, error2) {
17275
18225
  let didClose = false;
17276
18226
  let resolveClose = () => void 0;
17277
- const closed = new Promise((resolve9) => {
17278
- resolveClose = resolve9;
18227
+ const closed = new Promise((resolve10) => {
18228
+ resolveClose = resolve10;
17279
18229
  });
17280
18230
  child2.on("close", () => {
17281
18231
  didClose = true;
@@ -17395,7 +18345,7 @@ function friendlySpawnError(err, kind2 = "claude") {
17395
18345
  // ../../server/agents/tempImages.ts
17396
18346
  import { writeFile as writeFile3, unlink as unlink3 } from "node:fs/promises";
17397
18347
  import { tmpdir } from "node:os";
17398
- import { join as join16 } from "node:path";
18348
+ import { join as join17 } from "node:path";
17399
18349
  var IMAGE_EXT = {
17400
18350
  "image/jpeg": "jpg",
17401
18351
  "image/gif": "gif",
@@ -17406,7 +18356,7 @@ async function writeTempImageFiles(images) {
17406
18356
  const files = [];
17407
18357
  for (const [index, image] of images.entries()) {
17408
18358
  const ext = IMAGE_EXT[image.mediaType] ?? "png";
17409
- const filePath = join16(tmpdir(), `cogpit-codex-image-${Date.now()}-${index}.${ext}`);
18359
+ const filePath = join17(tmpdir(), `cogpit-codex-image-${Date.now()}-${index}.${ext}`);
17410
18360
  await writeFile3(filePath, Buffer.from(image.data, "base64"));
17411
18361
  files.push(filePath);
17412
18362
  }
@@ -17422,7 +18372,7 @@ async function cleanupTempFiles(paths) {
17422
18372
  }
17423
18373
 
17424
18374
  // ../../server/agents/codexExecution.ts
17425
- import { isAbsolute as isAbsolute2, relative as relative4, resolve as resolve8, sep as sep5 } from "node:path";
18375
+ import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve9, sep as sep5 } from "node:path";
17426
18376
 
17427
18377
  // ../../server/agents/codexBrowser.ts
17428
18378
  async function codexBrowserConfig(client2, cwd, sessionId = NO_COGPIT_SESSION) {
@@ -17565,6 +18515,7 @@ async function startCodexExecution(client2, options, onThreadStarted) {
17565
18515
  const input = buildCodexUserInput(options.message, options.images);
17566
18516
  const response = await client2.startThread(await threadSettings(client2, options));
17567
18517
  onThreadStarted?.(response.thread.id);
18518
+ await options.beforeFirstTurn?.({ sessionId: response.thread.id, cwd: typeof response.thread.cwd === "string" ? response.thread.cwd : options.cwd });
17568
18519
  const turn = await client2.startTurn({
17569
18520
  threadId: response.thread.id,
17570
18521
  input,
@@ -17614,11 +18565,11 @@ async function continueCodexExecution(client2, threadId, options) {
17614
18565
  }
17615
18566
  function getCodexThreadIdentity(thread, sessionsDir = storeFor("codex").sessionsRoot() ?? "") {
17616
18567
  const threadPath = typeof thread.path === "string" ? thread.path : "";
17617
- if (!thread.id || !threadPath || !isAbsolute2(threadPath)) return null;
17618
- const root = resolve8(sessionsDir);
17619
- const filePath = resolve8(threadPath);
18568
+ if (!thread.id || !threadPath || !isAbsolute3(threadPath)) return null;
18569
+ const root = resolve9(sessionsDir);
18570
+ const filePath = resolve9(threadPath);
17620
18571
  const fileName = uncompressedTranscriptName(relative4(root, filePath));
17621
- if (!fileName || fileName === ".." || fileName.startsWith(`..${sep5}`) || isAbsolute2(fileName) || !fileName.endsWith(".jsonl")) {
18572
+ if (!fileName || fileName === ".." || fileName.startsWith(`..${sep5}`) || isAbsolute3(fileName) || !fileName.endsWith(".jsonl")) {
17622
18573
  return null;
17623
18574
  }
17624
18575
  return {
@@ -17696,7 +18647,7 @@ async function waitForRollout(announcedId, scan, shouldStop) {
17696
18647
  if (Date.now() - eliminatedAt >= ANNOUNCEMENT_GRACE_MS) return match;
17697
18648
  }
17698
18649
  }
17699
- await new Promise((resolve9) => setTimeout(resolve9, SESSION_DISCOVERY_INTERVAL_MS));
18650
+ await new Promise((resolve10) => setTimeout(resolve10, SESSION_DISCOVERY_INTERVAL_MS));
17700
18651
  }
17701
18652
  return null;
17702
18653
  }
@@ -17747,6 +18698,7 @@ async function resolveStartedIdentity(thread) {
17747
18698
  throw new Error(`Codex created thread ${thread.id} but did not provide a rollout path`);
17748
18699
  }
17749
18700
  async function startLegacy(req, knownPaths) {
18701
+ await beforeFirstTurn(req, { cwd: req.cwd });
17750
18702
  const imagePaths = await writeTempImageFiles(req.images);
17751
18703
  const args = legacyArgs(req, imagePaths);
17752
18704
  const startedAt2 = Date.now();
@@ -17773,7 +18725,7 @@ async function startLegacy(req, knownPaths) {
17773
18725
  child2.stderr?.on("data", (data) => {
17774
18726
  stderr += data.toString();
17775
18727
  });
17776
- return new Promise((resolve9, reject) => {
18728
+ return new Promise((resolve10, reject) => {
17777
18729
  let settled = false;
17778
18730
  let sessionId = null;
17779
18731
  let announcedId = null;
@@ -17803,9 +18755,14 @@ async function startLegacy(req, knownPaths) {
17803
18755
  }
17804
18756
  }
17805
18757
  const worktreeIdentity = req.worktreeName ? await storeFor("codex").readIdentity(identity.filePath).catch(() => null) : null;
17806
- resolve9({
18758
+ if (req.worktreeName && !worktreeIdentity?.cwd) {
18759
+ reject(new AgentRuntimeError(409, "SESSION_UNCONFIRMED", "The native worktree directory is not available yet. Inspect the retained session before retrying."));
18760
+ return;
18761
+ }
18762
+ resolve10({
17807
18763
  sessionId: identity.sessionId,
17808
18764
  dirName: worktreeIdentity?.cwd ? descriptor3.dirName.encode(worktreeIdentity.cwd) : req.dirName,
18765
+ cwd: worktreeIdentity?.cwd ?? req.cwd,
17809
18766
  fileName: identity.fileName,
17810
18767
  filePath: identity.filePath,
17811
18768
  initialContent: content
@@ -17887,10 +18844,10 @@ async function sendLegacy(sessionId, req) {
17887
18844
  child2.stderr?.on("data", (data) => {
17888
18845
  stderr += data.toString();
17889
18846
  });
17890
- const completion = new Promise((resolve9) => {
18847
+ const completion = new Promise((resolve10) => {
17891
18848
  session.onResult = (result) => {
17892
18849
  session.onResult = null;
17893
- resolve9({
18850
+ resolve10({
17894
18851
  isError: result.is_error === true,
17895
18852
  message: result.result
17896
18853
  });
@@ -18124,6 +19081,8 @@ var codexRuntime = {
18124
19081
  kind: "codex",
18125
19082
  descriptor: descriptor3,
18126
19083
  async start(req) {
19084
+ req.signal?.throwIfAborted();
19085
+ req = await prepareManagedWorktree(req);
18127
19086
  validateContextWindow(req.contextWindowTokens);
18128
19087
  const knownPaths = new Set(
18129
19088
  (await storeFor("codex").listSessionFiles()).map((file) => file.filePath)
@@ -18131,7 +19090,7 @@ var codexRuntime = {
18131
19090
  if (req.worktreeName) return startLegacy(req, knownPaths);
18132
19091
  let threadStarted = false;
18133
19092
  try {
18134
- const options = executionOptions(req);
19093
+ const options = { ...executionOptions(req), beforeFirstTurn: (identity2) => beforeFirstTurn(req, identity2) };
18135
19094
  const started = await startCodexExecution(codexAppServer, options, (threadId) => {
18136
19095
  threadStarted = true;
18137
19096
  reportSessionId(req, threadId);
@@ -18143,14 +19102,17 @@ var codexRuntime = {
18143
19102
  initialContent = await readFile6(identity.filePath, "utf-8");
18144
19103
  } catch {
18145
19104
  }
19105
+ const initializedCwd = typeof started.thread.cwd === "string" ? started.thread.cwd : req.cwd;
18146
19106
  return {
18147
19107
  sessionId: identity.sessionId,
18148
- dirName: req.dirName,
19108
+ dirName: descriptor3.dirName.encode(initializedCwd),
19109
+ cwd: initializedCwd,
18149
19110
  fileName: identity.fileName,
18150
19111
  filePath: identity.filePath,
18151
19112
  initialContent
18152
19113
  };
18153
19114
  } catch (error2) {
19115
+ if (isStartupFailure(error2)) throw error2;
18154
19116
  if (threadStarted || !isCodexAppServerUnavailable(error2)) {
18155
19117
  throw new AgentRuntimeError(
18156
19118
  500,
@@ -18162,6 +19124,7 @@ var codexRuntime = {
18162
19124
  return startLegacy(req, knownPaths);
18163
19125
  },
18164
19126
  async send(sessionId, req) {
19127
+ if (isSessionStarting(sessionId)) return { delivery: "busy" };
18165
19128
  validateContextWindow(req.contextWindowTokens);
18166
19129
  const legacy = persistentSessions.get(sessionId);
18167
19130
  if (legacy && !legacy.dead) {
@@ -18321,7 +19284,7 @@ var codexRuntime = {
18321
19284
 
18322
19285
  // ../../server/agents/copilotTransport.ts
18323
19286
  import { spawn as spawnChild2 } from "node:child_process";
18324
- import { randomUUID as randomUUID6 } from "node:crypto";
19287
+ import { randomUUID as randomUUID7 } from "node:crypto";
18325
19288
  import {
18326
19289
  CancellationTokenSource,
18327
19290
  createMessageConnection,
@@ -18453,8 +19416,8 @@ var CopilotRuntime = class {
18453
19416
  }
18454
19417
  observeTurn(sessionId) {
18455
19418
  let observer;
18456
- const completion = new Promise((resolve9, reject) => {
18457
- observer = { resolve: resolve9, reject };
19419
+ const completion = new Promise((resolve10, reject) => {
19420
+ observer = { resolve: resolve10, reject };
18458
19421
  });
18459
19422
  const observers = this.turnObservers.get(sessionId) ?? /* @__PURE__ */ new Set();
18460
19423
  observers.add(observer);
@@ -18584,7 +19547,7 @@ var CopilotRuntime = class {
18584
19547
  }
18585
19548
  async createSession(options = {}) {
18586
19549
  options = await withPluginSkills(options);
18587
- const sessionId = options.sessionId ?? randomUUID6();
19550
+ const sessionId = options.sessionId ?? randomUUID7();
18588
19551
  this.assertSessionAvailable(sessionId);
18589
19552
  this.openingSessions.add(sessionId);
18590
19553
  try {
@@ -19021,7 +19984,7 @@ var CopilotRuntime = class {
19021
19984
  }
19022
19985
  waitForRequest(request, method, timeoutMs = this.transportTimeoutMs, onTimeout) {
19023
19986
  if (timeoutMs <= 0) return request;
19024
- return new Promise((resolve9, reject) => {
19987
+ return new Promise((resolve10, reject) => {
19025
19988
  const timer = setTimeout(() => {
19026
19989
  try {
19027
19990
  onTimeout?.();
@@ -19032,7 +19995,7 @@ var CopilotRuntime = class {
19032
19995
  request.then(
19033
19996
  (value) => {
19034
19997
  clearTimeout(timer);
19035
- resolve9(value);
19998
+ resolve10(value);
19036
19999
  },
19037
20000
  (error2) => {
19038
20001
  clearTimeout(timer);
@@ -19149,7 +20112,7 @@ var CopilotRuntime = class {
19149
20112
  throw new CopilotRuntimeError(`Copilot session ${params.sessionId} is not active`);
19150
20113
  }
19151
20114
  const sessionId = params.sessionId;
19152
- const requestId = typeof params.requestId === "string" ? params.requestId : randomUUID6();
20115
+ const requestId = typeof params.requestId === "string" ? params.requestId : randomUUID7();
19153
20116
  const choices = Array.isArray(params.choices) ? params.choices.filter((choice) => typeof choice === "string") : void 0;
19154
20117
  const value = {
19155
20118
  sessionId,
@@ -19159,10 +20122,10 @@ var CopilotRuntime = class {
19159
20122
  ...typeof params.allowFreeform === "boolean" ? { allowFreeform: params.allowFreeform } : {},
19160
20123
  askedAt: Date.now()
19161
20124
  };
19162
- return new Promise((resolve9, reject) => {
20125
+ return new Promise((resolve10, reject) => {
19163
20126
  this.pendingUserInputs.set(
19164
20127
  this.pendingInteractionKey(sessionId, requestId),
19165
- { value, resolve: resolve9, reject }
20128
+ { value, resolve: resolve10, reject }
19166
20129
  );
19167
20130
  });
19168
20131
  }
@@ -19171,7 +20134,7 @@ var CopilotRuntime = class {
19171
20134
  throw new CopilotRuntimeError("Invalid Copilot exit-plan request");
19172
20135
  }
19173
20136
  const sessionId = params.sessionId;
19174
- const requestId = randomUUID6();
20137
+ const requestId = randomUUID7();
19175
20138
  const actions = Array.isArray(params.actions) ? params.actions.filter((action) => typeof action === "string") : [];
19176
20139
  const value = {
19177
20140
  sessionId,
@@ -19182,10 +20145,10 @@ var CopilotRuntime = class {
19182
20145
  recommendedAction: typeof params.recommendedAction === "string" ? params.recommendedAction : actions[0] ?? "interactive",
19183
20146
  askedAt: Date.now()
19184
20147
  };
19185
- return new Promise((resolve9, reject) => {
20148
+ return new Promise((resolve10, reject) => {
19186
20149
  this.pendingExitPlans.set(
19187
20150
  this.pendingInteractionKey(sessionId, requestId),
19188
- { value, resolve: resolve9, reject }
20151
+ { value, resolve: resolve10, reject }
19189
20152
  );
19190
20153
  });
19191
20154
  }
@@ -19470,8 +20433,8 @@ function rolloutNamedThroughMs(filePath) {
19470
20433
  var FORK_COPY_MAX_GAP_MS = 1e3;
19471
20434
  function isForkedSessionMeta(payload) {
19472
20435
  if (typeof payload.forked_from_id === "string") return true;
19473
- const spawn4 = asRecord(asRecord(asRecord(payload.source)?.subagent)?.thread_spawn);
19474
- return typeof spawn4?.parent_thread_id === "string";
20436
+ const spawn5 = asRecord(asRecord(asRecord(payload.source)?.subagent)?.thread_spawn);
20437
+ return typeof spawn5?.parent_thread_id === "string";
19475
20438
  }
19476
20439
  function parseCodexUsageLine(line, state) {
19477
20440
  const record3 = parseJsonRecord(line);
@@ -19846,6 +20809,7 @@ var copilotRuntime2 = {
19846
20809
  kind: "copilot",
19847
20810
  descriptor: descriptor4,
19848
20811
  async start(req) {
20812
+ req.signal?.throwIfAborted();
19849
20813
  const sessionId = randomUUID3();
19850
20814
  reportSessionId(req, sessionId);
19851
20815
  let opened = false;
@@ -19862,6 +20826,7 @@ var copilotRuntime2 = {
19862
20826
  if (req.permissions?.mode) {
19863
20827
  await copilotRuntime.setPermissionMode(sessionId, allowAll(req.permissions.mode));
19864
20828
  }
20829
+ await beforeFirstTurn(req, { sessionId, cwd: req.cwd });
19865
20830
  const attachments = buildAttachments(req.images);
19866
20831
  await copilotRuntime.send(sessionId, {
19867
20832
  prompt: req.message || IMAGE_ONLY_PROMPT,
@@ -19875,10 +20840,10 @@ var copilotRuntime2 = {
19875
20840
  initialContent = await readFile6(filePath, "utf-8");
19876
20841
  break;
19877
20842
  } catch {
19878
- await new Promise((resolve9) => setTimeout(resolve9, TRANSCRIPT_POLL_INTERVAL_MS));
20843
+ await new Promise((resolve10) => setTimeout(resolve10, TRANSCRIPT_POLL_INTERVAL_MS));
19879
20844
  }
19880
20845
  }
19881
- return { sessionId, dirName: req.dirName, fileName, filePath, initialContent };
20846
+ return { sessionId, dirName: req.dirName, cwd: req.cwd, fileName, filePath, initialContent };
19882
20847
  } catch (error2) {
19883
20848
  if (opened) {
19884
20849
  if (copilotRuntime.isSessionActive(sessionId)) {
@@ -19888,6 +20853,7 @@ var copilotRuntime2 = {
19888
20853
  await copilotRuntime.deleteSession(sessionId).catch(() => {
19889
20854
  });
19890
20855
  }
20856
+ if (isStartupFailure(error2)) throw error2;
19891
20857
  throw new AgentRuntimeError(
19892
20858
  500,
19893
20859
  "SPAWN_FAILED",
@@ -19896,6 +20862,7 @@ var copilotRuntime2 = {
19896
20862
  }
19897
20863
  },
19898
20864
  async send(sessionId, req) {
20865
+ if (isSessionStarting(sessionId)) return { delivery: "busy" };
19899
20866
  if (req.onlyWhileRunning && !copilotRuntime.isTurnActive(sessionId)) return { delivery: "busy" };
19900
20867
  if (!copilotRuntime.isSessionActive(sessionId)) {
19901
20868
  await copilotRuntime.resumeSession(sessionId, {
@@ -20115,14 +21082,14 @@ var copilotRuntime2 = {
20115
21082
  // ../../server/agents/instanceRuntime.ts
20116
21083
  import { fork } from "node:child_process";
20117
21084
  import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
20118
- import { join as join17 } from "node:path";
21085
+ import { join as join18 } from "node:path";
20119
21086
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20120
21087
  function providerInstanceEnvironment(instance, dataRoot2) {
20121
21088
  const descriptor5 = descriptorFor(instance.agent);
20122
- const userRoot = join17(instance.homeDir, "user");
20123
- for (const directory of [userRoot, join17(userRoot, ".config"), join17(userRoot, ".cache"), join17(userRoot, ".local/share"), join17(userRoot, "tmp")]) mkdirSync4(directory, { recursive: true, mode: 448 });
21089
+ const userRoot = join18(instance.homeDir, "user");
21090
+ for (const directory of [userRoot, join18(userRoot, ".config"), join18(userRoot, ".cache"), join18(userRoot, ".local/share"), join18(userRoot, "tmp")]) mkdirSync4(directory, { recursive: true, mode: 448 });
20124
21091
  const env = Object.fromEntries(["PATH", "SystemRoot", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "TZ", "COGPIT_PORT"].flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
20125
- Object.assign(env, { HOME: userRoot, USERPROFILE: userRoot, XDG_CONFIG_HOME: join17(userRoot, ".config"), XDG_CACHE_HOME: join17(userRoot, ".cache"), XDG_DATA_HOME: join17(userRoot, ".local/share"), TMPDIR: join17(userRoot, "tmp"), TEMP: join17(userRoot, "tmp"), TMP: join17(userRoot, "tmp"), COGPIT_AGENT_WORKER: "1", COGPIT_AGENT_INSTANCE_ID: instance.id, COGPIT_WORKER_AGENT: instance.agent, COGPIT_WORKER_DATA_ROOT: join17(instance.homeDir, "cogpit"), COGPIT_WORKER_CONFIG_PATH: join17(instance.homeDir, "config.json"), COGPIT_ORCHESTRATION_ROOT: dataRoot2, ELECTRON_RUN_AS_NODE: "1" });
21092
+ Object.assign(env, { HOME: userRoot, USERPROFILE: userRoot, XDG_CONFIG_HOME: join18(userRoot, ".config"), XDG_CACHE_HOME: join18(userRoot, ".cache"), XDG_DATA_HOME: join18(userRoot, ".local/share"), TMPDIR: join18(userRoot, "tmp"), TEMP: join18(userRoot, "tmp"), TMP: join18(userRoot, "tmp"), COGPIT_AGENT_WORKER: "1", COGPIT_AGENT_INSTANCE_ID: instance.id, COGPIT_WORKER_AGENT: instance.agent, COGPIT_WORKER_DATA_ROOT: join18(instance.homeDir, "cogpit"), COGPIT_WORKER_CONFIG_PATH: join18(instance.homeDir, "config.json"), COGPIT_ORCHESTRATION_ROOT: dataRoot2, ELECTRON_RUN_AS_NODE: "1" });
20126
21093
  if (descriptor5.cli.homeEnvVar) env[descriptor5.cli.homeEnvVar] = instance.homeDir;
20127
21094
  if (descriptor5.cli.bundledBySdk) env.CLAUDE_CONFIG_DIR = instance.homeDir;
20128
21095
  env.COGPIT_BROWSER_HOME = browserHome();
@@ -20204,6 +21171,22 @@ function createInstanceRuntime(instance, dataRoot2) {
20204
21171
  }
20205
21172
  if (typeof message.id !== "number") return;
20206
21173
  const id = message.id;
21174
+ if (message.type === "startup-ready" && typeof message.cwd === "string") {
21175
+ const call3 = pending.get(id);
21176
+ const ready2 = call3?.beforeFirstTurn;
21177
+ void Promise.resolve().then(async () => {
21178
+ if (!ready2) throw new Error("Startup authorization is no longer available");
21179
+ await ready2({ cwd: message.cwd, nativeWorktree: message.nativeWorktree === true, sessionId: typeof message.sessionId === "string" ? qualified(message.sessionId) : void 0 });
21180
+ }).then(() => {
21181
+ if (child2 === worker && worker.connected) worker.send({ type: "startup-ready-result", id }, () => {
21182
+ });
21183
+ }, (error2) => {
21184
+ if (call3 && error2 instanceof Error) call3.startupError = error2;
21185
+ if (child2 === worker && worker.connected) worker.send({ type: "startup-ready-result", id, error: error2 instanceof Error ? error2.message : "Startup refused" }, () => {
21186
+ });
21187
+ });
21188
+ return;
21189
+ }
20207
21190
  if (message.type === "identity" && typeof message.sessionId === "string") {
20208
21191
  watched2.add(message.sessionId);
20209
21192
  const sessionId = qualified(message.sessionId);
@@ -20228,26 +21211,42 @@ function createInstanceRuntime(instance, dataRoot2) {
20228
21211
  if (!call2) return;
20229
21212
  pending.delete(id);
20230
21213
  clearTimeout(call2.timer);
20231
- if (typeof message.error === "string") call2.reject(new AgentRuntimeError(Number(message.status) || 502, typeof message.code === "string" ? message.code : "INSTANCE_ERROR", message.error));
21214
+ if (typeof message.error === "string") call2.reject(call2.startupError ?? new AgentRuntimeError(Number(message.status) || 502, typeof message.code === "string" ? message.code : "INSTANCE_ERROR", message.error));
20232
21215
  else call2.resolve(message.value);
20233
21216
  });
20234
21217
  return worker;
20235
21218
  }
20236
- async function call(method, args, identity, timeoutMs = 12e4) {
21219
+ async function call(method, args, identity, timeoutMs = 12e4, beforeFirstTurn2, signal) {
21220
+ signal?.throwIfAborted();
20237
21221
  if (pending.size >= 100) throw new AgentRuntimeError(429, "INSTANCE_BUSY", "Provider instance has too many pending operations");
20238
21222
  const worker = processFor();
20239
21223
  const id = ++nextId;
20240
- const value = await new Promise((resolve9, reject) => {
21224
+ const value = await new Promise((resolve10, reject) => {
20241
21225
  const timer = setTimeout(() => {
21226
+ const call2 = pending.get(id);
20242
21227
  pending.delete(id);
20243
- reject(new Error("Provider instance operation timed out; inspect session history before retrying"));
21228
+ if (method === "start") abort();
21229
+ call2?.reject(new Error("Provider instance operation timed out; inspect session history before retrying"));
20244
21230
  }, timeoutMs);
20245
- pending.set(id, { resolve: (value2) => resolve9(value2), reject, timer, identity });
21231
+ const abort = () => {
21232
+ if (worker.connected) worker.send({ type: "startup-abort", id }, () => {
21233
+ });
21234
+ };
21235
+ signal?.addEventListener("abort", abort, { once: true });
21236
+ if (signal?.aborted) abort();
21237
+ pending.set(id, { resolve: (value2) => {
21238
+ signal?.removeEventListener("abort", abort);
21239
+ resolve10(value2);
21240
+ }, reject: (error2) => {
21241
+ signal?.removeEventListener("abort", abort);
21242
+ reject(error2);
21243
+ }, timer, identity, beforeFirstTurn: beforeFirstTurn2 });
20246
21244
  worker.send({ id, method, args }, (error2) => {
20247
21245
  if (error2) {
20248
21246
  clearTimeout(timer);
21247
+ const call2 = pending.get(id);
20249
21248
  pending.delete(id);
20250
- reject(error2);
21249
+ call2?.reject(error2);
20251
21250
  }
20252
21251
  });
20253
21252
  });
@@ -20260,22 +21259,31 @@ function createInstanceRuntime(instance, dataRoot2) {
20260
21259
  descriptor: descriptor5,
20261
21260
  isBusy: () => pending.size > 0 || completions.size > 0 || active.length > 0,
20262
21261
  async start(request) {
20263
- const { value } = await call("start", [{ ...request, onSessionId: void 0, dirName: splitInstanceDirName(request.dirName).nativeDirName }], (id) => reportSessionId(request, id));
20264
- return { ...value, sessionId: qualified(value.sessionId), dirName: instanceDirName(instance.id, value.dirName) };
21262
+ let announced;
21263
+ try {
21264
+ const { value } = await call("start", [{ ...request, signal: void 0, beforeFirstTurn: Boolean(request.beforeFirstTurn), onSessionId: void 0, dirName: splitInstanceDirName(request.dirName).nativeDirName }], (id) => {
21265
+ announced = id;
21266
+ reportSessionId(request, id);
21267
+ }, request.beforeFirstTurn ? 24 * 60 * 6e4 : 12e4, request.beforeFirstTurn ? (identity) => beforeFirstTurn(request, identity) : void 0, request.signal);
21268
+ return { ...value, sessionId: qualified(value.sessionId), dirName: instanceDirName(instance.id, value.dirName) };
21269
+ } finally {
21270
+ if (announced) finishSessionStartup(announced);
21271
+ }
20265
21272
  },
20266
21273
  async send(sessionId, request) {
21274
+ if (isSessionStarting(sessionId)) return { delivery: "busy" };
20267
21275
  watched2.add(native(sessionId));
20268
21276
  const { id, value } = await call("send", [native(sessionId), request]);
20269
21277
  if (!value.hasCompletion) return value;
20270
- const completion = new Promise((resolve9, reject) => {
21278
+ const completion = new Promise((resolve10, reject) => {
20271
21279
  const early = earlyCompletions.get(id);
20272
21280
  if (early) {
20273
21281
  earlyCompletions.delete(id);
20274
21282
  if (early.error) reject(new Error(early.error));
20275
- else resolve9(early.value);
21283
+ else resolve10(early.value);
20276
21284
  return;
20277
21285
  }
20278
- completions.set(id, { resolve: resolve9, reject });
21286
+ completions.set(id, { resolve: resolve10, reject });
20279
21287
  });
20280
21288
  return { delivery: value.delivery, turnId: value.turnId, completion };
20281
21289
  },
@@ -20379,6 +21387,8 @@ await loadConfig();
20379
21387
  refreshDirs();
20380
21388
  installBundledSkills([kind]);
20381
21389
  var runtime = runtimeFor(kind);
21390
+ var startups = /* @__PURE__ */ new Map();
21391
+ var readiness = /* @__PURE__ */ new Map();
20382
21392
  var answers = /* @__PURE__ */ new Map();
20383
21393
  var methods = /* @__PURE__ */ new Set(["start", "send", "interrupt", "stop", "stopAll", "deleteSession", "respondToApproval", "respondToAllApprovals", "respondToPlan", "answerQuestion", "describeRuntime", "listModels", "liveUsageRecords", "fork", "shutdown", "snapshot", "commitAnswer"]);
20384
21394
  function send2(message) {
@@ -20386,18 +21396,18 @@ function send2(message) {
20386
21396
  }
20387
21397
  var skillRequestId = 0;
20388
21398
  var skillRequests = /* @__PURE__ */ new Map();
20389
- var unregisterSkills = registerPluginSkills((cwd) => new Promise((resolve9) => {
21399
+ var unregisterSkills = registerPluginSkills((cwd) => new Promise((resolve10) => {
20390
21400
  if (skillRequests.size >= 64) {
20391
- resolve9("");
21401
+ resolve10("");
20392
21402
  return;
20393
21403
  }
20394
21404
  const requestId = ++skillRequestId;
20395
21405
  const timer = setTimeout(() => {
20396
21406
  skillRequests.delete(requestId);
20397
- resolve9("");
21407
+ resolve10("");
20398
21408
  }, 5e3);
20399
21409
  timer.unref();
20400
- skillRequests.set(requestId, { resolve: resolve9, timer });
21410
+ skillRequests.set(requestId, { resolve: resolve10, timer });
20401
21411
  send2({ type: "plugin-skills", requestId, cwd });
20402
21412
  }));
20403
21413
  function snapshot() {
@@ -20423,6 +21433,19 @@ function publishSnapshot() {
20423
21433
  var poll = setInterval(publishSnapshot, 300);
20424
21434
  poll.unref();
20425
21435
  process.on("message", (message) => {
21436
+ if (isRecord(message) && typeof message.id === "number" && message.type === "startup-abort") {
21437
+ startups.get(message.id)?.abort(new Error("Session startup cancelled"));
21438
+ readiness.get(message.id)?.reject(new Error("Session startup cancelled"));
21439
+ readiness.delete(message.id);
21440
+ return;
21441
+ }
21442
+ if (isRecord(message) && typeof message.id === "number" && message.type === "startup-ready-result") {
21443
+ const pending = readiness.get(message.id);
21444
+ readiness.delete(message.id);
21445
+ if (typeof message.error === "string") pending?.reject(new Error(message.error));
21446
+ else pending?.resolve();
21447
+ return;
21448
+ }
20426
21449
  if (isRecord(message) && message.type === "plugin-skills-result" && typeof message.requestId === "number") {
20427
21450
  const request = skillRequests.get(message.requestId);
20428
21451
  if (request) {
@@ -20445,10 +21468,20 @@ process.on("message", (message) => {
20445
21468
  args[0] = new Map(args[0].map(([id2, models]) => [id2, new Map(models)]));
20446
21469
  args[1] = Array.isArray(args[1]) ? new Set(args[1]) : void 0;
20447
21470
  }
20448
- if (method === "start" && isRecord(args[0])) args[0].onSessionId = (sessionId) => {
20449
- watch2(sessionId);
20450
- send2({ type: "identity", id, sessionId });
20451
- };
21471
+ if (method === "start" && isRecord(args[0])) {
21472
+ args[0].onSessionId = (sessionId) => {
21473
+ watch2(sessionId);
21474
+ send2({ type: "identity", id, sessionId });
21475
+ };
21476
+ const controller = new AbortController();
21477
+ startups.set(id, controller);
21478
+ args[0].signal = controller.signal;
21479
+ if (args[0].beforeFirstTurn === true) args[0].beforeFirstTurn = (identity) => new Promise((resolve10, reject) => {
21480
+ readiness.set(id, { resolve: resolve10, reject });
21481
+ send2({ type: "startup-ready", id, ...identity });
21482
+ });
21483
+ else args[0].beforeFirstTurn = void 0;
21484
+ }
20452
21485
  if (typeof args[0] === "string") watch2(args[0]);
20453
21486
  const operation = runtime[method];
20454
21487
  if (typeof operation !== "function") throw new Error("Unsupported runtime operation");
@@ -20474,7 +21507,10 @@ process.on("message", (message) => {
20474
21507
  return { ...result, completion: void 0, hasCompletion: Boolean(completion) };
20475
21508
  }
20476
21509
  return result;
20477
- })().then((value) => send2({ id, value }), (error2) => send2({ id, error: error2 instanceof Error ? error2.message : "Instance operation failed", status: isRecord(error2) ? error2.status : void 0, code: isRecord(error2) ? error2.code : void 0 }));
21510
+ })().finally(() => {
21511
+ startups.delete(id);
21512
+ readiness.delete(id);
21513
+ }).then((value) => send2({ id, value }), (error2) => send2({ id, error: error2 instanceof Error ? error2.message : "Instance operation failed", status: isRecord(error2) ? error2.status : void 0, code: isRecord(error2) ? error2.code : void 0 }));
20478
21514
  });
20479
21515
  async function close() {
20480
21516
  clearInterval(poll);