@dudousxd/nestjs-agent-opencode 1.0.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.
@@ -2009,6 +2009,37 @@ var OpenCodeEventHub = class _OpenCodeEventHub {
2009
2009
  }
2010
2010
  };
2011
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
+
2012
2043
  // src/turns.ts
2013
2044
  function _ts_decorate2(decorators, target, key, desc) {
2014
2045
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2038,6 +2069,16 @@ function _ts_param2(paramIndex, decorator) {
2038
2069
  __name(_ts_param2, "_ts_param");
2039
2070
  var DEFAULT_TURN_TIMEOUT_MS = 30 * 6e4;
2040
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;
2041
2082
  var OpenCodeTurns = class {
2042
2083
  static {
2043
2084
  __name(this, "OpenCodeTurns");
@@ -2061,6 +2102,21 @@ var OpenCodeTurns = class {
2061
2102
  spent = /* @__PURE__ */ new Set();
2062
2103
  /** When the tools endpoint was last registered for a location (`server|boot|directory|actor`). */
2063
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();
2064
2120
  constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2, pricingStore, agentOptions) {
2065
2121
  this.host = host;
2066
2122
  this.sessions = sessions;
@@ -2073,6 +2129,10 @@ var OpenCodeTurns = class {
2073
2129
  this.memory = memory;
2074
2130
  this.queue = queue;
2075
2131
  this.toolsTokens = toolsTokens2;
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
+ }
2076
2136
  this.prices = new import_nestjs_agent_core5.ModelPriceResolver({
2077
2137
  pricingStore,
2078
2138
  catalog: agentOptions?.priceCatalog,
@@ -2344,8 +2404,12 @@ var OpenCodeTurns = class {
2344
2404
  }
2345
2405
  }
2346
2406
  onApplicationShutdown() {
2407
+ for (const runId of [
2408
+ ...this.watches.keys()
2409
+ ]) this.release(runId, "released");
2347
2410
  for (const live of this.live.values()) live.stop();
2348
2411
  this.live.clear();
2412
+ this.owners.clear();
2349
2413
  this.events.close();
2350
2414
  }
2351
2415
  // ---- begin ---------------------------------------------------------------------------------
@@ -2355,7 +2419,8 @@ var OpenCodeTurns = class {
2355
2419
  * durable `begin` re-run after a crash): the run's user message is written once.
2356
2420
  */
2357
2421
  async begin(runId, input) {
2358
- 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);
2359
2424
  const server = await this.host.server(input.actor);
2360
2425
  const known = await this.sessions.get(input.threadId);
2361
2426
  const reusable = known !== null && known.serverKey === server.key && known.bootId === server.bootId && (await this.host.reuse?.({
@@ -2401,7 +2466,12 @@ var OpenCodeTurns = class {
2401
2466
  created
2402
2467
  });
2403
2468
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2404
- 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;
2405
2475
  }
2406
2476
  async createSession(runId, input, server, earlier, note) {
2407
2477
  const { client } = server;
@@ -2471,10 +2541,16 @@ ${transcript}` : "",
2471
2541
  }
2472
2542
  const firstDropped = messages[lastUser + 1];
2473
2543
  if (firstDropped !== void 0) await this.store.truncateFrom(input.threadId, firstDropped.id);
2474
- return messages.slice(0, Math.max(lastUser, 0));
2544
+ return {
2545
+ earlier: messages.slice(0, Math.max(lastUser, 0)),
2546
+ rerun: false
2547
+ };
2475
2548
  }
2476
2549
  const mine = messages.findIndex((m) => m.role === "user" && m.runId === runId);
2477
- if (mine >= 0) return messages.slice(0, mine);
2550
+ if (mine >= 0) return {
2551
+ earlier: messages.slice(0, mine),
2552
+ rerun: true
2553
+ };
2478
2554
  await this.store.appendMessage({
2479
2555
  threadId: input.threadId,
2480
2556
  role: "user",
@@ -2490,7 +2566,10 @@ ${transcript}` : "",
2490
2566
  if (thread !== null && (thread.title === "" || thread.title === "New chat")) {
2491
2567
  await this.store.setTitle(input.threadId, titleOf(input.userText));
2492
2568
  }
2493
- return messages;
2569
+ return {
2570
+ earlier: messages,
2571
+ rerun: false
2572
+ };
2494
2573
  }
2495
2574
  /** Rewind the session to before its last user message, so the regenerated answer replaces it. */
2496
2575
  async revertLastExchange(client, sessionId) {
@@ -2706,6 +2785,7 @@ ${body}
2706
2785
  // ---- prompt / observe / reply --------------------------------------------------------------
2707
2786
  /** Start listening to the session (so nothing it says is missed), then send the user message. */
2708
2787
  async prompt(runId, input, handle, spent) {
2788
+ await this.adopt(runId, input, handle, true);
2709
2789
  const live = await this.ensureLive(runId, input, handle, false, spent);
2710
2790
  try {
2711
2791
  const prompt = await this.host.promptFor?.({
@@ -2748,7 +2828,24 @@ ${body}
2748
2828
  * back from the session's messages, when the execution ends, what nobody heard after that.
2749
2829
  */
2750
2830
  async observe(runId, input, handle, spent, progress) {
2831
+ const watch = await this.adopt(runId, input, handle, true, progress?.counts);
2751
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) {
2752
2849
  const { turn, client } = live;
2753
2850
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2754
2851
  let done = false;
@@ -2788,19 +2885,64 @@ ${body}
2788
2885
  * to go on.
2789
2886
  */
2790
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;
2791
2894
  const server = await this.host.server(input.actor);
2792
2895
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2793
- 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);
2794
2898
  }
2795
2899
  const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2796
- await live.turn.decide(ask, reply);
2900
+ await live.turn.decide(ask, answer);
2797
2901
  return handle;
2798
2902
  }
2799
- async continueAfterRestart(runId, input, server, ask, reply, spent) {
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
+ });
2933
+ return handle;
2934
+ }
2935
+ async continueAfterRestart(runId, input, server, ask, reply, spent, startedAt) {
2800
2936
  const approved = "approved" in reply && reply.approved === true;
2801
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.`;
2802
2938
  const thread = await this.store.getThread(input.threadId);
2803
- 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
+ };
2804
2946
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2805
2947
  this.drop(runId);
2806
2948
  const live = await this.ensureLive(runId, input, handle, false, spent);
@@ -2885,7 +3027,11 @@ ${body}
2885
3027
  this.grants.set(runId, grants);
2886
3028
  }, "onGranted")
2887
3029
  });
2888
- 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
+ });
2889
3035
  const live = {
2890
3036
  turn,
2891
3037
  input,
@@ -2894,6 +3040,15 @@ ${body}
2894
3040
  stop
2895
3041
  };
2896
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
+ }
2897
3052
  if (resumed) await turn.catchUp().catch(() => void 0);
2898
3053
  return live;
2899
3054
  }
@@ -2902,11 +3057,46 @@ ${body}
2902
3057
  this.live.get(runId)?.stop();
2903
3058
  this.live.delete(runId);
2904
3059
  }
2905
- /** 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
+ */
2906
3064
  forget(runId) {
2907
3065
  this.drop(runId);
3066
+ this.releasedUsage.delete(runId);
2908
3067
  for (const grant of this.grants.get(runId) ?? []) this.spent.delete(grant.id);
2909
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);
2910
3100
  }
2911
3101
  // ---- settle --------------------------------------------------------------------------------
2912
3102
  /** A person was asked something: tell the host (cards in other channels, wake-ups). */
@@ -2979,7 +3169,7 @@ ${body}
2979
3169
  * two milestones.
2980
3170
  */
2981
3171
  spentBy(runId, spent) {
2982
- const seen = this.live.get(runId)?.turn.totalUsage();
3172
+ const seen = this.live.get(runId)?.turn.totalUsage() ?? this.releasedUsage.get(runId);
2983
3173
  if (spent === void 0) return seen ?? emptyUsage();
2984
3174
  if (seen === void 0) return spent;
2985
3175
  const tokens = /* @__PURE__ */ __name((u) => u.inputTokens + u.outputTokens, "tokens");
@@ -2989,13 +3179,13 @@ ${body}
2989
3179
  * End the run's stream and bookkeeping for how its execution ended. `spent` is what the run spent
2990
3180
  * (the sum of its milestones' `usage`); omit → what the turn this process follows saw.
2991
3181
  */
2992
- 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));
2993
3184
  if (outcome.status === "failed") {
2994
- await this.settleFailed(runId, input, outcome.error, durationMs, spent);
2995
- return;
3185
+ return this.settleFailed(runId, input, outcome.error, durationMs, spent, outcome.stop);
2996
3186
  }
2997
3187
  const usage = this.spentBy(runId, spent);
2998
- this.forget(runId);
3188
+ const watch = this.forget(runId);
2999
3189
  const writer = await this.sink.open(runId);
3000
3190
  const result = await this.runResult(runId, input, outcome, durationMs, usage);
3001
3191
  await this.amend(writer, result);
@@ -3030,22 +3220,30 @@ ${body}
3030
3220
  });
3031
3221
  }
3032
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;
3033
3225
  }
3034
- async settleFailed(runId, input, error, durationMs = 0, spent) {
3226
+ async settleFailed(runId, input, error, durationMs = 0, spent, stop) {
3035
3227
  const usage = this.spentBy(runId, spent);
3036
- this.forget(runId);
3228
+ const watch = this.forget(runId);
3229
+ const stopped = stop ?? watch?.stop;
3037
3230
  const writer = await this.sink.open(runId);
3038
3231
  const result = await this.runResult(runId, input, {
3039
3232
  status: "failed",
3040
- error
3233
+ error,
3234
+ ...stopped !== void 0 ? {
3235
+ stop: stopped
3236
+ } : {}
3041
3237
  }, durationMs, usage);
3042
3238
  const worded = (await this.amend(writer, result)).error;
3043
- const message = worded ?? (0, import_nestjs_agent_core5.streamFailure)(new Error(error)).message;
3044
- this.logger.error(`agent run ${runId} failed: ${error}`);
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}`);
3045
3243
  (0, import_nestjs_agent_core5.publishAgentRunFailed)({
3046
3244
  runId,
3047
3245
  threadId: input.threadId,
3048
- code: "opencode_failed",
3246
+ code,
3049
3247
  message: error,
3050
3248
  durationMs,
3051
3249
  context: (0, import_nestjs_agent_core5.agentRunContext)(input)
@@ -3054,23 +3252,370 @@ ${body}
3054
3252
  runId,
3055
3253
  status: "failed",
3056
3254
  durationMs,
3057
- errorCode: "opencode_failed",
3255
+ errorCode: code,
3058
3256
  errorMessage: error
3059
3257
  });
3060
3258
  await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, import_nestjs_agent_core5.RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3061
3259
  await this.handoff(writer, input, runId, "failed", error);
3062
3260
  await (0, import_nestjs_agent_core5.releaseThreadRun)(this.store, input.threadId, runId);
3063
3261
  await writer.fail({
3064
- code: "opencode_failed",
3262
+ code,
3065
3263
  message
3066
3264
  });
3067
3265
  await this.settled({
3068
3266
  ...result,
3069
3267
  outcome: {
3070
3268
  status: "failed",
3071
- 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);
3072
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
3073
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));
3074
3619
  }
3075
3620
  /** Starts the next queued message of the thread; set by the runner (it owns `start`). */
3076
3621
  startNext;
@@ -3133,6 +3678,28 @@ OpenCodeTurns = _ts_decorate2([
3133
3678
  Object
3134
3679
  ])
3135
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");
3136
3703
  function titleOf(text) {
3137
3704
  const trimmed = text.trim().replace(/\s+/g, " ");
3138
3705
  return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed || "New chat";
@@ -3306,7 +3873,7 @@ var OpenCodeAgentRunner = class {
3306
3873
  await this.turns.interrupt(input).catch(() => void 0);
3307
3874
  await this.turns.settle(runId, input, {
3308
3875
  status: "interrupted"
3309
- }, Date.now() - started);
3876
+ }, Date.now() - started, spent);
3310
3877
  return;
3311
3878
  }
3312
3879
  await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"), Date.now() - started);
@@ -3500,14 +4067,14 @@ var OpenCodeRunWorkflow = class {
3500
4067
  spent = addUsage(spent, milestone.usage);
3501
4068
  const total = spent;
3502
4069
  if (milestone.kind === "finished") {
3503
- await ctx.localStep("finish", async () => {
3504
- if (await this.cancelled(ctx.runId)) this.turns.drop(ctx.runId);
3505
- else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
3506
- 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);
3507
4076
  });
3508
- return {
3509
- outcome: milestone.outcome.status
3510
- };
4077
+ return resultOf(settled === true ? milestone.outcome : settled);
3511
4078
  }
3512
4079
  let reply;
3513
4080
  try {
@@ -3528,13 +4095,10 @@ var OpenCodeRunWorkflow = class {
3528
4095
  }
3529
4096
  } catch (error) {
3530
4097
  if ((0, import_nestjs_durable_core.isWorkflowControlFlowSignal)(error)) throw error;
3531
- await ctx.localStep("fail", async () => {
3532
- await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent);
3533
- return true;
3534
- });
3535
- 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 ? {
3536
4100
  outcome: "failed"
3537
- };
4101
+ } : resultOf(settled);
3538
4102
  }
3539
4103
  }
3540
4104
  async cancelled(runId) {
@@ -3560,6 +4124,16 @@ OpenCodeRunWorkflow = _ts_decorate4([
3560
4124
  typeof RunGateway === "undefined" ? Object : RunGateway
3561
4125
  ])
3562
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");
3563
4137
 
3564
4138
  // src/durable/runner.ts
3565
4139
  function _ts_decorate5(decorators, target, key, desc) {