@dudousxd/nestjs-agent-opencode 1.0.0 → 2.0.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.
@@ -1981,6 +1981,37 @@ var OpenCodeEventHub = class _OpenCodeEventHub {
1981
1981
  }
1982
1982
  };
1983
1983
 
1984
+ // src/turn-limits.ts
1985
+ var STOPPED_ERROR_CODE = "opencode_stopped";
1986
+ function stopErrorText(stop) {
1987
+ switch (stop.reason) {
1988
+ case "steps":
1989
+ return `The turn was stopped after ${stop.counts.steps - 1} steps, its limit.`;
1990
+ case "compactions":
1991
+ return `The turn was stopped after ${stop.counts.compactions - 1} compactions, its limit.`;
1992
+ case "wall-clock":
1993
+ return "The turn was stopped: it ran longer than allowed.";
1994
+ default:
1995
+ return "The turn was stopped.";
1996
+ }
1997
+ }
1998
+ __name(stopErrorText, "stopErrorText");
1999
+ function cleanLimits(limits) {
2000
+ const ok = /* @__PURE__ */ __name((n) => typeof n === "number" && Number.isFinite(n) && n >= 0, "ok");
2001
+ return {
2002
+ ...ok(limits?.steps) ? {
2003
+ steps: limits.steps
2004
+ } : {},
2005
+ ...ok(limits?.compactions) ? {
2006
+ compactions: limits.compactions
2007
+ } : {},
2008
+ ...ok(limits?.wallClockMs) ? {
2009
+ wallClockMs: limits.wallClockMs
2010
+ } : {}
2011
+ };
2012
+ }
2013
+ __name(cleanLimits, "cleanLimits");
2014
+
1984
2015
  // src/turns.ts
1985
2016
  function _ts_decorate2(decorators, target, key, desc) {
1986
2017
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2010,6 +2041,16 @@ function _ts_param2(paramIndex, decorator) {
2010
2041
  __name(_ts_param2, "_ts_param");
2011
2042
  var DEFAULT_TURN_TIMEOUT_MS = 30 * 6e4;
2012
2043
  var DEFAULT_TOOLS_TTL_MS = 7 * 24 * 60 * 6e4;
2044
+ var DEFAULT_HOOK_TIMEOUT_MS = 5e3;
2045
+ var PARKED_RECHECKS_MS = [
2046
+ 250,
2047
+ 1e3,
2048
+ 3e3,
2049
+ 1e4,
2050
+ 3e4
2051
+ ];
2052
+ var PARKED_READ_GAP_MS = 250;
2053
+ var RELEASED_USAGE_MS = 10 * 6e4;
2013
2054
  var OpenCodeTurns = class {
2014
2055
  static {
2015
2056
  __name(this, "OpenCodeTurns");
@@ -2033,6 +2074,21 @@ var OpenCodeTurns = class {
2033
2074
  spent = /* @__PURE__ */ new Set();
2034
2075
  /** When the tools endpoint was last registered for a location (`server|boot|directory|actor`). */
2035
2076
  toolsIssued = /* @__PURE__ */ new Map();
2077
+ /**
2078
+ * What a turn let go of here had spent, for a settle that follows here (a cancel in this process
2079
+ * that settles after the turn was released). Dropped when the run settles here, or after
2080
+ * {@link RELEASED_USAGE_MS} when it settles elsewhere, so no run is kept for good.
2081
+ */
2082
+ releasedUsage = /* @__PURE__ */ new Map();
2083
+ keepReleasedUsage(runId, usage) {
2084
+ this.releasedUsage.set(runId, usage);
2085
+ setTimeout(() => {
2086
+ if (this.releasedUsage.get(runId) === usage) this.releasedUsage.delete(runId);
2087
+ }, RELEASED_USAGE_MS).unref?.();
2088
+ }
2089
+ /** The turns this process executes, and which turn owns each session (`server|session`). */
2090
+ watches = /* @__PURE__ */ new Map();
2091
+ owners = /* @__PURE__ */ new Map();
2036
2092
  constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2, pricingStore, agentOptions) {
2037
2093
  this.host = host;
2038
2094
  this.sessions = sessions;
@@ -2045,6 +2101,10 @@ var OpenCodeTurns = class {
2045
2101
  this.memory = memory;
2046
2102
  this.queue = queue;
2047
2103
  this.toolsTokens = toolsTokens2;
2104
+ const hookTimeoutMs = settings.hookTimeoutMs;
2105
+ if (hookTimeoutMs !== void 0 && !(Number.isFinite(hookTimeoutMs) && hookTimeoutMs > 0)) {
2106
+ throw new Error(`hookTimeoutMs must be a positive number of milliseconds, not ${hookTimeoutMs}`);
2107
+ }
2048
2108
  this.prices = new ModelPriceResolver({
2049
2109
  pricingStore,
2050
2110
  catalog: agentOptions?.priceCatalog,
@@ -2316,8 +2376,12 @@ var OpenCodeTurns = class {
2316
2376
  }
2317
2377
  }
2318
2378
  onApplicationShutdown() {
2379
+ for (const runId of [
2380
+ ...this.watches.keys()
2381
+ ]) this.release(runId, "released");
2319
2382
  for (const live of this.live.values()) live.stop();
2320
2383
  this.live.clear();
2384
+ this.owners.clear();
2321
2385
  this.events.close();
2322
2386
  }
2323
2387
  // ---- begin ---------------------------------------------------------------------------------
@@ -2327,7 +2391,8 @@ var OpenCodeTurns = class {
2327
2391
  * durable `begin` re-run after a crash): the run's user message is written once.
2328
2392
  */
2329
2393
  async begin(runId, input) {
2330
- const earlier = await this.prepareThread(runId, input);
2394
+ const startedAt = this.watches.get(runId)?.startedAt ?? Date.now();
2395
+ const { earlier, rerun } = await this.prepareThread(runId, input);
2331
2396
  const server = await this.host.server(input.actor);
2332
2397
  const known = await this.sessions.get(input.threadId);
2333
2398
  const reusable = known !== null && known.serverKey === server.key && known.bootId === server.bootId && (await this.host.reuse?.({
@@ -2373,7 +2438,12 @@ var OpenCodeTurns = class {
2373
2438
  created
2374
2439
  });
2375
2440
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2376
- return handle;
2441
+ const begun = this.limitsFor(input).wallClockMs !== void 0 ? {
2442
+ ...handle,
2443
+ startedAt
2444
+ } : handle;
2445
+ await this.adopt(runId, input, begun, rerun, void 0, startedAt);
2446
+ return begun;
2377
2447
  }
2378
2448
  async createSession(runId, input, server, earlier, note) {
2379
2449
  const { client } = server;
@@ -2443,10 +2513,16 @@ ${transcript}` : "",
2443
2513
  }
2444
2514
  const firstDropped = messages[lastUser + 1];
2445
2515
  if (firstDropped !== void 0) await this.store.truncateFrom(input.threadId, firstDropped.id);
2446
- return messages.slice(0, Math.max(lastUser, 0));
2516
+ return {
2517
+ earlier: messages.slice(0, Math.max(lastUser, 0)),
2518
+ rerun: false
2519
+ };
2447
2520
  }
2448
2521
  const mine = messages.findIndex((m) => m.role === "user" && m.runId === runId);
2449
- if (mine >= 0) return messages.slice(0, mine);
2522
+ if (mine >= 0) return {
2523
+ earlier: messages.slice(0, mine),
2524
+ rerun: true
2525
+ };
2450
2526
  await this.store.appendMessage({
2451
2527
  threadId: input.threadId,
2452
2528
  role: "user",
@@ -2462,7 +2538,10 @@ ${transcript}` : "",
2462
2538
  if (thread !== null && (thread.title === "" || thread.title === "New chat")) {
2463
2539
  await this.store.setTitle(input.threadId, titleOf(input.userText));
2464
2540
  }
2465
- return messages;
2541
+ return {
2542
+ earlier: messages,
2543
+ rerun: false
2544
+ };
2466
2545
  }
2467
2546
  /** Rewind the session to before its last user message, so the regenerated answer replaces it. */
2468
2547
  async revertLastExchange(client, sessionId) {
@@ -2678,6 +2757,7 @@ ${body}
2678
2757
  // ---- prompt / observe / reply --------------------------------------------------------------
2679
2758
  /** Start listening to the session (so nothing it says is missed), then send the user message. */
2680
2759
  async prompt(runId, input, handle, spent) {
2760
+ await this.adopt(runId, input, handle, true);
2681
2761
  const live = await this.ensureLive(runId, input, handle, false, spent);
2682
2762
  try {
2683
2763
  const prompt = await this.host.promptFor?.({
@@ -2720,7 +2800,24 @@ ${body}
2720
2800
  * back from the session's messages, when the execution ends, what nobody heard after that.
2721
2801
  */
2722
2802
  async observe(runId, input, handle, spent, progress) {
2803
+ const watch = await this.adopt(runId, input, handle, true, progress?.counts);
2723
2804
  const live = await this.ensureLive(runId, input, handle, true, spent, progress);
2805
+ watch.parkedOn = void 0;
2806
+ watch.parkedEnd = void 0;
2807
+ watch.series = false;
2808
+ watch.observing = true;
2809
+ const wall = this.watchClock(watch);
2810
+ try {
2811
+ if (watch.pending !== void 0) await this.requestStop(runId, watch.pending);
2812
+ const milestone = this.marked(watch, await this.observeLive(runId, input, handle, live));
2813
+ if (milestone.kind === "ask") watch.parkedOn = milestone.ask.id;
2814
+ return milestone;
2815
+ } finally {
2816
+ watch.observing = false;
2817
+ if (wall !== void 0) clearTimeout(wall);
2818
+ }
2819
+ }
2820
+ async observeLive(runId, input, handle, live) {
2724
2821
  const { turn, client } = live;
2725
2822
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2726
2823
  let done = false;
@@ -2760,19 +2857,64 @@ ${body}
2760
2857
  * to go on.
2761
2858
  */
2762
2859
  async reply(runId, input, handle, ask, reply, spent, progress) {
2860
+ const watch = await this.adopt(runId, input, handle, true, progress?.counts);
2861
+ watch.parkedOn = void 0;
2862
+ watch.parkedEnd = void 0;
2863
+ watch.series = false;
2864
+ const stopping = watch.pending !== void 0 || watch.stop !== void 0;
2865
+ const answer = stopping ? refusalOf(ask) : reply;
2763
2866
  const server = await this.host.server(input.actor);
2764
2867
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2765
- return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2868
+ if (stopping) return this.stopAfterRestart(watch, runId, input, handle, ask, answer, spent);
2869
+ return this.continueAfterRestart(runId, input, server, ask, answer, spent, handle.startedAt);
2766
2870
  }
2767
2871
  const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2768
- await live.turn.decide(ask, reply);
2872
+ await live.turn.decide(ask, answer);
2769
2873
  return handle;
2770
2874
  }
2771
- async continueAfterRestart(runId, input, server, ask, reply, spent) {
2875
+ /**
2876
+ * A stopping turn whose OpenCode server restarted while it waited: there is no session left to
2877
+ * abort, and none is opened or prompted. The ask is settled as refused and the turn ends stopped.
2878
+ */
2879
+ async stopAfterRestart(watch, runId, input, handle, ask, answer, spent) {
2880
+ const live = await this.ensureLive(runId, input, handle, false, spent);
2881
+ await live.turn.decide(ask, answer, {
2882
+ tellOpenCode: false
2883
+ });
2884
+ const reason = watch.pending ?? watch.stop?.reason ?? "stopped";
2885
+ if (watch.stop === void 0) {
2886
+ const stop = {
2887
+ reason,
2888
+ counts: this.counts(watch)
2889
+ };
2890
+ watch.stop = stop;
2891
+ this.hook("onStop", {
2892
+ turnId: runId,
2893
+ reason,
2894
+ counts: stop.counts
2895
+ });
2896
+ }
2897
+ watch.aborted = true;
2898
+ watch.pending = void 0;
2899
+ live.turn.handle({
2900
+ type: "session.execution.interrupted",
2901
+ data: {
2902
+ sessionID: handle.sessionId
2903
+ }
2904
+ });
2905
+ return handle;
2906
+ }
2907
+ async continueAfterRestart(runId, input, server, ask, reply, spent, startedAt) {
2772
2908
  const approved = "approved" in reply && reply.approved === true;
2773
2909
  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.`;
2774
2910
  const thread = await this.store.getThread(input.threadId);
2775
- const handle = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2911
+ const created = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2912
+ const handle = {
2913
+ ...created,
2914
+ ...startedAt !== void 0 ? {
2915
+ startedAt
2916
+ } : {}
2917
+ };
2776
2918
  await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2777
2919
  this.drop(runId);
2778
2920
  const live = await this.ensureLive(runId, input, handle, false, spent);
@@ -2857,7 +2999,11 @@ ${body}
2857
2999
  this.grants.set(runId, grants);
2858
3000
  }, "onGranted")
2859
3001
  });
2860
- const stop = await this.events.listen(server.key, server.client, handle.sessionId, (event) => turn.handle(event));
3002
+ const sessionId = handle.sessionId;
3003
+ const stop = await this.events.listen(server.key, server.client, sessionId, (event) => {
3004
+ turn.handle(event);
3005
+ this.sawEvent(runId, sessionId, event);
3006
+ });
2861
3007
  const live = {
2862
3008
  turn,
2863
3009
  input,
@@ -2866,6 +3012,15 @@ ${body}
2866
3012
  stop
2867
3013
  };
2868
3014
  this.live.set(runId, live);
3015
+ this.owners.set(ownerKey(handle), runId);
3016
+ const watch = this.watches.get(runId);
3017
+ if (watch !== void 0 && watch.handle.sessionId !== sessionId) {
3018
+ watch.handle = {
3019
+ ...handle,
3020
+ startedAt: watch.startedAt
3021
+ };
3022
+ watch.ending = false;
3023
+ }
2869
3024
  if (resumed) await turn.catchUp().catch(() => void 0);
2870
3025
  return live;
2871
3026
  }
@@ -2874,11 +3029,46 @@ ${body}
2874
3029
  this.live.get(runId)?.stop();
2875
3030
  this.live.delete(runId);
2876
3031
  }
2877
- /** Stop following a run and drop the approvals it was granted: it is over. */
3032
+ /**
3033
+ * Stop following a run and drop the approvals it was granted: it is over. Returns the turn this
3034
+ * process executed, now settled (no stop reaches it any more), for its `onTurnEnd`.
3035
+ */
2878
3036
  forget(runId) {
2879
3037
  this.drop(runId);
3038
+ this.releasedUsage.delete(runId);
2880
3039
  for (const grant of this.grants.get(runId) ?? []) this.spent.delete(grant.id);
2881
3040
  this.grants.delete(runId);
3041
+ const watch = this.watches.get(runId);
3042
+ if (watch === void 0) return void 0;
3043
+ watch.settled = true;
3044
+ this.watches.delete(runId);
3045
+ for (const [key, owner] of this.owners) if (owner === runId) this.owners.delete(key);
3046
+ return watch;
3047
+ }
3048
+ /**
3049
+ * A run that ended without settling here (cancelled while this process executed it; whoever
3050
+ * cancelled settled it): stop following it, and end the turn this process executed.
3051
+ */
3052
+ async abandon(runId) {
3053
+ const watch = this.forget(runId);
3054
+ if (watch !== void 0) this.ended(watch, "interrupted", void 0);
3055
+ }
3056
+ /**
3057
+ * Let go of a turn this process executed without settling it here: it was cancelled while parked
3058
+ * (its session ended under it, `interrupted`), another worker took it over, or this process shuts
3059
+ * down (`released`). The per-turn state goes, `onTurnEnd` fires once, and a later stop of the turn
3060
+ * here is ignored. Nothing settles the run here afterwards: whoever answered or cancelled it does.
3061
+ */
3062
+ release(runId, status) {
3063
+ const watch = this.watches.get(runId);
3064
+ if (watch === void 0) return;
3065
+ const seen = this.live.get(runId)?.turn.totalUsage();
3066
+ if (seen !== void 0) this.keepReleasedUsage(runId, seen);
3067
+ this.drop(runId);
3068
+ watch.settled = true;
3069
+ this.watches.delete(runId);
3070
+ for (const [key, owner] of this.owners) if (owner === runId) this.owners.delete(key);
3071
+ this.ended(watch, status, void 0);
2882
3072
  }
2883
3073
  // ---- settle --------------------------------------------------------------------------------
2884
3074
  /** A person was asked something: tell the host (cards in other channels, wake-ups). */
@@ -2951,7 +3141,7 @@ ${body}
2951
3141
  * two milestones.
2952
3142
  */
2953
3143
  spentBy(runId, spent) {
2954
- const seen = this.live.get(runId)?.turn.totalUsage();
3144
+ const seen = this.live.get(runId)?.turn.totalUsage() ?? this.releasedUsage.get(runId);
2955
3145
  if (spent === void 0) return seen ?? emptyUsage();
2956
3146
  if (seen === void 0) return spent;
2957
3147
  const tokens = /* @__PURE__ */ __name((u) => u.inputTokens + u.outputTokens, "tokens");
@@ -2961,13 +3151,13 @@ ${body}
2961
3151
  * End the run's stream and bookkeeping for how its execution ended. `spent` is what the run spent
2962
3152
  * (the sum of its milestones' `usage`); omit → what the turn this process follows saw.
2963
3153
  */
2964
- async settle(runId, input, outcome, durationMs, spent) {
3154
+ async settle(runId, input, ended, durationMs, spent) {
3155
+ const outcome = this.withStop(ended, this.watches.get(runId));
2965
3156
  if (outcome.status === "failed") {
2966
- await this.settleFailed(runId, input, outcome.error, durationMs, spent);
2967
- return;
3157
+ return this.settleFailed(runId, input, outcome.error, durationMs, spent, outcome.stop);
2968
3158
  }
2969
3159
  const usage = this.spentBy(runId, spent);
2970
- this.forget(runId);
3160
+ const watch = this.forget(runId);
2971
3161
  const writer = await this.sink.open(runId);
2972
3162
  const result = await this.runResult(runId, input, outcome, durationMs, usage);
2973
3163
  await this.amend(writer, result);
@@ -3002,22 +3192,30 @@ ${body}
3002
3192
  });
3003
3193
  }
3004
3194
  await this.settled(result);
3195
+ if (watch !== void 0) this.ended(watch, outcome.status, outcome.status === "succeeded" ? outcome.stop : void 0);
3196
+ return outcome;
3005
3197
  }
3006
- async settleFailed(runId, input, error, durationMs = 0, spent) {
3198
+ async settleFailed(runId, input, error, durationMs = 0, spent, stop) {
3007
3199
  const usage = this.spentBy(runId, spent);
3008
- this.forget(runId);
3200
+ const watch = this.forget(runId);
3201
+ const stopped = stop ?? watch?.stop;
3009
3202
  const writer = await this.sink.open(runId);
3010
3203
  const result = await this.runResult(runId, input, {
3011
3204
  status: "failed",
3012
- error
3205
+ error,
3206
+ ...stopped !== void 0 ? {
3207
+ stop: stopped
3208
+ } : {}
3013
3209
  }, durationMs, usage);
3014
3210
  const worded = (await this.amend(writer, result)).error;
3015
- const message = worded ?? streamFailure(new Error(error)).message;
3016
- this.logger.error(`agent run ${runId} failed: ${error}`);
3211
+ const message = worded ?? (stopped !== void 0 ? error : streamFailure(new Error(error)).message);
3212
+ const code = stopped !== void 0 ? STOPPED_ERROR_CODE : "opencode_failed";
3213
+ if (stopped !== void 0) this.logger.log(`agent run ${runId} stopped: ${stopped.reason}`);
3214
+ else this.logger.error(`agent run ${runId} failed: ${error}`);
3017
3215
  publishAgentRunFailed({
3018
3216
  runId,
3019
3217
  threadId: input.threadId,
3020
- code: "opencode_failed",
3218
+ code,
3021
3219
  message: error,
3022
3220
  durationMs,
3023
3221
  context: agentRunContext(input)
@@ -3026,23 +3224,370 @@ ${body}
3026
3224
  runId,
3027
3225
  status: "failed",
3028
3226
  durationMs,
3029
- errorCode: "opencode_failed",
3227
+ errorCode: code,
3030
3228
  errorMessage: error
3031
3229
  });
3032
3230
  await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
3033
3231
  await this.handoff(writer, input, runId, "failed", error);
3034
3232
  await releaseThreadRun(this.store, input.threadId, runId);
3035
3233
  await writer.fail({
3036
- code: "opencode_failed",
3234
+ code,
3037
3235
  message
3038
3236
  });
3039
3237
  await this.settled({
3040
3238
  ...result,
3041
3239
  outcome: {
3042
3240
  status: "failed",
3043
- error: worded ?? error
3241
+ error: worded ?? error,
3242
+ ...stopped !== void 0 ? {
3243
+ stop: stopped
3244
+ } : {}
3245
+ }
3246
+ });
3247
+ if (watch !== void 0) this.ended(watch, "failed", stopped);
3248
+ return {
3249
+ status: "failed",
3250
+ error,
3251
+ ...stopped !== void 0 ? {
3252
+ stop: stopped
3253
+ } : {}
3254
+ };
3255
+ }
3256
+ // ---- limits, stops and the turn's lifecycle -------------------------------------------------
3257
+ /**
3258
+ * Stop a turn's own work: abort its session so OpenCode starts no new round, only while the turn
3259
+ * still owns the session (a thread's session is reused by its next turn) — checked immediately
3260
+ * before the abort. Acts in the process executing the turn; see {@link OpenCodeStopResult}. The
3261
+ * turn ends failed with `outcome.stop` (code `opencode_stopped`), and `onStop` is called.
3262
+ */
3263
+ stopTurn(turnId, reason) {
3264
+ return this.requestStop(turnId, reason);
3265
+ }
3266
+ /** The turn this process executes, made known here on its first step in this process. */
3267
+ async adopt(runId, input, handle, resumed, counts, startedAt) {
3268
+ const known = this.watches.get(runId);
3269
+ if (known !== void 0) return known;
3270
+ const watch = {
3271
+ runId,
3272
+ input,
3273
+ handle,
3274
+ limits: this.limitsFor(input),
3275
+ startedAt: handle.startedAt ?? startedAt ?? Date.now(),
3276
+ steps: counts?.steps ?? 0,
3277
+ compactions: counts?.compactions ?? 0,
3278
+ observing: false,
3279
+ ending: false,
3280
+ settled: false,
3281
+ aborted: false,
3282
+ checking: false,
3283
+ lastRead: 0
3284
+ };
3285
+ this.watches.set(runId, watch);
3286
+ await this.hook("onTurnStart", {
3287
+ turnId: runId,
3288
+ threadId: input.threadId,
3289
+ sessionId: handle.sessionId,
3290
+ resumed
3291
+ });
3292
+ return watch;
3293
+ }
3294
+ limitsFor(input) {
3295
+ const limits = this.settings.limits;
3296
+ return cleanLimits(typeof limits === "function" ? limits(input) : limits);
3297
+ }
3298
+ /**
3299
+ * One event of the turn's session: counted for the limits, and told to `onTurnEvent`. While the
3300
+ * turn is parked on a person here, it may also say the turn went on without this process
3301
+ * ({@link wentOnElsewhere}).
3302
+ */
3303
+ sawEvent(runId, sessionId, event) {
3304
+ if (sessionOf(event) !== sessionId) return;
3305
+ const watch = this.watches.get(runId);
3306
+ if (watch === void 0 || watch.settled || watch.handle.sessionId !== sessionId) return;
3307
+ if (watch.parkedOn !== void 0 && this.wentOnElsewhere(watch, event)) return;
3308
+ let reached;
3309
+ if (event.type.startsWith("session.execution.")) watch.ending = true;
3310
+ else if (event.type === "session.step.started") {
3311
+ watch.steps += 1;
3312
+ if (passed(watch.steps, watch.limits.steps)) reached = "steps";
3313
+ } else if (event.type === "session.compaction.started") {
3314
+ watch.compactions += 1;
3315
+ if (passed(watch.compactions, watch.limits.compactions)) reached = "compactions";
3316
+ }
3317
+ this.hook("onTurnEvent", {
3318
+ turnId: runId,
3319
+ type: event.type,
3320
+ sessionId
3321
+ });
3322
+ if (reached !== void 0) void this.limitStop(runId, reached);
3323
+ }
3324
+ /**
3325
+ * Whether a turn parked here went on without this process, which then lets it go. While a tool of
3326
+ * the step waits on a person, OpenCode keeps running the step's other tools, so their events say
3327
+ * nothing. What does: a new round (`session.step.started`, which cannot begin while the step
3328
+ * waits), or the store showing the parked ask decided by someone else (another worker answered
3329
+ * it, or a cancel elsewhere settled it) — this process clears the park before it answers. When
3330
+ * the session's execution ends while parked, the store is asked again for a while, as a cancel
3331
+ * elsewhere settles the ask just after it interrupted the session; with the ask still open, the
3332
+ * turn stays here for its answer.
3333
+ */
3334
+ wentOnElsewhere(watch, event) {
3335
+ if (event.type === "session.step.started") {
3336
+ this.release(watch.runId, "released");
3337
+ return true;
3338
+ }
3339
+ const ended = event.type.startsWith("session.execution.");
3340
+ if (ended) watch.parkedEnd = event.type === "session.execution.interrupted" ? "interrupted" : "released";
3341
+ this.checkParked(watch, ended);
3342
+ return false;
3343
+ }
3344
+ /**
3345
+ * Ask the store whether the parked ask was decided elsewhere, and let the turn go when it was —
3346
+ * one read at a time, at most one every {@link PARKED_READ_GAP_MS}. `series`: the session's
3347
+ * execution ended, so the store is asked again over {@link parkedRechecksMs}. An execution that
3348
+ * was interrupted under a parked turn means a cancel elsewhere: when the store cannot say (it has
3349
+ * no `toolCallApproval`, the read fails, the ask is not on file) past the first recheck, or the
3350
+ * rechecks run out with the ask still open, the turn is let go as `interrupted` anyway.
3351
+ */
3352
+ checkParked(watch, series) {
3353
+ if (series) watch.series = true;
3354
+ if (watch.checking) {
3355
+ watch.again = true;
3356
+ return;
3357
+ }
3358
+ watch.checking = true;
3359
+ void this.runParkedChecks(watch);
3360
+ }
3361
+ /**
3362
+ * The checks of {@link checkParked}. `checking` is cleared in the same step the loop returns in,
3363
+ * so a check asked for afterwards starts a new loop; one asked for while it ran (`again`) but not
3364
+ * read because the loop returned starts one too.
3365
+ */
3366
+ async runParkedChecks(watch) {
3367
+ try {
3368
+ await this.parkedChecks(watch);
3369
+ } finally {
3370
+ watch.checking = false;
3371
+ if (watch.again && !watch.settled && watch.parkedOn !== void 0) {
3372
+ watch.checking = true;
3373
+ void this.runParkedChecks(watch);
3044
3374
  }
3375
+ }
3376
+ }
3377
+ async parkedChecks(watch) {
3378
+ const ask = watch.parkedOn;
3379
+ const gone = /* @__PURE__ */ __name(() => watch.settled || ask === void 0 || watch.parkedOn !== ask, "gone");
3380
+ let step = 0;
3381
+ for (; ; ) {
3382
+ watch.again = false;
3383
+ const wait = watch.lastRead + PARKED_READ_GAP_MS - Date.now();
3384
+ if (wait > 0) await sleep(wait);
3385
+ if (gone()) return;
3386
+ const answer = await this.parkedAsk(ask);
3387
+ watch.lastRead = Date.now();
3388
+ if (gone()) return;
3389
+ if (answer === "decided") {
3390
+ this.release(watch.runId, watch.parkedEnd ?? "released");
3391
+ return;
3392
+ }
3393
+ if (watch.series) {
3394
+ const cancelled = watch.parkedEnd === "interrupted";
3395
+ if (cancelled && answer === "unknown" && step > 0) {
3396
+ this.release(watch.runId, "interrupted");
3397
+ return;
3398
+ }
3399
+ const gap = this.parkedRechecksMs[step];
3400
+ step += 1;
3401
+ if (gap === void 0) {
3402
+ if (cancelled) this.release(watch.runId, "interrupted");
3403
+ return;
3404
+ }
3405
+ await sleep(gap);
3406
+ continue;
3407
+ }
3408
+ if (!watch.again) return;
3409
+ }
3410
+ }
3411
+ /** What the store says of an ask: decided, still open, or nothing it can tell. */
3412
+ async parkedAsk(id) {
3413
+ if (this.store.toolCallApproval === void 0) return "unknown";
3414
+ try {
3415
+ const state = await this.store.toolCallApproval(id);
3416
+ if (state == null) return "unknown";
3417
+ return state.status === "pending_approval" ? "open" : "decided";
3418
+ } catch {
3419
+ return "unknown";
3420
+ }
3421
+ }
3422
+ /**
3423
+ * The waits between the store's rereads of a parked ask after its execution ended (cumulative
3424
+ * 0.25, 1.25, 4.25, 14.25 and 44.25 seconds after the first). Overridden by tests only.
3425
+ *
3426
+ * @internal
3427
+ */
3428
+ parkedRechecksMs = PARKED_RECHECKS_MS;
3429
+ /** A limit's stop: what it did is the engine's to log, there is no caller to tell. */
3430
+ async limitStop(runId, reason) {
3431
+ const result = await this.requestStop(runId, reason);
3432
+ if (result === "unconfirmed") this.logger.warn(`run ${runId}: passed its ${reason} limit, but its owner could not be read`);
3433
+ }
3434
+ /** The wall-clock bound of an observation: a timer for what is left of it, if anything. */
3435
+ watchClock(watch) {
3436
+ const max = watch.limits.wallClockMs;
3437
+ if (max === void 0) return void 0;
3438
+ const left = watch.startedAt + max - Date.now();
3439
+ if (left <= 0) {
3440
+ watch.pending ??= "wall-clock";
3441
+ return void 0;
3442
+ }
3443
+ const timer = setTimeout(() => void this.limitStop(watch.runId, "wall-clock"), left);
3444
+ timer.unref?.();
3445
+ return timer;
3446
+ }
3447
+ counts(watch) {
3448
+ return {
3449
+ steps: watch.steps,
3450
+ compactions: watch.compactions,
3451
+ elapsedMs: Date.now() - watch.startedAt
3452
+ };
3453
+ }
3454
+ async requestStop(runId, reason) {
3455
+ const watch = this.watches.get(runId);
3456
+ if (watch === void 0 || watch.settled || watch.stop !== void 0) return "ignored";
3457
+ const live = this.live.get(runId);
3458
+ if (watch.parkedOn !== void 0) {
3459
+ watch.pending ??= reason;
3460
+ return "pending";
3461
+ }
3462
+ if (watch.ending || live?.turn.finished === true) return this.landed(watch, reason);
3463
+ if (!watch.observing || live === void 0) {
3464
+ watch.pending ??= reason;
3465
+ return "pending";
3466
+ }
3467
+ watch.pending = void 0;
3468
+ let active;
3469
+ try {
3470
+ active = await this.activeRun(watch.input.threadId);
3471
+ } catch (error) {
3472
+ this.logger.warn(`run ${runId}: could not read the thread's run: ${errorText(error, "error")}`);
3473
+ return "unconfirmed";
3474
+ }
3475
+ if (watch.settled || watch.stop !== void 0 || this.live.get(runId) !== live) return "ignored";
3476
+ if (active !== null && active !== runId) return "ignored";
3477
+ if (this.owners.get(ownerKey(watch.handle)) !== runId) return "ignored";
3478
+ if (watch.ending || live.turn.finished) return this.landed(watch, reason);
3479
+ const stop = {
3480
+ reason,
3481
+ counts: this.counts(watch)
3482
+ };
3483
+ watch.stop = stop;
3484
+ watch.aborted = true;
3485
+ try {
3486
+ await live.client.session.interrupt({
3487
+ sessionID: watch.handle.sessionId
3488
+ });
3489
+ } catch (error) {
3490
+ this.logger.warn(`run ${runId}: could not abort the session: ${errorText(error, "error")}`);
3491
+ }
3492
+ this.hook("onStop", {
3493
+ turnId: runId,
3494
+ reason,
3495
+ counts: stop.counts
3045
3496
  });
3497
+ return "aborted";
3498
+ }
3499
+ /** A stop that arrived while the answer settles: recorded and reported, the answer kept. */
3500
+ landed(watch, reason) {
3501
+ const stop = {
3502
+ reason,
3503
+ counts: this.counts(watch)
3504
+ };
3505
+ watch.stop = stop;
3506
+ watch.pending = void 0;
3507
+ this.hook("onStop", {
3508
+ turnId: watch.runId,
3509
+ reason,
3510
+ counts: stop.counts
3511
+ });
3512
+ return "settling";
3513
+ }
3514
+ /** The milestone as the runner journals it: the limits' counts on an ask, the stop on an end. */
3515
+ marked(watch, milestone) {
3516
+ if (milestone.kind === "finished") return {
3517
+ ...milestone,
3518
+ outcome: this.withStop(milestone.outcome, watch)
3519
+ };
3520
+ if (milestone.progress === void 0) return milestone;
3521
+ const counts = {
3522
+ steps: watch.steps,
3523
+ compactions: watch.compactions
3524
+ };
3525
+ return {
3526
+ ...milestone,
3527
+ progress: {
3528
+ ...milestone.progress,
3529
+ counts
3530
+ }
3531
+ };
3532
+ }
3533
+ /**
3534
+ * How a turn a stop landed on ends. An answer that settled anyway is kept, with the stop on it; an
3535
+ * execution that failed on its own keeps its error, with the stop on it; an execution the stop
3536
+ * aborted ends failed with the stop's own text. One interrupted by a person's cancel stays so.
3537
+ */
3538
+ withStop(outcome, watch) {
3539
+ const stop = watch?.stop;
3540
+ if (stop === void 0) return outcome;
3541
+ if (outcome.status !== "interrupted") return outcome.stop !== void 0 ? outcome : {
3542
+ ...outcome,
3543
+ stop
3544
+ };
3545
+ if (!watch?.aborted) return outcome;
3546
+ return {
3547
+ status: "failed",
3548
+ error: stopErrorText(stop),
3549
+ stop
3550
+ };
3551
+ }
3552
+ /** `onTurnEnd`, once per turn this process executed; not waited for. */
3553
+ ended(watch, status, stop) {
3554
+ this.hook("onTurnEnd", {
3555
+ turnId: watch.runId,
3556
+ status,
3557
+ ...stop !== void 0 ? {
3558
+ stop
3559
+ } : {}
3560
+ });
3561
+ }
3562
+ /**
3563
+ * A lifecycle hook, from the settings and the host, called at once. The promise settles when both
3564
+ * have, or after `hookTimeoutMs` (default 5 s) — only `onTurnStart` is waited for. Errors are
3565
+ * logged, never the turn's.
3566
+ */
3567
+ hook(name, event) {
3568
+ const calls = [];
3569
+ for (const target of [
3570
+ this.settings,
3571
+ this.host
3572
+ ]) {
3573
+ const fn = target[name];
3574
+ if (typeof fn !== "function") continue;
3575
+ calls.push(Promise.resolve().then(() => fn.call(target, event)).catch((error) => this.logger.warn(`${name} failed: ${errorText(error, "error")}`)));
3576
+ }
3577
+ if (calls.length === 0) return Promise.resolve();
3578
+ const ms = this.settings.hookTimeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
3579
+ let timer;
3580
+ const late = new Promise((resolve) => {
3581
+ timer = setTimeout(() => {
3582
+ this.logger.warn(`${name} took longer than ${ms}ms; the turn went on`);
3583
+ resolve();
3584
+ }, ms);
3585
+ timer.unref?.();
3586
+ });
3587
+ return Promise.race([
3588
+ Promise.all(calls).then(() => void 0),
3589
+ late
3590
+ ]).finally(() => clearTimeout(timer));
3046
3591
  }
3047
3592
  /** Starts the next queued message of the thread; set by the runner (it owns `start`). */
3048
3593
  startNext;
@@ -3105,6 +3650,28 @@ OpenCodeTurns = _ts_decorate2([
3105
3650
  Object
3106
3651
  ])
3107
3652
  ], OpenCodeTurns);
3653
+ function passed(count, limit) {
3654
+ return limit !== void 0 && count > limit;
3655
+ }
3656
+ __name(passed, "passed");
3657
+ function sleep(ms) {
3658
+ return new Promise((resolve) => setTimeout(resolve, ms).unref?.());
3659
+ }
3660
+ __name(sleep, "sleep");
3661
+ function refusalOf(ask) {
3662
+ return ask.kind === "approval" ? {
3663
+ approved: false,
3664
+ reason: "The turn was stopped."
3665
+ } : {
3666
+ answers: {},
3667
+ skipped: true
3668
+ };
3669
+ }
3670
+ __name(refusalOf, "refusalOf");
3671
+ function ownerKey(handle) {
3672
+ return `${handle.serverKey}|${handle.sessionId}`;
3673
+ }
3674
+ __name(ownerKey, "ownerKey");
3108
3675
  function titleOf(text) {
3109
3676
  const trimmed = text.trim().replace(/\s+/g, " ");
3110
3677
  return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed || "New chat";
@@ -3278,7 +3845,7 @@ var OpenCodeAgentRunner = class {
3278
3845
  await this.turns.interrupt(input).catch(() => void 0);
3279
3846
  await this.turns.settle(runId, input, {
3280
3847
  status: "interrupted"
3281
- }, Date.now() - started);
3848
+ }, Date.now() - started, spent);
3282
3849
  return;
3283
3850
  }
3284
3851
  await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"), Date.now() - started);
@@ -3403,13 +3970,13 @@ __name(openCodeProviders, "openCodeProviders");
3403
3970
  // src/durable/runner.ts
3404
3971
  import { randomUUID as randomUUID2 } from "crypto";
3405
3972
  import { AGENT_STORE as AGENT_STORE2 } from "@dudousxd/nestjs-agent-core";
3406
- import { RUN_GATEWAY as RUN_GATEWAY2, WorkflowService } from "@dudousxd/nestjs-durable";
3407
- import { isWorkflowControlFlowSignal as isWorkflowControlFlowSignal2 } from "@dudousxd/nestjs-durable-core";
3973
+ import { WorkflowService } from "@dudousxd/nestjs-durable";
3974
+ import { RunGateway as RunGateway2, isWorkflowControlFlowSignal as isWorkflowControlFlowSignal2 } from "@dudousxd/nestjs-durable-core";
3408
3975
  import { Inject as Inject5, Injectable as Injectable4, Logger as Logger4 } from "@nestjs/common";
3409
3976
 
3410
3977
  // src/durable/workflow.ts
3411
- import { RUN_GATEWAY, Workflow } from "@dudousxd/nestjs-durable";
3412
- import { SignalTimeoutError, isWorkflowControlFlowSignal } from "@dudousxd/nestjs-durable-core";
3978
+ import { Workflow } from "@dudousxd/nestjs-durable";
3979
+ import { RunGateway, SignalTimeoutError, isWorkflowControlFlowSignal } from "@dudousxd/nestjs-durable-core";
3413
3980
  import { Inject as Inject4, Injectable as Injectable3, Optional as Optional2 } from "@nestjs/common";
3414
3981
  function _ts_decorate4(decorators, target, key, desc) {
3415
3982
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3472,14 +4039,14 @@ var OpenCodeRunWorkflow = class {
3472
4039
  spent = addUsage(spent, milestone.usage);
3473
4040
  const total = spent;
3474
4041
  if (milestone.kind === "finished") {
3475
- await ctx.localStep("finish", async () => {
3476
- if (await this.cancelled(ctx.runId)) this.turns.drop(ctx.runId);
3477
- else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
3478
- return true;
4042
+ const settled = await ctx.localStep("finish", async () => {
4043
+ if (await this.cancelled(ctx.runId)) {
4044
+ await this.turns.abandon(ctx.runId);
4045
+ return milestone.outcome;
4046
+ }
4047
+ return this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt, total);
3479
4048
  });
3480
- return {
3481
- outcome: milestone.outcome.status
3482
- };
4049
+ return resultOf(settled === true ? milestone.outcome : settled);
3483
4050
  }
3484
4051
  let reply;
3485
4052
  try {
@@ -3500,13 +4067,10 @@ var OpenCodeRunWorkflow = class {
3500
4067
  }
3501
4068
  } catch (error) {
3502
4069
  if (isWorkflowControlFlowSignal(error)) throw error;
3503
- await ctx.localStep("fail", async () => {
3504
- await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent);
3505
- return true;
3506
- });
3507
- return {
4070
+ const settled = await ctx.localStep("fail", async () => this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"), 0, spent));
4071
+ return settled === true ? {
3508
4072
  outcome: "failed"
3509
- };
4073
+ } : resultOf(settled);
3510
4074
  }
3511
4075
  }
3512
4076
  async cancelled(runId) {
@@ -3525,13 +4089,23 @@ OpenCodeRunWorkflow = _ts_decorate4([
3525
4089
  version: "1"
3526
4090
  }),
3527
4091
  _ts_param4(1, Optional2()),
3528
- _ts_param4(1, Inject4(RUN_GATEWAY)),
4092
+ _ts_param4(1, Inject4(RunGateway)),
3529
4093
  _ts_metadata4("design:type", Function),
3530
4094
  _ts_metadata4("design:paramtypes", [
3531
4095
  typeof OpenCodeTurns === "undefined" ? Object : OpenCodeTurns,
3532
4096
  typeof RunGateway === "undefined" ? Object : RunGateway
3533
4097
  ])
3534
4098
  ], OpenCodeRunWorkflow);
4099
+ function resultOf(outcome) {
4100
+ const stop = outcome.status === "interrupted" ? void 0 : outcome.stop;
4101
+ return {
4102
+ outcome: outcome.status,
4103
+ ...stop !== void 0 ? {
4104
+ stop: stop.reason
4105
+ } : {}
4106
+ };
4107
+ }
4108
+ __name(resultOf, "resultOf");
3535
4109
 
3536
4110
  // src/durable/runner.ts
3537
4111
  function _ts_decorate5(decorators, target, key, desc) {
@@ -3648,14 +4222,14 @@ var DurableOpenCodeAgentRunner = class _DurableOpenCodeAgentRunner {
3648
4222
  };
3649
4223
  DurableOpenCodeAgentRunner = _ts_decorate5([
3650
4224
  Injectable4(),
3651
- _ts_param5(1, Inject5(RUN_GATEWAY2)),
4225
+ _ts_param5(1, Inject5(RunGateway2)),
3652
4226
  _ts_param5(3, Inject5(OPENCODE_OPTIONS)),
3653
4227
  _ts_param5(4, Inject5(OPENCODE_HOST)),
3654
4228
  _ts_param5(5, Inject5(AGENT_STORE2)),
3655
4229
  _ts_metadata5("design:type", Function),
3656
4230
  _ts_metadata5("design:paramtypes", [
3657
4231
  typeof WorkflowService === "undefined" ? Object : WorkflowService,
3658
- typeof RunGateway === "undefined" ? Object : RunGateway,
4232
+ typeof RunGateway2 === "undefined" ? Object : RunGateway2,
3659
4233
  typeof OpenCodeTurns === "undefined" ? Object : OpenCodeTurns,
3660
4234
  typeof OpenCodeEngineSettings === "undefined" ? Object : OpenCodeEngineSettings,
3661
4235
  typeof OpenCodeHost === "undefined" ? Object : OpenCodeHost,