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

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
@@ -18196,6 +18196,13 @@ function getCustomOpenCodeConfigDir() {
18196
18196
  const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
18197
18197
  return configDir || undefined;
18198
18198
  }
18199
+ function getConfigDir() {
18200
+ const customConfigDir = getCustomOpenCodeConfigDir();
18201
+ if (customConfigDir) {
18202
+ return customConfigDir;
18203
+ }
18204
+ return getDefaultOpenCodeConfigDir();
18205
+ }
18199
18206
  function getConfigSearchDirs() {
18200
18207
  const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
18201
18208
  return dirs.filter((dir, index) => {
@@ -18203,7 +18210,7 @@ function getConfigSearchDirs() {
18203
18210
  });
18204
18211
  }
18205
18212
  function getOpenCodeConfigPaths() {
18206
- const configDir = getDefaultOpenCodeConfigDir();
18213
+ const configDir = getConfigDir();
18207
18214
  return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
18208
18215
  }
18209
18216
 
@@ -18232,6 +18239,12 @@ var CUSTOM_SKILLS = [
18232
18239
  description: "Heavy/complex coding sessions and large modifications workflow",
18233
18240
  allowedAgents: ["orchestrator"],
18234
18241
  sourcePath: "src/skills/deepwork"
18242
+ },
18243
+ {
18244
+ name: "oh-my-opencode-slim",
18245
+ description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
18246
+ allowedAgents: ["orchestrator"],
18247
+ sourcePath: "src/skills/oh-my-opencode-slim"
18235
18248
  }
18236
18249
  ];
18237
18250
 
@@ -18308,7 +18321,7 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
18308
18321
  var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
18309
18322
  var MAX_POLL_TIME_MS = 5 * 60 * 1000;
18310
18323
  var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
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!`;
18324
+ var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
18312
18325
  var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
18313
18326
  - 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
18327
  - Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
@@ -18597,12 +18610,6 @@ var DivoomConfigSchema = z2.object({
18597
18610
  posterizeBits: z2.number().int().min(1).max(8).default(3),
18598
18611
  gifs: z2.record(z2.string(), z2.string().min(1)).optional()
18599
18612
  });
18600
- var TodoContinuationConfigSchema = z2.object({
18601
- maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
18602
- cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
18603
- autoEnable: z2.boolean().default(false).describe("Automatically enable auto-continue when the orchestrator session has enough todos"),
18604
- autoEnableThreshold: z2.number().int().min(1).max(50).default(4).describe("Number of todos that triggers auto-enable (only used when autoEnable is true)")
18605
- });
18606
18613
  var FailoverConfigSchema = z2.object({
18607
18614
  enabled: z2.boolean().default(true),
18608
18615
  timeoutMs: z2.number().min(0).default(15000),
@@ -18649,7 +18656,6 @@ var PluginConfigSchema = z2.object({
18649
18656
  interview: InterviewConfigSchema.optional(),
18650
18657
  backgroundJobs: BackgroundJobsConfigSchema.optional(),
18651
18658
  divoom: DivoomConfigSchema.optional(),
18652
- todoContinuation: TodoContinuationConfigSchema.optional(),
18653
18659
  fallback: FailoverConfigSchema.optional(),
18654
18660
  council: CouncilConfigSchema.optional()
18655
18661
  }).superRefine((value, ctx) => {
@@ -18903,34 +18909,32 @@ function parseModelReference(model) {
18903
18909
  async function promptWithTimeout(client, args, timeoutMs, signal) {
18904
18910
  if (signal?.aborted)
18905
18911
  throw new Error("Prompt cancelled");
18906
- if (timeoutMs <= 0) {
18907
- await client.session.prompt(args);
18908
- return;
18909
- }
18910
18912
  const sessionId = args.path.id;
18913
+ const hasTimeout = timeoutMs > 0;
18911
18914
  let timer;
18912
18915
  let onAbort;
18913
18916
  try {
18914
18917
  const promptPromise = client.session.prompt(args);
18915
18918
  promptPromise.catch(() => {});
18916
- await Promise.race([
18917
- promptPromise,
18918
- new Promise((_, reject) => {
18919
+ const racers = [promptPromise];
18920
+ if (hasTimeout) {
18921
+ racers.push(new Promise((_, reject) => {
18919
18922
  timer = setTimeout(() => {
18920
18923
  reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
18921
18924
  }, timeoutMs);
18922
- }),
18923
- new Promise((_, reject) => {
18924
- if (!signal)
18925
- return;
18925
+ }));
18926
+ }
18927
+ if (signal) {
18928
+ racers.push(new Promise((_, reject) => {
18926
18929
  if (signal.aborted) {
18927
18930
  reject(new Error("Prompt cancelled"));
18928
18931
  return;
18929
18932
  }
18930
18933
  onAbort = () => reject(new Error("Prompt cancelled"));
18931
18934
  signal.addEventListener("abort", onAbort, { once: true });
18932
- })
18933
- ]);
18935
+ }));
18936
+ }
18937
+ await Promise.race(racers);
18934
18938
  } catch (error) {
18935
18939
  if (error instanceof OperationTimeoutError) {
18936
18940
  try {
@@ -19122,31 +19126,15 @@ ${enabledParallelExamples}
19122
19126
 
19123
19127
  Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
19124
19128
 
19125
- ### OpenCode scheduler model
19126
- - Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
19127
- - A dispatch returns a task/session ID immediately; it does not mean completion.
19128
- - Track each task ID with specialist, objective, state, and any advisory ownership/dependency labels from the dispatch plan.
19129
- - Background completion is event/hook-driven: when a background task finishes, OpenCode injects a follow-up message with the terminal result.
19130
- - Continue orchestration while tasks run only when useful: planning, scheduling independent lanes, preparing synthesis, or asking needed user questions.
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.
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.
19129
+ ### Background Task Discipline
19130
+ - Prefer \`task(..., background: true)\` for delegated work that can run independently.
19131
+ - Track each task's specialist, objective, task/session ID, and file/topic ownership.
19132
+ - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
19133
+ - Before local edits or another writer task, compare against running task scopes.
19135
19134
  - Parallel background tasks are allowed only when their write scopes do not conflict.
19136
- - Final response requires relevant tasks to be terminal and reconciled.
19137
-
19138
- ### Background Job Discipline
19139
- - Every background task owns its declared lane until terminal.
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.
19142
- - After dispatch, classify the next step:
19143
- 1. independent: continue,
19144
- 2. dependent: wait/poll,
19145
- 3. no useful independent work: stop and let hook-driven completion resume.
19146
- - Before editing files or spawning another writer, compare against running job scopes.
19135
+ - Before final response, reconcile any terminal jobs shown in the Background Job Board.
19147
19136
  - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
19148
19137
  - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
19149
- - Never finalize work that depends on unresolved background jobs.
19150
19138
 
19151
19139
  ### Design Handoff Discipline
19152
19140
  - When @designer completes UI/UX work, treat layout, spacing, hierarchy, motion, color, affordances, and component feel as intentional design output.
@@ -19160,13 +19148,9 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
19160
19148
  - When too much unrelated, and really needed, start a fresh session with the specialist
19161
19149
  - If multiple remembered sessions fit, prefer the most recently used matching session.
19162
19150
  - Prefer re-uses over creating new sessions all the time
19163
-
19164
- ### Auto-Continue
19165
- When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
19166
- - **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
19167
- - **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
19168
- - Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
19169
- - The user can toggle this anytime via the \`/auto-continue\` command.
19151
+ - When reusing a specialist session, you MUST pass the existing session or alias in the task tool's \`task_id\` argument. Saying "reuse" in prose is not enough.
19152
+ - If the Background Job Board lists \`fix-1 / ses_abc / fixer\`, call task with \`subagent_type: "fixer"\` and \`task_id: "fix-1"\` or \`task_id: "ses_abc"\`.
19153
+ - Do not leave \`task_id\` empty when intending to reuse; omitted or empty \`task_id\` creates a new specialist session.
19170
19154
 
19171
19155
  ### Validation routing
19172
19156
  - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
@@ -20039,10 +20023,6 @@ function setActiveRuntimePresetWithPrevious(name) {
20039
20023
  previousRuntimePreset = activeRuntimePreset;
20040
20024
  activeRuntimePreset = name;
20041
20025
  }
20042
- function rollbackRuntimePreset(previous) {
20043
- activeRuntimePreset = previous;
20044
- previousRuntimePreset = null;
20045
- }
20046
20026
 
20047
20027
  // src/utils/logger.ts
20048
20028
  import * as fs2 from "node:fs";
@@ -20055,7 +20035,7 @@ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
20055
20035
  var logFile = null;
20056
20036
  var writeChain = Promise.resolve();
20057
20037
  function getLogDir() {
20058
- return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
20038
+ return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode/log");
20059
20039
  }
20060
20040
  function cleanupOldLogs(logDir) {
20061
20041
  try {
@@ -22647,6 +22627,9 @@ var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
22647
22627
  "Task is not running in this process and has not produced output."
22648
22628
  ]);
22649
22629
  function parseTaskIdFromTaskOutput(output) {
22630
+ const xmlMatch = /<task\s+[^>]*\bid=["']([^"']+)["'][^>]*>/i.exec(output);
22631
+ if (xmlMatch)
22632
+ return xmlMatch[1];
22650
22633
  const lines = output.split(/\r?\n/);
22651
22634
  for (const line of lines) {
22652
22635
  const trimmed = line.trim();
@@ -22681,20 +22664,10 @@ function parseTaskStatusOutput(output) {
22681
22664
  result: parseTaskResultFromOutput(output)
22682
22665
  };
22683
22666
  }
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
- }
22697
22667
  function parseTaskStateFromOutput(output) {
22668
+ const xmlMatch = /<task\s+[^>]*\bstate=["'](running|completed|error|cancelled)["'][^>]*>/i.exec(output);
22669
+ if (xmlMatch)
22670
+ return xmlMatch[1].toLowerCase();
22698
22671
  for (const line of getTaskHeader(output).split(/\r?\n/)) {
22699
22672
  const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
22700
22673
  if (match)
@@ -22830,7 +22803,7 @@ class BackgroundJobBoard {
22830
22803
  if (!existing)
22831
22804
  return;
22832
22805
  const isStaleTerminal = TERMINAL_STATES.has(existing.state) || existing.state === "reconciled";
22833
- if (!isStaleTerminal || existing.cancellationRequested) {
22806
+ if (isStaleTerminal) {
22834
22807
  const updated2 = {
22835
22808
  ...existing,
22836
22809
  lastLiveBusyAt: now
@@ -22840,17 +22813,8 @@ class BackgroundJobBoard {
22840
22813
  }
22841
22814
  const updated = {
22842
22815
  ...existing,
22843
- state: "running",
22844
- timedOut: false,
22845
- statusUncertain: false,
22846
- cancellationRequested: false,
22847
- terminalUnreconciled: false,
22848
22816
  updatedAt: now,
22849
- lastLiveBusyAt: now,
22850
- completedAt: undefined,
22851
- terminalState: undefined,
22852
- resultSummary: undefined,
22853
- lastStatusError: undefined
22817
+ lastLiveBusyAt: now
22854
22818
  };
22855
22819
  this.jobs.set(taskID, updated);
22856
22820
  return updated;
@@ -22909,9 +22873,6 @@ class BackgroundJobBoard {
22909
22873
  const value = taskIDOrAlias.trim();
22910
22874
  return this.list(parentSessionID).find((job) => job.taskID === value || job.alias === value);
22911
22875
  }
22912
- resolveForStatus(parentSessionID, taskIDOrAlias) {
22913
- return this.resolve(parentSessionID, taskIDOrAlias);
22914
- }
22915
22876
  resolveReusable(parentSessionID, taskIDOrAlias, agent) {
22916
22877
  const job = this.resolve(parentSessionID, taskIDOrAlias);
22917
22878
  if (!job || !isReusable(job))
@@ -22967,7 +22928,7 @@ class BackgroundJobBoard {
22967
22928
  return [
22968
22929
  "### Background Job Board",
22969
22930
  "SENTINEL: background-job-board-v2",
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.",
22931
+ "Do not poll running jobs. Wait for hook-driven completion, or use cancel_task only for explicit cancellation. Reconcile terminal jobs before final response. Reuse only completed sessions for the same specialist/context; never reuse cancelled or errored sessions.",
22971
22932
  "",
22972
22933
  "#### Active / Unreconciled",
22973
22934
  ...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
@@ -23230,7 +23191,7 @@ function activationPrompt(task2) {
23230
23191
  "- draft a plan and get `@oracle` review before implementation;",
23231
23192
  "- create and review a phased implementation/delegation plan;",
23232
23193
  "- execute phase by phase with background specialists where useful;",
23233
- "- poll `task_status`, reconcile results, validate, and ask `@oracle` to review each phase;",
23194
+ "- wait for hook-driven background completion, reconcile results, validate, and ask `@oracle` to review each phase;",
23234
23195
  "- ask `@oracle` to include simplify/readability feedback in phase reviews;",
23235
23196
  "- fix actionable review issues before continuing.",
23236
23197
  "",
@@ -24008,6 +23969,7 @@ var BACKGROUND_JOB_BOARD_SENTINEL = "SENTINEL: background-job-board-v2";
24008
23969
  var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
24009
23970
  var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
24010
23971
  var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
23972
+ var RAW_SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
24011
23973
  function djb2Hash(str) {
24012
23974
  let hash = 5381;
24013
23975
  for (let i = 0;i < str.length; i++) {
@@ -24042,6 +24004,10 @@ function isObjectRecord(value) {
24042
24004
  function extractPath(output) {
24043
24005
  return /<path>([^<]+)<\/path>/.exec(output)?.[1];
24044
24006
  }
24007
+ function extractTaskSummary(output) {
24008
+ const summary = /<summary>\s*([\s\S]*?)\s*<\/summary>/i.exec(output)?.[1];
24009
+ return summary?.trim() || undefined;
24010
+ }
24045
24011
  function normalizePath(root, file) {
24046
24012
  const relative = path9.relative(root, file);
24047
24013
  if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
@@ -24133,7 +24099,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24133
24099
  const status = parseTaskStatusOutput(output);
24134
24100
  if (!status)
24135
24101
  return;
24136
- log("[task-session-manager] parsed task status output", {
24102
+ log("[task-session-manager] parsed task output status", {
24137
24103
  taskID: status.taskID,
24138
24104
  state: status.state,
24139
24105
  timedOut: status.timedOut,
@@ -24178,73 +24144,23 @@ function createTaskSessionManagerHook(_ctx, options) {
24178
24144
  }
24179
24145
  return updated;
24180
24146
  }
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
- }
24236
24147
  function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
24237
24148
  if (part.type !== "text" || typeof part.text !== "string") {
24238
24149
  return;
24239
24150
  }
24240
- const isCompleted = BACKGROUND_COMPLETION_COMPLETED.test(part.text);
24241
- const isFailed = BACKGROUND_COMPLETION_FAILED.test(part.text);
24242
- if (part.synthetic !== true || !isCompleted && !isFailed) {
24151
+ if (part.synthetic !== true)
24243
24152
  return;
24244
- }
24245
24153
  const status = parseTaskStatusOutput(part.text);
24246
24154
  if (!status)
24247
24155
  return;
24156
+ if (status.state !== "completed" && status.state !== "error") {
24157
+ return;
24158
+ }
24159
+ const summary = extractTaskSummary(part.text);
24160
+ const isCompleted = summary ? BACKGROUND_COMPLETION_COMPLETED.test(summary) : status.state === "completed";
24161
+ const isFailed = summary ? BACKGROUND_COMPLETION_FAILED.test(summary) : status.state === "error";
24162
+ if (summary && !isCompleted && !isFailed)
24163
+ return;
24248
24164
  const occurrenceId = createOccurrenceId(part, message, partIndex);
24249
24165
  const existing = backgroundJobBoard.get(status.taskID);
24250
24166
  if (isFailed && isLateCancelledTaskError(existing, status.state)) {
@@ -24357,23 +24273,13 @@ function createTaskSessionManagerHook(_ctx, options) {
24357
24273
  return {
24358
24274
  "tool.execute.before": async (input, output) => {
24359
24275
  const toolName = input.tool.toLowerCase();
24360
- if (toolName !== "task" && toolName !== "task_status")
24276
+ if (toolName !== "task")
24361
24277
  return;
24362
24278
  if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24363
24279
  return;
24364
24280
  }
24365
24281
  if (!isObjectRecord(output.args))
24366
24282
  return;
24367
- if (toolName === "task_status") {
24368
- const args2 = output.args;
24369
- if (typeof args2.task_id !== "string" || args2.task_id.trim() === "") {
24370
- return;
24371
- }
24372
- const resolved = backgroundJobBoard.resolveForStatus(input.sessionID, args2.task_id.trim());
24373
- if (resolved)
24374
- args2.task_id = resolved.taskID;
24375
- return;
24376
- }
24377
24283
  const args = output.args;
24378
24284
  if (!isAgentName(args.subagent_type)) {
24379
24285
  if (typeof args.task_id === "string" && args.task_id.trim() !== "") {
@@ -24402,6 +24308,11 @@ function createTaskSessionManagerHook(_ctx, options) {
24402
24308
  const requested = args.task_id.trim();
24403
24309
  const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, args.subagent_type);
24404
24310
  if (!remembered) {
24311
+ if (RAW_SESSION_ID_PATTERN.test(requested)) {
24312
+ pendingCall.resumedTaskId = requested;
24313
+ rememberPendingCall(pendingCall);
24314
+ return;
24315
+ }
24405
24316
  delete args.task_id;
24406
24317
  return;
24407
24318
  }
@@ -24418,24 +24329,13 @@ function createTaskSessionManagerHook(_ctx, options) {
24418
24329
  }
24419
24330
  return;
24420
24331
  }
24421
- if (input.tool.toLowerCase() === "task_status") {
24422
- if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
24423
- return;
24424
- }
24425
- normalizeLateCancelledToolStatus(output);
24426
- if (await handleTransientTaskStatusOutput(output)) {
24427
- return;
24428
- }
24429
- updateBackgroundJobFromOutput(output.output);
24430
- return;
24431
- }
24432
24332
  if (input.tool.toLowerCase() !== "task")
24433
24333
  return;
24434
24334
  const pending = takePendingCall(input.callID, input.sessionID);
24435
24335
  if (!pending || typeof output.output !== "string")
24436
24336
  return;
24437
24337
  const launch = parseTaskLaunchOutput(output.output);
24438
- if (launch) {
24338
+ if (launch && !launch.result?.match(/Timed out after \d+ms/i)) {
24439
24339
  const record = backgroundJobBoard.registerLaunch({
24440
24340
  taskID: launch.taskID,
24441
24341
  parentSessionID: pending.parentSessionId,
@@ -24455,6 +24355,39 @@ function createTaskSessionManagerHook(_ctx, options) {
24455
24355
  pendingManagedTaskIds.add(launch.taskID);
24456
24356
  return;
24457
24357
  }
24358
+ normalizeLateCancelledTaskOutput(output);
24359
+ const status = parseTaskStatusOutput(output.output);
24360
+ if (status) {
24361
+ const existing = backgroundJobBoard.get(status.taskID);
24362
+ const record = existing ?? backgroundJobBoard.registerLaunch({
24363
+ taskID: status.taskID,
24364
+ parentSessionID: pending.parentSessionId,
24365
+ agent: pending.agentType,
24366
+ description: pending.label,
24367
+ objective: pending.label
24368
+ });
24369
+ const updated = backgroundJobBoard.updateStatus({
24370
+ taskID: status.taskID,
24371
+ state: status.state,
24372
+ timedOut: status.timedOut,
24373
+ resultSummary: status.result
24374
+ });
24375
+ log("[task-session-manager] foreground task status registered", {
24376
+ taskID: status.taskID,
24377
+ alias: updated?.alias ?? record.alias,
24378
+ parentSessionID: pending.parentSessionId,
24379
+ agent: pending.agentType,
24380
+ state: updated?.state ?? record.state
24381
+ });
24382
+ if (pending.resumedTaskId && pending.resumedTaskId !== status.taskID) {
24383
+ backgroundJobBoard.drop(pending.resumedTaskId);
24384
+ }
24385
+ pendingManagedTaskIds.delete(status.taskID);
24386
+ const contextFiles2 = contextFilesForPrompt(contextByTask.get(status.taskID));
24387
+ backgroundJobBoard.addContext(status.taskID, contextFiles2);
24388
+ pruneContext();
24389
+ return;
24390
+ }
24458
24391
  const taskId = parseTaskIdFromTaskOutput(output.output);
24459
24392
  if (!taskId) {
24460
24393
  if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
@@ -24602,7 +24535,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24602
24535
  }
24603
24536
  }
24604
24537
  };
24605
- function normalizeLateCancelledToolStatus(output) {
24538
+ function normalizeLateCancelledTaskOutput(output) {
24606
24539
  if (typeof output.output !== "string")
24607
24540
  return;
24608
24541
  const status = parseTaskStatusOutput(output.output);
@@ -24611,7 +24544,7 @@ function createTaskSessionManagerHook(_ctx, options) {
24611
24544
  const existing = backgroundJobBoard.get(status.taskID);
24612
24545
  if (!isLateCancelledTaskError(existing, status.state))
24613
24546
  return;
24614
- log("[task-session-manager] normalized late cancelled task_status output", {
24547
+ log("[task-session-manager] normalized late cancelled task output", {
24615
24548
  taskID: status.taskID,
24616
24549
  alias: existing?.alias,
24617
24550
  state: existing?.state,
@@ -24642,715 +24575,6 @@ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
24642
24575
  ].join(`
24643
24576
  `);
24644
24577
  }
24645
- // src/hooks/todo-continuation/index.ts
24646
- import { tool } from "@opencode-ai/plugin";
24647
-
24648
- // src/hooks/todo-continuation/todo-hygiene.ts
24649
- var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
24650
- var TODO_FINAL_ACTIVE_REMINDER = "If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.";
24651
- var RESET = new Set(["todowrite"]);
24652
- var IGNORE = new Set(["auto_continue"]);
24653
- function createTodoHygiene(options) {
24654
- const pending = new Map;
24655
- const active = new Set;
24656
- function clearCycle(sessionID) {
24657
- pending.delete(sessionID);
24658
- }
24659
- function clear(sessionID) {
24660
- clearCycle(sessionID);
24661
- active.delete(sessionID);
24662
- }
24663
- function isFinalActive(state) {
24664
- return state.inProgressCount === 1 && state.pendingCount === 0 && state.openCount === 1;
24665
- }
24666
- function mark(sessionID, reason) {
24667
- const reasons = pending.get(sessionID) ?? new Set;
24668
- reasons.add(reason);
24669
- pending.set(sessionID, reasons);
24670
- }
24671
- function pick(reasons) {
24672
- if (reasons.has("final_active")) {
24673
- return TODO_FINAL_ACTIVE_REMINDER;
24674
- }
24675
- return TODO_HYGIENE_REMINDER;
24676
- }
24677
- return {
24678
- handleRequestStart(input) {
24679
- clear(input.sessionID);
24680
- },
24681
- async handleToolExecuteAfter(input, _output) {
24682
- if (!input.sessionID) {
24683
- return;
24684
- }
24685
- const tool = input.tool.toLowerCase();
24686
- if (IGNORE.has(tool)) {
24687
- return;
24688
- }
24689
- try {
24690
- if (RESET.has(tool)) {
24691
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
24692
- clear(input.sessionID);
24693
- return;
24694
- }
24695
- active.add(input.sessionID);
24696
- clearCycle(input.sessionID);
24697
- const state2 = await options.getTodoState(input.sessionID);
24698
- if (!state2.hasOpenTodos) {
24699
- active.delete(input.sessionID);
24700
- options.log?.("Cleared todo hygiene cycle", {
24701
- sessionID: input.sessionID,
24702
- tool
24703
- });
24704
- return;
24705
- }
24706
- if (!isFinalActive(state2)) {
24707
- options.log?.("Reset todo hygiene cycle", {
24708
- sessionID: input.sessionID,
24709
- tool
24710
- });
24711
- return;
24712
- }
24713
- mark(input.sessionID, "final_active");
24714
- options.log?.("Armed final-active todo hygiene reminder", {
24715
- sessionID: input.sessionID,
24716
- tool
24717
- });
24718
- return;
24719
- }
24720
- if (!active.has(input.sessionID)) {
24721
- return;
24722
- }
24723
- if (pending.get(input.sessionID)?.has("final_active")) {
24724
- return;
24725
- }
24726
- if (options.shouldInject && !options.shouldInject(input.sessionID)) {
24727
- clear(input.sessionID);
24728
- return;
24729
- }
24730
- const state = await options.getTodoState(input.sessionID);
24731
- if (!state.hasOpenTodos) {
24732
- clear(input.sessionID);
24733
- return;
24734
- }
24735
- if (isFinalActive(state)) {
24736
- mark(input.sessionID, "final_active");
24737
- } else {
24738
- mark(input.sessionID, "general");
24739
- }
24740
- options.log?.("Armed todo hygiene reminder", {
24741
- sessionID: input.sessionID,
24742
- tool,
24743
- reasons: Array.from(pending.get(input.sessionID) ?? [])
24744
- });
24745
- } catch (error) {
24746
- options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
24747
- sessionID: input.sessionID,
24748
- tool,
24749
- error: error instanceof Error ? error.message : String(error)
24750
- });
24751
- }
24752
- },
24753
- getPendingReminder(sessionID) {
24754
- const reasons = pending.get(sessionID);
24755
- if (!reasons || reasons.size === 0) {
24756
- return null;
24757
- }
24758
- if (options.shouldInject && !options.shouldInject(sessionID)) {
24759
- clear(sessionID);
24760
- return null;
24761
- }
24762
- const reminder = pick(reasons);
24763
- options.log?.("Read todo hygiene reminder", {
24764
- sessionID,
24765
- reminder,
24766
- reasons: Array.from(reasons)
24767
- });
24768
- return reminder;
24769
- },
24770
- handleEvent(event) {
24771
- if (event.type !== "session.deleted") {
24772
- return;
24773
- }
24774
- const sessionID = event.properties?.sessionID ?? event.properties?.info?.id;
24775
- if (!sessionID) {
24776
- return;
24777
- }
24778
- clear(sessionID);
24779
- }
24780
- };
24781
- }
24782
-
24783
- // src/hooks/todo-continuation/index.ts
24784
- var HOOK_NAME = "todo-continuation";
24785
- var COMMAND_NAME2 = "auto-continue";
24786
- var TODO_STATE_TIMEOUT_MS = 500;
24787
- var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
24788
- var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
24789
- var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
24790
- var SUPPRESS_AFTER_ABORT_MS = 5000;
24791
- var NOTIFICATION_BUSY_GRACE_MS = 250;
24792
- var QUESTION_PHRASES = [
24793
- "would you like",
24794
- "should i",
24795
- "do you want",
24796
- "please review",
24797
- "let me know",
24798
- "what do you think",
24799
- "can you confirm",
24800
- "would you prefer",
24801
- "shall i",
24802
- "any thoughts"
24803
- ];
24804
- var TERMINAL_TODO_STATUSES = ["completed", "cancelled"];
24805
- function isQuestion(text) {
24806
- const lowerText = text.toLowerCase().trim();
24807
- if (/\?\s*$/.test(lowerText)) {
24808
- return true;
24809
- }
24810
- return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
24811
- }
24812
- function cancelPendingTimer(state) {
24813
- if (state.pendingTimer) {
24814
- clearTimeout(state.pendingTimer);
24815
- state.pendingTimer = null;
24816
- }
24817
- state.pendingTimerSessionId = null;
24818
- }
24819
- function resetState(state) {
24820
- cancelPendingTimer(state);
24821
- state.consecutiveContinuations = 0;
24822
- state.suppressUntil = 0;
24823
- state.isAutoInjecting = false;
24824
- state.notifyingSessionIds.clear();
24825
- state.notificationBusyUntilBySession.clear();
24826
- }
24827
- function stripTodoHygieneInstruction(text) {
24828
- const trimmed = text.trimEnd();
24829
- if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
24830
- return trimmed;
24831
- }
24832
- const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
24833
- if (start === -1) {
24834
- return trimmed;
24835
- }
24836
- return trimmed.slice(0, start).trimEnd();
24837
- }
24838
- function appendTodoHygieneInstruction(message, reminder) {
24839
- const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
24840
- if (!textPart)
24841
- return;
24842
- const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
24843
- const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
24844
- ${reminder}
24845
- ${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
24846
- textPart.text = baseText ? `${baseText}
24847
-
24848
- ${instruction}` : instruction;
24849
- }
24850
- function stripTodoHygieneInstructionFromMessage(message) {
24851
- const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
24852
- if (!textPart)
24853
- return;
24854
- textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
24855
- }
24856
- function createTodoContinuationHook(ctx, config) {
24857
- const maxContinuations = config?.maxContinuations ?? 5;
24858
- const cooldownMs = config?.cooldownMs ?? 3000;
24859
- const autoEnable = config?.autoEnable ?? false;
24860
- const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
24861
- const backgroundJobBoard = config?.backgroundJobBoard;
24862
- const requestSignatureBySession = new Map;
24863
- const state = {
24864
- enabled: false,
24865
- consecutiveContinuations: 0,
24866
- pendingTimer: null,
24867
- pendingTimerSessionId: null,
24868
- suppressUntil: 0,
24869
- orchestratorSessionIds: new Set,
24870
- sawChatMessage: false,
24871
- isAutoInjecting: false,
24872
- notifyingSessionIds: new Set,
24873
- notificationBusyUntilBySession: new Map
24874
- };
24875
- async function fetchTodos(sessionID) {
24876
- const result = await withTimeout(ctx.client.session.todo({
24877
- path: { id: sessionID }
24878
- }), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
24879
- return result.data;
24880
- }
24881
- const hygiene = createTodoHygiene({
24882
- getTodoState: async (sessionID) => {
24883
- const todos = await fetchTodos(sessionID);
24884
- const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
24885
- return {
24886
- hasOpenTodos: openTodos.length > 0,
24887
- openCount: openTodos.length,
24888
- inProgressCount: openTodos.filter((todo) => todo.status === "in_progress").length,
24889
- pendingCount: openTodos.filter((todo) => todo.status === "pending").length
24890
- };
24891
- },
24892
- shouldInject: (sessionID) => isOrchestratorSession(sessionID),
24893
- log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta)
24894
- });
24895
- function inferSessionID(messages, index) {
24896
- const direct = messages[index]?.info.sessionID;
24897
- if (direct) {
24898
- return direct;
24899
- }
24900
- for (let i = index - 1;i >= 0; i--) {
24901
- const sessionID = messages[i]?.info.sessionID;
24902
- if (sessionID) {
24903
- return sessionID;
24904
- }
24905
- }
24906
- for (let i = index + 1;i < messages.length; i++) {
24907
- const sessionID = messages[i]?.info.sessionID;
24908
- if (sessionID) {
24909
- return sessionID;
24910
- }
24911
- }
24912
- if (state.orchestratorSessionIds.size === 1) {
24913
- return Array.from(state.orchestratorSessionIds)[0];
24914
- }
24915
- return;
24916
- }
24917
- function isExternalUserMessage(message) {
24918
- if (message.info.role !== "user") {
24919
- return false;
24920
- }
24921
- const visibleText = message.parts.filter((part) => part.type === "text" && typeof part.text === "string" && !part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)).map((part) => part.text?.trim() ?? "").filter(Boolean).join(`
24922
- `);
24923
- const hasNonTextPart = message.parts.some((part) => part.type !== "text");
24924
- return !(!visibleText && !hasNonTextPart && message.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)));
24925
- }
24926
- function getLastExternalUserMessage(messages) {
24927
- for (let i = messages.length - 1;i >= 0; i--) {
24928
- const message = messages[i];
24929
- if (!isExternalUserMessage(message)) {
24930
- continue;
24931
- }
24932
- const sessionID = inferSessionID(messages, i);
24933
- const partSignature = message.parts.map((part) => {
24934
- if (part.type === "text" && typeof part.text === "string") {
24935
- const text = stripTodoHygieneInstruction(part.text);
24936
- return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
24937
- }
24938
- return part.type ?? "unknown";
24939
- }).join("|");
24940
- const ordinal = messages.slice(0, i + 1).filter((item) => isExternalUserMessage(item)).length;
24941
- return {
24942
- sessionID,
24943
- agent: message.info.agent,
24944
- message,
24945
- signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
24946
- };
24947
- }
24948
- return null;
24949
- }
24950
- async function handleMessagesTransform(output) {
24951
- const lastUserMessage = getLastExternalUserMessage(output.messages);
24952
- if (!lastUserMessage) {
24953
- return;
24954
- }
24955
- if (lastUserMessage.agent && lastUserMessage.agent !== "orchestrator") {
24956
- return;
24957
- }
24958
- if (!lastUserMessage.sessionID) {
24959
- for (const sessionID of state.orchestratorSessionIds) {
24960
- requestSignatureBySession.delete(sessionID);
24961
- hygiene.handleRequestStart({ sessionID });
24962
- }
24963
- return;
24964
- }
24965
- const knownOrchestrator = isOrchestratorSession(lastUserMessage.sessionID);
24966
- if (lastUserMessage.agent === "orchestrator") {
24967
- registerOrchestratorSession(lastUserMessage.sessionID);
24968
- } else if (!knownOrchestrator) {
24969
- return;
24970
- }
24971
- if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
24972
- const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
24973
- const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
24974
- const combinedReminder = [reminder, guardrail].filter((item) => Boolean(item)).join(" ");
24975
- if (combinedReminder) {
24976
- appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
24977
- } else {
24978
- stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
24979
- }
24980
- return;
24981
- }
24982
- requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
24983
- stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
24984
- hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
24985
- }
24986
- function markNotificationStarted(sessionID) {
24987
- state.notifyingSessionIds.add(sessionID);
24988
- }
24989
- function markNotificationFinished(sessionID) {
24990
- state.notifyingSessionIds.delete(sessionID);
24991
- state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
24992
- }
24993
- function clearNotificationState(sessionID) {
24994
- state.notifyingSessionIds.delete(sessionID);
24995
- state.notificationBusyUntilBySession.delete(sessionID);
24996
- }
24997
- function isNotificationBusy(sessionID) {
24998
- if (state.notifyingSessionIds.has(sessionID)) {
24999
- return true;
25000
- }
25001
- const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
25002
- if (until <= Date.now()) {
25003
- state.notificationBusyUntilBySession.delete(sessionID);
25004
- return false;
25005
- }
25006
- return true;
25007
- }
25008
- function isOrchestratorSession(sessionID) {
25009
- return state.orchestratorSessionIds.has(sessionID);
25010
- }
25011
- function registerOrchestratorSession(sessionID) {
25012
- state.orchestratorSessionIds.add(sessionID);
25013
- }
25014
- function backgroundGuardrail(sessionID) {
25015
- if (!backgroundJobBoard)
25016
- return;
25017
- const hasRunning = backgroundJobBoard.hasRunning(sessionID);
25018
- const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
25019
- if (hasRunning && hasTerminal) {
25020
- return "Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.";
25021
- }
25022
- if (hasTerminal) {
25023
- return "Background jobs have terminal results: reconcile the Background Job Board results before finalizing.";
25024
- }
25025
- if (hasRunning) {
25026
- return "Background jobs are still running: call task_status before dependent work or finalizing.";
25027
- }
25028
- return;
25029
- }
25030
- function continuationPrompt(sessionID) {
25031
- const guardrail = backgroundGuardrail(sessionID);
25032
- if (!guardrail)
25033
- return CONTINUATION_PROMPT;
25034
- return `${CONTINUATION_PROMPT} ${guardrail}`;
25035
- }
25036
- function handleChatMessage(input) {
25037
- if (!input.agent) {
25038
- return;
25039
- }
25040
- state.sawChatMessage = true;
25041
- if (input.agent === "orchestrator") {
25042
- registerOrchestratorSession(input.sessionID);
25043
- }
25044
- }
25045
- const autoContinue = tool({
25046
- description: "Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.",
25047
- args: { enabled: tool.schema.boolean() },
25048
- execute: async (args) => {
25049
- const enabled = args.enabled;
25050
- state.enabled = enabled;
25051
- state.consecutiveContinuations = 0;
25052
- if (enabled) {
25053
- state.suppressUntil = 0;
25054
- log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
25055
- return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
25056
- }
25057
- cancelPendingTimer(state);
25058
- log(`[${HOOK_NAME}] Auto-continue disabled`);
25059
- return "Auto-continue disabled.";
25060
- }
25061
- });
25062
- async function handleEvent(input) {
25063
- const { event } = input;
25064
- const properties = event.properties ?? {};
25065
- hygiene.handleEvent({
25066
- type: event.type,
25067
- properties: {
25068
- info: properties.info,
25069
- sessionID: properties.sessionID
25070
- }
25071
- });
25072
- if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
25073
- const sessionID = properties.sessionID;
25074
- if (!sessionID) {
25075
- return;
25076
- }
25077
- log(`[${HOOK_NAME}] Session idle`, { sessionID });
25078
- if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
25079
- registerOrchestratorSession(sessionID);
25080
- log(`[${HOOK_NAME}] Tracked orchestrator session`, {
25081
- sessionID
25082
- });
25083
- }
25084
- if (!isOrchestratorSession(sessionID)) {
25085
- log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
25086
- sessionID
25087
- });
25088
- return;
25089
- }
25090
- if (autoEnable && !state.enabled) {
25091
- try {
25092
- const todos = await fetchTodos(sessionID);
25093
- const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
25094
- if (incompleteCount2 >= autoEnableThreshold) {
25095
- state.enabled = true;
25096
- state.consecutiveContinuations = 0;
25097
- state.suppressUntil = 0;
25098
- log(`[${HOOK_NAME}] Auto-enabled: ${incompleteCount2} incomplete todos >= threshold ${autoEnableThreshold}`, { sessionID });
25099
- } else {
25100
- log(`[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount2} incomplete todos < threshold ${autoEnableThreshold}`, { sessionID });
25101
- }
25102
- } catch (error) {
25103
- log(`[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`, {
25104
- sessionID,
25105
- error: error instanceof Error ? error.message : String(error)
25106
- });
25107
- }
25108
- }
25109
- if (!state.enabled) {
25110
- log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
25111
- sessionID
25112
- });
25113
- return;
25114
- }
25115
- let hasIncompleteTodos = false;
25116
- let incompleteCount = 0;
25117
- try {
25118
- const todos = await fetchTodos(sessionID);
25119
- incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
25120
- hasIncompleteTodos = incompleteCount > 0;
25121
- log(`[${HOOK_NAME}] Fetched todos`, {
25122
- sessionID,
25123
- hasIncompleteTodos,
25124
- total: todos.length
25125
- });
25126
- } catch (error) {
25127
- log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
25128
- sessionID,
25129
- error: error instanceof Error ? error.message : String(error)
25130
- });
25131
- return;
25132
- }
25133
- if (!hasIncompleteTodos) {
25134
- log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
25135
- return;
25136
- }
25137
- let lastAssistantIsQuestion = false;
25138
- try {
25139
- const messagesResult = await ctx.client.session.messages({
25140
- path: { id: sessionID }
25141
- });
25142
- const messages = messagesResult.data;
25143
- const lastAssistantMessage = messages.slice().reverse().find((m) => m.info?.role === "assistant");
25144
- if (lastAssistantMessage?.parts) {
25145
- const lastText = lastAssistantMessage.parts.map((p) => p.text ?? "").join(" ");
25146
- lastAssistantIsQuestion = isQuestion(lastText);
25147
- }
25148
- log(`[${HOOK_NAME}] Fetched messages`, {
25149
- sessionID,
25150
- lastAssistantIsQuestion
25151
- });
25152
- } catch (error) {
25153
- log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
25154
- sessionID,
25155
- error: error instanceof Error ? error.message : String(error)
25156
- });
25157
- return;
25158
- }
25159
- if (lastAssistantIsQuestion) {
25160
- log(`[${HOOK_NAME}] Skipped: last message is question`, {
25161
- sessionID
25162
- });
25163
- return;
25164
- }
25165
- if (state.consecutiveContinuations >= maxContinuations) {
25166
- log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
25167
- sessionID,
25168
- consecutive: state.consecutiveContinuations,
25169
- max: maxContinuations
25170
- });
25171
- return;
25172
- }
25173
- const now = Date.now();
25174
- if (now < state.suppressUntil) {
25175
- log(`[${HOOK_NAME}] Skipped: in suppress window`, {
25176
- sessionID,
25177
- suppressUntil: state.suppressUntil
25178
- });
25179
- return;
25180
- }
25181
- if (state.pendingTimer !== null || state.isAutoInjecting) {
25182
- log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
25183
- sessionID
25184
- });
25185
- return;
25186
- }
25187
- log(`[${HOOK_NAME}] Scheduling continuation`, {
25188
- sessionID,
25189
- delayMs: cooldownMs
25190
- });
25191
- markNotificationStarted(sessionID);
25192
- ctx.client.session.prompt({
25193
- path: { id: sessionID },
25194
- body: {
25195
- noReply: true,
25196
- parts: [
25197
- {
25198
- type: "text",
25199
- text: [
25200
- `⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
25201
- "",
25202
- "[system status: continue without acknowledging this notification]"
25203
- ].join(`
25204
- `)
25205
- }
25206
- ]
25207
- }
25208
- }).catch(() => {}).finally(() => {
25209
- markNotificationFinished(sessionID);
25210
- });
25211
- state.pendingTimerSessionId = sessionID;
25212
- state.pendingTimer = setTimeout(async () => {
25213
- state.pendingTimer = null;
25214
- state.pendingTimerSessionId = null;
25215
- clearNotificationState(sessionID);
25216
- if (!state.enabled) {
25217
- log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
25218
- sessionID
25219
- });
25220
- return;
25221
- }
25222
- state.isAutoInjecting = true;
25223
- try {
25224
- await ctx.client.session.prompt({
25225
- path: { id: sessionID },
25226
- body: {
25227
- parts: [
25228
- createInternalAgentTextPart(continuationPrompt(sessionID))
25229
- ]
25230
- }
25231
- });
25232
- state.consecutiveContinuations++;
25233
- log(`[${HOOK_NAME}] Continuation injected`, {
25234
- sessionID,
25235
- consecutive: state.consecutiveContinuations
25236
- });
25237
- } catch (error) {
25238
- log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
25239
- sessionID,
25240
- error: error instanceof Error ? error.message : String(error)
25241
- });
25242
- } finally {
25243
- state.isAutoInjecting = false;
25244
- }
25245
- }, cooldownMs);
25246
- } else if (event.type === "session.status") {
25247
- const status = properties.status;
25248
- const sessionID = properties.sessionID;
25249
- if (status?.type === "busy") {
25250
- const isOrchestrator = isOrchestratorSession(sessionID);
25251
- const isNotification = isNotificationBusy(sessionID);
25252
- if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
25253
- cancelPendingTimer(state);
25254
- }
25255
- if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
25256
- state.consecutiveContinuations = 0;
25257
- log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
25258
- sessionID
25259
- });
25260
- }
25261
- }
25262
- } else if (event.type === "session.error") {
25263
- const error = properties.error;
25264
- const sessionID = properties.sessionID;
25265
- const errorName = error?.name;
25266
- const isOrchestrator = isOrchestratorSession(sessionID);
25267
- if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
25268
- state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
25269
- log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
25270
- sessionID,
25271
- errorName
25272
- });
25273
- }
25274
- if (isOrchestrator) {
25275
- cancelPendingTimer(state);
25276
- log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
25277
- sessionID
25278
- });
25279
- }
25280
- } else if (event.type === "session.deleted") {
25281
- const deletedSessionId = properties.info?.id ?? properties.sessionID;
25282
- if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
25283
- requestSignatureBySession.delete(deletedSessionId);
25284
- if (state.pendingTimerSessionId === deletedSessionId) {
25285
- cancelPendingTimer(state);
25286
- log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
25287
- sessionID: deletedSessionId
25288
- });
25289
- }
25290
- state.orchestratorSessionIds.delete(deletedSessionId);
25291
- clearNotificationState(deletedSessionId);
25292
- if (state.orchestratorSessionIds.size === 0) {
25293
- resetState(state);
25294
- state.sawChatMessage = false;
25295
- }
25296
- log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
25297
- sessionID: deletedSessionId
25298
- });
25299
- }
25300
- }
25301
- }
25302
- async function handleCommandExecuteBefore(input, output) {
25303
- if (input.command !== COMMAND_NAME2) {
25304
- return;
25305
- }
25306
- registerOrchestratorSession(input.sessionID);
25307
- output.parts.length = 0;
25308
- const arg = input.arguments.trim().toLowerCase();
25309
- let newEnabled;
25310
- if (arg === "on") {
25311
- newEnabled = true;
25312
- } else if (arg === "off") {
25313
- newEnabled = false;
25314
- } else {
25315
- newEnabled = !state.enabled;
25316
- }
25317
- state.enabled = newEnabled;
25318
- state.consecutiveContinuations = 0;
25319
- if (!newEnabled) {
25320
- cancelPendingTimer(state);
25321
- output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
25322
- log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME2} command`);
25323
- return;
25324
- }
25325
- state.suppressUntil = 0;
25326
- log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME2} command`, {
25327
- maxContinuations
25328
- });
25329
- let hasIncompleteTodos = false;
25330
- try {
25331
- const todos = await fetchTodos(input.sessionID);
25332
- hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
25333
- } catch (error) {
25334
- log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
25335
- sessionID: input.sessionID,
25336
- error: error instanceof Error ? error.message : String(error)
25337
- });
25338
- }
25339
- if (hasIncompleteTodos) {
25340
- output.parts.push(createInternalAgentTextPart(`${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
25341
- } else {
25342
- output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
25343
- }
25344
- }
25345
- return {
25346
- tool: { auto_continue: autoContinue },
25347
- handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
25348
- handleMessagesTransform,
25349
- handleEvent,
25350
- handleChatMessage,
25351
- handleCommandExecuteBefore
25352
- };
25353
- }
25354
24578
  // src/interview/manager.ts
25355
24579
  import path13 from "node:path";
25356
24580
 
@@ -28338,7 +27562,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
28338
27562
  }
28339
27563
 
28340
27564
  // src/interview/service.ts
28341
- var COMMAND_NAME3 = "interview";
27565
+ var COMMAND_NAME2 = "interview";
28342
27566
  var DEFAULT_MAX_QUESTIONS = 2;
28343
27567
  function isTruthyEnvFlag(value) {
28344
27568
  if (!value) {
@@ -28585,11 +27809,11 @@ function createInterviewService(ctx, config, deps) {
28585
27809
  }
28586
27810
  function registerCommand(opencodeConfig) {
28587
27811
  const configCommand = opencodeConfig.command;
28588
- if (!configCommand?.[COMMAND_NAME3]) {
27812
+ if (!configCommand?.[COMMAND_NAME2]) {
28589
27813
  if (!opencodeConfig.command) {
28590
27814
  opencodeConfig.command = {};
28591
27815
  }
28592
- opencodeConfig.command[COMMAND_NAME3] = {
27816
+ opencodeConfig.command[COMMAND_NAME2] = {
28593
27817
  template: "Start an interview and write a live markdown spec",
28594
27818
  description: "Open a localhost interview UI linked to the current OpenCode session"
28595
27819
  };
@@ -28663,7 +27887,7 @@ function createInterviewService(ctx, config, deps) {
28663
27887
  }
28664
27888
  }
28665
27889
  async function handleCommandExecuteBefore(input, output) {
28666
- if (input.command !== COMMAND_NAME3) {
27890
+ if (input.command !== COMMAND_NAME2) {
28667
27891
  return;
28668
27892
  }
28669
27893
  const idea = input.arguments.trim();
@@ -30320,7 +29544,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
30320
29544
  return false;
30321
29545
  }
30322
29546
  // src/tools/ast-grep/tools.ts
30323
- import { tool as tool2 } from "@opencode-ai/plugin";
29547
+ import { tool } from "@opencode-ai/plugin";
30324
29548
 
30325
29549
  // src/tools/ast-grep/cli.ts
30326
29550
  import { existsSync as existsSync9 } from "node:fs";
@@ -30799,14 +30023,14 @@ function showOutputToUser(context, output) {
30799
30023
  const ctx = context;
30800
30024
  ctx.metadata?.({ metadata: { output } });
30801
30025
  }
30802
- var ast_grep_search = tool2({
30026
+ var ast_grep_search = tool({
30803
30027
  description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
30804
30028
  args: {
30805
- pattern: tool2.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
30806
- lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
30807
- paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search (default: ['.'])"),
30808
- globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
30809
- context: tool2.schema.number().optional().describe("Context lines around match")
30029
+ pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
30030
+ lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30031
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
30032
+ globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
30033
+ context: tool.schema.number().optional().describe("Context lines around match")
30810
30034
  },
30811
30035
  execute: async (args, context) => {
30812
30036
  try {
@@ -30835,15 +30059,15 @@ ${hint}`;
30835
30059
  }
30836
30060
  }
30837
30061
  });
30838
- var ast_grep_replace = tool2({
30062
+ var ast_grep_replace = tool({
30839
30063
  description: "Replace code patterns across filesystem with AST-aware rewriting. " + "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
30840
30064
  args: {
30841
- pattern: tool2.schema.string().describe("AST pattern to match"),
30842
- rewrite: tool2.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
30843
- lang: tool2.schema.enum(CLI_LANGUAGES).describe("Target language"),
30844
- paths: tool2.schema.array(tool2.schema.string()).optional().describe("Paths to search"),
30845
- globs: tool2.schema.array(tool2.schema.string()).optional().describe("Include/exclude globs"),
30846
- dryRun: tool2.schema.boolean().optional().describe("Preview changes without applying (default: true)")
30065
+ pattern: tool.schema.string().describe("AST pattern to match"),
30066
+ rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
30067
+ lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
30068
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
30069
+ globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
30070
+ dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
30847
30071
  },
30848
30072
  execute: async (args, context) => {
30849
30073
  try {
@@ -30867,14 +30091,14 @@ var ast_grep_replace = tool2({
30867
30091
  });
30868
30092
  // src/tools/cancel-task.ts
30869
30093
  import {
30870
- tool as tool3
30094
+ tool as tool2
30871
30095
  } from "@opencode-ai/plugin";
30872
- var z4 = tool3.schema;
30096
+ var z4 = tool2.schema;
30873
30097
 
30874
30098
  class SessionStillRunningError extends Error {
30875
30099
  }
30876
30100
  function createCancelTaskTool(options) {
30877
- const cancel_task = tool3({
30101
+ const cancel_task = tool2({
30878
30102
  description: `Cancel a tracked background specialist task.
30879
30103
 
30880
30104
  Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board. Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before replacing the lane.`,
@@ -31264,9 +30488,9 @@ function unknownTaskOutput(taskID, message) {
31264
30488
  }
31265
30489
  // src/tools/council.ts
31266
30490
  import {
31267
- tool as tool4
30491
+ tool as tool3
31268
30492
  } from "@opencode-ai/plugin";
31269
- var z5 = tool4.schema;
30493
+ var z5 = tool3.schema;
31270
30494
  function formatModelComposition(councillorResults) {
31271
30495
  return councillorResults.map((cr) => {
31272
30496
  const shortModel = shortModelLabel(cr.model);
@@ -31274,7 +30498,7 @@ function formatModelComposition(councillorResults) {
31274
30498
  }).join(", ");
31275
30499
  }
31276
30500
  function createCouncilTool(_ctx, councilManager) {
31277
- const council_session = tool4({
30501
+ const council_session = tool3({
31278
30502
  description: `Launch a multi-LLM council session for consensus-based analysis.
31279
30503
 
31280
30504
  Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
@@ -31328,6 +30552,9 @@ Returns the councillor responses with a summary footer.`,
31328
30552
  });
31329
30553
  return { council_session };
31330
30554
  }
30555
+ // src/tools/preset-manager.ts
30556
+ import * as fs10 from "node:fs";
30557
+
31331
30558
  // src/tui-state.ts
31332
30559
  import * as fs9 from "node:fs";
31333
30560
  import * as os5 from "node:os";
@@ -31397,11 +30624,11 @@ function recordTuiAgentModel(input) {
31397
30624
  }
31398
30625
 
31399
30626
  // src/tools/preset-manager.ts
31400
- var COMMAND_NAME4 = "preset";
30627
+ var COMMAND_NAME3 = "preset";
31401
30628
  function createPresetManager(ctx, config) {
31402
30629
  let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
31403
30630
  async function handleCommandExecuteBefore(input, output) {
31404
- if (input.command !== COMMAND_NAME4) {
30631
+ if (input.command !== COMMAND_NAME3) {
31405
30632
  return;
31406
30633
  }
31407
30634
  output.parts.length = 0;
@@ -31420,11 +30647,11 @@ function createPresetManager(ctx, config) {
31420
30647
  }
31421
30648
  function registerCommand(opencodeConfig) {
31422
30649
  const configCommand = opencodeConfig.command;
31423
- if (!configCommand?.[COMMAND_NAME4]) {
30650
+ if (!configCommand?.[COMMAND_NAME3]) {
31424
30651
  if (!opencodeConfig.command) {
31425
30652
  opencodeConfig.command = {};
31426
30653
  }
31427
- opencodeConfig.command[COMMAND_NAME4] = {
30654
+ opencodeConfig.command[COMMAND_NAME3] = {
31428
30655
  template: "List available presets and switch between them",
31429
30656
  description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
31430
30657
  };
@@ -31446,64 +30673,47 @@ function createPresetManager(ctx, config) {
31446
30673
  agentUpdates[resolvedName] = agentConfig;
31447
30674
  }
31448
30675
  }
31449
- const currentRuntimePreset = getActiveRuntimePreset();
31450
- const resetUpdates = {};
31451
- if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
31452
- const oldPreset = config.presets[currentRuntimePreset];
31453
- for (const rawName of Object.keys(oldPreset)) {
31454
- const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
31455
- if (resolvedOld in agentUpdates)
31456
- continue;
31457
- const baseline = config.agents?.[resolvedOld];
31458
- if (baseline) {
31459
- resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
31460
- }
31461
- }
31462
- }
31463
30676
  const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
31464
- const allUpdates = { ...resetUpdates, ...agentUpdates };
31465
30677
  if (!hasAgentUpdates) {
31466
30678
  output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
31467
30679
  return;
31468
30680
  }
31469
- const previousPreset = activePreset;
31470
30681
  setActiveRuntimePresetWithPrevious(presetName);
31471
30682
  try {
31472
- await ctx.client.config.update({
31473
- body: { agent: allUpdates }
31474
- });
31475
- const snapshot = readTuiSnapshot();
31476
- const agentModels = { ...snapshot.agentModels };
31477
- for (const [agentName, agentConfig] of Object.entries(allUpdates)) {
31478
- if (typeof agentConfig.model === "string") {
31479
- agentModels[agentName] = agentConfig.model;
31480
- }
31481
- }
31482
- recordTuiAgentModels({ agentModels });
31483
- activePreset = presetName;
31484
- const summaryParts = [];
31485
- for (const [name, cfg] of Object.entries(agentUpdates)) {
31486
- const parts = [name];
31487
- if (cfg.model)
31488
- parts.push(`model: ${cfg.model}`);
31489
- if (cfg.variant)
31490
- parts.push(`variant: ${cfg.variant}`);
31491
- if (cfg.temperature !== undefined)
31492
- parts.push(`temp: ${cfg.temperature}`);
31493
- if (cfg.options)
31494
- parts.push("options: yes");
31495
- summaryParts.push(parts.join(" → "));
31496
- }
31497
- if (Object.keys(resetUpdates).length > 0) {
31498
- summaryParts.push(`Reset to baseline: ${Object.keys(resetUpdates).join(", ")}`);
31499
- }
31500
- output.parts.push(createInternalAgentTextPart(`Switched to preset "${presetName}":
30683
+ const { userConfigPath } = findPluginConfigPaths(ctx.directory);
30684
+ if (userConfigPath) {
30685
+ const raw = fs10.readFileSync(userConfigPath, "utf-8");
30686
+ const persisted = JSON.parse(stripJsonComments(raw));
30687
+ persisted.preset = presetName;
30688
+ fs10.writeFileSync(userConfigPath, `${JSON.stringify(persisted, null, 2)}
30689
+ `);
30690
+ }
30691
+ } catch {}
30692
+ const snapshot = readTuiSnapshot();
30693
+ const agentModels = { ...snapshot.agentModels };
30694
+ for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
30695
+ if (typeof agentConfig.model === "string") {
30696
+ agentModels[agentName] = agentConfig.model;
30697
+ }
30698
+ }
30699
+ recordTuiAgentModels({ agentModels });
30700
+ activePreset = presetName;
30701
+ const summaryParts = [];
30702
+ for (const [name, cfg] of Object.entries(agentUpdates)) {
30703
+ const parts = [name];
30704
+ if (cfg.model)
30705
+ parts.push(`model: ${cfg.model}`);
30706
+ if (cfg.variant)
30707
+ parts.push(`variant: ${cfg.variant}`);
30708
+ if (cfg.temperature !== undefined)
30709
+ parts.push(`temp: ${cfg.temperature}`);
30710
+ if (cfg.options)
30711
+ parts.push("options: yes");
30712
+ summaryParts.push(parts.join(" → "));
30713
+ }
30714
+ output.parts.push(createInternalAgentTextPart(`Saved preset "${presetName}". Restart or reload OpenCode to apply it to agent configuration. The current session was not reloaded to avoid interrupting the active conversation.
31501
30715
  ${summaryParts.join(`
31502
30716
  `)}`));
31503
- } catch (err) {
31504
- rollbackRuntimePreset(previousPreset);
31505
- output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
31506
- }
31507
30717
  }
31508
30718
  function mapOverrideToAgentConfig(override) {
31509
30719
  const agentConfig = {};
@@ -31593,7 +30803,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
31593
30803
  import os6 from "node:os";
31594
30804
  import path18 from "node:path";
31595
30805
  import {
31596
- tool as tool5
30806
+ tool as tool4
31597
30807
  } from "@opencode-ai/plugin";
31598
30808
 
31599
30809
  // src/tools/smartfetch/binary.ts
@@ -33375,10 +32585,10 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
33375
32585
  }
33376
32586
 
33377
32587
  // src/tools/smartfetch/tool.ts
33378
- var z6 = tool5.schema;
32588
+ var z6 = tool4.schema;
33379
32589
  function createWebfetchTool(pluginCtx, options = {}) {
33380
32590
  const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
33381
- return tool5({
32591
+ return tool4({
33382
32592
  description: WEBFETCH_DESCRIPTION,
33383
32593
  args: {
33384
32594
  url: z6.httpUrl(),
@@ -33969,7 +33179,6 @@ var OhMyOpenCodeLite = async (ctx) => {
33969
33179
  let applyPatchHook;
33970
33180
  let jsonErrorRecoveryHook;
33971
33181
  let foregroundFallback;
33972
- let todoContinuationHook;
33973
33182
  let deepworkCommandHook;
33974
33183
  let taskSessionManagerHook;
33975
33184
  let backgroundJobBoard;
@@ -34062,13 +33271,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34062
33271
  applyPatchHook = createApplyPatchHook(ctx);
34063
33272
  jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
34064
33273
  foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
34065
- todoContinuationHook = createTodoContinuationHook(ctx, {
34066
- maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
34067
- cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
34068
- autoEnable: config.todoContinuation?.autoEnable ?? false,
34069
- autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
34070
- backgroundJobBoard
34071
- });
34072
33274
  deepworkCommandHook = createDeepworkCommandHook();
34073
33275
  taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
34074
33276
  maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
@@ -34085,7 +33287,7 @@ var OhMyOpenCodeLite = async (ctx) => {
34085
33287
  backgroundJobBoard,
34086
33288
  shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
34087
33289
  });
34088
- toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
33290
+ toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + 1 + 2;
34089
33291
  } catch (err) {
34090
33292
  log("[plugin] FATAL: init failed", String(err));
34091
33293
  await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
@@ -34129,7 +33331,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34129
33331
  ...councilTools,
34130
33332
  ...cancelTaskTools,
34131
33333
  webfetch,
34132
- ...todoContinuationHook.tool,
34133
33334
  ast_grep_search,
34134
33335
  ast_grep_replace
34135
33336
  },
@@ -34316,16 +33517,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34316
33517
  }
34317
33518
  agentConfigEntry.permission = agentPermission;
34318
33519
  }
34319
- const configCommand = opencodeConfig.command;
34320
- if (!configCommand?.["auto-continue"]) {
34321
- if (!opencodeConfig.command) {
34322
- opencodeConfig.command = {};
34323
- }
34324
- opencodeConfig.command["auto-continue"] = {
34325
- template: "Call the auto_continue tool with enabled=true",
34326
- description: "Enable auto-continuation — orchestrator keeps working through incomplete todos"
34327
- };
34328
- }
34329
33520
  interviewManager.registerCommand(opencodeConfig);
34330
33521
  deepworkCommandHook.registerCommand(opencodeConfig);
34331
33522
  presetManager.registerCommand(opencodeConfig);
@@ -34352,7 +33543,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34352
33543
  await multiplexerSessionManager.onSessionStatus(event);
34353
33544
  await multiplexerSessionManager.onSessionDeleted(event);
34354
33545
  await foregroundFallback.handleEvent(input.event);
34355
- await todoContinuationHook.handleEvent(input);
34356
33546
  await autoUpdateChecker.event(input);
34357
33547
  await interviewManager.handleEvent(input);
34358
33548
  await taskSessionManagerHook.event(input);
@@ -34410,7 +33600,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34410
33600
  }
34411
33601
  },
34412
33602
  "command.execute.before": async (input, output) => {
34413
- await todoContinuationHook.handleCommandExecuteBefore(input, output);
34414
33603
  await interviewManager.handleCommandExecuteBefore(input, output);
34415
33604
  await presetManager.handleCommandExecuteBefore(input, output);
34416
33605
  await deepworkCommandHook.handleCommandExecuteBefore(input, output);
@@ -34425,10 +33614,6 @@ var OhMyOpenCodeLite = async (ctx) => {
34425
33614
  if (agent) {
34426
33615
  sessionAgentMap.set(input.sessionID, agent);
34427
33616
  }
34428
- todoContinuationHook.handleChatMessage({
34429
- sessionID: input.sessionID,
34430
- agent
34431
- });
34432
33617
  },
34433
33618
  "experimental.chat.system.transform": async (input, output) => {
34434
33619
  const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
@@ -34463,9 +33648,6 @@ ${output.system[0]}` : "");
34463
33648
  disabledAgents,
34464
33649
  log
34465
33650
  });
34466
- await todoContinuationHook.handleMessagesTransform({
34467
- messages: typedOutput.messages
34468
- });
34469
33651
  await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
34470
33652
  await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
34471
33653
  await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
@@ -34487,7 +33669,6 @@ ${output.system[0]}` : "");
34487
33669
  };
34488
33670
  await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
34489
33671
  await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
34490
- await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
34491
33672
  await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
34492
33673
  await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
34493
33674
  if (input.tool.toLowerCase() === "task") {