oh-my-opencode 2.11.0 → 2.12.1

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/cli/index.js CHANGED
@@ -2657,7 +2657,7 @@ var require_napi = __commonJS((exports, module) => {
2657
2657
  var require_package = __commonJS((exports, module) => {
2658
2658
  module.exports = {
2659
2659
  name: "oh-my-opencode",
2660
- version: "2.10.0",
2660
+ version: "2.12.0",
2661
2661
  description: "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
2662
2662
  main: "dist/index.js",
2663
2663
  types: "dist/index.d.ts",
@@ -22575,7 +22575,8 @@ var HookNameSchema = exports_external.enum([
22575
22575
  "preemptive-compaction",
22576
22576
  "compaction-context-injector",
22577
22577
  "claude-code-hooks",
22578
- "auto-slash-command"
22578
+ "auto-slash-command",
22579
+ "edit-error-recovery"
22579
22580
  ]);
22580
22581
  var BuiltinCommandNameSchema = exports_external.enum([
22581
22582
  "init-deep"
@@ -61,6 +61,7 @@ export declare const HookNameSchema: z.ZodEnum<{
61
61
  "compaction-context-injector": "compaction-context-injector";
62
62
  "claude-code-hooks": "claude-code-hooks";
63
63
  "auto-slash-command": "auto-slash-command";
64
+ "edit-error-recovery": "edit-error-recovery";
64
65
  }>;
65
66
  export declare const BuiltinCommandNameSchema: z.ZodEnum<{
66
67
  "init-deep": "init-deep";
@@ -816,6 +817,7 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
816
817
  "compaction-context-injector": "compaction-context-injector";
817
818
  "claude-code-hooks": "claude-code-hooks";
818
819
  "auto-slash-command": "auto-slash-command";
820
+ "edit-error-recovery": "edit-error-recovery";
819
821
  }>>>;
820
822
  disabled_commands: z.ZodOptional<z.ZodArray<z.ZodEnum<{
821
823
  "init-deep": "init-deep";
@@ -35,6 +35,7 @@ export declare class BackgroundManager {
35
35
  private notifyParentSession;
36
36
  private formatDuration;
37
37
  private hasRunningTasks;
38
+ private pruneStaleTasksAndNotifications;
38
39
  private pollRunningTasks;
39
40
  }
40
41
  export {};
@@ -0,0 +1,31 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ /**
3
+ * Known Edit tool error patterns that indicate the AI made a mistake
4
+ */
5
+ export declare const EDIT_ERROR_PATTERNS: readonly ["oldString and newString must be different", "oldString not found", "oldString found multiple times"];
6
+ /**
7
+ * System reminder injected when Edit tool fails due to AI mistake
8
+ * Short, direct, and commanding - forces immediate corrective action
9
+ */
10
+ export declare const EDIT_ERROR_REMINDER = "\n[EDIT ERROR - IMMEDIATE ACTION REQUIRED]\n\nYou made an Edit mistake. STOP and do this NOW:\n\n1. READ the file immediately to see its ACTUAL current state\n2. VERIFY what the content really looks like (your assumption was wrong)\n3. APOLOGIZE briefly to the user for the error\n4. CONTINUE with corrected action based on the real file content\n\nDO NOT attempt another edit until you've read and verified the file state.\n";
11
+ /**
12
+ * Detects Edit tool errors caused by AI mistakes and injects a recovery reminder
13
+ *
14
+ * This hook catches common Edit tool failures:
15
+ * - oldString and newString must be different (trying to "edit" to same content)
16
+ * - oldString not found (wrong assumption about file content)
17
+ * - oldString found multiple times (ambiguous match, need more context)
18
+ *
19
+ * @see https://github.com/sst/opencode/issues/4718
20
+ */
21
+ export declare function createEditErrorRecoveryHook(_ctx: PluginInput): {
22
+ "tool.execute.after": (input: {
23
+ tool: string;
24
+ sessionID: string;
25
+ callID: string;
26
+ }, output: {
27
+ title: string;
28
+ output: string;
29
+ metadata: unknown;
30
+ }) => Promise<void>;
31
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -23,3 +23,4 @@ export { createEmptyMessageSanitizerHook } from "./empty-message-sanitizer";
23
23
  export { createThinkingBlockValidatorHook } from "./thinking-block-validator";
24
24
  export { createRalphLoopHook, type RalphLoopHook } from "./ralph-loop";
25
25
  export { createAutoSlashCommandHook } from "./auto-slash-command";
26
+ export { createEditErrorRecoveryHook } from "./edit-error-recovery";
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -8557,6 +8557,7 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
8557
8557
  - Do not stop until all tasks are done`;
8558
8558
  var COUNTDOWN_SECONDS = 2;
8559
8559
  var TOAST_DURATION_MS = 900;
8560
+ var COUNTDOWN_GRACE_PERIOD_MS = 500;
8560
8561
  function getMessageDir(sessionID) {
8561
8562
  if (!existsSync2(MESSAGE_STORAGE))
8562
8563
  return null;
@@ -8616,6 +8617,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8616
8617
  clearInterval(state2.countdownInterval);
8617
8618
  state2.countdownInterval = undefined;
8618
8619
  }
8620
+ state2.countdownStartedAt = undefined;
8619
8621
  }
8620
8622
  function cleanup(sessionID) {
8621
8623
  cancelCountdown(sessionID);
@@ -8683,12 +8685,14 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8683
8685
  const prompt = `${CONTINUATION_PROMPT}
8684
8686
 
8685
8687
  [Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]`;
8688
+ const modelField = prevMessage?.model?.providerID && prevMessage?.model?.modelID ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } : undefined;
8686
8689
  try {
8687
- log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: prevMessage?.agent, incompleteCount: freshIncompleteCount });
8690
+ log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: prevMessage?.agent, model: modelField, incompleteCount: freshIncompleteCount });
8688
8691
  await ctx.client.session.prompt({
8689
8692
  path: { id: sessionID },
8690
8693
  body: {
8691
8694
  agent: prevMessage?.agent,
8695
+ model: modelField,
8692
8696
  parts: [{ type: "text", text: prompt }]
8693
8697
  },
8694
8698
  query: { directory: ctx.directory }
@@ -8703,6 +8707,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8703
8707
  cancelCountdown(sessionID);
8704
8708
  let secondsRemaining = COUNTDOWN_SECONDS;
8705
8709
  showCountdownToast(secondsRemaining, incompleteCount);
8710
+ state2.countdownStartedAt = Date.now();
8706
8711
  state2.countdownInterval = setInterval(() => {
8707
8712
  secondsRemaining--;
8708
8713
  if (secondsRemaining > 0) {
@@ -8786,6 +8791,13 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8786
8791
  state2.lastEventWasAbortError = false;
8787
8792
  }
8788
8793
  if (role === "user") {
8794
+ if (state2?.countdownStartedAt) {
8795
+ const elapsed = Date.now() - state2.countdownStartedAt;
8796
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
8797
+ log(`[${HOOK_NAME}] Ignoring user message in grace period`, { sessionID, elapsed });
8798
+ return;
8799
+ }
8800
+ }
8789
8801
  cancelCountdown(sessionID);
8790
8802
  log(`[${HOOK_NAME}] User message: cleared abort state`, { sessionID });
8791
8803
  }
@@ -10726,9 +10738,19 @@ var TOKEN_LIMIT_KEYWORDS = [
10726
10738
  "token limit",
10727
10739
  "context length",
10728
10740
  "too many tokens",
10729
- "non-empty content",
10730
- "invalid_request_error"
10741
+ "non-empty content"
10742
+ ];
10743
+ var THINKING_BLOCK_ERROR_PATTERNS = [
10744
+ /thinking.*first block/i,
10745
+ /first block.*thinking/i,
10746
+ /must.*start.*thinking/i,
10747
+ /thinking.*redacted_thinking/i,
10748
+ /expected.*thinking.*found/i,
10749
+ /thinking.*disabled.*cannot.*contain/i
10731
10750
  ];
10751
+ function isThinkingBlockError(text) {
10752
+ return THINKING_BLOCK_ERROR_PATTERNS.some((pattern) => pattern.test(text));
10753
+ }
10732
10754
  var MESSAGE_INDEX_PATTERN = /messages\.(\d+)/;
10733
10755
  function extractTokensFromMessage(message) {
10734
10756
  for (const pattern of TOKEN_LIMIT_PATTERNS) {
@@ -10749,6 +10771,9 @@ function extractMessageIndex2(text) {
10749
10771
  return;
10750
10772
  }
10751
10773
  function isTokenLimitError(text) {
10774
+ if (isThinkingBlockError(text)) {
10775
+ return false;
10776
+ }
10752
10777
  const lower = text.toLowerCase();
10753
10778
  return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
10754
10779
  }
@@ -11829,7 +11854,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
11829
11854
  setTimeout(async () => {
11830
11855
  try {
11831
11856
  await client.session.prompt_async({
11832
- path: { sessionID },
11857
+ path: { id: sessionID },
11833
11858
  body: { parts: [{ type: "text", text: "Continue" }] },
11834
11859
  query: { directory }
11835
11860
  });
@@ -11895,7 +11920,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
11895
11920
  setTimeout(async () => {
11896
11921
  try {
11897
11922
  await client.session.prompt_async({
11898
- path: { sessionID },
11923
+ path: { id: sessionID },
11899
11924
  body: { parts: [{ type: "text", text: "Continue" }] },
11900
11925
  query: { directory }
11901
11926
  });
@@ -17381,10 +17406,6 @@ function createClaudeCodeHooksHook(ctx, config = {}) {
17381
17406
  } catch {}
17382
17407
  const isFirstMessage = !sessionFirstMessageProcessed.has(input.sessionID);
17383
17408
  sessionFirstMessageProcessed.add(input.sessionID);
17384
- if (isFirstMessage) {
17385
- log("Skipping UserPromptSubmit hooks on first message for title generation", { sessionID: input.sessionID });
17386
- return;
17387
- }
17388
17409
  if (!isHookDisabled(config, "UserPromptSubmit")) {
17389
17410
  const userPromptCtx = {
17390
17411
  sessionId: input.sessionID,
@@ -17406,17 +17427,27 @@ function createClaudeCodeHooksHook(ctx, config = {}) {
17406
17427
  const hookContent = result.messages.join(`
17407
17428
 
17408
17429
  `);
17409
- log(`[claude-code-hooks] Injecting ${result.messages.length} hook messages`, { sessionID: input.sessionID, contentLength: hookContent.length });
17410
- const message = output.message;
17411
- const success = injectHookMessage(input.sessionID, hookContent, {
17412
- agent: message.agent,
17413
- model: message.model,
17414
- path: message.path ?? { cwd: ctx.directory, root: "/" },
17415
- tools: message.tools
17416
- });
17417
- log(success ? "Hook message injected via file system" : "File injection failed", {
17418
- sessionID: input.sessionID
17419
- });
17430
+ log(`[claude-code-hooks] Injecting ${result.messages.length} hook messages`, { sessionID: input.sessionID, contentLength: hookContent.length, isFirstMessage });
17431
+ if (isFirstMessage) {
17432
+ const idx = output.parts.findIndex((p) => p.type === "text" && p.text);
17433
+ if (idx >= 0) {
17434
+ output.parts[idx].text = `${hookContent}
17435
+
17436
+ ${output.parts[idx].text ?? ""}`;
17437
+ log("UserPromptSubmit hooks prepended to first message parts directly", { sessionID: input.sessionID });
17438
+ }
17439
+ } else {
17440
+ const message = output.message;
17441
+ const success = injectHookMessage(input.sessionID, hookContent, {
17442
+ agent: message.agent,
17443
+ model: message.model,
17444
+ path: message.path ?? { cwd: ctx.directory, root: "/" },
17445
+ tools: message.tools
17446
+ });
17447
+ log(success ? "Hook message injected via file system" : "File injection failed", {
17448
+ sessionID: input.sessionID
17449
+ });
17450
+ }
17420
17451
  }
17421
17452
  }
17422
17453
  },
@@ -19018,6 +19049,17 @@ function detectBannedCommand(command) {
19018
19049
  }
19019
19050
  return;
19020
19051
  }
19052
+ function shellEscape(value) {
19053
+ if (value === "")
19054
+ return "''";
19055
+ if (/[^a-zA-Z0-9_\-.:\/]/.test(value)) {
19056
+ return `'${value.replace(/'/g, "'\\''")}'`;
19057
+ }
19058
+ return value;
19059
+ }
19060
+ function buildEnvPrefix(env) {
19061
+ return Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(" \\\n");
19062
+ }
19021
19063
  function createNonInteractiveEnvHook(_ctx) {
19022
19064
  return {
19023
19065
  "tool.execute.before": async (input, output) => {
@@ -19028,18 +19070,19 @@ function createNonInteractiveEnvHook(_ctx) {
19028
19070
  if (!command) {
19029
19071
  return;
19030
19072
  }
19031
- output.args.env = {
19032
- ...process.env,
19033
- ...output.args.env,
19034
- ...NON_INTERACTIVE_ENV
19035
- };
19036
19073
  const bannedCmd = detectBannedCommand(command);
19037
19074
  if (bannedCmd) {
19038
19075
  output.message = `\u26A0\uFE0F Warning: '${bannedCmd}' is an interactive command that may hang in non-interactive environments.`;
19039
19076
  }
19040
- log(`[${HOOK_NAME2}] Set non-interactive environment variables`, {
19077
+ const isGitCommand = /\bgit\b/.test(command);
19078
+ if (!isGitCommand) {
19079
+ return;
19080
+ }
19081
+ const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV);
19082
+ output.args.command = `${envPrefix} ${command}`;
19083
+ log(`[${HOOK_NAME2}] Prepended non-interactive env vars to git command`, {
19041
19084
  sessionID: input.sessionID,
19042
- env: NON_INTERACTIVE_ENV
19085
+ envPrefix
19043
19086
  });
19044
19087
  }
19045
19088
  };
@@ -19807,7 +19850,7 @@ function extractPromptText3(parts) {
19807
19850
 
19808
19851
  // src/hooks/auto-slash-command/executor.ts
19809
19852
  import { existsSync as existsSync35, readdirSync as readdirSync12, readFileSync as readFileSync24 } from "fs";
19810
- import { join as join43, basename as basename2, dirname as dirname9 } from "path";
19853
+ import { join as join43, basename as basename2, dirname as dirname8 } from "path";
19811
19854
  import { homedir as homedir13 } from "os";
19812
19855
  // src/features/opencode-skill-loader/loader.ts
19813
19856
  import { existsSync as existsSync33, readdirSync as readdirSync11, readFileSync as readFileSync22 } from "fs";
@@ -20009,7 +20052,7 @@ function discoverOpencodeProjectSkills() {
20009
20052
  }
20010
20053
  // src/features/opencode-skill-loader/merger.ts
20011
20054
  import { readFileSync as readFileSync23, existsSync as existsSync34 } from "fs";
20012
- import { dirname as dirname8, resolve as resolve5, isAbsolute as isAbsolute2 } from "path";
20055
+ import { dirname as dirname7, resolve as resolve5, isAbsolute as isAbsolute2 } from "path";
20013
20056
  import { homedir as homedir12 } from "os";
20014
20057
  var SCOPE_PRIORITY = {
20015
20058
  builtin: 1,
@@ -20082,7 +20125,7 @@ function configEntryToLoaded(name, entry, configDir) {
20082
20125
  return null;
20083
20126
  }
20084
20127
  const description = entry.description || fileMetadata.description || "";
20085
- const resolvedPath = entry.from ? dirname8(resolveFilePath2(entry.from, configDir)) : configDir || process.cwd();
20128
+ const resolvedPath = entry.from ? dirname7(resolveFilePath2(entry.from, configDir)) : configDir || process.cwd();
20086
20129
  const wrappedTemplate = `<skill-instruction>
20087
20130
  Base directory for this skill: ${resolvedPath}/
20088
20131
  File references (@path) in this skill are relative to this directory.
@@ -20312,7 +20355,7 @@ async function formatCommandTemplate(cmd, args) {
20312
20355
  `);
20313
20356
  sections.push(`## Command Instructions
20314
20357
  `);
20315
- const commandDir = cmd.path ? dirname9(cmd.path) : process.cwd();
20358
+ const commandDir = cmd.path ? dirname8(cmd.path) : process.cwd();
20316
20359
  const withFileRefs = await resolveFileReferencesInText(cmd.content || "", commandDir);
20317
20360
  const resolvedContent = await resolveCommandsInText(withFileRefs);
20318
20361
  sections.push(resolvedContent.trim());
@@ -20403,6 +20446,38 @@ ${AUTO_SLASH_COMMAND_TAG_CLOSE}`;
20403
20446
  }
20404
20447
  };
20405
20448
  }
20449
+ // src/hooks/edit-error-recovery/index.ts
20450
+ var EDIT_ERROR_PATTERNS = [
20451
+ "oldString and newString must be different",
20452
+ "oldString not found",
20453
+ "oldString found multiple times"
20454
+ ];
20455
+ var EDIT_ERROR_REMINDER = `
20456
+ [EDIT ERROR - IMMEDIATE ACTION REQUIRED]
20457
+
20458
+ You made an Edit mistake. STOP and do this NOW:
20459
+
20460
+ 1. READ the file immediately to see its ACTUAL current state
20461
+ 2. VERIFY what the content really looks like (your assumption was wrong)
20462
+ 3. APOLOGIZE briefly to the user for the error
20463
+ 4. CONTINUE with corrected action based on the real file content
20464
+
20465
+ DO NOT attempt another edit until you've read and verified the file state.
20466
+ `;
20467
+ function createEditErrorRecoveryHook(_ctx) {
20468
+ return {
20469
+ "tool.execute.after": async (input, output) => {
20470
+ if (input.tool.toLowerCase() !== "edit")
20471
+ return;
20472
+ const outputLower = output.output.toLowerCase();
20473
+ const hasEditError = EDIT_ERROR_PATTERNS.some((pattern) => outputLower.includes(pattern.toLowerCase()));
20474
+ if (hasEditError) {
20475
+ output.output += `
20476
+ ${EDIT_ERROR_REMINDER}`;
20477
+ }
20478
+ }
20479
+ };
20480
+ }
20406
20481
  // src/auth/antigravity/constants.ts
20407
20482
  var ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
20408
20483
  var ANTIGRAVITY_CLIENT_SECRET = "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";
@@ -36437,7 +36512,7 @@ var lsp_code_action_resolve = tool({
36437
36512
  });
36438
36513
  // src/tools/ast-grep/constants.ts
36439
36514
  import { createRequire as createRequire4 } from "module";
36440
- import { dirname as dirname10, join as join47 } from "path";
36515
+ import { dirname as dirname9, join as join47 } from "path";
36441
36516
  import { existsSync as existsSync40, statSync as statSync4 } from "fs";
36442
36517
 
36443
36518
  // src/tools/ast-grep/downloader.ts
@@ -36580,7 +36655,7 @@ function findSgCliPathSync() {
36580
36655
  try {
36581
36656
  const require2 = createRequire4(import.meta.url);
36582
36657
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
36583
- const cliDir = dirname10(cliPkgPath);
36658
+ const cliDir = dirname9(cliPkgPath);
36584
36659
  const sgPath = join47(cliDir, binaryName);
36585
36660
  if (existsSync40(sgPath) && isValidBinary(sgPath)) {
36586
36661
  return sgPath;
@@ -36591,7 +36666,7 @@ function findSgCliPathSync() {
36591
36666
  try {
36592
36667
  const require2 = createRequire4(import.meta.url);
36593
36668
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
36594
- const pkgDir = dirname10(pkgPath);
36669
+ const pkgDir = dirname9(pkgPath);
36595
36670
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
36596
36671
  const binaryPath = join47(pkgDir, astGrepName);
36597
36672
  if (existsSync40(binaryPath) && isValidBinary(binaryPath)) {
@@ -36968,7 +37043,7 @@ var {spawn: spawn9 } = globalThis.Bun;
36968
37043
 
36969
37044
  // src/tools/grep/constants.ts
36970
37045
  import { existsSync as existsSync43 } from "fs";
36971
- import { join as join49, dirname as dirname11 } from "path";
37046
+ import { join as join49, dirname as dirname10 } from "path";
36972
37047
  import { spawnSync } from "child_process";
36973
37048
 
36974
37049
  // src/tools/grep/downloader.ts
@@ -37133,7 +37208,7 @@ function findExecutable(name) {
37133
37208
  }
37134
37209
  function getOpenCodeBundledRg() {
37135
37210
  const execPath = process.execPath;
37136
- const execDir = dirname11(execPath);
37211
+ const execDir = dirname10(execPath);
37137
37212
  const isWindows2 = process.platform === "win32";
37138
37213
  const rgName = isWindows2 ? "rg.exe" : "rg";
37139
37214
  const candidates = [
@@ -37598,7 +37673,7 @@ var glob = tool({
37598
37673
  });
37599
37674
  // src/tools/slashcommand/tools.ts
37600
37675
  import { existsSync as existsSync44, readdirSync as readdirSync14, readFileSync as readFileSync29 } from "fs";
37601
- import { join as join50, basename as basename3, dirname as dirname12 } from "path";
37676
+ import { join as join50, basename as basename3, dirname as dirname11 } from "path";
37602
37677
  function discoverCommandsFromDir2(commandsDir, scope) {
37603
37678
  if (!existsSync44(commandsDir)) {
37604
37679
  return [];
@@ -37704,7 +37779,7 @@ async function formatLoadedCommand(cmd) {
37704
37779
  `);
37705
37780
  sections.push(`## Command Instructions
37706
37781
  `);
37707
- const commandDir = cmd.path ? dirname12(cmd.path) : process.cwd();
37782
+ const commandDir = cmd.path ? dirname11(cmd.path) : process.cwd();
37708
37783
  const withFileRefs = await resolveFileReferencesInText(cmd.content || "", commandDir);
37709
37784
  const resolvedContent = await resolveCommandsInText(withFileRefs);
37710
37785
  sections.push(resolvedContent.trim());
@@ -38490,7 +38565,7 @@ var TOOL_DESCRIPTION_PREFIX = `Load a skill to get detailed instructions for a s
38490
38565
  Skills provide specialized knowledge and step-by-step guidance.
38491
38566
  Use this when a task matches an available skill's description.`;
38492
38567
  // src/tools/skill/tools.ts
38493
- import { dirname as dirname13 } from "path";
38568
+ import { dirname as dirname12 } from "path";
38494
38569
  import { readFileSync as readFileSync30 } from "fs";
38495
38570
  function loadedSkillToInfo(skill) {
38496
38571
  return {
@@ -38612,7 +38687,7 @@ function createSkillTool(options = {}) {
38612
38687
  throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`);
38613
38688
  }
38614
38689
  const body = extractSkillBody(skill);
38615
- const dir = skill.path ? dirname13(skill.path) : skill.resolvedPath || process.cwd();
38690
+ const dir = skill.path ? dirname12(skill.path) : skill.resolvedPath || process.cwd();
38616
38691
  const output = [
38617
38692
  `## Skill: ${skill.name}`,
38618
38693
  "",
@@ -39384,6 +39459,7 @@ var builtinTools = {
39384
39459
  // src/features/background-agent/manager.ts
39385
39460
  import { existsSync as existsSync47, readdirSync as readdirSync17 } from "fs";
39386
39461
  import { join as join54 } from "path";
39462
+ var TASK_TTL_MS = 30 * 60 * 1000;
39387
39463
  function getMessageDir11(sessionID) {
39388
39464
  if (!existsSync47(MESSAGE_STORAGE))
39389
39465
  return null;
@@ -39649,11 +39725,11 @@ class BackgroundManager {
39649
39725
  },
39650
39726
  query: { directory: this.directory }
39651
39727
  });
39652
- this.clearNotificationsForTask(taskId);
39653
39728
  log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
39654
39729
  } catch (error45) {
39655
39730
  log("[background-agent] prompt failed:", String(error45));
39656
39731
  } finally {
39732
+ this.clearNotificationsForTask(taskId);
39657
39733
  this.tasks.delete(taskId);
39658
39734
  log("[background-agent] Removed completed task from memory:", taskId);
39659
39735
  }
@@ -39678,7 +39754,38 @@ class BackgroundManager {
39678
39754
  }
39679
39755
  return false;
39680
39756
  }
39757
+ pruneStaleTasksAndNotifications() {
39758
+ const now = Date.now();
39759
+ for (const [taskId, task] of this.tasks.entries()) {
39760
+ const age = now - task.startedAt.getTime();
39761
+ if (age > TASK_TTL_MS) {
39762
+ log("[background-agent] Pruning stale task:", { taskId, age: Math.round(age / 1000) + "s" });
39763
+ task.status = "error";
39764
+ task.error = "Task timed out after 30 minutes";
39765
+ task.completedAt = new Date;
39766
+ this.clearNotificationsForTask(taskId);
39767
+ this.tasks.delete(taskId);
39768
+ subagentSessions.delete(task.sessionID);
39769
+ }
39770
+ }
39771
+ for (const [sessionID, notifications] of this.notifications.entries()) {
39772
+ if (notifications.length === 0) {
39773
+ this.notifications.delete(sessionID);
39774
+ continue;
39775
+ }
39776
+ const validNotifications = notifications.filter((task) => {
39777
+ const age = now - task.startedAt.getTime();
39778
+ return age <= TASK_TTL_MS;
39779
+ });
39780
+ if (validNotifications.length === 0) {
39781
+ this.notifications.delete(sessionID);
39782
+ } else if (validNotifications.length !== notifications.length) {
39783
+ this.notifications.set(sessionID, validNotifications);
39784
+ }
39785
+ }
39786
+ }
39681
39787
  async pollRunningTasks() {
39788
+ this.pruneStaleTasksAndNotifications();
39682
39789
  const statusResult = await this.client.session.status();
39683
39790
  const allStatuses = statusResult.data ?? {};
39684
39791
  for (const task of this.tasks.values()) {
@@ -42490,7 +42597,8 @@ var HookNameSchema = exports_external.enum([
42490
42597
  "preemptive-compaction",
42491
42598
  "compaction-context-injector",
42492
42599
  "claude-code-hooks",
42493
- "auto-slash-command"
42600
+ "auto-slash-command",
42601
+ "edit-error-recovery"
42494
42602
  ]);
42495
42603
  var BuiltinCommandNameSchema = exports_external.enum([
42496
42604
  "init-deep"
@@ -45625,6 +45733,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
45625
45733
  const thinkingBlockValidator = isHookEnabled("thinking-block-validator") ? createThinkingBlockValidatorHook() : null;
45626
45734
  const ralphLoop = isHookEnabled("ralph-loop") ? createRalphLoopHook(ctx, { config: pluginConfig.ralph_loop }) : null;
45627
45735
  const autoSlashCommand = isHookEnabled("auto-slash-command") ? createAutoSlashCommandHook() : null;
45736
+ const editErrorRecovery = isHookEnabled("edit-error-recovery") ? createEditErrorRecoveryHook(ctx) : null;
45628
45737
  const backgroundManager = new BackgroundManager(ctx);
45629
45738
  const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") ? createTodoContinuationEnforcer(ctx, { backgroundManager }) : null;
45630
45739
  if (sessionRecovery && todoContinuationEnforcer) {
@@ -45819,6 +45928,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
45819
45928
  await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
45820
45929
  await agentUsageReminder?.["tool.execute.after"](input, output);
45821
45930
  await interactiveBashSession?.["tool.execute.after"](input, output);
45931
+ await editErrorRecovery?.["tool.execute.after"](input, output);
45822
45932
  }
45823
45933
  };
45824
45934
  };
@@ -2,4 +2,4 @@ import type { AnalyzeResult, SgResult } from "./types";
2
2
  export declare function formatSearchResult(result: SgResult): string;
3
3
  export declare function formatReplaceResult(result: SgResult, isDryRun: boolean): string;
4
4
  export declare function formatAnalyzeResult(results: AnalyzeResult[], extractedMetaVars: boolean): string;
5
- export declare function formatTransformResult(original: string, transformed: string, editCount: number): string;
5
+ export declare function formatTransformResult(_original: string, transformed: string, editCount: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",