oh-my-opencode-slim 2.0.0-beta.12 → 2.0.0-beta.13

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.
package/dist/index.js CHANGED
@@ -22642,6 +22642,10 @@ function createDisplayNameMentionRewriter(config) {
22642
22642
  };
22643
22643
  }
22644
22644
  // src/utils/task.ts
22645
+ var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
22646
+ "Task is not running in this process and has no final output.",
22647
+ "Task is not running in this process and has not produced output."
22648
+ ]);
22645
22649
  function parseTaskIdFromTaskOutput(output) {
22646
22650
  const lines = output.split(/\r?\n/);
22647
22651
  for (const line of lines) {
@@ -22677,6 +22681,19 @@ function parseTaskStatusOutput(output) {
22677
22681
  result: parseTaskResultFromOutput(output)
22678
22682
  };
22679
22683
  }
22684
+ function classifyTaskStatusOutput(status) {
22685
+ if (status.timedOut)
22686
+ return "timeout";
22687
+ if (status.state === "running")
22688
+ return "running";
22689
+ if (status.state === "completed" || status.state === "cancelled") {
22690
+ return "terminal";
22691
+ }
22692
+ if (TRANSIENT_PROCESS_ERROR_TEXT.has(status.result ?? "")) {
22693
+ return "transient_process_error";
22694
+ }
22695
+ return "unknown_error";
22696
+ }
22680
22697
  function parseTaskStateFromOutput(output) {
22681
22698
  for (const line of getTaskHeader(output).split(/\r?\n/)) {
22682
22699
  const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
@@ -22735,11 +22752,15 @@ class BackgroundJobBoard {
22735
22752
  objective: input.objective ?? existing.objective,
22736
22753
  state: "running",
22737
22754
  timedOut: false,
22755
+ statusUncertain: false,
22756
+ cancellationRequested: false,
22738
22757
  terminalUnreconciled: false,
22739
22758
  completedAt: undefined,
22740
22759
  resultSummary: undefined,
22760
+ lastStatusError: undefined,
22741
22761
  terminalState: undefined,
22742
22762
  lastLaunchedAt: now,
22763
+ lastLiveBusyAt: now,
22743
22764
  lastUsedAt: now,
22744
22765
  updatedAt: now
22745
22766
  };
@@ -22754,9 +22775,12 @@ class BackgroundJobBoard {
22754
22775
  objective: input.objective,
22755
22776
  state: "running",
22756
22777
  timedOut: false,
22778
+ statusUncertain: false,
22779
+ cancellationRequested: false,
22757
22780
  terminalUnreconciled: false,
22758
22781
  launchedAt: now,
22759
22782
  lastLaunchedAt: now,
22783
+ lastLiveBusyAt: now,
22760
22784
  lastUsedAt: now,
22761
22785
  updatedAt: now,
22762
22786
  alias: this.nextAlias(input.parentSessionID, input.agent),
@@ -22778,11 +22802,13 @@ class BackgroundJobBoard {
22778
22802
  ...existing,
22779
22803
  state: input.state,
22780
22804
  timedOut: input.timedOut ?? false,
22805
+ statusUncertain: input.statusUncertain ?? false,
22781
22806
  terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
22782
22807
  updatedAt: now,
22783
22808
  completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
22784
22809
  terminalState: terminal ? input.state : existing.terminalState,
22785
- resultSummary: input.resultSummary ?? existing.resultSummary
22810
+ resultSummary: input.resultSummary ?? existing.resultSummary,
22811
+ lastStatusError: input.lastStatusError
22786
22812
  };
22787
22813
  this.jobs.set(input.taskID, updated);
22788
22814
  this.trimReusable(input.taskID);
@@ -22803,18 +22829,28 @@ class BackgroundJobBoard {
22803
22829
  const existing = this.jobs.get(taskID);
22804
22830
  if (!existing)
22805
22831
  return;
22806
- const isStaleCancellation = existing.state === "cancelled" || existing.state === "reconciled" && existing.terminalState === "cancelled";
22807
- if (!isStaleCancellation)
22808
- return existing;
22832
+ const isStaleTerminal = TERMINAL_STATES.has(existing.state) || existing.state === "reconciled";
22833
+ if (!isStaleTerminal || existing.cancellationRequested) {
22834
+ const updated2 = {
22835
+ ...existing,
22836
+ lastLiveBusyAt: now
22837
+ };
22838
+ this.jobs.set(taskID, updated2);
22839
+ return updated2;
22840
+ }
22809
22841
  const updated = {
22810
22842
  ...existing,
22811
22843
  state: "running",
22812
22844
  timedOut: false,
22845
+ statusUncertain: false,
22846
+ cancellationRequested: false,
22813
22847
  terminalUnreconciled: false,
22814
22848
  updatedAt: now,
22849
+ lastLiveBusyAt: now,
22815
22850
  completedAt: undefined,
22816
22851
  terminalState: undefined,
22817
- resultSummary: undefined
22852
+ resultSummary: undefined,
22853
+ lastStatusError: undefined
22818
22854
  };
22819
22855
  this.jobs.set(taskID, updated);
22820
22856
  return updated;
@@ -22830,6 +22866,7 @@ class BackgroundJobBoard {
22830
22866
  ...existing,
22831
22867
  state: "reconciled",
22832
22868
  terminalUnreconciled: false,
22869
+ statusUncertain: false,
22833
22870
  updatedAt: now,
22834
22871
  lastUsedAt: now,
22835
22872
  terminalState: existing.terminalState ?? terminalStateOf(existing.state)
@@ -22838,24 +22875,29 @@ class BackgroundJobBoard {
22838
22875
  this.trimReusable(taskID);
22839
22876
  return updated;
22840
22877
  }
22841
- markCancelled(taskID, reason, now = Date.now()) {
22878
+ markCancelled(taskID, reason, now = Date.now(), options = {}) {
22842
22879
  const existing = this.jobs.get(taskID);
22843
22880
  if (!existing)
22844
22881
  return;
22845
- if (existing.state === "reconciled")
22846
- return existing;
22847
- if (TERMINAL_STATES.has(existing.state))
22848
- return existing;
22882
+ if (!options.force) {
22883
+ if (existing.state === "reconciled")
22884
+ return existing;
22885
+ if (TERMINAL_STATES.has(existing.state))
22886
+ return existing;
22887
+ }
22849
22888
  const summary = normalizeCancelReason(reason);
22850
22889
  const updated = {
22851
22890
  ...existing,
22852
22891
  state: "cancelled",
22853
22892
  timedOut: false,
22893
+ statusUncertain: false,
22894
+ cancellationRequested: true,
22854
22895
  terminalUnreconciled: true,
22855
22896
  updatedAt: now,
22856
22897
  completedAt: existing.completedAt ?? now,
22857
22898
  terminalState: "cancelled",
22858
- resultSummary: summary
22899
+ resultSummary: summary,
22900
+ lastStatusError: undefined
22859
22901
  };
22860
22902
  this.jobs.set(taskID, updated);
22861
22903
  return updated;
@@ -22925,7 +22967,7 @@ class BackgroundJobBoard {
22925
22967
  return [
22926
22968
  "### Background Job Board",
22927
22969
  "SENTINEL: background-job-board-v2",
22928
- "Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse any non-running session for the same specialist/context.",
22970
+ "Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse only completed sessions for the same specialist/context; never reuse cancelled or errored sessions.",
22929
22971
  "",
22930
22972
  "#### Active / Unreconciled",
22931
22973
  ...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
@@ -22981,7 +23023,8 @@ function deriveTaskSessionLabel(input) {
22981
23023
  return firstPromptLine ? firstPromptLine.slice(0, 48) : `recent ${input.agentType} task`;
22982
23024
  }
22983
23025
  function isReusable(job) {
22984
- return job.state !== "running";
23026
+ const terminal = job.terminalState ?? terminalStateOf(job.state);
23027
+ return terminal === "completed" && !job.terminalUnreconciled;
22985
23028
  }
22986
23029
  function terminalStateOf(state) {
22987
23030
  return state === "completed" || state === "error" || state === "cancelled" ? state : undefined;
@@ -23001,13 +23044,15 @@ function formatJob(job, now = Date.now()) {
23001
23044
  const ageMs = now - job.lastLaunchedAt;
23002
23045
  const isResume = job.lastLaunchedAt !== job.launchedAt;
23003
23046
  const ageLabel = job.state === "running" && ageMs < 30000 ? ` [${isResume ? "resumed" : "just launched"}, ${Math.floor(ageMs / 1000)}s ago]` : "";
23004
- const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
23047
+ const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.statusUncertain ? `${job.state}, status uncertain` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
23005
23048
  const lines = [
23006
23049
  `- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
23007
23050
  ` Objective: ${job.objective || job.description}`
23008
23051
  ];
23009
23052
  if (job.resultSummary && job.terminalUnreconciled) {
23010
23053
  lines.push(` Result: ${singleLine(job.resultSummary)}`);
23054
+ } else if (job.lastStatusError && job.statusUncertain) {
23055
+ lines.push(` Status: ${singleLine(job.lastStatusError)}`);
23011
23056
  }
23012
23057
  return lines.join(`
23013
23058
  `);
@@ -24094,6 +24139,17 @@ function createTaskSessionManagerHook(_ctx, options) {
24094
24139
  timedOut: status.timedOut,
24095
24140
  hasResult: Boolean(status.result)
24096
24141
  });
24142
+ const existing = backgroundJobBoard.get(status.taskID);
24143
+ if (isLateCancelledTaskError(existing, status.state)) {
24144
+ log("[task-session-manager] suppressed late cancelled task error", {
24145
+ taskID: status.taskID,
24146
+ alias: existing?.alias,
24147
+ state: existing?.state,
24148
+ terminalState: existing?.terminalState,
24149
+ result: status.result
24150
+ });
24151
+ return existing;
24152
+ }
24097
24153
  const updated = backgroundJobBoard.updateStatus({
24098
24154
  taskID: status.taskID,
24099
24155
  state: status.state,
@@ -24122,6 +24178,61 @@ function createTaskSessionManagerHook(_ctx, options) {
24122
24178
  }
24123
24179
  return updated;
24124
24180
  }
24181
+ async function handleTransientTaskStatusOutput(output) {
24182
+ if (typeof output.output !== "string")
24183
+ return false;
24184
+ const status = parseTaskStatusOutput(output.output);
24185
+ if (!status)
24186
+ return false;
24187
+ if (classifyTaskStatusOutput(status) !== "transient_process_error") {
24188
+ return false;
24189
+ }
24190
+ const existing = backgroundJobBoard.get(status.taskID);
24191
+ const liveStatus = existing && existing.state === "running" ? undefined : await getLiveSessionStatus(status.taskID);
24192
+ const recentLiveBusy = !!existing?.lastLiveBusyAt && (!existing.completedAt || existing.lastLiveBusyAt >= existing.completedAt);
24193
+ const isStillRunning = existing?.state === "running" || recentLiveBusy || liveStatus === "busy" || liveStatus === "retry";
24194
+ if (!isStillRunning)
24195
+ return false;
24196
+ const updated = existing?.state === "running" ? backgroundJobBoard.updateStatus({
24197
+ taskID: status.taskID,
24198
+ state: "running",
24199
+ statusUncertain: true,
24200
+ lastStatusError: status.result
24201
+ }) : undefined;
24202
+ log("[task-session-manager] classified transient task_status error", {
24203
+ taskID: status.taskID,
24204
+ alias: existing?.alias,
24205
+ parentSessionID: existing?.parentSessionID,
24206
+ previousState: existing?.state,
24207
+ updatedState: updated?.state,
24208
+ liveStatus,
24209
+ recentLiveBusy
24210
+ });
24211
+ return true;
24212
+ }
24213
+ async function getLiveSessionStatus(sessionID) {
24214
+ try {
24215
+ const response = await _ctx.client.session.status();
24216
+ const data = response.data;
24217
+ if (!isObjectRecord(data))
24218
+ return;
24219
+ const item = data[sessionID];
24220
+ if (item === undefined)
24221
+ return "idle";
24222
+ if (isObjectRecord(item) && typeof item.type === "string") {
24223
+ return item.type;
24224
+ }
24225
+ const directType = data.type;
24226
+ if (typeof directType === "string")
24227
+ return directType;
24228
+ const nestedStatus = data.status;
24229
+ if (!isObjectRecord(nestedStatus))
24230
+ return;
24231
+ return typeof nestedStatus.type === "string" ? nestedStatus.type : undefined;
24232
+ } catch {
24233
+ return;
24234
+ }
24235
+ }
24125
24236
  function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
24126
24237
  if (part.type !== "text" || typeof part.text !== "string") {
24127
24238
  return;
@@ -24134,11 +24245,24 @@ function createTaskSessionManagerHook(_ctx, options) {
24134
24245
  const status = parseTaskStatusOutput(part.text);
24135
24246
  if (!status)
24136
24247
  return;
24248
+ const occurrenceId = createOccurrenceId(part, message, partIndex);
24249
+ const existing = backgroundJobBoard.get(status.taskID);
24250
+ if (isFailed && isLateCancelledTaskError(existing, status.state)) {
24251
+ part.text = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
24252
+ log("[task-session-manager] normalized late cancelled injected failure", {
24253
+ taskID: status.taskID,
24254
+ alias: existing?.alias,
24255
+ state: existing?.state,
24256
+ terminalState: existing?.terminalState,
24257
+ result: status.result
24258
+ });
24259
+ rememberProcessedInjectedCompletion(occurrenceId);
24260
+ return existing;
24261
+ }
24137
24262
  if (isCompleted && status.state !== "completed")
24138
24263
  return;
24139
24264
  if (isFailed && status.state !== "error")
24140
24265
  return;
24141
- const occurrenceId = createOccurrenceId(part, message, partIndex);
24142
24266
  if (processedInjectedCompletions.has(occurrenceId))
24143
24267
  return;
24144
24268
  const updated = updateBackgroundJobFromOutput(part.text);
@@ -24298,6 +24422,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24298
24422
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24299
24423
  return;
24300
24424
  }
24425
+ normalizeLateCancelledToolStatus(output);
24426
+ if (await handleTransientTaskStatusOutput(output)) {
24427
+ return;
24428
+ }
24301
24429
  updateBackgroundJobFromOutput(output.output);
24302
24430
  return;
24303
24431
  }
@@ -24421,12 +24549,27 @@ function createTaskSessionManagerHook(_ctx, options) {
24421
24549
  const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24422
24550
  const before = sessionId2 ? backgroundJobBoard.get(sessionId2) : undefined;
24423
24551
  const updated = sessionId2 ? backgroundJobBoard.markRunningFromLiveSession(sessionId2) : undefined;
24552
+ if (before?.cancellationRequested) {
24553
+ log("[task-session-manager] busy observed after cancel request", {
24554
+ sessionID: sessionId2,
24555
+ previousState: before.state,
24556
+ previousTerminalState: before.terminalState,
24557
+ terminalUnreconciled: before.terminalUnreconciled,
24558
+ resultSummary: before.resultSummary,
24559
+ updatedState: updated?.state,
24560
+ updatedCancellationRequested: updated?.cancellationRequested
24561
+ });
24562
+ }
24424
24563
  log("[task-session-manager] busy/status busy observed", {
24425
24564
  sessionID: sessionId2,
24426
24565
  managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
24427
24566
  previousState: before?.state,
24428
24567
  previousTerminalState: before?.terminalState,
24429
- updatedState: updated?.state
24568
+ previousCancellationRequested: before?.cancellationRequested,
24569
+ previousLastLiveBusyAt: before?.lastLiveBusyAt,
24570
+ updatedState: updated?.state,
24571
+ updatedCancellationRequested: updated?.cancellationRequested,
24572
+ updatedLastLiveBusyAt: updated?.lastLiveBusyAt
24430
24573
  });
24431
24574
  return;
24432
24575
  }
@@ -24459,6 +24602,45 @@ function createTaskSessionManagerHook(_ctx, options) {
24459
24602
  }
24460
24603
  }
24461
24604
  };
24605
+ function normalizeLateCancelledToolStatus(output) {
24606
+ if (typeof output.output !== "string")
24607
+ return;
24608
+ const status = parseTaskStatusOutput(output.output);
24609
+ if (!status)
24610
+ return;
24611
+ const existing = backgroundJobBoard.get(status.taskID);
24612
+ if (!isLateCancelledTaskError(existing, status.state))
24613
+ return;
24614
+ log("[task-session-manager] normalized late cancelled task_status output", {
24615
+ taskID: status.taskID,
24616
+ alias: existing?.alias,
24617
+ state: existing?.state,
24618
+ terminalState: existing?.terminalState,
24619
+ result: status.result
24620
+ });
24621
+ output.output = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
24622
+ if (isObjectRecord(output) && isObjectRecord(output.metadata)) {
24623
+ output.metadata.state = "cancelled";
24624
+ }
24625
+ }
24626
+ }
24627
+ function isLateCancelledTaskError(job, state) {
24628
+ if (state !== "error")
24629
+ return false;
24630
+ if (!job?.cancellationRequested)
24631
+ return false;
24632
+ return job.state === "cancelled" || job.terminalState === "cancelled";
24633
+ }
24634
+ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
24635
+ return [
24636
+ `task_id: ${taskID}`,
24637
+ "state: cancelled",
24638
+ "",
24639
+ "<task_error>",
24640
+ summary,
24641
+ "</task_error>"
24642
+ ].join(`
24643
+ `);
24462
24644
  }
24463
24645
  // src/hooks/todo-continuation/index.ts
24464
24646
  import { tool } from "@opencode-ai/plugin";
@@ -29951,7 +30133,7 @@ class MultiplexerSessionManager {
29951
30133
  });
29952
30134
  return;
29953
30135
  }
29954
- if (tracked.ownerInstanceId !== this.instanceId) {
30136
+ if (reason !== "deleted" && tracked.ownerInstanceId !== this.instanceId) {
29955
30137
  log("[multiplexer-session-manager] close skipped; non-owner instance", {
29956
30138
  instanceId: this.instanceId,
29957
30139
  ownerInstanceId: tracked.ownerInstanceId,
@@ -29961,6 +30143,15 @@ class MultiplexerSessionManager {
29961
30143
  });
29962
30144
  return;
29963
30145
  }
30146
+ if (reason === "deleted" && tracked.ownerInstanceId !== this.instanceId) {
30147
+ log("[multiplexer-session-manager] closing deleted pane as non-owner", {
30148
+ instanceId: this.instanceId,
30149
+ ownerInstanceId: tracked.ownerInstanceId,
30150
+ sessionId,
30151
+ paneId: tracked.paneId,
30152
+ reason
30153
+ });
30154
+ }
29964
30155
  if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
29965
30156
  log("[multiplexer-session-manager] close skipped; background job running", {
29966
30157
  instanceId: this.instanceId,
@@ -30679,6 +30870,9 @@ import {
30679
30870
  tool as tool3
30680
30871
  } from "@opencode-ai/plugin";
30681
30872
  var z4 = tool3.schema;
30873
+
30874
+ class SessionStillRunningError extends Error {
30875
+ }
30682
30876
  function createCancelTaskTool(options) {
30683
30877
  const cancel_task = tool3({
30684
30878
  description: `Cancel a tracked background specialist task.
@@ -30702,43 +30896,68 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30702
30896
  if (!requested)
30703
30897
  throw new Error("cancel_task requires task_id");
30704
30898
  const job = options.backgroundJobBoard.resolve(parentSessionID, requested);
30899
+ log("[cancel-task] request received", {
30900
+ parentSessionID,
30901
+ requested,
30902
+ resolvedTaskID: job?.taskID,
30903
+ alias: job?.alias,
30904
+ state: job?.state,
30905
+ terminalState: job?.terminalState,
30906
+ cancellationRequested: job?.cancellationRequested
30907
+ });
30705
30908
  if (!job) {
30706
- return [
30707
- `task_id: ${requested}`,
30708
- "state: unknown",
30709
- "",
30710
- "<task_error>",
30711
- "unknown or unowned background task",
30712
- "</task_error>"
30713
- ].join(`
30714
- `);
30715
- }
30716
- const shouldAbort = job.state === "running" || job.state === "cancelled" || job.state === "reconciled" && job.terminalState === "cancelled";
30717
- if (!shouldAbort) {
30718
- return [
30719
- `task_id: ${job.taskID}`,
30720
- `state: ${job.state}`,
30721
- "",
30722
- "<task_result>",
30723
- `not cancelled: task is already ${job.state}`,
30724
- "</task_result>"
30725
- ].join(`
30726
- `);
30909
+ if (isSessionID(requested)) {
30910
+ if (requested === parentSessionID) {
30911
+ log("[cancel-task] rejected parent session cancellation", {
30912
+ parentSessionID,
30913
+ taskID: requested
30914
+ });
30915
+ return unknownTaskOutput(requested, "cannot cancel parent session");
30916
+ }
30917
+ const knownJob = options.backgroundJobBoard.get(requested);
30918
+ if (knownJob && knownJob.parentSessionID !== parentSessionID) {
30919
+ log("[cancel-task] rejected unowned tracked raw session", {
30920
+ parentSessionID,
30921
+ taskID: requested,
30922
+ ownerParentSessionID: knownJob.parentSessionID
30923
+ });
30924
+ return unknownTaskOutput(requested, "unknown or unowned background task");
30925
+ }
30926
+ const parentID = await getSessionParentID(options.client, requested);
30927
+ if (parentID !== parentSessionID) {
30928
+ log("[cancel-task] rejected raw session without parent ownership", {
30929
+ parentSessionID,
30930
+ taskID: requested,
30931
+ actualParentID: parentID
30932
+ });
30933
+ return unknownTaskOutput(requested, "unknown or unowned background task");
30934
+ }
30935
+ log("[cancel-task] falling back to owned raw session abort", {
30936
+ parentSessionID,
30937
+ taskID: requested
30938
+ });
30939
+ return cancelSessionByID(options, requested, args.reason);
30940
+ }
30941
+ return unknownTaskOutput(requested, "unknown or unowned background task");
30727
30942
  }
30728
30943
  try {
30729
- await abortSessionWithTimeout(options.client, job.taskID, options.abortTimeoutMs ?? 1e4);
30944
+ await abortAndVerifySession(options, job.taskID);
30730
30945
  } catch (error) {
30731
- const timedOut = error instanceof OperationTimeoutError;
30732
- if (timedOut) {
30733
- options.backgroundJobBoard.updateStatus({
30734
- taskID: job.taskID,
30735
- state: "error",
30736
- resultSummary: error instanceof Error ? error.message : "cancel request timed out"
30737
- });
30738
- }
30946
+ const stillRunning = error instanceof SessionStillRunningError;
30947
+ log("[cancel-task] abort failed", {
30948
+ taskID: job.taskID,
30949
+ stillRunning,
30950
+ error: error instanceof Error ? error.message : String(error)
30951
+ });
30952
+ options.backgroundJobBoard.updateStatus({
30953
+ taskID: job.taskID,
30954
+ state: "running",
30955
+ statusUncertain: true,
30956
+ lastStatusError: error instanceof Error ? error.message : String(error)
30957
+ });
30739
30958
  return [
30740
30959
  `task_id: ${job.taskID}`,
30741
- `state: ${timedOut ? "error" : "cancel_error"}`,
30960
+ "state: running",
30742
30961
  "",
30743
30962
  "<task_error>",
30744
30963
  error instanceof Error ? error.message : String(error),
@@ -30746,7 +30965,14 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30746
30965
  ].join(`
30747
30966
  `);
30748
30967
  }
30749
- const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason);
30968
+ const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason, Date.now(), { force: true });
30969
+ log("[cancel-task] marked job cancelled after verified abort", {
30970
+ taskID: job.taskID,
30971
+ alias: job.alias,
30972
+ previousState: job.state,
30973
+ state: cancelled?.state,
30974
+ cancellationRequested: cancelled?.cancellationRequested
30975
+ });
30750
30976
  return [
30751
30977
  `task_id: ${job.taskID}`,
30752
30978
  `state: ${cancelled?.state ?? "cancelled"}`,
@@ -30760,6 +30986,282 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30760
30986
  });
30761
30987
  return { cancel_task };
30762
30988
  }
30989
+ async function cancelSessionByID(options, taskID, reason) {
30990
+ try {
30991
+ await abortAndVerifySession(options, taskID);
30992
+ } catch (error) {
30993
+ const stillRunning = error instanceof SessionStillRunningError;
30994
+ log("[cancel-task] raw session abort failed", {
30995
+ taskID,
30996
+ stillRunning,
30997
+ error: error instanceof Error ? error.message : String(error)
30998
+ });
30999
+ return [
31000
+ `task_id: ${taskID}`,
31001
+ `state: ${stillRunning ? "running" : "error"}`,
31002
+ "",
31003
+ "<task_error>",
31004
+ error instanceof Error ? error.message : String(error),
31005
+ "</task_error>"
31006
+ ].join(`
31007
+ `);
31008
+ }
31009
+ return [
31010
+ `task_id: ${taskID}`,
31011
+ "state: cancelled",
31012
+ "",
31013
+ "<task_error>",
31014
+ normalizeCancelReason2(reason),
31015
+ "</task_error>"
31016
+ ].join(`
31017
+ `);
31018
+ }
31019
+ async function abortAndVerifySession(options, taskID) {
31020
+ log("[cancel-task] abort attempt starting", { taskID });
31021
+ const abortStartedAt = Date.now();
31022
+ try {
31023
+ await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
31024
+ log("[cancel-task] abort call returned", { taskID });
31025
+ } catch (error) {
31026
+ log("[cancel-task] abort call failed", {
31027
+ taskID,
31028
+ error: error instanceof Error ? error.message : String(error),
31029
+ canDelete: canDeleteSession(options.client)
31030
+ });
31031
+ if (!canDeleteSession(options.client))
31032
+ throw error;
31033
+ }
31034
+ if (canDeleteSession(options.client)) {
31035
+ await deleteAndVerifySession(options, taskID, "cancel-task-after-abort");
31036
+ return;
31037
+ }
31038
+ const verifyAbortMs = options.verifyAbortMs ?? 8000;
31039
+ const stableStoppedMs = options.stableStoppedMs ?? 3000;
31040
+ const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
31041
+ const deadline = Date.now() + verifyAbortMs;
31042
+ log("[cancel-task] abort verification starting", {
31043
+ taskID,
31044
+ verifyAbortMs,
31045
+ stableStoppedMs,
31046
+ retryIntervalMs
31047
+ });
31048
+ let attempts = 0;
31049
+ let stableStoppedSince;
31050
+ let lastStatus;
31051
+ while (Date.now() <= deadline) {
31052
+ attempts += 1;
31053
+ const statusSnapshot = await getSessionStatus(options.client, taskID);
31054
+ lastStatus = statusSnapshot.status;
31055
+ log("[cancel-task] abort verification status", {
31056
+ taskID,
31057
+ attempts,
31058
+ status: statusSnapshot.status,
31059
+ statusSource: statusSnapshot.source,
31060
+ statusKeys: statusSnapshot.keys,
31061
+ stableStoppedSince,
31062
+ stableStoppedForMs: stableStoppedSince ? Date.now() - stableStoppedSince : 0,
31063
+ boardState: options.backgroundJobBoard.get(taskID)?.state,
31064
+ boardLastLiveBusyAt: options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt
31065
+ });
31066
+ const boardLastLiveBusyAt = options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt;
31067
+ if (boardLastLiveBusyAt && boardLastLiveBusyAt >= abortStartedAt) {
31068
+ log("[cancel-task] abort verification saw board busy after abort", {
31069
+ taskID,
31070
+ attempts,
31071
+ abortStartedAt,
31072
+ boardLastLiveBusyAt,
31073
+ status: statusSnapshot.status,
31074
+ statusSource: statusSnapshot.source
31075
+ });
31076
+ await deleteAndVerifySession(options, taskID, "board-busy-after-abort");
31077
+ return;
31078
+ }
31079
+ if (statusSnapshot.status === "busy" || statusSnapshot.status === "retry") {
31080
+ if (stableStoppedSince !== undefined) {
31081
+ log("[cancel-task] abort verification saw busy after idle", {
31082
+ taskID,
31083
+ attempts,
31084
+ stableStoppedForMs: Date.now() - stableStoppedSince
31085
+ });
31086
+ await deleteAndVerifySession(options, taskID, "busy-after-idle");
31087
+ return;
31088
+ }
31089
+ stableStoppedSince = undefined;
31090
+ await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
31091
+ log("[cancel-task] abort retry returned", {
31092
+ taskID,
31093
+ attempts,
31094
+ status: statusSnapshot.status
31095
+ });
31096
+ await delay(retryIntervalMs);
31097
+ continue;
31098
+ }
31099
+ stableStoppedSince ??= Date.now();
31100
+ if (Date.now() - stableStoppedSince >= stableStoppedMs) {
31101
+ log("[cancel-task] abort verified stopped", {
31102
+ taskID,
31103
+ attempts,
31104
+ status: statusSnapshot.status,
31105
+ stableStoppedMs
31106
+ });
31107
+ return;
31108
+ }
31109
+ await delay(retryIntervalMs);
31110
+ }
31111
+ log("[cancel-task] abort verification timed out", {
31112
+ taskID,
31113
+ attempts,
31114
+ lastStatus,
31115
+ stableStoppedSince
31116
+ });
31117
+ if (lastStatus === "busy" || lastStatus === "retry") {
31118
+ await deleteAndVerifySession(options, taskID, "still-busy-after-abort");
31119
+ return;
31120
+ }
31121
+ throw new SessionStillRunningError(`Session abort returned but task did not stay stopped: ${taskID}`);
31122
+ }
31123
+ async function deleteAndVerifySession(options, taskID, reason) {
31124
+ const session2 = options.client.session;
31125
+ if (!session2.delete) {
31126
+ log("[cancel-task] session delete unavailable", { taskID, reason });
31127
+ throw new SessionStillRunningError(`Session resumed after abort and delete is unavailable: ${taskID}`);
31128
+ }
31129
+ log("[cancel-task] deleting session after unstable abort", {
31130
+ taskID,
31131
+ reason
31132
+ });
31133
+ try {
31134
+ await withTimeout(session2.delete({ path: { id: taskID } }), options.deleteTimeoutMs ?? 1e4, `Session delete timed out after ${options.deleteTimeoutMs ?? 1e4}ms`);
31135
+ log("[cancel-task] session delete returned", { taskID, reason });
31136
+ } catch (error) {
31137
+ log("[cancel-task] session delete failed; verifying live state", {
31138
+ taskID,
31139
+ reason,
31140
+ error: error instanceof Error ? error.message : String(error)
31141
+ });
31142
+ const status = await getSessionStatus(options.client, taskID);
31143
+ log("[cancel-task] delete failure verification status", {
31144
+ taskID,
31145
+ reason,
31146
+ status: status.status,
31147
+ statusSource: status.source,
31148
+ statusKeys: status.keys
31149
+ });
31150
+ if (status.status === "busy" || status.status === "retry") {
31151
+ throw new SessionStillRunningError(`Session delete failed and task is still busy: ${taskID}`);
31152
+ }
31153
+ if (status.status !== "idle")
31154
+ throw error;
31155
+ }
31156
+ const deadline = Date.now() + (options.deleteVerifyMs ?? 1500);
31157
+ const stableStoppedMs = options.deleteStableStoppedMs ?? 300;
31158
+ const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
31159
+ let stableStoppedSince;
31160
+ let attempts = 0;
31161
+ let lastStatus;
31162
+ while (Date.now() <= deadline) {
31163
+ attempts += 1;
31164
+ const status = await getSessionStatus(options.client, taskID);
31165
+ lastStatus = status.status;
31166
+ log("[cancel-task] delete verification status", {
31167
+ taskID,
31168
+ reason,
31169
+ attempts,
31170
+ status: status.status,
31171
+ statusSource: status.source,
31172
+ statusKeys: status.keys,
31173
+ stableStoppedSince
31174
+ });
31175
+ if (status.status === "busy" || status.status === "retry") {
31176
+ stableStoppedSince = undefined;
31177
+ await delay(retryIntervalMs);
31178
+ continue;
31179
+ }
31180
+ stableStoppedSince ??= Date.now();
31181
+ if (Date.now() - stableStoppedSince >= stableStoppedMs)
31182
+ return;
31183
+ await delay(retryIntervalMs);
31184
+ }
31185
+ throw new SessionStillRunningError(`Session delete returned but task did not stay stopped: ${taskID} (${lastStatus ?? "unknown"})`);
31186
+ }
31187
+ function canDeleteSession(client) {
31188
+ const session2 = client.session;
31189
+ return typeof session2.delete === "function";
31190
+ }
31191
+ async function getSessionStatus(client, taskID) {
31192
+ try {
31193
+ const result = await client.session.status();
31194
+ const data = result.data;
31195
+ if (!isObjectRecord2(data)) {
31196
+ return { status: undefined, source: "invalid-data", keys: [] };
31197
+ }
31198
+ const keys = Object.keys(data).slice(0, 20);
31199
+ const item = data[taskID];
31200
+ if (item === undefined) {
31201
+ return { status: "idle", source: "missing-from-map", keys };
31202
+ }
31203
+ if (isObjectRecord2(item) && typeof item.type === "string") {
31204
+ return { status: item.type, source: "task-map-entry", keys };
31205
+ }
31206
+ if (typeof data.type === "string") {
31207
+ return { status: data.type, source: "legacy-data-type", keys };
31208
+ }
31209
+ const nested = data.status;
31210
+ if (isObjectRecord2(nested) && typeof nested.type === "string") {
31211
+ return { status: nested.type, source: "legacy-data-status", keys };
31212
+ }
31213
+ return { status: undefined, source: "unknown-shape", keys };
31214
+ } catch (error) {
31215
+ log("[cancel-task] session status lookup failed", {
31216
+ taskID,
31217
+ error: error instanceof Error ? error.message : String(error)
31218
+ });
31219
+ return { status: undefined, source: "lookup-error", keys: [] };
31220
+ }
31221
+ }
31222
+ function delay(ms) {
31223
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
31224
+ }
31225
+ function isObjectRecord2(value) {
31226
+ return typeof value === "object" && value !== null;
31227
+ }
31228
+ function isSessionID(value) {
31229
+ return /^ses_[\w-]+$/.test(value);
31230
+ }
31231
+ function normalizeCancelReason2(reason) {
31232
+ const normalized = reason?.replace(/\s+/g, " ").trim();
31233
+ return normalized ? `cancelled: ${normalized}` : "cancelled";
31234
+ }
31235
+ async function getSessionParentID(client, taskID) {
31236
+ const session2 = client.session;
31237
+ if (!session2.get)
31238
+ return;
31239
+ try {
31240
+ const response = await session2.get({ path: { id: taskID } });
31241
+ const data = response.data;
31242
+ if (!isObjectRecord2(data))
31243
+ return;
31244
+ const parentID = data.parentID;
31245
+ return typeof parentID === "string" ? parentID : undefined;
31246
+ } catch (error) {
31247
+ log("[cancel-task] session metadata lookup failed", {
31248
+ taskID,
31249
+ error: error instanceof Error ? error.message : String(error)
31250
+ });
31251
+ return;
31252
+ }
31253
+ }
31254
+ function unknownTaskOutput(taskID, message) {
31255
+ return [
31256
+ `task_id: ${taskID}`,
31257
+ "state: unknown",
31258
+ "",
31259
+ "<task_error>",
31260
+ message,
31261
+ "</task_error>"
31262
+ ].join(`
31263
+ `);
31264
+ }
30763
31265
  // src/tools/council.ts
30764
31266
  import {
30765
31267
  tool as tool4
@@ -5,6 +5,12 @@ interface CancelTaskToolOptions {
5
5
  backgroundJobBoard: BackgroundJobBoard;
6
6
  shouldManageSession: (sessionID: string) => boolean;
7
7
  abortTimeoutMs?: number;
8
+ verifyAbortMs?: number;
9
+ abortRetryIntervalMs?: number;
10
+ stableStoppedMs?: number;
11
+ deleteTimeoutMs?: number;
12
+ deleteVerifyMs?: number;
13
+ deleteStableStoppedMs?: number;
8
14
  }
9
15
  export declare function createCancelTaskTool(options: CancelTaskToolOptions): Record<string, ToolDefinition>;
10
16
  export {};
@@ -14,12 +14,16 @@ export interface BackgroundJobRecord {
14
14
  objective?: string;
15
15
  state: BackgroundJobState;
16
16
  timedOut: boolean;
17
+ statusUncertain: boolean;
18
+ cancellationRequested: boolean;
17
19
  terminalUnreconciled: boolean;
18
20
  launchedAt: number;
19
21
  lastLaunchedAt: number;
20
22
  updatedAt: number;
23
+ lastLiveBusyAt?: number;
21
24
  completedAt?: number;
22
25
  resultSummary?: string;
26
+ lastStatusError?: string;
23
27
  alias: string;
24
28
  lastUsedAt: number;
25
29
  terminalState?: TaskOutputState;
@@ -42,7 +46,9 @@ export interface BackgroundJobStatusInput {
42
46
  taskID: string;
43
47
  state: TaskOutputState;
44
48
  timedOut?: boolean;
49
+ statusUncertain?: boolean;
45
50
  resultSummary?: string;
51
+ lastStatusError?: string;
46
52
  now?: number;
47
53
  }
48
54
  export declare class BackgroundJobBoard {
@@ -57,7 +63,9 @@ export declare class BackgroundJobBoard {
57
63
  updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
58
64
  markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
59
65
  markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
60
- markCancelled(taskID: string, reason?: string, now?: number): BackgroundJobRecord | undefined;
66
+ markCancelled(taskID: string, reason?: string, now?: number, options?: {
67
+ force?: boolean;
68
+ }): BackgroundJobRecord | undefined;
61
69
  get(taskID: string): BackgroundJobRecord | undefined;
62
70
  resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
63
71
  resolveForStatus(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
@@ -13,8 +13,10 @@ export interface TaskStatusOutput {
13
13
  timedOut: boolean;
14
14
  result?: string;
15
15
  }
16
+ export type TaskStatusClassification = 'running' | 'terminal' | 'timeout' | 'transient_process_error' | 'unknown_error';
16
17
  export declare function parseTaskIdFromTaskOutput(output: string): string | undefined;
17
18
  export declare function parseTaskLaunchOutput(output: string): TaskLaunchOutput | undefined;
18
19
  export declare function parseTaskStatusOutput(output: string): TaskStatusOutput | undefined;
20
+ export declare function classifyTaskStatusOutput(status: TaskStatusOutput): TaskStatusClassification;
19
21
  export declare function parseTaskStateFromOutput(output: string): TaskOutputState | undefined;
20
22
  export declare function parseTaskResultFromOutput(output: string): string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-slim",
3
- "version": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.13",
4
4
  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",