oh-my-opencode 5.1.56 → 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.
- package/dist/cli/index.js +23 -19
- package/dist/cli-node/index.js +23 -19
- package/dist/index.js +521 -348
- package/dist/skills/browser/runtime/omowright/index.js +20 -19
- package/dist/skills/browser/runtime/omowright/manifest.json +3 -3
- package/dist/skills/ultimate-browsing/engine/executor.py +56 -5
- package/dist/skills/ultimate-browsing/engine/tests/test_executor_timeout.py +144 -0
- package/dist/tui.js +20 -16
- package/package.json +15 -15
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +5 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +20 -19
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/executor.py +56 -5
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +7 -3
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +20 -19
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/shared-skills/skills/ultimate-browsing/engine/executor.py +56 -5
- 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": "
|
|
3
|
+
"sourceDigest": "27243299c68de06d4321588d998bd106e0554086d268dc448aa70571db719c1d",
|
|
4
4
|
"files": {
|
|
5
|
-
"index.js": "
|
|
5
|
+
"index.js": "3a2e4f2575bdfc15448f13768cd8113f8493bf04611c9fefb09b13430d6520de",
|
|
6
6
|
"page-bundle.js": "bfd51e8383cf47200d57387bbbed258bce5a3a3d00c38fc108034d2888e23f0c"
|
|
7
7
|
},
|
|
8
|
-
"stagedAtUtc": "2026-11-
|
|
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.
|
|
78
|
+
proc = subprocess.Popen(
|
|
74
79
|
["node", path],
|
|
75
|
-
|
|
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
|
-
|
|
85
|
+
start_new_session=True,
|
|
80
86
|
)
|
|
81
|
-
|
|
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()
|