oh-my-opencode-slim 2.0.0-beta.11 → 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.
@@ -21,6 +21,9 @@ export declare const MAX_POLL_TIME_MS: number;
21
21
  export declare const FALLBACK_FAILOVER_TIMEOUT_MS = 15000;
22
22
  export declare const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
23
23
  export declare const PHASE_REMINDER_TEXT = "!IMPORTANT! Scheduler workflow: plan lanes/dependencies \u2192 dispatch background specialists \u2192 track task IDs \u2192 wait for hook-driven completion or use task_status only when needed \u2192 reconcile terminal results \u2192 verify. Do not consume running-job output or advance dependent work. !END!";
24
+ export declare const WRITABLE_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.\n- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.\n- Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.\n- Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.\n- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.";
25
+ export declare const READONLY_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- READ-ONLY: inspect and report; do not modify files.\n- Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.\n- Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.\n- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.";
26
+ export declare const NO_SHELL_READONLY_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- READ-ONLY: inspect and report; do not modify files.\n- Use glob/grep/ast_grep_search for discovery and read for file contents.\n- Do not use bash or shell commands.";
24
27
  export declare const TMUX_SPAWN_DELAY_MS = 500;
25
28
  export declare const COUNCILLOR_STAGGER_MS = 250;
26
29
  export declare const STABLE_POLLS_THRESHOLD = 3;
package/dist/index.js CHANGED
@@ -18309,6 +18309,21 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18309
18309
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18310
18310
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
18311
18311
  var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion or use task_status only when needed → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
18312
+ var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18313
+ - Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
18314
+ - Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
18315
+ - Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.
18316
+ - Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.
18317
+ - Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
18318
+ var READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18319
+ - READ-ONLY: inspect and report; do not modify files.
18320
+ - Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.
18321
+ - Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.
18322
+ - Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
18323
+ var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18324
+ - READ-ONLY: inspect and report; do not modify files.
18325
+ - Use glob/grep/ast_grep_search for discovery and read for file contents.
18326
+ - Do not use bash or shell commands.`;
18312
18327
  var TMUX_SPAWN_DELAY_MS = 500;
18313
18328
  var COUNCILLOR_STAGGER_MS = 250;
18314
18329
  var DEFAULT_DISABLED_AGENTS = ["observer"];
@@ -19093,12 +19108,7 @@ Review available agents and lane rules.
19093
19108
  - Do not immediately wait after spawning independent background tasks unless the next step truly depends on their result
19094
19109
  - Reconcile results, resolve conflicts, and gate dependent lanes
19095
19110
 
19096
- **File operations rules:**
19097
- - Always use dedicated file tools for file I/O.
19098
- - Search files/code with \`glob\`, \`grep\`, or \`ast_grep_search\`.
19099
- - Read files with \`read\`. Never use \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or bash commands to read file contents.
19100
- - Edit files with \`apply_patch\`. Never use shell redirection, \`echo\`, \`printf\`, or heredocs for file content unless no file tool can do the job.
19101
- - Use \`bash\` only for execution: git, package managers, tests, builds, scripts, or diagnostics.
19111
+ ${WRITABLE_FILE_OPERATIONS_RULES}
19102
19112
 
19103
19113
  ## 4. Plan and Parallelize
19104
19114
  Build a short work graph before dispatching:
@@ -19120,12 +19130,15 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19120
19130
  - Continue orchestration while tasks run only when useful: planning, scheduling independent lanes, preparing synthesis, or asking needed user questions.
19121
19131
  - If no useful independent work remains, stop after a brief status response; do not call \`task_status\` just to wait. OpenCode will resume you when the background completion event arrives.
19122
19132
  - Use \`task_status(wait: true, timeout_ms: ...)\` only when you actively need a result before a dependent step or final response and no completion event has arrived yet.
19133
+ - If \`task_status(wait: true)\` times out and reports the task still \`running\`, the delegated lane is still owned by that specialist. Do not treat the timeout as failure, cancellation, or permission to do the same work yourself.
19134
+ - For dependent work, either call \`task_status(wait: true)\` again with the same reasonable interval, or stop with a brief waiting status and let the completion event resume you.
19123
19135
  - Parallel background tasks are allowed only when their write scopes do not conflict.
19124
19136
  - Final response requires relevant tasks to be terminal and reconciled.
19125
19137
 
19126
19138
  ### Background Job Discipline
19127
19139
  - Every background task owns its declared lane until terminal.
19128
19140
  - Do not duplicate, undermine, or race a running lane.
19141
+ - A polling timeout is not terminal. The lane remains running until a terminal completion/error/cancel event is observed or the user explicitly cancels it.
19129
19142
  - After dispatch, classify the next step:
19130
19143
  1. independent: continue,
19131
19144
  2. dependent: wait/poll,
@@ -19255,11 +19268,7 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
19255
19268
  - Be transparent about trade-offs when different approaches have valid pros/cons
19256
19269
  - Don't just average responses — choose the best approach and improve upon it
19257
19270
 
19258
- **File Operations Rules**:
19259
- - Use dedicated tools for file I/O if local files must be inspected
19260
- - Search files/code with glob, grep, or ast_grep_search
19261
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19262
- - Use bash only for execution/diagnostics, never for file I/O
19271
+ ${READONLY_FILE_OPERATIONS_RULES}
19263
19272
 
19264
19273
  **Required Output Format**:
19265
19274
  Always include these sections in your final response:
@@ -19372,11 +19381,7 @@ var COUNCILLOR_PROMPT = `You are a councillor in a multi-model council.
19372
19381
 
19373
19382
  You CANNOT edit files, write files, run shell commands, or delegate to other agents. You are an advisor, not an implementer.
19374
19383
 
19375
- **File Operations Rules**:
19376
- - READ-ONLY: do not modify files
19377
- - Search files/code with glob, grep, or ast_grep_search
19378
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19379
- - Do not use bash or shell commands
19384
+ ${NO_SHELL_READONLY_FILE_OPERATIONS_RULES}
19380
19385
 
19381
19386
  **Behavior**:
19382
19387
  - **Examine the codebase** before answering — your read access is what makes council valuable. Don't guess at code you can see.
@@ -19466,12 +19471,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
19466
19471
  - Prioritize visual excellence—code perfection comes second
19467
19472
  - Use grounded, normal, regular english - don't use jargon or overly technical language
19468
19473
 
19469
- ## File Operations Rules
19470
- - Always use dedicated file tools for file I/O
19471
- - Search files/code with glob, grep, or ast_grep_search
19472
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19473
- - Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
19474
- - Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
19474
+ ${WRITABLE_FILE_OPERATIONS_RULES}
19475
19475
 
19476
19476
  ## Review Responsibilities
19477
19477
  - Review existing UI for usability, responsiveness, visual consistency, and polish when asked
@@ -19510,11 +19510,7 @@ var EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
19510
19510
  - **Structural patterns** (function shapes, class structures): ast_grep_search
19511
19511
  - **File discovery** (find by name/extension): glob
19512
19512
 
19513
- **File Operations Rules**:
19514
- - READ-ONLY: Search and report, don't modify files
19515
- - Search files/code with glob, grep, or ast_grep_search
19516
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19517
- - Use bash only for execution/diagnostics, never for file I/O
19513
+ ${READONLY_FILE_OPERATIONS_RULES}
19518
19514
 
19519
19515
  **Behavior**:
19520
19516
  - Be fast and thorough
@@ -19570,12 +19566,7 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
19570
19566
  - Run relevant validation when requested or clearly applicable (otherwise note as skipped with reason)
19571
19567
  - Report completion with summary of changes
19572
19568
 
19573
- **File Operations Rules**:
19574
- - Always use dedicated file tools for file I/O
19575
- - Search files/code with glob, grep, or ast_grep_search
19576
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19577
- - Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
19578
- - Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
19569
+ ${WRITABLE_FILE_OPERATIONS_RULES}
19579
19570
 
19580
19571
  **Constraints**:
19581
19572
  - NO external research (no websearch, context7, grep_app)
@@ -19642,11 +19633,7 @@ var LIBRARIAN_PROMPT = `You are Librarian - a research specialist for codebases
19642
19633
  - grep_app: Search GitHub repositories
19643
19634
  - websearch: General web search for docs
19644
19635
 
19645
- **File Operations Rules**:
19646
- - Use dedicated tools for file I/O when local files must be inspected
19647
- - Search files/code with glob, grep, or ast_grep_search
19648
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19649
- - Use bash only for execution/diagnostics, never for file I/O
19636
+ ${READONLY_FILE_OPERATIONS_RULES}
19650
19637
 
19651
19638
  **Behavior**:
19652
19639
  - Provide evidence-based answers with sources
@@ -19693,10 +19680,7 @@ var OBSERVER_PROMPT = `You are Observer — a visual analysis specialist.
19693
19680
  - Match the language of the request
19694
19681
  - If info not found, state clearly what's missing
19695
19682
 
19696
- **File Operations Rules**:
19697
- - READ-ONLY: do not modify files
19698
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19699
- - Use bash only for execution/diagnostics, never for file I/O
19683
+ ${READONLY_FILE_OPERATIONS_RULES}
19700
19684
  `;
19701
19685
  function createObserverAgent(model, customPrompt, customAppendPrompt) {
19702
19686
  let prompt = OBSERVER_PROMPT;
@@ -19742,11 +19726,7 @@ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor and code rev
19742
19726
  - Focus on strategy, not execution
19743
19727
  - Point to specific files/lines when relevant
19744
19728
 
19745
- **File Operations Rules**:
19746
- - READ-ONLY: do not modify files
19747
- - Search files/code with glob, grep, or ast_grep_search
19748
- - Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
19749
- - Use bash only for execution/diagnostics, never for file I/O
19729
+ ${READONLY_FILE_OPERATIONS_RULES}
19750
19730
  `;
19751
19731
  function createOracleAgent(model, customPrompt, customAppendPrompt) {
19752
19732
  let prompt = ORACLE_PROMPT;
@@ -22662,6 +22642,10 @@ function createDisplayNameMentionRewriter(config) {
22662
22642
  };
22663
22643
  }
22664
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
+ ]);
22665
22649
  function parseTaskIdFromTaskOutput(output) {
22666
22650
  const lines = output.split(/\r?\n/);
22667
22651
  for (const line of lines) {
@@ -22697,6 +22681,19 @@ function parseTaskStatusOutput(output) {
22697
22681
  result: parseTaskResultFromOutput(output)
22698
22682
  };
22699
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
+ }
22700
22697
  function parseTaskStateFromOutput(output) {
22701
22698
  for (const line of getTaskHeader(output).split(/\r?\n/)) {
22702
22699
  const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
@@ -22755,11 +22752,15 @@ class BackgroundJobBoard {
22755
22752
  objective: input.objective ?? existing.objective,
22756
22753
  state: "running",
22757
22754
  timedOut: false,
22755
+ statusUncertain: false,
22756
+ cancellationRequested: false,
22758
22757
  terminalUnreconciled: false,
22759
22758
  completedAt: undefined,
22760
22759
  resultSummary: undefined,
22760
+ lastStatusError: undefined,
22761
22761
  terminalState: undefined,
22762
22762
  lastLaunchedAt: now,
22763
+ lastLiveBusyAt: now,
22763
22764
  lastUsedAt: now,
22764
22765
  updatedAt: now
22765
22766
  };
@@ -22774,9 +22775,12 @@ class BackgroundJobBoard {
22774
22775
  objective: input.objective,
22775
22776
  state: "running",
22776
22777
  timedOut: false,
22778
+ statusUncertain: false,
22779
+ cancellationRequested: false,
22777
22780
  terminalUnreconciled: false,
22778
22781
  launchedAt: now,
22779
22782
  lastLaunchedAt: now,
22783
+ lastLiveBusyAt: now,
22780
22784
  lastUsedAt: now,
22781
22785
  updatedAt: now,
22782
22786
  alias: this.nextAlias(input.parentSessionID, input.agent),
@@ -22798,11 +22802,13 @@ class BackgroundJobBoard {
22798
22802
  ...existing,
22799
22803
  state: input.state,
22800
22804
  timedOut: input.timedOut ?? false,
22805
+ statusUncertain: input.statusUncertain ?? false,
22801
22806
  terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
22802
22807
  updatedAt: now,
22803
22808
  completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
22804
22809
  terminalState: terminal ? input.state : existing.terminalState,
22805
- resultSummary: input.resultSummary ?? existing.resultSummary
22810
+ resultSummary: input.resultSummary ?? existing.resultSummary,
22811
+ lastStatusError: input.lastStatusError
22806
22812
  };
22807
22813
  this.jobs.set(input.taskID, updated);
22808
22814
  this.trimReusable(input.taskID);
@@ -22819,6 +22825,36 @@ class BackgroundJobBoard {
22819
22825
  resultSummary: status.result
22820
22826
  });
22821
22827
  }
22828
+ markRunningFromLiveSession(taskID, now = Date.now()) {
22829
+ const existing = this.jobs.get(taskID);
22830
+ if (!existing)
22831
+ return;
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
+ }
22841
+ const updated = {
22842
+ ...existing,
22843
+ state: "running",
22844
+ timedOut: false,
22845
+ statusUncertain: false,
22846
+ cancellationRequested: false,
22847
+ terminalUnreconciled: false,
22848
+ updatedAt: now,
22849
+ lastLiveBusyAt: now,
22850
+ completedAt: undefined,
22851
+ terminalState: undefined,
22852
+ resultSummary: undefined,
22853
+ lastStatusError: undefined
22854
+ };
22855
+ this.jobs.set(taskID, updated);
22856
+ return updated;
22857
+ }
22822
22858
  markReconciled(taskID, now = Date.now()) {
22823
22859
  const existing = this.jobs.get(taskID);
22824
22860
  if (!existing)
@@ -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 only completed/reconciled sessions 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"],
@@ -22953,8 +22995,10 @@ class BackgroundJobBoard {
22953
22995
  }
22954
22996
  }
22955
22997
  formatReusableJob(job) {
22998
+ const terminal = job.terminalState ?? terminalStateOf(job.state);
22999
+ const reconciliation = job.terminalUnreconciled ? "unreconciled" : "reconciled";
22956
23000
  const lines = [
22957
- `- ${job.alias} / ${job.taskID} / ${job.agent} / completed, reconciled`,
23001
+ `- ${job.alias} / ${job.taskID} / ${job.agent} / ${terminal ?? job.state}, ${reconciliation}`,
22958
23002
  ` Objective: ${job.objective || job.description}`
22959
23003
  ];
22960
23004
  const context = formatContextFiles(job.contextFiles, this.readContextMaxFiles);
@@ -22979,7 +23023,8 @@ function deriveTaskSessionLabel(input) {
22979
23023
  return firstPromptLine ? firstPromptLine.slice(0, 48) : `recent ${input.agentType} task`;
22980
23024
  }
22981
23025
  function isReusable(job) {
22982
- return job.state === "reconciled" && job.terminalState === "completed";
23026
+ const terminal = job.terminalState ?? terminalStateOf(job.state);
23027
+ return terminal === "completed" && !job.terminalUnreconciled;
22983
23028
  }
22984
23029
  function terminalStateOf(state) {
22985
23030
  return state === "completed" || state === "error" || state === "cancelled" ? state : undefined;
@@ -22999,13 +23044,15 @@ function formatJob(job, now = Date.now()) {
22999
23044
  const ageMs = now - job.lastLaunchedAt;
23000
23045
  const isResume = job.lastLaunchedAt !== job.launchedAt;
23001
23046
  const ageLabel = job.state === "running" && ageMs < 30000 ? ` [${isResume ? "resumed" : "just launched"}, ${Math.floor(ageMs / 1000)}s ago]` : "";
23002
- 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}`;
23003
23048
  const lines = [
23004
23049
  `- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
23005
23050
  ` Objective: ${job.objective || job.description}`
23006
23051
  ];
23007
23052
  if (job.resultSummary && job.terminalUnreconciled) {
23008
23053
  lines.push(` Result: ${singleLine(job.resultSummary)}`);
23054
+ } else if (job.lastStatusError && job.statusUncertain) {
23055
+ lines.push(` Status: ${singleLine(job.lastStatusError)}`);
23009
23056
  }
23010
23057
  return lines.join(`
23011
23058
  `);
@@ -24086,14 +24133,44 @@ function createTaskSessionManagerHook(_ctx, options) {
24086
24133
  const status = parseTaskStatusOutput(output);
24087
24134
  if (!status)
24088
24135
  return;
24136
+ log("[task-session-manager] parsed task status output", {
24137
+ taskID: status.taskID,
24138
+ state: status.state,
24139
+ timedOut: status.timedOut,
24140
+ hasResult: Boolean(status.result)
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
+ }
24089
24153
  const updated = backgroundJobBoard.updateStatus({
24090
24154
  taskID: status.taskID,
24091
24155
  state: status.state,
24092
24156
  timedOut: status.timedOut,
24093
24157
  resultSummary: status.result
24094
24158
  });
24095
- if (!updated)
24159
+ if (!updated) {
24160
+ log("[task-session-manager] ignored status for unknown background job", {
24161
+ taskID: status.taskID,
24162
+ state: status.state
24163
+ });
24096
24164
  return;
24165
+ }
24166
+ log("[task-session-manager] background job status updated", {
24167
+ taskID: updated.taskID,
24168
+ alias: updated.alias,
24169
+ parentSessionID: updated.parentSessionID,
24170
+ state: updated.state,
24171
+ terminalUnreconciled: updated.terminalUnreconciled,
24172
+ timedOut: updated.timedOut
24173
+ });
24097
24174
  if (updated.terminalUnreconciled) {
24098
24175
  pendingManagedTaskIds.delete(updated.taskID);
24099
24176
  backgroundJobBoard.addContext(updated.taskID, contextFilesForPrompt(contextByTask.get(updated.taskID)));
@@ -24101,6 +24178,61 @@ function createTaskSessionManagerHook(_ctx, options) {
24101
24178
  }
24102
24179
  return updated;
24103
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
+ }
24104
24236
  function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
24105
24237
  if (part.type !== "text" || typeof part.text !== "string") {
24106
24238
  return;
@@ -24113,16 +24245,36 @@ function createTaskSessionManagerHook(_ctx, options) {
24113
24245
  const status = parseTaskStatusOutput(part.text);
24114
24246
  if (!status)
24115
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
+ }
24116
24262
  if (isCompleted && status.state !== "completed")
24117
24263
  return;
24118
24264
  if (isFailed && status.state !== "error")
24119
24265
  return;
24120
- const occurrenceId = createOccurrenceId(part, message, partIndex);
24121
24266
  if (processedInjectedCompletions.has(occurrenceId))
24122
24267
  return;
24123
24268
  const updated = updateBackgroundJobFromOutput(part.text);
24124
24269
  if (!updated)
24125
24270
  return;
24271
+ log("[task-session-manager] processed injected background completion", {
24272
+ taskID: updated.taskID,
24273
+ alias: updated.alias,
24274
+ parentSessionID: updated.parentSessionID,
24275
+ state: updated.state,
24276
+ occurrenceId
24277
+ });
24126
24278
  rememberProcessedInjectedCompletion(occurrenceId);
24127
24279
  return updated;
24128
24280
  }
@@ -24179,6 +24331,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24179
24331
  const taskIDs = backgroundJobBoard.list(parentSessionID).filter((job) => job.terminalUnreconciled).map((job) => job.taskID);
24180
24332
  if (taskIDs.length === 0)
24181
24333
  return;
24334
+ log("[task-session-manager] terminal jobs injected for reconciliation", {
24335
+ parentSessionID,
24336
+ taskIDs
24337
+ });
24182
24338
  const existing = terminalJobsInjectedByParent.get(parentSessionID) ?? new Set;
24183
24339
  for (const taskID of taskIDs) {
24184
24340
  existing.add(taskID);
@@ -24189,6 +24345,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24189
24345
  const taskIDs = terminalJobsInjectedByParent.get(parentSessionID);
24190
24346
  if (!taskIDs)
24191
24347
  return;
24348
+ log("[task-session-manager] reconciling injected terminal jobs", {
24349
+ parentSessionID,
24350
+ taskIDs: [...taskIDs]
24351
+ });
24192
24352
  for (const taskID of taskIDs) {
24193
24353
  backgroundJobBoard.markReconciled(taskID);
24194
24354
  }
@@ -24262,6 +24422,10 @@ function createTaskSessionManagerHook(_ctx, options) {
24262
24422
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24263
24423
  return;
24264
24424
  }
24425
+ normalizeLateCancelledToolStatus(output);
24426
+ if (await handleTransientTaskStatusOutput(output)) {
24427
+ return;
24428
+ }
24265
24429
  updateBackgroundJobFromOutput(output.output);
24266
24430
  return;
24267
24431
  }
@@ -24272,13 +24436,21 @@ function createTaskSessionManagerHook(_ctx, options) {
24272
24436
  return;
24273
24437
  const launch = parseTaskLaunchOutput(output.output);
24274
24438
  if (launch) {
24275
- backgroundJobBoard.registerLaunch({
24439
+ const record = backgroundJobBoard.registerLaunch({
24276
24440
  taskID: launch.taskID,
24277
24441
  parentSessionID: pending.parentSessionId,
24278
24442
  agent: pending.agentType,
24279
24443
  description: pending.label,
24280
24444
  objective: pending.label
24281
24445
  });
24446
+ log("[task-session-manager] background task launch registered", {
24447
+ taskID: record.taskID,
24448
+ alias: record.alias,
24449
+ parentSessionID: record.parentSessionID,
24450
+ agent: record.agent,
24451
+ description: record.description,
24452
+ state: record.state
24453
+ });
24282
24454
  backgroundJobBoard.addContext(launch.taskID, contextFilesForPrompt(contextByTask.get(launch.taskID)));
24283
24455
  pendingManagedTaskIds.add(launch.taskID);
24284
24456
  return;
@@ -24344,6 +24516,11 @@ function createTaskSessionManagerHook(_ctx, options) {
24344
24516
  event: async (input) => {
24345
24517
  if (input.event.type === "session.created") {
24346
24518
  const info = input.event.properties?.info;
24519
+ log("[task-session-manager] session.created observed", {
24520
+ sessionID: info?.id,
24521
+ parentSessionID: info?.parentID,
24522
+ managesParent: info?.parentID ? options.shouldManageSession(info.parentID) : false
24523
+ });
24347
24524
  if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
24348
24525
  pendingManagedTaskIds.add(info.id);
24349
24526
  }
@@ -24351,6 +24528,11 @@ function createTaskSessionManagerHook(_ctx, options) {
24351
24528
  }
24352
24529
  if (input.event.type === "session.idle" || input.event.type === "session.status" && input.event.properties?.status?.type === "idle") {
24353
24530
  const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24531
+ log("[task-session-manager] idle/status idle observed", {
24532
+ sessionID: sessionId2,
24533
+ managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
24534
+ terminalJobsPending: sessionId2 ? terminalJobsInjectedByParent.get(sessionId2)?.size ?? 0 : 0
24535
+ });
24354
24536
  if (sessionId2 && options.shouldManageSession(sessionId2)) {
24355
24537
  reconcileInjectedTerminalJobs(sessionId2);
24356
24538
  }
@@ -24363,11 +24545,49 @@ function createTaskSessionManagerHook(_ctx, options) {
24363
24545
  }
24364
24546
  return;
24365
24547
  }
24548
+ if (input.event.type === "session.status" && input.event.properties?.status?.type === "busy") {
24549
+ const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24550
+ const before = sessionId2 ? backgroundJobBoard.get(sessionId2) : undefined;
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
+ }
24563
+ log("[task-session-manager] busy/status busy observed", {
24564
+ sessionID: sessionId2,
24565
+ managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
24566
+ previousState: before?.state,
24567
+ previousTerminalState: before?.terminalState,
24568
+ previousCancellationRequested: before?.cancellationRequested,
24569
+ previousLastLiveBusyAt: before?.lastLiveBusyAt,
24570
+ updatedState: updated?.state,
24571
+ updatedCancellationRequested: updated?.cancellationRequested,
24572
+ updatedLastLiveBusyAt: updated?.lastLiveBusyAt
24573
+ });
24574
+ return;
24575
+ }
24366
24576
  if (input.event.type !== "session.deleted")
24367
24577
  return;
24368
24578
  const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
24369
24579
  if (!sessionId)
24370
24580
  return;
24581
+ log("[task-session-manager] session.deleted observed; clearing job state", {
24582
+ sessionID: sessionId,
24583
+ deletedJob: backgroundJobBoard.get(sessionId) ? {
24584
+ state: backgroundJobBoard.get(sessionId)?.state,
24585
+ parentSessionID: backgroundJobBoard.get(sessionId)?.parentSessionID,
24586
+ alias: backgroundJobBoard.get(sessionId)?.alias
24587
+ } : undefined,
24588
+ childJobCount: backgroundJobBoard.list(sessionId).length,
24589
+ managesSession: options.shouldManageSession(sessionId)
24590
+ });
24371
24591
  backgroundJobBoard.drop(sessionId);
24372
24592
  backgroundJobBoard.clearParent(sessionId);
24373
24593
  terminalJobsInjectedByParent.delete(sessionId);
@@ -24382,6 +24602,45 @@ function createTaskSessionManagerHook(_ctx, options) {
24382
24602
  }
24383
24603
  }
24384
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
+ `);
24385
24644
  }
24386
24645
  // src/hooks/todo-continuation/index.ts
24387
24646
  import { tool } from "@opencode-ai/plugin";
@@ -29690,6 +29949,7 @@ class MultiplexerSessionManager {
29690
29949
  parentId,
29691
29950
  title,
29692
29951
  directory,
29952
+ ownerInstanceId: this.instanceId,
29693
29953
  createdAt: now,
29694
29954
  lastSeenAt: now,
29695
29955
  seenInStatus: false
@@ -29715,7 +29975,9 @@ class MultiplexerSessionManager {
29715
29975
  instanceId: this.instanceId,
29716
29976
  sessionId: sessionId2,
29717
29977
  tracked: this.sessions.has(sessionId2),
29718
- known: this.knownSessions.has(sessionId2)
29978
+ known: this.knownSessions.has(sessionId2),
29979
+ ownerInstanceId: this.sessions.get(sessionId2)?.ownerInstanceId,
29980
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId2)?.state
29719
29981
  });
29720
29982
  await this.closeSession(sessionId2, "idle");
29721
29983
  return;
@@ -29726,6 +29988,14 @@ class MultiplexerSessionManager {
29726
29988
  if (!sessionId)
29727
29989
  return;
29728
29990
  if (event.properties?.status?.type === "idle") {
29991
+ log("[multiplexer-session-manager] session status idle received", {
29992
+ instanceId: this.instanceId,
29993
+ sessionId,
29994
+ tracked: this.sessions.has(sessionId),
29995
+ known: this.knownSessions.has(sessionId),
29996
+ ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
29997
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
29998
+ });
29729
29999
  await this.closeSession(sessionId, "idle");
29730
30000
  return;
29731
30001
  }
@@ -29734,7 +30004,9 @@ class MultiplexerSessionManager {
29734
30004
  instanceId: this.instanceId,
29735
30005
  sessionId,
29736
30006
  tracked: this.sessions.has(sessionId),
29737
- known: this.knownSessions.has(sessionId)
30007
+ known: this.knownSessions.has(sessionId),
30008
+ ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
30009
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
29738
30010
  });
29739
30011
  await this.respawnIfKnown(sessionId);
29740
30012
  }
@@ -29749,7 +30021,11 @@ class MultiplexerSessionManager {
29749
30021
  return;
29750
30022
  log("[multiplexer-session-manager] session deleted, closing pane", {
29751
30023
  instanceId: this.instanceId,
29752
- sessionId
30024
+ sessionId,
30025
+ tracked: this.sessions.has(sessionId),
30026
+ known: this.knownSessions.has(sessionId),
30027
+ ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
30028
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
29753
30029
  });
29754
30030
  await this.closeSession(sessionId, "deleted");
29755
30031
  }
@@ -29780,6 +30056,15 @@ class MultiplexerSessionManager {
29780
30056
  const now = Date.now();
29781
30057
  const sessionsToClose = [];
29782
30058
  for (const [sessionId, tracked] of this.sessions.entries()) {
30059
+ if (tracked.ownerInstanceId !== this.instanceId) {
30060
+ log("[multiplexer-session-manager] skipping non-owner poll close", {
30061
+ instanceId: this.instanceId,
30062
+ ownerInstanceId: tracked.ownerInstanceId,
30063
+ sessionId,
30064
+ paneId: tracked.paneId
30065
+ });
30066
+ continue;
30067
+ }
29783
30068
  const status = allStatuses[sessionId];
29784
30069
  const isIdle = status?.type === "idle";
29785
30070
  if (status) {
@@ -29790,7 +30075,7 @@ class MultiplexerSessionManager {
29790
30075
  tracked.missingSince = now;
29791
30076
  }
29792
30077
  const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
29793
- const shouldKeepRunningBackgroundJob = missingTooLong && this.isRunningBackgroundJob(sessionId);
30078
+ const shouldKeepRunningBackgroundJob = (isIdle || missingTooLong) && this.isRunningBackgroundJob(sessionId);
29794
30079
  if (isIdle || missingTooLong) {
29795
30080
  if (shouldKeepRunningBackgroundJob) {
29796
30081
  log("[multiplexer-session-manager] keeping running background pane", {
@@ -29848,12 +30133,44 @@ class MultiplexerSessionManager {
29848
30133
  });
29849
30134
  return;
29850
30135
  }
30136
+ if (reason !== "deleted" && tracked.ownerInstanceId !== this.instanceId) {
30137
+ log("[multiplexer-session-manager] close skipped; non-owner instance", {
30138
+ instanceId: this.instanceId,
30139
+ ownerInstanceId: tracked.ownerInstanceId,
30140
+ sessionId,
30141
+ paneId: tracked.paneId,
30142
+ reason
30143
+ });
30144
+ return;
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
+ }
30155
+ if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
30156
+ log("[multiplexer-session-manager] close skipped; background job running", {
30157
+ instanceId: this.instanceId,
30158
+ sessionId,
30159
+ paneId: tracked.paneId,
30160
+ reason,
30161
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
30162
+ });
30163
+ return;
30164
+ }
29851
30165
  this.sessions.delete(sessionId);
29852
30166
  log("[multiplexer-session-manager] closing session pane", {
29853
30167
  instanceId: this.instanceId,
29854
30168
  sessionId,
29855
30169
  paneId: tracked.paneId,
29856
- reason
30170
+ reason,
30171
+ backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state,
30172
+ parentId: tracked.parentId,
30173
+ title: tracked.title
29857
30174
  });
29858
30175
  const closePromise = this.multiplexer.closePane(tracked.paneId).then(() => {
29859
30176
  return;
@@ -29927,6 +30244,7 @@ class MultiplexerSessionManager {
29927
30244
  parentId: known.parentId,
29928
30245
  title: known.title,
29929
30246
  directory: known.directory,
30247
+ ownerInstanceId: this.instanceId,
29930
30248
  createdAt: now,
29931
30249
  lastSeenAt: now,
29932
30250
  seenInStatus: false
@@ -30552,6 +30870,9 @@ import {
30552
30870
  tool as tool3
30553
30871
  } from "@opencode-ai/plugin";
30554
30872
  var z4 = tool3.schema;
30873
+
30874
+ class SessionStillRunningError extends Error {
30875
+ }
30555
30876
  function createCancelTaskTool(options) {
30556
30877
  const cancel_task = tool3({
30557
30878
  description: `Cancel a tracked background specialist task.
@@ -30575,42 +30896,68 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30575
30896
  if (!requested)
30576
30897
  throw new Error("cancel_task requires task_id");
30577
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
+ });
30578
30908
  if (!job) {
30579
- return [
30580
- `task_id: ${requested}`,
30581
- "state: unknown",
30582
- "",
30583
- "<task_error>",
30584
- "unknown or unowned background task",
30585
- "</task_error>"
30586
- ].join(`
30587
- `);
30588
- }
30589
- if (job.state !== "running") {
30590
- return [
30591
- `task_id: ${job.taskID}`,
30592
- `state: ${job.state}`,
30593
- "",
30594
- "<task_result>",
30595
- `not cancelled: task is already ${job.state}`,
30596
- "</task_result>"
30597
- ].join(`
30598
- `);
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");
30599
30942
  }
30600
30943
  try {
30601
- await abortSessionWithTimeout(options.client, job.taskID, options.abortTimeoutMs ?? 1e4);
30944
+ await abortAndVerifySession(options, job.taskID);
30602
30945
  } catch (error) {
30603
- const timedOut = error instanceof OperationTimeoutError;
30604
- if (timedOut) {
30605
- options.backgroundJobBoard.updateStatus({
30606
- taskID: job.taskID,
30607
- state: "error",
30608
- resultSummary: error instanceof Error ? error.message : "cancel request timed out"
30609
- });
30610
- }
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
+ });
30611
30958
  return [
30612
30959
  `task_id: ${job.taskID}`,
30613
- `state: ${timedOut ? "error" : "cancel_error"}`,
30960
+ "state: running",
30614
30961
  "",
30615
30962
  "<task_error>",
30616
30963
  error instanceof Error ? error.message : String(error),
@@ -30618,7 +30965,14 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30618
30965
  ].join(`
30619
30966
  `);
30620
30967
  }
30621
- 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
+ });
30622
30976
  return [
30623
30977
  `task_id: ${job.taskID}`,
30624
30978
  `state: ${cancelled?.state ?? "cancelled"}`,
@@ -30632,6 +30986,282 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
30632
30986
  });
30633
30987
  return { cancel_task };
30634
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
+ }
30635
31265
  // src/tools/council.ts
30636
31266
  import {
30637
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 {};
package/dist/tui.js CHANGED
@@ -69,6 +69,21 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
69
69
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
70
70
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
71
71
  var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion or use task_status only when needed → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
72
+ var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
73
+ - Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
74
+ - Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
75
+ - Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.
76
+ - Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.
77
+ - Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
78
+ var READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
79
+ - READ-ONLY: inspect and report; do not modify files.
80
+ - Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.
81
+ - Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.
82
+ - Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
83
+ var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
84
+ - READ-ONLY: inspect and report; do not modify files.
85
+ - Use glob/grep/ast_grep_search for discovery and read for file contents.
86
+ - Do not use bash or shell commands.`;
72
87
  var TMUX_SPAWN_DELAY_MS = 500;
73
88
  var COUNCILLOR_STAGGER_MS = 250;
74
89
  var DEFAULT_DISABLED_AGENTS = ["observer"];
@@ -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 {
@@ -55,8 +61,11 @@ export declare class BackgroundJobBoard {
55
61
  registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
56
62
  updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
57
63
  updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
64
+ markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
58
65
  markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
59
- markCancelled(taskID: string, reason?: string, now?: number): BackgroundJobRecord | undefined;
66
+ markCancelled(taskID: string, reason?: string, now?: number, options?: {
67
+ force?: boolean;
68
+ }): BackgroundJobRecord | undefined;
60
69
  get(taskID: string): BackgroundJobRecord | undefined;
61
70
  resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
62
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.11",
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",