oh-my-opencode 5.1.57 → 5.1.58

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.
Files changed (65) hide show
  1. package/dist/cli/index.js +23 -19
  2. package/dist/cli-node/index.js +23 -19
  3. package/dist/index.js +204 -185
  4. package/dist/skills/browser/runtime/omowright/index.js +20 -19
  5. package/dist/skills/browser/runtime/omowright/manifest.json +3 -3
  6. package/dist/skills/ultimate-browsing/engine/executor.py +56 -5
  7. package/dist/skills/ultimate-browsing/engine/tests/test_executor_timeout.py +144 -0
  8. package/dist/tui.js +20 -16
  9. package/package.json +15 -15
  10. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  11. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +5 -1
  12. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  13. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  14. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  15. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  19. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  21. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  22. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  24. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  30. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  33. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  34. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  35. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  36. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  37. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  38. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  39. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  40. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  41. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  42. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  43. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  44. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  56. package/packages/omo-codex/plugin/package-lock.json +12 -12
  57. package/packages/omo-codex/plugin/package.json +1 -1
  58. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +20 -19
  59. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
  60. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/executor.py +56 -5
  61. package/packages/omo-codex/scripts/install-dist/install-local.mjs +7 -3
  62. package/packages/shared-skills/skills/browser/runtime/omowright/index.js +20 -19
  63. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
  64. package/packages/shared-skills/skills/ultimate-browsing/engine/executor.py +56 -5
  65. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_executor_timeout.py +144 -0
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "version": "0.0.1",
3
- "sourceDigest": "c9eaae75e948cc52d5b60bb654a5565869d68627046453d87d6f695a5b618045",
3
+ "sourceDigest": "27243299c68de06d4321588d998bd106e0554086d268dc448aa70571db719c1d",
4
4
  "files": {
5
- "index.js": "8b14c1775f397e36967a0d094c67e2a31f8ae1e92add172925548723ca339046",
5
+ "index.js": "3a2e4f2575bdfc15448f13768cd8113f8493bf04611c9fefb09b13430d6520de",
6
6
  "page-bundle.js": "bfd51e8383cf47200d57387bbbed258bce5a3a3d00c38fc108034d2888e23f0c"
7
7
  },
8
- "stagedAtUtc": "2026-11-01T20:36:54.907Z"
8
+ "stagedAtUtc": "2026-11-02T13:36:54.191Z"
9
9
  }
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import json
22
22
  import os
23
23
  import shutil
24
+ import signal
24
25
  import subprocess
25
26
  import tempfile
26
27
  import time
@@ -32,6 +33,10 @@ from .result_schema import Attempt
32
33
 
33
34
 
34
35
  TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")
36
+ # Chrome removes its per-launch state (the macOS code-sign clone, the temp
37
+ # profile) only on a clean quit, so a timed-out template's process group gets
38
+ # SIGTERM and this long to let `ctx.close()` run before SIGKILL (omo#12067).
39
+ TERM_GRACE_S = 5
35
40
 
36
41
 
37
42
  def _node_available() -> bool:
@@ -70,19 +75,65 @@ def _run_node_template(template: str, args: dict, timeout: int = 90) -> tuple[in
70
75
  if not os.path.isfile(path):
71
76
  return 127, "", f"template not found: {path}"
72
77
  try:
73
- proc = subprocess.run(
78
+ proc = subprocess.Popen(
74
79
  ["node", path],
75
- input=json.dumps(args),
80
+ stdin=subprocess.PIPE,
81
+ stdout=subprocess.PIPE,
82
+ stderr=subprocess.PIPE,
76
83
  cwd=TEMPLATES_DIR,
77
- capture_output=True,
78
84
  text=True,
79
- timeout=timeout,
85
+ start_new_session=True,
80
86
  )
81
- return proc.returncode, proc.stdout, proc.stderr
87
+ except Exception as e:
88
+ return 1, "", f"{type(e).__name__}:{e}"
89
+ try:
90
+ stdout, stderr = proc.communicate(input=json.dumps(args), timeout=timeout)
91
+ return proc.returncode, stdout, stderr
82
92
  except subprocess.TimeoutExpired:
93
+ _terminate_template_group(proc)
83
94
  return 124, "", f"timeout after {timeout}s"
84
95
  except Exception as e:
96
+ _terminate_template_group(proc)
85
97
  return 1, "", f"{type(e).__name__}:{e}"
98
+ except BaseException:
99
+ # Cancellation (KeyboardInterrupt, SystemExit): start_new_session keeps a
100
+ # terminal Ctrl-C from reaching the template's group, so quit it here.
101
+ _terminate_template_group(proc)
102
+ raise
103
+
104
+
105
+ def _signal_group(pgid: int, sig: int) -> bool:
106
+ try:
107
+ os.killpg(pgid, sig)
108
+ except ProcessLookupError:
109
+ return False
110
+ return True
111
+
112
+
113
+ def _terminate_template_group(proc: subprocess.Popen[str]) -> None:
114
+ """SIGTERM the template's whole group (node and the Chrome it launched), wait up to TERM_GRACE_S, then SIGKILL."""
115
+ if os.name == "nt":
116
+ proc.terminate()
117
+ try:
118
+ proc.wait(timeout=TERM_GRACE_S)
119
+ except subprocess.TimeoutExpired:
120
+ proc.kill()
121
+ else:
122
+ pgid = proc.pid
123
+ if _signal_group(pgid, signal.SIGTERM):
124
+ deadline = time.monotonic() + TERM_GRACE_S
125
+ while time.monotonic() < deadline:
126
+ proc.poll()
127
+ if not _signal_group(pgid, 0):
128
+ break
129
+ time.sleep(0.05)
130
+ else:
131
+ _signal_group(pgid, signal.SIGKILL)
132
+ try:
133
+ proc.communicate(timeout=TERM_GRACE_S)
134
+ except subprocess.TimeoutExpired:
135
+ proc.kill()
136
+ proc.communicate()
86
137
 
87
138
 
88
139
  class _FakeResp:
@@ -0,0 +1,144 @@
1
+ """A timed-out template must give its browser a graceful quit before any SIGKILL.
2
+
3
+ Chrome removes its per-launch state (the macOS code-sign clone, a temp profile)
4
+ only on a clean exit, so killing the node template alone, or killing anything
5
+ without a SIGTERM grace window first, leaks it (omo#12067). A fake template and a
6
+ fake "browser" grandchild stand in for Playwright and Chrome.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ import textwrap
16
+ import time
17
+ import unittest
18
+ from pathlib import Path
19
+ from unittest import mock
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
22
+
23
+ from engine import executor # noqa: E402
24
+
25
+ FAKE_TEMPLATE = textwrap.dedent(
26
+ """
27
+ const { spawn } = require('node:child_process');
28
+ const fs = require('node:fs');
29
+ const log = process.env.EXECUTOR_TEST_LOG;
30
+ const record = (line) => fs.appendFileSync(log, `${line} ${Date.now()}\\n`);
31
+ const browser = spawn(process.execPath, ['-e', `
32
+ const fs = require('node:fs');
33
+ process.on('SIGTERM', () => { fs.appendFileSync(${JSON.stringify(log)}, 'BROWSER_TERM ' + Date.now() + '\\\\n'); process.exit(0); });
34
+ fs.appendFileSync(${JSON.stringify(log)}, 'BROWSER_READY ' + Date.now() + '\\\\n');
35
+ setInterval(() => {}, 60000);
36
+ `], { stdio: 'ignore' });
37
+ record(`PIDS ${process.pid}:${browser.pid}`);
38
+ process.on('SIGTERM', () => {
39
+ record('TEMPLATE_TERM');
40
+ if (process.env.EXECUTOR_TEST_TEMPLATE_ON_TERM === 'exit') process.exit(0);
41
+ });
42
+ setInterval(() => {}, 60000);
43
+ """
44
+ )
45
+
46
+
47
+ def _alive(pid: int) -> bool:
48
+ try:
49
+ os.kill(pid, 0)
50
+ except ProcessLookupError:
51
+ return False
52
+ return True
53
+
54
+
55
+ @unittest.skipIf(os.name == "nt" or shutil.which("node") is None, "POSIX process groups and node required")
56
+ class TimedOutTemplateTermination(unittest.TestCase):
57
+ def _run(self, on_term: str, communicate=None) -> tuple[int, str, dict[str, list[int]], float]:
58
+ tmp = Path(tempfile.mkdtemp(prefix="ultimate-browsing-executor-test-"))
59
+ self.addCleanup(shutil.rmtree, tmp, True)
60
+ (tmp / "fake_template.js").write_text(FAKE_TEMPLATE, encoding="utf-8")
61
+ log = tmp / "events.log"
62
+ env = {"EXECUTOR_TEST_LOG": str(log), "EXECUTOR_TEST_TEMPLATE_ON_TERM": on_term}
63
+ try:
64
+ with mock.patch.object(executor, "TEMPLATES_DIR", str(tmp)), mock.patch.dict(os.environ, env):
65
+ if communicate is None:
66
+ code, _out, err = executor._run_node_template("fake_template.js", {}, timeout=1)
67
+ else:
68
+ with mock.patch.object(executor.subprocess.Popen, "communicate", communicate(log)):
69
+ code, _out, err = executor._run_node_template("fake_template.js", {}, timeout=30)
70
+ finally:
71
+ events = self._read_events(log)
72
+ returned_at = time.time() * 1000
73
+ return code, err, events, returned_at
74
+
75
+ def _read_events(self, log: Path) -> dict[str, list[int]]:
76
+ events: dict[str, list[int]] = {}
77
+ if not log.exists():
78
+ return events
79
+ for line in log.read_text(encoding="utf-8").splitlines():
80
+ name, *rest = line.split(" ")
81
+ events.setdefault(name, []).append(int(rest[-1]))
82
+ if name == "PIDS":
83
+ template_pid, browser_pid = rest[0].split(":")
84
+ events["template_pid"] = [int(template_pid)]
85
+ events["browser_pid"] = [int(browser_pid)]
86
+ for pid in (int(template_pid), int(browser_pid)):
87
+ self.addCleanup(lambda p=pid: _alive(p) and os.kill(p, 9))
88
+ self.last_events = events
89
+ return events
90
+
91
+ def _assert_gone(self, pid: int) -> None:
92
+ deadline = time.time() + 2
93
+ while _alive(pid) and time.time() < deadline:
94
+ time.sleep(0.05)
95
+ self.assertFalse(_alive(pid), f"pid {pid} outlived the timed-out template")
96
+
97
+ def test_timeout_terms_the_whole_group_and_waits_before_kill(self) -> None:
98
+ code, err, events, returned_at = self._run(on_term="ignore")
99
+ self.assertEqual(code, 124, err)
100
+ self.assertIn("BROWSER_TERM", events, "the browser grandchild never got SIGTERM")
101
+ self.assertIn("TEMPLATE_TERM", events, "the template never got SIGTERM")
102
+ grace_ms = returned_at - events["TEMPLATE_TERM"][0]
103
+ self.assertGreaterEqual(grace_ms, executor.TERM_GRACE_S * 1000 - 500, "SIGKILL came without a grace window")
104
+ self._assert_gone(events["template_pid"][0])
105
+ self._assert_gone(events["browser_pid"][0])
106
+
107
+ def test_template_that_quits_on_term_is_not_held_for_the_grace(self) -> None:
108
+ code, err, events, returned_at = self._run(on_term="exit")
109
+ self.assertEqual(code, 124, err)
110
+ self.assertIn("TEMPLATE_TERM", events)
111
+ self.assertLess(returned_at - events["TEMPLATE_TERM"][0], 3000)
112
+ self._assert_gone(events["browser_pid"][0])
113
+
114
+ def test_cancelled_run_tears_the_group_down_and_reraises(self) -> None:
115
+ # A KeyboardInterrupt (or any BaseException) during communicate() must still
116
+ # quit node and its browser: start_new_session means a terminal Ctrl-C no
117
+ # longer reaches the template's group, so nobody else will.
118
+ original = subprocess.Popen.communicate
119
+
120
+ def interrupting(log: Path):
121
+ calls = {"n": 0}
122
+
123
+ def communicate(proc, *args, **kwargs):
124
+ calls["n"] += 1
125
+ if calls["n"] > 1:
126
+ return original(proc, *args, **kwargs)
127
+ deadline = time.time() + 10
128
+ while time.time() < deadline and not (log.exists() and "BROWSER_READY" in log.read_text(encoding="utf-8")):
129
+ time.sleep(0.02)
130
+ raise KeyboardInterrupt
131
+
132
+ return communicate
133
+
134
+ with self.assertRaises(KeyboardInterrupt):
135
+ self._run(on_term="exit", communicate=interrupting)
136
+ events = self.last_events
137
+ self.assertIn("browser_pid", events, "the fake template never started")
138
+ self.assertIn("BROWSER_TERM", events, "the browser never got SIGTERM after the run was cancelled")
139
+ self._assert_gone(events["template_pid"][0])
140
+ self._assert_gone(events["browser_pid"][0])
141
+
142
+
143
+ if __name__ == "__main__":
144
+ unittest.main()