@dudousxd/nestjs-agent-opencode 0.5.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,7 @@ var InMemoryOpenCodeSessionStore = class {
47
47
  };
48
48
 
49
49
  // src/mcp.controller.ts
50
+ var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
50
51
  var import_common = require("@nestjs/common");
51
52
 
52
53
  // src/tokens.ts
@@ -143,6 +144,7 @@ _ts_decorate([
143
144
  _ts_metadata("design:returntype", void 0)
144
145
  ], OpenCodeMcpController.prototype, "delete", null);
145
146
  OpenCodeMcpController = _ts_decorate([
147
+ (0, import_nestjs_agent_core.CanAnyone)(),
146
148
  (0, import_common.Controller)("opencode/mcp"),
147
149
  _ts_param(0, (0, import_common.Inject)(OPENCODE_MCP_ENDPOINT)),
148
150
  _ts_metadata("design:type", Function),
@@ -153,7 +155,7 @@ OpenCodeMcpController = _ts_decorate([
153
155
 
154
156
  // src/mcp.ts
155
157
  var import_node_crypto = require("crypto");
156
- var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
158
+ var import_nestjs_agent_core4 = require("@dudousxd/nestjs-agent-core");
157
159
  var import_genui = require("@dudousxd/nestjs-agent-core/genui");
158
160
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
159
161
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -161,11 +163,11 @@ var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zo
161
163
  var import_types = require("@modelcontextprotocol/sdk/types.js");
162
164
 
163
165
  // src/turn.ts
164
- var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
166
+ var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
165
167
  var import_common2 = require("@nestjs/common");
166
168
 
167
169
  // src/forms.ts
168
- var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
170
+ var import_nestjs_agent_core2 = require("@dudousxd/nestjs-agent-core");
169
171
  function toQuestion(field) {
170
172
  const required = field.required === true ? {
171
173
  required: true
@@ -235,7 +237,7 @@ function toFormAnswer(form, answers = {}) {
235
237
  for (const field of formFields(form)) {
236
238
  const question = toQuestion(field);
237
239
  const values = answers[field.key] ?? question.defaults ?? [];
238
- const problem = (0, import_nestjs_agent_core.validateElicitationAnswer)(question, values);
240
+ const problem = (0, import_nestjs_agent_core2.validateElicitationAnswer)(question, values);
239
241
  if (problem) throw new FormAnswerError(`answers["${field.key}"] ${problem}`);
240
242
  const present = values.filter((v) => v !== "");
241
243
  const [value] = present;
@@ -547,7 +549,7 @@ var OpenCodeTurn = class {
547
549
  });
548
550
  }
549
551
  async write(event) {
550
- await this.a.writer.write((0, import_nestjs_agent_core2.encodeStreamEvent)(event));
552
+ await this.a.writer.write((0, import_nestjs_agent_core3.encodeStreamEvent)(event));
551
553
  }
552
554
  async step() {
553
555
  if (this.stepOpen) return;
@@ -1081,7 +1083,7 @@ ${delta}` : delta;
1081
1083
  const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
1082
1084
  const expiresAt = ttlMs !== void 0 ? new Date(Date.now() + ttlMs).toISOString() : void 0;
1083
1085
  const tool = this.a.approvalPresentation?.(action);
1084
- const confirmation = tool !== void 0 ? (0, import_nestjs_agent_core2.resolveToolConfirmation)(tool.name, args, tool.presentation) : void 0;
1086
+ const confirmation = tool !== void 0 ? (0, import_nestjs_agent_core3.resolveToolConfirmation)(tool.name, args, tool.presentation) : void 0;
1085
1087
  this.segment.calls.push({
1086
1088
  id: req.id,
1087
1089
  name: action,
@@ -1308,7 +1310,7 @@ ${delta}` : delta;
1308
1310
  answer: toFormAnswer(form, answer.answers)
1309
1311
  });
1310
1312
  }
1311
- const result = (0, import_nestjs_agent_core2.settleElicitation)(request, answer);
1313
+ const result = (0, import_nestjs_agent_core3.settleElicitation)(request, answer);
1312
1314
  await this.a.store.updateToolCall({
1313
1315
  toolCallId: form.id,
1314
1316
  status: result.skipped ? "rejected" : "executed",
@@ -1663,7 +1665,7 @@ function isZodSchema(schema) {
1663
1665
  }
1664
1666
  __name(isZodSchema, "isZodSchema");
1665
1667
  function toInputSchema(schema) {
1666
- const extension = (0, import_nestjs_agent_core3.schemaExtensionOf)(schema);
1668
+ const extension = (0, import_nestjs_agent_core4.schemaExtensionOf)(schema);
1667
1669
  if (extension !== void 0) {
1668
1670
  const base = toInputSchema(extension.base);
1669
1671
  const properties = {
@@ -1707,7 +1709,7 @@ var OpenCodeMcpEndpoint2 = class {
1707
1709
  turns;
1708
1710
  tokens;
1709
1711
  /** Genui telemetry of the UI calls OpenCode makes (its code mode calls tools here, not in the loop). */
1710
- genui = new import_nestjs_agent_core3.GenuiRunRecorder();
1712
+ genui = new import_nestjs_agent_core4.GenuiRunRecorder();
1711
1713
  constructor(turns, tokens) {
1712
1714
  this.turns = turns;
1713
1715
  this.tokens = tokens;
@@ -1788,9 +1790,9 @@ var OpenCodeMcpEndpoint2 = class {
1788
1790
  }));
1789
1791
  if (this.turns.memoryWritable()) {
1790
1792
  tools.push({
1791
- name: import_nestjs_agent_core3.REMEMBER_TOOL_NAME,
1792
- description: import_nestjs_agent_core3.REMEMBER_TOOL_DESCRIPTION,
1793
- inputSchema: toInputSchema(import_nestjs_agent_core3.rememberInputSchema),
1793
+ name: import_nestjs_agent_core4.REMEMBER_TOOL_NAME,
1794
+ description: import_nestjs_agent_core4.REMEMBER_TOOL_DESCRIPTION,
1795
+ inputSchema: toInputSchema(import_nestjs_agent_core4.rememberInputSchema),
1794
1796
  annotations: {
1795
1797
  readOnlyHint: false
1796
1798
  }
@@ -1807,7 +1809,7 @@ var OpenCodeMcpEndpoint2 = class {
1807
1809
  if (turn === void 0) {
1808
1810
  throw new OpenCodeToolRefusedError("This endpoint serves OpenCode turns: no turn of yours is running on this session.");
1809
1811
  }
1810
- const output = name === import_nestjs_agent_core3.REMEMBER_TOOL_NAME && this.turns.memoryWritable() ? await this.remember(turn, args) : await this.invoke(turn, actor, name, args, String(extra.requestId ?? ""));
1812
+ const output = name === import_nestjs_agent_core4.REMEMBER_TOOL_NAME && this.turns.memoryWritable() ? await this.remember(turn, args) : await this.invoke(turn, actor, name, args, String(extra.requestId ?? ""));
1811
1813
  return {
1812
1814
  content: [
1813
1815
  {
@@ -1833,9 +1835,9 @@ var OpenCodeMcpEndpoint2 = class {
1833
1835
  async invoke(turn, actor, name, args, requestId) {
1834
1836
  const { registry } = this.turns;
1835
1837
  const spec = registry.spec(name);
1836
- if (spec === void 0 || LOOP_SERVED_KINDS.has(spec.kind)) throw new import_nestjs_agent_core3.ToolNotFoundError(name);
1838
+ if (spec === void 0 || LOOP_SERVED_KINDS.has(spec.kind)) throw new import_nestjs_agent_core4.ToolNotFoundError(name);
1837
1839
  const allowed = this.turns.allowedTools(turn.input);
1838
- if (allowed !== void 0 && !allowed.includes(name)) throw new import_nestjs_agent_core3.ToolNotFoundError(name);
1840
+ if (allowed !== void 0 && !allowed.includes(name)) throw new import_nestjs_agent_core4.ToolNotFoundError(name);
1839
1841
  if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
1840
1842
  throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
1841
1843
  }
@@ -1877,7 +1879,7 @@ var OpenCodeMcpEndpoint2 = class {
1877
1879
  }
1878
1880
  /** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
1879
1881
  async remember(turn, args) {
1880
- const parsed = await import_nestjs_agent_core3.rememberInputSchema["~standard"].validate(args ?? {});
1882
+ const parsed = await import_nestjs_agent_core4.rememberInputSchema["~standard"].validate(args ?? {});
1881
1883
  if (parsed.issues !== void 0) {
1882
1884
  throw new OpenCodeToolRefusedError(`remember: ${parsed.issues.map((issue) => issue.message).join("; ")}`);
1883
1885
  }
@@ -1886,13 +1888,13 @@ var OpenCodeMcpEndpoint2 = class {
1886
1888
  };
1887
1889
 
1888
1890
  // src/runner.ts
1889
- var import_nestjs_agent_core5 = require("@dudousxd/nestjs-agent-core");
1891
+ var import_nestjs_agent_core6 = require("@dudousxd/nestjs-agent-core");
1890
1892
  var import_common5 = require("@nestjs/common");
1891
1893
 
1892
1894
  // src/turns.ts
1893
1895
  var import_node_crypto2 = require("crypto");
1894
1896
  var import_nestjs_agent = require("@dudousxd/nestjs-agent");
1895
- var import_nestjs_agent_core4 = require("@dudousxd/nestjs-agent-core");
1897
+ var import_nestjs_agent_core5 = require("@dudousxd/nestjs-agent-core");
1896
1898
  var import_common4 = require("@nestjs/common");
1897
1899
 
1898
1900
  // src/event-hub.ts
@@ -2007,6 +2009,37 @@ var OpenCodeEventHub = class _OpenCodeEventHub {
2007
2009
  }
2008
2010
  };
2009
2011
 
2012
+ // src/turn-limits.ts
2013
+ var STOPPED_ERROR_CODE = "opencode_stopped";
2014
+ function stopErrorText(stop) {
2015
+ switch (stop.reason) {
2016
+ case "steps":
2017
+ return `The turn was stopped after ${stop.counts.steps - 1} steps, its limit.`;
2018
+ case "compactions":
2019
+ return `The turn was stopped after ${stop.counts.compactions - 1} compactions, its limit.`;
2020
+ case "wall-clock":
2021
+ return "The turn was stopped: it ran longer than allowed.";
2022
+ default:
2023
+ return "The turn was stopped.";
2024
+ }
2025
+ }
2026
+ __name(stopErrorText, "stopErrorText");
2027
+ function cleanLimits(limits) {
2028
+ const ok = /* @__PURE__ */ __name((n) => typeof n === "number" && Number.isFinite(n) && n >= 0, "ok");
2029
+ return {
2030
+ ...ok(limits?.steps) ? {
2031
+ steps: limits.steps
2032
+ } : {},
2033
+ ...ok(limits?.compactions) ? {
2034
+ compactions: limits.compactions
2035
+ } : {},
2036
+ ...ok(limits?.wallClockMs) ? {
2037
+ wallClockMs: limits.wallClockMs
2038
+ } : {}
2039
+ };
2040
+ }
2041
+ __name(cleanLimits, "cleanLimits");
2042
+
2010
2043
  // src/turns.ts
2011
2044
  function _ts_decorate2(decorators, target, key, desc) {
2012
2045
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2036,6 +2069,16 @@ function _ts_param2(paramIndex, decorator) {
2036
2069
  __name(_ts_param2, "_ts_param");
2037
2070
  var DEFAULT_TURN_TIMEOUT_MS = 30 * 6e4;
2038
2071
  var DEFAULT_TOOLS_TTL_MS = 7 * 24 * 60 * 6e4;
2072
+ var DEFAULT_HOOK_TIMEOUT_MS = 5e3;
2073
+ var PARKED_RECHECKS_MS = [
2074
+ 250,
2075
+ 1e3,
2076
+ 3e3,
2077
+ 1e4,
2078
+ 3e4
2079
+ ];
2080
+ var PARKED_READ_GAP_MS = 250;
2081
+ var RELEASED_USAGE_MS = 10 * 6e4;
2039
2082
  var OpenCodeTurns = class {
2040
2083
  static {
2041
2084
  __name(this, "OpenCodeTurns");
@@ -2059,6 +2102,21 @@ var OpenCodeTurns = class {
2059
2102
  spent = /* @__PURE__ */ new Set();
2060
2103
  /** When the tools endpoint was last registered for a location (`server|boot|directory|actor`). */
2061
2104
  toolsIssued = /* @__PURE__ */ new Map();
2105
+ /**
2106
+ * What a turn let go of here had spent, for a settle that follows here (a cancel in this process
2107
+ * that settles after the turn was released). Dropped when the run settles here, or after
2108
+ * {@link RELEASED_USAGE_MS} when it settles elsewhere, so no run is kept for good.
2109
+ */
2110
+ releasedUsage = /* @__PURE__ */ new Map();
2111
+ keepReleasedUsage(runId, usage) {
2112
+ this.releasedUsage.set(runId, usage);
2113
+ setTimeout(() => {
2114
+ if (this.releasedUsage.get(runId) === usage) this.releasedUsage.delete(runId);
2115
+ }, RELEASED_USAGE_MS).unref?.();
2116
+ }
2117
+ /** The turns this process executes, and which turn owns each session (`server|session`). */
2118
+ watches = /* @__PURE__ */ new Map();
2119
+ owners = /* @__PURE__ */ new Map();
2062
2120
  constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2, pricingStore, agentOptions) {
2063
2121
  this.host = host;
2064
2122
  this.sessions = sessions;
@@ -2071,7 +2129,11 @@ var OpenCodeTurns = class {
2071
2129
  this.memory = memory;
2072
2130
  this.queue = queue;
2073
2131
  this.toolsTokens = toolsTokens2;
2074
- this.prices = new import_nestjs_agent_core4.ModelPriceResolver({
2132
+ const hookTimeoutMs = settings.hookTimeoutMs;
2133
+ if (hookTimeoutMs !== void 0 && !(Number.isFinite(hookTimeoutMs) && hookTimeoutMs > 0)) {
2134
+ throw new Error(`hookTimeoutMs must be a positive number of milliseconds, not ${hookTimeoutMs}`);
2135
+ }
2136
+ this.prices = new import_nestjs_agent_core5.ModelPriceResolver({
2075
2137
  pricingStore,
2076
2138
  catalog: agentOptions?.priceCatalog,
2077
2139
  log: {
@@ -2091,10 +2153,10 @@ var OpenCodeTurns = class {
2091
2153
  const tokens = usage.inputTokens + usage.outputTokens;
2092
2154
  const reported = reportedCostUsd !== void 0 && (reportedCostUsd > 0 || tokens === 0);
2093
2155
  if (reported && this.settings.cost !== "estimate") {
2094
- return (0, import_nestjs_agent_core4.resolveUsageCost)(usage, reportedCostUsd, void 0);
2156
+ return (0, import_nestjs_agent_core5.resolveUsageCost)(usage, reportedCostUsd, void 0);
2095
2157
  }
2096
2158
  const price = model !== void 0 && tokens > 0 ? await this.prices.priceFor(priceRef(model)) : void 0;
2097
- return (0, import_nestjs_agent_core4.resolveUsageCost)(usage, price === void 0 ? reportedCostUsd : void 0, price);
2159
+ return (0, import_nestjs_agent_core5.resolveUsageCost)(usage, price === void 0 ? reportedCostUsd : void 0, price);
2098
2160
  }
2099
2161
  /** The tools server's name in OpenCode. */
2100
2162
  get toolsServer() {
@@ -2149,7 +2211,7 @@ var OpenCodeTurns = class {
2149
2211
  const memory = this.memory;
2150
2212
  const write = memory?.provider.write?.bind(memory.provider);
2151
2213
  if (memory === void 0 || write === void 0) return "Memory is read-only here.";
2152
- const max = memory.maxFactChars ?? import_nestjs_agent_core4.DEFAULT_MAX_FACT_CHARS;
2214
+ const max = memory.maxFactChars ?? import_nestjs_agent_core5.DEFAULT_MAX_FACT_CHARS;
2153
2215
  if (input.fact.length > max) return `Not recorded: a fact is at most ${max} characters.`;
2154
2216
  const actor = turn.input.actor;
2155
2217
  const scopeCtx = {
@@ -2159,9 +2221,9 @@ var OpenCodeTurns = class {
2159
2221
  agentName: turn.input.agentName
2160
2222
  } : {}
2161
2223
  };
2162
- const scopes = await (memory.scopes ?? import_nestjs_agent_core4.defaultScopeResolver).resolve(scopeCtx);
2163
- const scope = (0, import_nestjs_agent_core4.actorScope)(actor);
2164
- const verdict = (0, import_nestjs_agent_core4.memoryWriteVerdict)({
2224
+ const scopes = await (memory.scopes ?? import_nestjs_agent_core5.defaultScopeResolver).resolve(scopeCtx);
2225
+ const scope = (0, import_nestjs_agent_core5.actorScope)(actor);
2226
+ const verdict = (0, import_nestjs_agent_core5.memoryWriteVerdict)({
2165
2227
  scope,
2166
2228
  scopes,
2167
2229
  actor,
@@ -2243,7 +2305,7 @@ var OpenCodeTurns = class {
2243
2305
  runId,
2244
2306
  emitUi: uiEmitter(scope(runId), async (component) => {
2245
2307
  const writer = await this.sink.open(runId);
2246
- await writer.write((0, import_nestjs_agent_core4.encodeStreamEvent)({
2308
+ await writer.write((0, import_nestjs_agent_core5.encodeStreamEvent)({
2247
2309
  kind: "ui",
2248
2310
  ...component
2249
2311
  }));
@@ -2342,8 +2404,12 @@ var OpenCodeTurns = class {
2342
2404
  }
2343
2405
  }
2344
2406
  onApplicationShutdown() {
2407
+ for (const runId of [
2408
+ ...this.watches.keys()
2409
+ ]) this.release(runId, "released");
2345
2410
  for (const live of this.live.values()) live.stop();
2346
2411
  this.live.clear();
2412
+ this.owners.clear();
2347
2413
  this.events.close();
2348
2414
  }
2349
2415
  // ---- begin ---------------------------------------------------------------------------------
@@ -2353,7 +2419,8 @@ var OpenCodeTurns = class {
2353
2419
  * durable `begin` re-run after a crash): the run's user message is written once.
2354
2420
  */
2355
2421
  async begin(runId, input) {
2356
- const earlier = await this.prepareThread(runId, input);
2422
+ const startedAt = this.watches.get(runId)?.startedAt ?? Date.now();
2423
+ const { earlier, rerun } = await this.prepareThread(runId, input);
2357
2424
  const server = await this.host.server(input.actor);
2358
2425
  const known = await this.sessions.get(input.threadId);
2359
2426
  const reusable = known !== null && known.serverKey === server.key && known.bootId === server.bootId && (await this.host.reuse?.({
@@ -2399,7 +2466,12 @@ var OpenCodeTurns = class {
2399
2466
  created
2400
2467
  });
2401
2468
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2402
- return handle;
2469
+ const begun = this.limitsFor(input).wallClockMs !== void 0 ? {
2470
+ ...handle,
2471
+ startedAt
2472
+ } : handle;
2473
+ await this.adopt(runId, input, begun, rerun, void 0, startedAt);
2474
+ return begun;
2403
2475
  }
2404
2476
  async createSession(runId, input, server, earlier, note) {
2405
2477
  const { client } = server;
@@ -2469,10 +2541,16 @@ ${transcript}` : "",
2469
2541
  }
2470
2542
  const firstDropped = messages[lastUser + 1];
2471
2543
  if (firstDropped !== void 0) await this.store.truncateFrom(input.threadId, firstDropped.id);
2472
- return messages.slice(0, Math.max(lastUser, 0));
2544
+ return {
2545
+ earlier: messages.slice(0, Math.max(lastUser, 0)),
2546
+ rerun: false
2547
+ };
2473
2548
  }
2474
2549
  const mine = messages.findIndex((m) => m.role === "user" && m.runId === runId);
2475
- if (mine >= 0) return messages.slice(0, mine);
2550
+ if (mine >= 0) return {
2551
+ earlier: messages.slice(0, mine),
2552
+ rerun: true
2553
+ };
2476
2554
  await this.store.appendMessage({
2477
2555
  threadId: input.threadId,
2478
2556
  role: "user",
@@ -2488,7 +2566,10 @@ ${transcript}` : "",
2488
2566
  if (thread !== null && (thread.title === "" || thread.title === "New chat")) {
2489
2567
  await this.store.setTitle(input.threadId, titleOf(input.userText));
2490
2568
  }
2491
- return messages;
2569
+ return {
2570
+ earlier: messages,
2571
+ rerun: false
2572
+ };
2492
2573
  }
2493
2574
  /** Rewind the session to before its last user message, so the regenerated answer replaces it. */
2494
2575
  async revertLastExchange(client, sessionId) {
@@ -2571,12 +2652,12 @@ ${transcript}` : "",
2571
2652
  const memory = this.memory;
2572
2653
  if (memory === void 0) return void 0;
2573
2654
  const ctx = this.scopeContext(input);
2574
- const scopes = await (memory.scopes ?? import_nestjs_agent_core4.defaultScopeResolver).resolve(ctx);
2655
+ const scopes = await (memory.scopes ?? import_nestjs_agent_core5.defaultScopeResolver).resolve(ctx);
2575
2656
  const records = await memory.provider.list({
2576
2657
  scopes,
2577
2658
  ctx
2578
2659
  });
2579
- const digest = (0, import_nestjs_agent_core4.resolveMemoryDigest)({
2660
+ const digest = (0, import_nestjs_agent_core5.resolveMemoryDigest)({
2580
2661
  records,
2581
2662
  scopes,
2582
2663
  ...memory.maxMemories !== void 0 ? {
@@ -2584,7 +2665,7 @@ ${transcript}` : "",
2584
2665
  } : {}
2585
2666
  });
2586
2667
  if (digest.entries.length === 0) return void 0;
2587
- return (0, import_nestjs_agent_core4.buildMemoryBlock)({
2668
+ return (0, import_nestjs_agent_core5.buildMemoryBlock)({
2588
2669
  entries: digest.entries,
2589
2670
  writable: this.memoryWritable(),
2590
2671
  partial: digest.omitted > 0
@@ -2598,12 +2679,12 @@ ${transcript}` : "",
2598
2679
  const skills = this.skills;
2599
2680
  if (skills === void 0 || client.file === void 0 || directory === void 0) return [];
2600
2681
  const ctx = this.scopeContext(input);
2601
- const scopes = await (skills.scopes ?? import_nestjs_agent_core4.defaultScopeResolver).resolve(ctx);
2682
+ const scopes = await (skills.scopes ?? import_nestjs_agent_core5.defaultScopeResolver).resolve(ctx);
2602
2683
  const summaries = await skills.provider.list({
2603
2684
  scopes,
2604
2685
  ctx
2605
2686
  });
2606
- const { entries } = (0, import_nestjs_agent_core4.resolveSkillCatalog)(summaries, scopes, skills.maxSkills);
2687
+ const { entries } = (0, import_nestjs_agent_core5.resolveSkillCatalog)(summaries, scopes, skills.maxSkills);
2607
2688
  const dir = this.settings.skillsDir ?? ".opencode/skills";
2608
2689
  const written = [];
2609
2690
  for (const entry of entries) {
@@ -2704,6 +2785,7 @@ ${body}
2704
2785
  // ---- prompt / observe / reply --------------------------------------------------------------
2705
2786
  /** Start listening to the session (so nothing it says is missed), then send the user message. */
2706
2787
  async prompt(runId, input, handle, spent) {
2788
+ await this.adopt(runId, input, handle, true);
2707
2789
  const live = await this.ensureLive(runId, input, handle, false, spent);
2708
2790
  try {
2709
2791
  const prompt = await this.host.promptFor?.({
@@ -2746,7 +2828,24 @@ ${body}
2746
2828
  * back from the session's messages, when the execution ends, what nobody heard after that.
2747
2829
  */
2748
2830
  async observe(runId, input, handle, spent, progress) {
2831
+ const watch = await this.adopt(runId, input, handle, true, progress?.counts);
2749
2832
  const live = await this.ensureLive(runId, input, handle, true, spent, progress);
2833
+ watch.parkedOn = void 0;
2834
+ watch.parkedEnd = void 0;
2835
+ watch.series = false;
2836
+ watch.observing = true;
2837
+ const wall = this.watchClock(watch);
2838
+ try {
2839
+ if (watch.pending !== void 0) await this.requestStop(runId, watch.pending);
2840
+ const milestone = this.marked(watch, await this.observeLive(runId, input, handle, live));
2841
+ if (milestone.kind === "ask") watch.parkedOn = milestone.ask.id;
2842
+ return milestone;
2843
+ } finally {
2844
+ watch.observing = false;
2845
+ if (wall !== void 0) clearTimeout(wall);
2846
+ }
2847
+ }
2848
+ async observeLive(runId, input, handle, live) {
2750
2849
  const { turn, client } = live;
2751
2850
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2752
2851
  let done = false;
@@ -2786,19 +2885,64 @@ ${body}
2786
2885
  * to go on.
2787
2886
  */
2788
2887
  async reply(runId, input, handle, ask, reply, spent, progress) {
2888
+ const watch = await this.adopt(runId, input, handle, true, progress?.counts);
2889
+ watch.parkedOn = void 0;
2890
+ watch.parkedEnd = void 0;
2891
+ watch.series = false;
2892
+ const stopping = watch.pending !== void 0 || watch.stop !== void 0;
2893
+ const answer = stopping ? refusalOf(ask) : reply;
2789
2894
  const server = await this.host.server(input.actor);
2790
2895
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2791
- return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2896
+ if (stopping) return this.stopAfterRestart(watch, runId, input, handle, ask, answer, spent);
2897
+ return this.continueAfterRestart(runId, input, server, ask, answer, spent, handle.startedAt);
2792
2898
  }
2793
2899
  const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2794
- await live.turn.decide(ask, reply);
2900
+ await live.turn.decide(ask, answer);
2901
+ return handle;
2902
+ }
2903
+ /**
2904
+ * A stopping turn whose OpenCode server restarted while it waited: there is no session left to
2905
+ * abort, and none is opened or prompted. The ask is settled as refused and the turn ends stopped.
2906
+ */
2907
+ async stopAfterRestart(watch, runId, input, handle, ask, answer, spent) {
2908
+ const live = await this.ensureLive(runId, input, handle, false, spent);
2909
+ await live.turn.decide(ask, answer, {
2910
+ tellOpenCode: false
2911
+ });
2912
+ const reason = watch.pending ?? watch.stop?.reason ?? "stopped";
2913
+ if (watch.stop === void 0) {
2914
+ const stop = {
2915
+ reason,
2916
+ counts: this.counts(watch)
2917
+ };
2918
+ watch.stop = stop;
2919
+ this.hook("onStop", {
2920
+ turnId: runId,
2921
+ reason,
2922
+ counts: stop.counts
2923
+ });
2924
+ }
2925
+ watch.aborted = true;
2926
+ watch.pending = void 0;
2927
+ live.turn.handle({
2928
+ type: "session.execution.interrupted",
2929
+ data: {
2930
+ sessionID: handle.sessionId
2931
+ }
2932
+ });
2795
2933
  return handle;
2796
2934
  }
2797
- async continueAfterRestart(runId, input, server, ask, reply, spent) {
2935
+ async continueAfterRestart(runId, input, server, ask, reply, spent, startedAt) {
2798
2936
  const approved = "approved" in reply && reply.approved === true;
2799
2937
  const note = ask.kind === "approval" ? approved ? `The user approved the action ${ask.action} you asked permission for. Continue the task.` : `The user did not approve the action ${ask.action}. Continue without it and explain.` : `The user answered your question: ${JSON.stringify("answers" in reply ? reply.answers : {})}. Continue.`;
2800
2938
  const thread = await this.store.getThread(input.threadId);
2801
- const handle = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2939
+ const created = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2940
+ const handle = {
2941
+ ...created,
2942
+ ...startedAt !== void 0 ? {
2943
+ startedAt
2944
+ } : {}
2945
+ };
2802
2946
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2803
2947
  this.drop(runId);
2804
2948
  const live = await this.ensureLive(runId, input, handle, false, spent);
@@ -2842,7 +2986,7 @@ ${body}
2842
2986
  current?.stop();
2843
2987
  const server = await this.host.server(input.actor);
2844
2988
  const deps = this.deps.forAgent(input.agentName);
2845
- const policy = deps.approvalPolicy ?? new import_nestjs_agent_core4.DefaultApprovalPolicy();
2989
+ const policy = deps.approvalPolicy ?? new import_nestjs_agent_core5.DefaultApprovalPolicy();
2846
2990
  const writer = await this.sink.open(input.sinkRunId ?? runId);
2847
2991
  const turn = new OpenCodeTurn({
2848
2992
  runId,
@@ -2883,7 +3027,11 @@ ${body}
2883
3027
  this.grants.set(runId, grants);
2884
3028
  }, "onGranted")
2885
3029
  });
2886
- const stop = await this.events.listen(server.key, server.client, handle.sessionId, (event) => turn.handle(event));
3030
+ const sessionId = handle.sessionId;
3031
+ const stop = await this.events.listen(server.key, server.client, sessionId, (event) => {
3032
+ turn.handle(event);
3033
+ this.sawEvent(runId, sessionId, event);
3034
+ });
2887
3035
  const live = {
2888
3036
  turn,
2889
3037
  input,
@@ -2892,6 +3040,15 @@ ${body}
2892
3040
  stop
2893
3041
  };
2894
3042
  this.live.set(runId, live);
3043
+ this.owners.set(ownerKey(handle), runId);
3044
+ const watch = this.watches.get(runId);
3045
+ if (watch !== void 0 && watch.handle.sessionId !== sessionId) {
3046
+ watch.handle = {
3047
+ ...handle,
3048
+ startedAt: watch.startedAt
3049
+ };
3050
+ watch.ending = false;
3051
+ }
2895
3052
  if (resumed) await turn.catchUp().catch(() => void 0);
2896
3053
  return live;
2897
3054
  }
@@ -2900,11 +3057,46 @@ ${body}
2900
3057
  this.live.get(runId)?.stop();
2901
3058
  this.live.delete(runId);
2902
3059
  }
2903
- /** Stop following a run and drop the approvals it was granted: it is over. */
3060
+ /**
3061
+ * Stop following a run and drop the approvals it was granted: it is over. Returns the turn this
3062
+ * process executed, now settled (no stop reaches it any more), for its `onTurnEnd`.
3063
+ */
2904
3064
  forget(runId) {
2905
3065
  this.drop(runId);
3066
+ this.releasedUsage.delete(runId);
2906
3067
  for (const grant of this.grants.get(runId) ?? []) this.spent.delete(grant.id);
2907
3068
  this.grants.delete(runId);
3069
+ const watch = this.watches.get(runId);
3070
+ if (watch === void 0) return void 0;
3071
+ watch.settled = true;
3072
+ this.watches.delete(runId);
3073
+ for (const [key, owner] of this.owners) if (owner === runId) this.owners.delete(key);
3074
+ return watch;
3075
+ }
3076
+ /**
3077
+ * A run that ended without settling here (cancelled while this process executed it; whoever
3078
+ * cancelled settled it): stop following it, and end the turn this process executed.
3079
+ */
3080
+ async abandon(runId) {
3081
+ const watch = this.forget(runId);
3082
+ if (watch !== void 0) this.ended(watch, "interrupted", void 0);
3083
+ }
3084
+ /**
3085
+ * Let go of a turn this process executed without settling it here: it was cancelled while parked
3086
+ * (its session ended under it, `interrupted`), another worker took it over, or this process shuts
3087
+ * down (`released`). The per-turn state goes, `onTurnEnd` fires once, and a later stop of the turn
3088
+ * here is ignored. Nothing settles the run here afterwards: whoever answered or cancelled it does.
3089
+ */
3090
+ release(runId, status) {
3091
+ const watch = this.watches.get(runId);
3092
+ if (watch === void 0) return;
3093
+ const seen = this.live.get(runId)?.turn.totalUsage();
3094
+ if (seen !== void 0) this.keepReleasedUsage(runId, seen);
3095
+ this.drop(runId);
3096
+ watch.settled = true;
3097
+ this.watches.delete(runId);
3098
+ for (const [key, owner] of this.owners) if (owner === runId) this.owners.delete(key);
3099
+ this.ended(watch, status, void 0);
2908
3100
  }
2909
3101
  // ---- settle --------------------------------------------------------------------------------
2910
3102
  /** A person was asked something: tell the host (cards in other channels, wake-ups). */
@@ -2952,7 +3144,7 @@ ${body}
2952
3144
  const ui = amendment.ui ?? [];
2953
3145
  const last = result.messages.at(-1);
2954
3146
  if (ui.length > 0) {
2955
- for (const component of ui) await writer.write((0, import_nestjs_agent_core4.encodeStreamEvent)({
3147
+ for (const component of ui) await writer.write((0, import_nestjs_agent_core5.encodeStreamEvent)({
2956
3148
  kind: "ui",
2957
3149
  ...component
2958
3150
  }));
@@ -2977,7 +3169,7 @@ ${body}
2977
3169
  * two milestones.
2978
3170
  */
2979
3171
  spentBy(runId, spent) {
2980
- const seen = this.live.get(runId)?.turn.totalUsage();
3172
+ const seen = this.live.get(runId)?.turn.totalUsage() ?? this.releasedUsage.get(runId);
2981
3173
  if (spent === void 0) return seen ?? emptyUsage();
2982
3174
  if (seen === void 0) return spent;
2983
3175
  const tokens = /* @__PURE__ */ __name((u) => u.inputTokens + u.outputTokens, "tokens");
@@ -2987,30 +3179,30 @@ ${body}
2987
3179
  * End the run's stream and bookkeeping for how its execution ended. `spent` is what the run spent
2988
3180
  * (the sum of its milestones' `usage`); omit → what the turn this process follows saw.
2989
3181
  */
2990
- async settle(runId, input, outcome, durationMs, spent) {
3182
+ async settle(runId, input, ended, durationMs, spent) {
3183
+ const outcome = this.withStop(ended, this.watches.get(runId));
2991
3184
  if (outcome.status === "failed") {
2992
- await this.settleFailed(runId, input, outcome.error, durationMs, spent);
2993
- return;
3185
+ return this.settleFailed(runId, input, outcome.error, durationMs, spent, outcome.stop);
2994
3186
  }
2995
3187
  const usage = this.spentBy(runId, spent);
2996
- this.forget(runId);
3188
+ const watch = this.forget(runId);
2997
3189
  const writer = await this.sink.open(runId);
2998
3190
  const result = await this.runResult(runId, input, outcome, durationMs, usage);
2999
3191
  await this.amend(writer, result);
3000
3192
  if (outcome.status === "interrupted") {
3001
3193
  this.logger.log(`agent run ${runId} cancelled`);
3002
- await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, import_nestjs_agent_core4.RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3194
+ await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, import_nestjs_agent_core5.RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3003
3195
  await this.handoff(writer, input, runId, "cancelled");
3004
- await (0, import_nestjs_agent_core4.releaseThreadRun)(this.store, input.threadId, runId);
3005
- await writer.write((0, import_nestjs_agent_core4.encodeStreamEvent)({
3196
+ await (0, import_nestjs_agent_core5.releaseThreadRun)(this.store, input.threadId, runId);
3197
+ await writer.write((0, import_nestjs_agent_core5.encodeStreamEvent)({
3006
3198
  kind: "cancelled"
3007
3199
  }));
3008
3200
  await writer.end();
3009
- (0, import_nestjs_agent_core4.publishAgentRunCancelled)({
3201
+ (0, import_nestjs_agent_core5.publishAgentRunCancelled)({
3010
3202
  runId,
3011
3203
  threadId: input.threadId,
3012
3204
  durationMs,
3013
- context: (0, import_nestjs_agent_core4.agentRunContext)(input)
3205
+ context: (0, import_nestjs_agent_core5.agentRunContext)(input)
3014
3206
  });
3015
3207
  await this.store.recordRunEnd?.({
3016
3208
  runId,
@@ -3019,7 +3211,7 @@ ${body}
3019
3211
  });
3020
3212
  } else {
3021
3213
  await this.handoff(writer, input, runId, "completed");
3022
- await (0, import_nestjs_agent_core4.releaseThreadRun)(this.store, input.threadId, runId);
3214
+ await (0, import_nestjs_agent_core5.releaseThreadRun)(this.store, input.threadId, runId);
3023
3215
  await writer.end();
3024
3216
  await this.store.recordRunEnd?.({
3025
3217
  runId,
@@ -3028,47 +3220,402 @@ ${body}
3028
3220
  });
3029
3221
  }
3030
3222
  await this.settled(result);
3223
+ if (watch !== void 0) this.ended(watch, outcome.status, outcome.status === "succeeded" ? outcome.stop : void 0);
3224
+ return outcome;
3031
3225
  }
3032
- async settleFailed(runId, input, error, durationMs = 0, spent) {
3226
+ async settleFailed(runId, input, error, durationMs = 0, spent, stop) {
3033
3227
  const usage = this.spentBy(runId, spent);
3034
- this.forget(runId);
3228
+ const watch = this.forget(runId);
3229
+ const stopped = stop ?? watch?.stop;
3035
3230
  const writer = await this.sink.open(runId);
3036
3231
  const result = await this.runResult(runId, input, {
3037
3232
  status: "failed",
3038
- error
3233
+ error,
3234
+ ...stopped !== void 0 ? {
3235
+ stop: stopped
3236
+ } : {}
3039
3237
  }, durationMs, usage);
3040
3238
  const worded = (await this.amend(writer, result)).error;
3041
- const message = worded ?? (0, import_nestjs_agent_core4.streamFailure)(new Error(error)).message;
3042
- this.logger.error(`agent run ${runId} failed: ${error}`);
3043
- (0, import_nestjs_agent_core4.publishAgentRunFailed)({
3239
+ const message = worded ?? (stopped !== void 0 ? error : (0, import_nestjs_agent_core5.streamFailure)(new Error(error)).message);
3240
+ const code = stopped !== void 0 ? STOPPED_ERROR_CODE : "opencode_failed";
3241
+ if (stopped !== void 0) this.logger.log(`agent run ${runId} stopped: ${stopped.reason}`);
3242
+ else this.logger.error(`agent run ${runId} failed: ${error}`);
3243
+ (0, import_nestjs_agent_core5.publishAgentRunFailed)({
3044
3244
  runId,
3045
3245
  threadId: input.threadId,
3046
- code: "opencode_failed",
3246
+ code,
3047
3247
  message: error,
3048
3248
  durationMs,
3049
- context: (0, import_nestjs_agent_core4.agentRunContext)(input)
3249
+ context: (0, import_nestjs_agent_core5.agentRunContext)(input)
3050
3250
  });
3051
3251
  await this.store.recordRunEnd?.({
3052
3252
  runId,
3053
3253
  status: "failed",
3054
3254
  durationMs,
3055
- errorCode: "opencode_failed",
3255
+ errorCode: code,
3056
3256
  errorMessage: error
3057
3257
  });
3058
- await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, import_nestjs_agent_core4.RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3258
+ await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, import_nestjs_agent_core5.RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3059
3259
  await this.handoff(writer, input, runId, "failed", error);
3060
- await (0, import_nestjs_agent_core4.releaseThreadRun)(this.store, input.threadId, runId);
3260
+ await (0, import_nestjs_agent_core5.releaseThreadRun)(this.store, input.threadId, runId);
3061
3261
  await writer.fail({
3062
- code: "opencode_failed",
3262
+ code,
3063
3263
  message
3064
3264
  });
3065
3265
  await this.settled({
3066
3266
  ...result,
3067
3267
  outcome: {
3068
3268
  status: "failed",
3069
- error: worded ?? error
3269
+ error: worded ?? error,
3270
+ ...stopped !== void 0 ? {
3271
+ stop: stopped
3272
+ } : {}
3273
+ }
3274
+ });
3275
+ if (watch !== void 0) this.ended(watch, "failed", stopped);
3276
+ return {
3277
+ status: "failed",
3278
+ error,
3279
+ ...stopped !== void 0 ? {
3280
+ stop: stopped
3281
+ } : {}
3282
+ };
3283
+ }
3284
+ // ---- limits, stops and the turn's lifecycle -------------------------------------------------
3285
+ /**
3286
+ * Stop a turn's own work: abort its session so OpenCode starts no new round, only while the turn
3287
+ * still owns the session (a thread's session is reused by its next turn) — checked immediately
3288
+ * before the abort. Acts in the process executing the turn; see {@link OpenCodeStopResult}. The
3289
+ * turn ends failed with `outcome.stop` (code `opencode_stopped`), and `onStop` is called.
3290
+ */
3291
+ stopTurn(turnId, reason) {
3292
+ return this.requestStop(turnId, reason);
3293
+ }
3294
+ /** The turn this process executes, made known here on its first step in this process. */
3295
+ async adopt(runId, input, handle, resumed, counts, startedAt) {
3296
+ const known = this.watches.get(runId);
3297
+ if (known !== void 0) return known;
3298
+ const watch = {
3299
+ runId,
3300
+ input,
3301
+ handle,
3302
+ limits: this.limitsFor(input),
3303
+ startedAt: handle.startedAt ?? startedAt ?? Date.now(),
3304
+ steps: counts?.steps ?? 0,
3305
+ compactions: counts?.compactions ?? 0,
3306
+ observing: false,
3307
+ ending: false,
3308
+ settled: false,
3309
+ aborted: false,
3310
+ checking: false,
3311
+ lastRead: 0
3312
+ };
3313
+ this.watches.set(runId, watch);
3314
+ await this.hook("onTurnStart", {
3315
+ turnId: runId,
3316
+ threadId: input.threadId,
3317
+ sessionId: handle.sessionId,
3318
+ resumed
3319
+ });
3320
+ return watch;
3321
+ }
3322
+ limitsFor(input) {
3323
+ const limits = this.settings.limits;
3324
+ return cleanLimits(typeof limits === "function" ? limits(input) : limits);
3325
+ }
3326
+ /**
3327
+ * One event of the turn's session: counted for the limits, and told to `onTurnEvent`. While the
3328
+ * turn is parked on a person here, it may also say the turn went on without this process
3329
+ * ({@link wentOnElsewhere}).
3330
+ */
3331
+ sawEvent(runId, sessionId, event) {
3332
+ if (sessionOf(event) !== sessionId) return;
3333
+ const watch = this.watches.get(runId);
3334
+ if (watch === void 0 || watch.settled || watch.handle.sessionId !== sessionId) return;
3335
+ if (watch.parkedOn !== void 0 && this.wentOnElsewhere(watch, event)) return;
3336
+ let reached;
3337
+ if (event.type.startsWith("session.execution.")) watch.ending = true;
3338
+ else if (event.type === "session.step.started") {
3339
+ watch.steps += 1;
3340
+ if (passed(watch.steps, watch.limits.steps)) reached = "steps";
3341
+ } else if (event.type === "session.compaction.started") {
3342
+ watch.compactions += 1;
3343
+ if (passed(watch.compactions, watch.limits.compactions)) reached = "compactions";
3344
+ }
3345
+ this.hook("onTurnEvent", {
3346
+ turnId: runId,
3347
+ type: event.type,
3348
+ sessionId
3349
+ });
3350
+ if (reached !== void 0) void this.limitStop(runId, reached);
3351
+ }
3352
+ /**
3353
+ * Whether a turn parked here went on without this process, which then lets it go. While a tool of
3354
+ * the step waits on a person, OpenCode keeps running the step's other tools, so their events say
3355
+ * nothing. What does: a new round (`session.step.started`, which cannot begin while the step
3356
+ * waits), or the store showing the parked ask decided by someone else (another worker answered
3357
+ * it, or a cancel elsewhere settled it) — this process clears the park before it answers. When
3358
+ * the session's execution ends while parked, the store is asked again for a while, as a cancel
3359
+ * elsewhere settles the ask just after it interrupted the session; with the ask still open, the
3360
+ * turn stays here for its answer.
3361
+ */
3362
+ wentOnElsewhere(watch, event) {
3363
+ if (event.type === "session.step.started") {
3364
+ this.release(watch.runId, "released");
3365
+ return true;
3366
+ }
3367
+ const ended = event.type.startsWith("session.execution.");
3368
+ if (ended) watch.parkedEnd = event.type === "session.execution.interrupted" ? "interrupted" : "released";
3369
+ this.checkParked(watch, ended);
3370
+ return false;
3371
+ }
3372
+ /**
3373
+ * Ask the store whether the parked ask was decided elsewhere, and let the turn go when it was —
3374
+ * one read at a time, at most one every {@link PARKED_READ_GAP_MS}. `series`: the session's
3375
+ * execution ended, so the store is asked again over {@link parkedRechecksMs}. An execution that
3376
+ * was interrupted under a parked turn means a cancel elsewhere: when the store cannot say (it has
3377
+ * no `toolCallApproval`, the read fails, the ask is not on file) past the first recheck, or the
3378
+ * rechecks run out with the ask still open, the turn is let go as `interrupted` anyway.
3379
+ */
3380
+ checkParked(watch, series) {
3381
+ if (series) watch.series = true;
3382
+ if (watch.checking) {
3383
+ watch.again = true;
3384
+ return;
3385
+ }
3386
+ watch.checking = true;
3387
+ void this.runParkedChecks(watch);
3388
+ }
3389
+ /**
3390
+ * The checks of {@link checkParked}. `checking` is cleared in the same step the loop returns in,
3391
+ * so a check asked for afterwards starts a new loop; one asked for while it ran (`again`) but not
3392
+ * read because the loop returned starts one too.
3393
+ */
3394
+ async runParkedChecks(watch) {
3395
+ try {
3396
+ await this.parkedChecks(watch);
3397
+ } finally {
3398
+ watch.checking = false;
3399
+ if (watch.again && !watch.settled && watch.parkedOn !== void 0) {
3400
+ watch.checking = true;
3401
+ void this.runParkedChecks(watch);
3070
3402
  }
3403
+ }
3404
+ }
3405
+ async parkedChecks(watch) {
3406
+ const ask = watch.parkedOn;
3407
+ const gone = /* @__PURE__ */ __name(() => watch.settled || ask === void 0 || watch.parkedOn !== ask, "gone");
3408
+ let step = 0;
3409
+ for (; ; ) {
3410
+ watch.again = false;
3411
+ const wait = watch.lastRead + PARKED_READ_GAP_MS - Date.now();
3412
+ if (wait > 0) await sleep(wait);
3413
+ if (gone()) return;
3414
+ const answer = await this.parkedAsk(ask);
3415
+ watch.lastRead = Date.now();
3416
+ if (gone()) return;
3417
+ if (answer === "decided") {
3418
+ this.release(watch.runId, watch.parkedEnd ?? "released");
3419
+ return;
3420
+ }
3421
+ if (watch.series) {
3422
+ const cancelled = watch.parkedEnd === "interrupted";
3423
+ if (cancelled && answer === "unknown" && step > 0) {
3424
+ this.release(watch.runId, "interrupted");
3425
+ return;
3426
+ }
3427
+ const gap = this.parkedRechecksMs[step];
3428
+ step += 1;
3429
+ if (gap === void 0) {
3430
+ if (cancelled) this.release(watch.runId, "interrupted");
3431
+ return;
3432
+ }
3433
+ await sleep(gap);
3434
+ continue;
3435
+ }
3436
+ if (!watch.again) return;
3437
+ }
3438
+ }
3439
+ /** What the store says of an ask: decided, still open, or nothing it can tell. */
3440
+ async parkedAsk(id) {
3441
+ if (this.store.toolCallApproval === void 0) return "unknown";
3442
+ try {
3443
+ const state = await this.store.toolCallApproval(id);
3444
+ if (state == null) return "unknown";
3445
+ return state.status === "pending_approval" ? "open" : "decided";
3446
+ } catch {
3447
+ return "unknown";
3448
+ }
3449
+ }
3450
+ /**
3451
+ * The waits between the store's rereads of a parked ask after its execution ended (cumulative
3452
+ * 0.25, 1.25, 4.25, 14.25 and 44.25 seconds after the first). Overridden by tests only.
3453
+ *
3454
+ * @internal
3455
+ */
3456
+ parkedRechecksMs = PARKED_RECHECKS_MS;
3457
+ /** A limit's stop: what it did is the engine's to log, there is no caller to tell. */
3458
+ async limitStop(runId, reason) {
3459
+ const result = await this.requestStop(runId, reason);
3460
+ if (result === "unconfirmed") this.logger.warn(`run ${runId}: passed its ${reason} limit, but its owner could not be read`);
3461
+ }
3462
+ /** The wall-clock bound of an observation: a timer for what is left of it, if anything. */
3463
+ watchClock(watch) {
3464
+ const max = watch.limits.wallClockMs;
3465
+ if (max === void 0) return void 0;
3466
+ const left = watch.startedAt + max - Date.now();
3467
+ if (left <= 0) {
3468
+ watch.pending ??= "wall-clock";
3469
+ return void 0;
3470
+ }
3471
+ const timer = setTimeout(() => void this.limitStop(watch.runId, "wall-clock"), left);
3472
+ timer.unref?.();
3473
+ return timer;
3474
+ }
3475
+ counts(watch) {
3476
+ return {
3477
+ steps: watch.steps,
3478
+ compactions: watch.compactions,
3479
+ elapsedMs: Date.now() - watch.startedAt
3480
+ };
3481
+ }
3482
+ async requestStop(runId, reason) {
3483
+ const watch = this.watches.get(runId);
3484
+ if (watch === void 0 || watch.settled || watch.stop !== void 0) return "ignored";
3485
+ const live = this.live.get(runId);
3486
+ if (watch.parkedOn !== void 0) {
3487
+ watch.pending ??= reason;
3488
+ return "pending";
3489
+ }
3490
+ if (watch.ending || live?.turn.finished === true) return this.landed(watch, reason);
3491
+ if (!watch.observing || live === void 0) {
3492
+ watch.pending ??= reason;
3493
+ return "pending";
3494
+ }
3495
+ watch.pending = void 0;
3496
+ let active;
3497
+ try {
3498
+ active = await this.activeRun(watch.input.threadId);
3499
+ } catch (error) {
3500
+ this.logger.warn(`run ${runId}: could not read the thread's run: ${errorText(error, "error")}`);
3501
+ return "unconfirmed";
3502
+ }
3503
+ if (watch.settled || watch.stop !== void 0 || this.live.get(runId) !== live) return "ignored";
3504
+ if (active !== null && active !== runId) return "ignored";
3505
+ if (this.owners.get(ownerKey(watch.handle)) !== runId) return "ignored";
3506
+ if (watch.ending || live.turn.finished) return this.landed(watch, reason);
3507
+ const stop = {
3508
+ reason,
3509
+ counts: this.counts(watch)
3510
+ };
3511
+ watch.stop = stop;
3512
+ watch.aborted = true;
3513
+ try {
3514
+ await live.client.session.interrupt({
3515
+ sessionID: watch.handle.sessionId
3516
+ });
3517
+ } catch (error) {
3518
+ this.logger.warn(`run ${runId}: could not abort the session: ${errorText(error, "error")}`);
3519
+ }
3520
+ this.hook("onStop", {
3521
+ turnId: runId,
3522
+ reason,
3523
+ counts: stop.counts
3071
3524
  });
3525
+ return "aborted";
3526
+ }
3527
+ /** A stop that arrived while the answer settles: recorded and reported, the answer kept. */
3528
+ landed(watch, reason) {
3529
+ const stop = {
3530
+ reason,
3531
+ counts: this.counts(watch)
3532
+ };
3533
+ watch.stop = stop;
3534
+ watch.pending = void 0;
3535
+ this.hook("onStop", {
3536
+ turnId: watch.runId,
3537
+ reason,
3538
+ counts: stop.counts
3539
+ });
3540
+ return "settling";
3541
+ }
3542
+ /** The milestone as the runner journals it: the limits' counts on an ask, the stop on an end. */
3543
+ marked(watch, milestone) {
3544
+ if (milestone.kind === "finished") return {
3545
+ ...milestone,
3546
+ outcome: this.withStop(milestone.outcome, watch)
3547
+ };
3548
+ if (milestone.progress === void 0) return milestone;
3549
+ const counts = {
3550
+ steps: watch.steps,
3551
+ compactions: watch.compactions
3552
+ };
3553
+ return {
3554
+ ...milestone,
3555
+ progress: {
3556
+ ...milestone.progress,
3557
+ counts
3558
+ }
3559
+ };
3560
+ }
3561
+ /**
3562
+ * How a turn a stop landed on ends. An answer that settled anyway is kept, with the stop on it; an
3563
+ * execution that failed on its own keeps its error, with the stop on it; an execution the stop
3564
+ * aborted ends failed with the stop's own text. One interrupted by a person's cancel stays so.
3565
+ */
3566
+ withStop(outcome, watch) {
3567
+ const stop = watch?.stop;
3568
+ if (stop === void 0) return outcome;
3569
+ if (outcome.status !== "interrupted") return outcome.stop !== void 0 ? outcome : {
3570
+ ...outcome,
3571
+ stop
3572
+ };
3573
+ if (!watch?.aborted) return outcome;
3574
+ return {
3575
+ status: "failed",
3576
+ error: stopErrorText(stop),
3577
+ stop
3578
+ };
3579
+ }
3580
+ /** `onTurnEnd`, once per turn this process executed; not waited for. */
3581
+ ended(watch, status, stop) {
3582
+ this.hook("onTurnEnd", {
3583
+ turnId: watch.runId,
3584
+ status,
3585
+ ...stop !== void 0 ? {
3586
+ stop
3587
+ } : {}
3588
+ });
3589
+ }
3590
+ /**
3591
+ * A lifecycle hook, from the settings and the host, called at once. The promise settles when both
3592
+ * have, or after `hookTimeoutMs` (default 5 s) — only `onTurnStart` is waited for. Errors are
3593
+ * logged, never the turn's.
3594
+ */
3595
+ hook(name, event) {
3596
+ const calls = [];
3597
+ for (const target of [
3598
+ this.settings,
3599
+ this.host
3600
+ ]) {
3601
+ const fn = target[name];
3602
+ if (typeof fn !== "function") continue;
3603
+ calls.push(Promise.resolve().then(() => fn.call(target, event)).catch((error) => this.logger.warn(`${name} failed: ${errorText(error, "error")}`)));
3604
+ }
3605
+ if (calls.length === 0) return Promise.resolve();
3606
+ const ms = this.settings.hookTimeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
3607
+ let timer;
3608
+ const late = new Promise((resolve) => {
3609
+ timer = setTimeout(() => {
3610
+ this.logger.warn(`${name} took longer than ${ms}ms; the turn went on`);
3611
+ resolve();
3612
+ }, ms);
3613
+ timer.unref?.();
3614
+ });
3615
+ return Promise.race([
3616
+ Promise.all(calls).then(() => void 0),
3617
+ late
3618
+ ]).finally(() => clearTimeout(timer));
3072
3619
  }
3073
3620
  /** Starts the next queued message of the thread; set by the runner (it owns `start`). */
3074
3621
  startNext;
@@ -3087,7 +3634,7 @@ ${body}
3087
3634
  error
3088
3635
  } : {}
3089
3636
  }, (next, nextRunId) => startNext(next, nextRunId));
3090
- if (frame !== void 0) await writer.write((0, import_nestjs_agent_core4.encodeStreamEvent)(frame));
3637
+ if (frame !== void 0) await writer.write((0, import_nestjs_agent_core5.encodeStreamEvent)(frame));
3091
3638
  } catch (failure) {
3092
3639
  this.logger.error(`could not advance the queue of thread ${input.threadId} after run ${runId}: ${failure instanceof Error ? failure.message : String(failure)}`);
3093
3640
  }
@@ -3098,22 +3645,22 @@ OpenCodeTurns = _ts_decorate2([
3098
3645
  _ts_param2(0, (0, import_common4.Inject)(OPENCODE_HOST)),
3099
3646
  _ts_param2(1, (0, import_common4.Inject)(OPENCODE_SESSIONS)),
3100
3647
  _ts_param2(2, (0, import_common4.Inject)(OPENCODE_OPTIONS)),
3101
- _ts_param2(3, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_STORE)),
3102
- _ts_param2(4, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_SINK)),
3103
- _ts_param2(5, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_DEPS_FACTORY)),
3104
- _ts_param2(6, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_TOOL_REGISTRY)),
3648
+ _ts_param2(3, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_STORE)),
3649
+ _ts_param2(4, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_SINK)),
3650
+ _ts_param2(5, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_DEPS_FACTORY)),
3651
+ _ts_param2(6, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_TOOL_REGISTRY)),
3105
3652
  _ts_param2(7, (0, import_common4.Optional)()),
3106
- _ts_param2(7, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_SKILLS)),
3653
+ _ts_param2(7, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_SKILLS)),
3107
3654
  _ts_param2(8, (0, import_common4.Optional)()),
3108
- _ts_param2(8, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_MEMORY)),
3655
+ _ts_param2(8, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_MEMORY)),
3109
3656
  _ts_param2(9, (0, import_common4.Optional)()),
3110
3657
  _ts_param2(9, (0, import_common4.Inject)(import_nestjs_agent.AGENT_CHAT_QUEUE)),
3111
3658
  _ts_param2(10, (0, import_common4.Optional)()),
3112
3659
  _ts_param2(10, (0, import_common4.Inject)(OPENCODE_TOOLS_TOKENS)),
3113
3660
  _ts_param2(11, (0, import_common4.Optional)()),
3114
- _ts_param2(11, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_PRICING_STORE)),
3661
+ _ts_param2(11, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_PRICING_STORE)),
3115
3662
  _ts_param2(12, (0, import_common4.Optional)()),
3116
- _ts_param2(12, (0, import_common4.Inject)(import_nestjs_agent_core4.AGENT_OPTIONS)),
3663
+ _ts_param2(12, (0, import_common4.Inject)(import_nestjs_agent_core5.AGENT_OPTIONS)),
3117
3664
  _ts_metadata2("design:type", Function),
3118
3665
  _ts_metadata2("design:paramtypes", [
3119
3666
  typeof OpenCodeHost === "undefined" ? Object : OpenCodeHost,
@@ -3131,6 +3678,28 @@ OpenCodeTurns = _ts_decorate2([
3131
3678
  Object
3132
3679
  ])
3133
3680
  ], OpenCodeTurns);
3681
+ function passed(count, limit) {
3682
+ return limit !== void 0 && count > limit;
3683
+ }
3684
+ __name(passed, "passed");
3685
+ function sleep(ms) {
3686
+ return new Promise((resolve) => setTimeout(resolve, ms).unref?.());
3687
+ }
3688
+ __name(sleep, "sleep");
3689
+ function refusalOf(ask) {
3690
+ return ask.kind === "approval" ? {
3691
+ approved: false,
3692
+ reason: "The turn was stopped."
3693
+ } : {
3694
+ answers: {},
3695
+ skipped: true
3696
+ };
3697
+ }
3698
+ __name(refusalOf, "refusalOf");
3699
+ function ownerKey(handle) {
3700
+ return `${handle.serverKey}|${handle.sessionId}`;
3701
+ }
3702
+ __name(ownerKey, "ownerKey");
3134
3703
  function titleOf(text) {
3135
3704
  const trimmed = text.trim().replace(/\s+/g, " ");
3136
3705
  return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed || "New chat";
@@ -3175,7 +3744,7 @@ function priceRef(model) {
3175
3744
  model.id
3176
3745
  ];
3177
3746
  const tail = model.id.slice(model.id.lastIndexOf("/") + 1);
3178
- if (tail !== model.id && (0, import_nestjs_agent_core4.parseBedrockModelId)(tail) !== void 0) aliases.push(tail);
3747
+ if (tail !== model.id && (0, import_nestjs_agent_core5.parseBedrockModelId)(tail) !== void 0) aliases.push(tail);
3179
3748
  return {
3180
3749
  modelId: `${model.providerID}/${model.id}`,
3181
3750
  provider: model.providerID,
@@ -3275,7 +3844,7 @@ var OpenCodeAgentRunner = class {
3275
3844
  ]) {
3276
3845
  if (!key.startsWith(`${runId}:`)) continue;
3277
3846
  this.pending.delete(key);
3278
- waiter.reject(new import_nestjs_agent_core5.RunCancelledError());
3847
+ waiter.reject(new import_nestjs_agent_core6.RunCancelledError());
3279
3848
  }
3280
3849
  const input = this.inputs.get(runId);
3281
3850
  if (input !== void 0) await this.turns.interrupt(input).catch(() => void 0);
@@ -3287,7 +3856,7 @@ var OpenCodeAgentRunner = class {
3287
3856
  let spent = emptyUsage();
3288
3857
  try {
3289
3858
  let handle = await this.turns.begin(runId, input);
3290
- if (this.cancelled.has(runId)) throw new import_nestjs_agent_core5.RunCancelledError();
3859
+ if (this.cancelled.has(runId)) throw new import_nestjs_agent_core6.RunCancelledError();
3291
3860
  await this.turns.prompt(runId, input, handle, spent);
3292
3861
  for (; ; ) {
3293
3862
  const milestone = await this.turns.observe(runId, input, handle, spent);
@@ -3300,11 +3869,11 @@ var OpenCodeAgentRunner = class {
3300
3869
  handle = await this.turns.reply(runId, input, handle, milestone.ask, reply, spent);
3301
3870
  }
3302
3871
  } catch (error) {
3303
- if (error instanceof import_nestjs_agent_core5.RunCancelledError) {
3872
+ if (error instanceof import_nestjs_agent_core6.RunCancelledError) {
3304
3873
  await this.turns.interrupt(input).catch(() => void 0);
3305
3874
  await this.turns.settle(runId, input, {
3306
3875
  status: "interrupted"
3307
- }, Date.now() - started);
3876
+ }, Date.now() - started, spent);
3308
3877
  return;
3309
3878
  }
3310
3879
  await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"), Date.now() - started);
@@ -3428,7 +3997,7 @@ __name(openCodeProviders, "openCodeProviders");
3428
3997
 
3429
3998
  // src/durable/runner.ts
3430
3999
  var import_node_crypto3 = require("crypto");
3431
- var import_nestjs_agent_core6 = require("@dudousxd/nestjs-agent-core");
4000
+ var import_nestjs_agent_core7 = require("@dudousxd/nestjs-agent-core");
3432
4001
  var import_nestjs_durable2 = require("@dudousxd/nestjs-durable");
3433
4002
  var import_nestjs_durable_core2 = require("@dudousxd/nestjs-durable-core");
3434
4003
  var import_common8 = require("@nestjs/common");
@@ -3498,14 +4067,14 @@ var OpenCodeRunWorkflow = class {
3498
4067
  spent = addUsage(spent, milestone.usage);
3499
4068
  const total = spent;
3500
4069
  if (milestone.kind === "finished") {
3501
- await ctx.localStep("finish", async () => {
3502
- if (await this.cancelled(ctx.runId)) this.turns.drop(ctx.runId);
3503
- else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
3504
- return true;
4070
+ const settled = await ctx.localStep("finish", async () => {
4071
+ if (await this.cancelled(ctx.runId)) {
4072
+ await this.turns.abandon(ctx.runId);
4073
+ return milestone.outcome;
4074
+ }
4075
+ return this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
3505
4076
  });
3506
- return {
3507
- outcome: milestone.outcome.status
3508
- };
4077
+ return resultOf(settled === true ? milestone.outcome : settled);
3509
4078
  }
3510
4079
  let reply;
3511
4080
  try {
@@ -3526,13 +4095,10 @@ var OpenCodeRunWorkflow = class {
3526
4095
  }
3527
4096
  } catch (error) {
3528
4097
  if ((0, import_nestjs_durable_core.isWorkflowControlFlowSignal)(error)) throw error;
3529
- await ctx.localStep("fail", async () => {
3530
- await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent);
3531
- return true;
3532
- });
3533
- return {
4098
+ const settled = await ctx.localStep("fail", async () => this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent));
4099
+ return settled === true ? {
3534
4100
  outcome: "failed"
3535
- };
4101
+ } : resultOf(settled);
3536
4102
  }
3537
4103
  }
3538
4104
  async cancelled(runId) {
@@ -3558,6 +4124,16 @@ OpenCodeRunWorkflow = _ts_decorate4([
3558
4124
  typeof RunGateway === "undefined" ? Object : RunGateway
3559
4125
  ])
3560
4126
  ], OpenCodeRunWorkflow);
4127
+ function resultOf(outcome) {
4128
+ const stop = outcome.status === "interrupted" ? void 0 : outcome.stop;
4129
+ return {
4130
+ outcome: outcome.status,
4131
+ ...stop !== void 0 ? {
4132
+ stop: stop.reason
4133
+ } : {}
4134
+ };
4135
+ }
4136
+ __name(resultOf, "resultOf");
3561
4137
 
3562
4138
  // src/durable/runner.ts
3563
4139
  function _ts_decorate5(decorators, target, key, desc) {
@@ -3677,7 +4253,7 @@ DurableOpenCodeAgentRunner = _ts_decorate5([
3677
4253
  _ts_param5(1, (0, import_common8.Inject)(import_nestjs_durable2.RUN_GATEWAY)),
3678
4254
  _ts_param5(3, (0, import_common8.Inject)(OPENCODE_OPTIONS)),
3679
4255
  _ts_param5(4, (0, import_common8.Inject)(OPENCODE_HOST)),
3680
- _ts_param5(5, (0, import_common8.Inject)(import_nestjs_agent_core6.AGENT_STORE)),
4256
+ _ts_param5(5, (0, import_common8.Inject)(import_nestjs_agent_core7.AGENT_STORE)),
3681
4257
  _ts_metadata5("design:type", Function),
3682
4258
  _ts_metadata5("design:paramtypes", [
3683
4259
  typeof import_nestjs_durable2.WorkflowService === "undefined" ? Object : import_nestjs_durable2.WorkflowService,