oh-my-codex 0.16.1 → 0.16.3
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/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/dist/cli/__tests__/doctor-warning-copy.test.js +37 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +2 -2
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +173 -3
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +58 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +1 -0
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +82 -2
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +12 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +161 -0
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +14 -9
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +44 -2
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +109 -19
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +17 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +3 -4
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +34 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +72 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +54 -15
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +26 -8
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +68 -5
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts +10 -2
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +99 -5
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +269 -2
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +60 -2
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +59 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/wiki-config-contract.test.js +2 -1
- package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +52 -4
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +268 -7
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/generator.d.ts +13 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +207 -12
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +37 -25
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +29 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +20 -4
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +52 -0
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +148 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +3 -0
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/wiki-docs-contract.test.js +5 -4
- package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2 -4
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +145 -6
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +97 -1
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +11 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/planning/__tests__/artifacts.test.js +64 -0
- package/dist/planning/__tests__/artifacts.test.js.map +1 -1
- package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts +2 -0
- package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts.map +1 -0
- package/dist/planning/__tests__/ready-context-pack-role-refs.test.js +90 -0
- package/dist/planning/__tests__/ready-context-pack-role-refs.test.js.map +1 -0
- package/dist/planning/artifacts.d.ts +7 -2
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +62 -8
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/planning/context-pack-status.d.ts +6 -0
- package/dist/planning/context-pack-status.d.ts.map +1 -1
- package/dist/planning/context-pack-status.js +25 -0
- package/dist/planning/context-pack-status.js.map +1 -1
- package/dist/ralph/persistence.d.ts +1 -1
- package/dist/ralph/persistence.d.ts.map +1 -1
- package/dist/ralph/persistence.js +8 -2
- package/dist/ralph/persistence.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +190 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-execution-surface.d.ts +1 -1
- package/dist/scripts/codex-execution-surface.d.ts.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +18 -3
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-dispatcher.d.ts +7 -0
- package/dist/scripts/notify-dispatcher.d.ts.map +1 -0
- package/dist/scripts/notify-dispatcher.js +58 -0
- package/dist/scripts/notify-dispatcher.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +4 -0
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +96 -8
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +6 -2
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +3 -3
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.js +124 -0
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +79 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +10 -18
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +1 -20
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +1 -0
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +28 -18
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +1 -0
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js +45 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +173 -19
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +37 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/approved-execution.d.ts +1 -0
- package/dist/team/approved-execution.d.ts.map +1 -1
- package/dist/team/approved-execution.js +50 -0
- package/dist/team/approved-execution.js.map +1 -1
- package/dist/team/delivery-log.d.ts.map +1 -1
- package/dist/team/delivery-log.js +8 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +104 -18
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/mailbox.d.ts +1 -0
- package/dist/team/state/mailbox.d.ts.map +1 -1
- package/dist/team/state/mailbox.js +10 -1
- package/dist/team/state/mailbox.js.map +1 -1
- package/dist/team/state-root.js +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +2 -2
- package/dist/team/state.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +7 -2
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +17 -4
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +81 -7
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +8 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +4 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +72 -4
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/paths.d.ts +3 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +6 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/wiki/__tests__/ingest.test.js +35 -1
- package/dist/wiki/__tests__/ingest.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js +14 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js +28 -3
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/session-hooks.test.js +30 -2
- package/dist/wiki/__tests__/session-hooks.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js +62 -22
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/index.d.ts +2 -2
- package/dist/wiki/index.d.ts.map +1 -1
- package/dist/wiki/index.js +2 -2
- package/dist/wiki/index.js.map +1 -1
- package/dist/wiki/ingest.js +2 -2
- package/dist/wiki/ingest.js.map +1 -1
- package/dist/wiki/lifecycle.d.ts +5 -0
- package/dist/wiki/lifecycle.d.ts.map +1 -1
- package/dist/wiki/lifecycle.js +31 -4
- package/dist/wiki/lifecycle.js.map +1 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +12 -8
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.d.ts.map +1 -1
- package/dist/wiki/query.js +3 -2
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.d.ts +4 -0
- package/dist/wiki/storage.d.ts.map +1 -1
- package/dist/wiki/storage.js +54 -18
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/plan/SKILL.md +4 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +11 -7
- package/plugins/oh-my-codex/skills/wiki/SKILL.md +5 -5
- package/prompts/planner.md +1 -1
- package/skills/deep-interview/SKILL.md +2 -2
- package/skills/plan/SKILL.md +4 -4
- package/skills/ralplan/SKILL.md +2 -2
- package/skills/ultragoal/SKILL.md +11 -7
- package/skills/wiki/SKILL.md +5 -5
- package/src/scripts/__tests__/codex-native-hook.test.ts +218 -1
- package/src/scripts/codex-execution-surface.ts +2 -0
- package/src/scripts/codex-native-hook.ts +22 -3
- package/src/scripts/notify-dispatcher.ts +74 -0
- package/src/scripts/notify-fallback-watcher.ts +6 -2
- package/src/scripts/notify-hook/ralph-session-resume.ts +117 -8
- package/src/scripts/notify-hook/state-io.ts +4 -2
- package/src/scripts/notify-hook/visual-verdict.ts +3 -3
- package/src/scripts/notify-hook.ts +116 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* oh-my-codex notify dispatcher.
|
|
5
|
+
* Runs a pre-existing user notify command first, then the OMX notify hook.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
|
|
11
|
+
interface NotifyDispatcherMetadata {
|
|
12
|
+
managedBy?: string;
|
|
13
|
+
version?: number;
|
|
14
|
+
previousNotify?: string[] | null;
|
|
15
|
+
omxNotify?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(): { metadataPath: string; payloadArg: string } {
|
|
19
|
+
let metadataPath = "";
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
22
|
+
if (args[i] === "--metadata") {
|
|
23
|
+
metadataPath = args[i + 1] || "";
|
|
24
|
+
i += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
metadataPath,
|
|
29
|
+
payloadArg: process.argv[process.argv.length - 1] || "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isCommand(value: unknown): value is string[] {
|
|
34
|
+
return (
|
|
35
|
+
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readMetadata(
|
|
40
|
+
path: string,
|
|
41
|
+
): Promise<NotifyDispatcherMetadata | null> {
|
|
42
|
+
if (!path) return null;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(await readFile(path, "utf-8")) as unknown;
|
|
45
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
46
|
+
return parsed as NotifyDispatcherMetadata;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runNotify(
|
|
53
|
+
command: string[] | null | undefined,
|
|
54
|
+
payloadArg: string,
|
|
55
|
+
): void {
|
|
56
|
+
if (!isCommand(command) || command.length === 0) return;
|
|
57
|
+
const [bin, ...args] = command;
|
|
58
|
+
spawnSync(bin, [...args, payloadArg], {
|
|
59
|
+
stdio: "ignore",
|
|
60
|
+
env: process.env,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
timeout: 30_000,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main(): Promise<void> {
|
|
67
|
+
const { metadataPath, payloadArg } = parseArgs();
|
|
68
|
+
if (!payloadArg || payloadArg.startsWith("-")) return;
|
|
69
|
+
const metadata = await readMetadata(metadataPath);
|
|
70
|
+
runNotify(metadata?.previousNotify, payloadArg);
|
|
71
|
+
runNotify(metadata?.omxNotify, payloadArg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch(() => {});
|
|
@@ -1649,8 +1649,12 @@ async function invokeNotifyHook(payload: Record<string, unknown>, filePath: stri
|
|
|
1649
1649
|
const result = spawnSync(process.execPath, [notifyScript, JSON.stringify(payload)], {
|
|
1650
1650
|
cwd,
|
|
1651
1651
|
encoding: 'utf-8',
|
|
1652
|
-
|
|
1653
|
-
|
|
1652
|
+
env: {
|
|
1653
|
+
...process.env,
|
|
1654
|
+
OMX_NOTIFY_HOOK_TRUSTED_MANAGED_CWD: cwd,
|
|
1655
|
+
},
|
|
1656
|
+
windowsHide: true,
|
|
1657
|
+
});
|
|
1654
1658
|
const ok = result.status === 0;
|
|
1655
1659
|
await eventLog({
|
|
1656
1660
|
type: 'fallback_notify',
|
|
@@ -2,12 +2,13 @@ import { existsSync } from 'fs';
|
|
|
2
2
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'fs/promises';
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { captureTmuxPaneFromEnv } from '../../state/mode-state-context.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSessionStateUsable } from '../../hooks/session.js';
|
|
6
6
|
import { resolveCodexPane } from '../tmux-hook-engine.js';
|
|
7
7
|
import { safeString } from './utils.js';
|
|
8
8
|
|
|
9
9
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10
|
-
const RALPH_TERMINAL_PHASES = new Set(['blocked_on_user', 'complete', 'failed', 'cancelled']);
|
|
10
|
+
const RALPH_TERMINAL_PHASES = new Set(['blocked_on_user', 'complete', 'failed', 'cancelled', 'interrupted']);
|
|
11
|
+
const DEFAULT_RALPH_ACTIVE_STATE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
11
12
|
const RALPH_RESUME_LOCK_STALE_MS = 10_000;
|
|
12
13
|
const RALPH_RESUME_LOCK_TIMEOUT_MS = 5_000;
|
|
13
14
|
const RALPH_RESUME_LOCK_RETRY_MS = 25;
|
|
@@ -40,6 +41,14 @@ interface RalphStateCandidate {
|
|
|
40
41
|
state: Record<string, unknown>;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
interface RalphStateFreshness {
|
|
45
|
+
stale: boolean;
|
|
46
|
+
ageMs: number;
|
|
47
|
+
checkedAtMs: number;
|
|
48
|
+
staleThresholdMs: number;
|
|
49
|
+
timestampSource: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
function lockOwnerToken(): string {
|
|
44
53
|
return `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
45
54
|
}
|
|
@@ -128,6 +137,80 @@ function isActiveRalphCandidate(state: Record<string, unknown> | null): state is
|
|
|
128
137
|
return state.active === true && !isTerminalRalphPhase(state.current_phase);
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
function parsePositiveInteger(value: unknown): number | null {
|
|
141
|
+
const parsed = Number.parseInt(safeString(value).trim(), 10);
|
|
142
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveActiveStateStaleThresholdMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
146
|
+
return parsePositiveInteger(env.OMX_RALPH_ACTIVE_STATE_STALE_MS)
|
|
147
|
+
?? parsePositiveInteger(env.OMX_RALPH_RESUME_STALE_MS)
|
|
148
|
+
?? DEFAULT_RALPH_ACTIVE_STATE_STALE_MS;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseTimestampMs(value: unknown): number | null {
|
|
152
|
+
const raw = safeString(value).trim();
|
|
153
|
+
if (!raw) return null;
|
|
154
|
+
const parsed = Date.parse(raw);
|
|
155
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stateActivityTimestampMs(state: Record<string, unknown>): { ms: number; source: string } | null {
|
|
159
|
+
let newest: { ms: number; source: string } | null = null;
|
|
160
|
+
for (const key of ['updated_at', 'last_turn_at', 'tmux_pane_set_at']) {
|
|
161
|
+
const ms = parseTimestampMs(state[key]);
|
|
162
|
+
if (ms !== null && (!newest || ms > newest.ms)) {
|
|
163
|
+
newest = { ms, source: key };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return newest;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readRalphStateFreshness(
|
|
170
|
+
path: string,
|
|
171
|
+
state: Record<string, unknown>,
|
|
172
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
173
|
+
): Promise<RalphStateFreshness> {
|
|
174
|
+
const checkedAtMs = Date.now();
|
|
175
|
+
const threshold = resolveActiveStateStaleThresholdMs(env);
|
|
176
|
+
let timestamp = stateActivityTimestampMs(state);
|
|
177
|
+
if (!timestamp) {
|
|
178
|
+
try {
|
|
179
|
+
const info = await stat(path);
|
|
180
|
+
timestamp = { ms: info.mtimeMs, source: 'mtime' };
|
|
181
|
+
} catch {
|
|
182
|
+
timestamp = { ms: checkedAtMs, source: 'missing_mtime' };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const ageMs = Math.max(0, checkedAtMs - timestamp.ms);
|
|
186
|
+
return {
|
|
187
|
+
stale: ageMs > threshold,
|
|
188
|
+
ageMs,
|
|
189
|
+
checkedAtMs,
|
|
190
|
+
staleThresholdMs: threshold,
|
|
191
|
+
timestampSource: timestamp.source,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function markRalphStateAbandoned(
|
|
196
|
+
path: string,
|
|
197
|
+
state: Record<string, unknown>,
|
|
198
|
+
freshness: RalphStateFreshness,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const nowIso = new Date(freshness.checkedAtMs).toISOString();
|
|
201
|
+
await writeJsonAtomic(path, {
|
|
202
|
+
...state,
|
|
203
|
+
active: false,
|
|
204
|
+
current_phase: 'cancelled',
|
|
205
|
+
completed_at: nowIso,
|
|
206
|
+
abandoned_at: nowIso,
|
|
207
|
+
stop_reason: 'stale_active_state',
|
|
208
|
+
stale_resume_age_ms: freshness.ageMs,
|
|
209
|
+
stale_resume_threshold_ms: freshness.staleThresholdMs,
|
|
210
|
+
stale_resume_timestamp_source: freshness.timestampSource,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
131
214
|
function readSessionIdFromEnvironment(env: NodeJS.ProcessEnv = process.env): string {
|
|
132
215
|
const candidates = [env.OMX_SESSION_ID, env.CODEX_SESSION_ID, env.SESSION_ID];
|
|
133
216
|
for (const candidate of candidates) {
|
|
@@ -144,7 +227,10 @@ async function readCurrentOmxSessionId(stateDir: string, env: NodeJS.ProcessEnv
|
|
|
144
227
|
if (existsSync(envScopedDir)) return envSessionId;
|
|
145
228
|
}
|
|
146
229
|
|
|
147
|
-
const
|
|
230
|
+
const cwd = resolve(stateDir, '..', '..');
|
|
231
|
+
const session = await readJson(join(stateDir, 'session.json'));
|
|
232
|
+
if (!session || typeof session !== 'object') return '';
|
|
233
|
+
if (!isSessionStateUsable(session as any, cwd)) return '';
|
|
148
234
|
const sessionId = safeString(session?.session_id).trim();
|
|
149
235
|
return SESSION_ID_PATTERN.test(sessionId) ? sessionId : '';
|
|
150
236
|
}
|
|
@@ -171,12 +257,14 @@ async function scanMatchingRalphCandidates(
|
|
|
171
257
|
currentOmxSessionId: string,
|
|
172
258
|
payloadSessionId: string,
|
|
173
259
|
payloadThreadId: string,
|
|
174
|
-
|
|
260
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
261
|
+
): Promise<{ candidates: RalphStateCandidate[]; abandonedCount: number }> {
|
|
175
262
|
const sessionsRoot = join(stateDir, 'sessions');
|
|
176
|
-
if (!existsSync(sessionsRoot)) return [];
|
|
263
|
+
if (!existsSync(sessionsRoot)) return { candidates: [], abandonedCount: 0 };
|
|
177
264
|
|
|
178
265
|
const entries = await readdir(sessionsRoot, { withFileTypes: true }).catch(() => []);
|
|
179
266
|
const matches: RalphStateCandidate[] = [];
|
|
267
|
+
let abandonedCount = 0;
|
|
180
268
|
for (const entry of entries) {
|
|
181
269
|
if (!entry.isDirectory() || !SESSION_ID_PATTERN.test(entry.name) || entry.name === currentOmxSessionId) continue;
|
|
182
270
|
const path = join(sessionsRoot, entry.name, 'ralph-state.json');
|
|
@@ -190,13 +278,19 @@ async function scanMatchingRalphCandidates(
|
|
|
190
278
|
} else if (!payloadThreadId || !ownerThreadId || ownerThreadId !== payloadThreadId) {
|
|
191
279
|
continue;
|
|
192
280
|
}
|
|
281
|
+
const freshness = await readRalphStateFreshness(path, state, env);
|
|
282
|
+
if (freshness.stale) {
|
|
283
|
+
await markRalphStateAbandoned(path, state, freshness);
|
|
284
|
+
abandonedCount += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
193
287
|
matches.push({
|
|
194
288
|
sessionId: entry.name,
|
|
195
289
|
path,
|
|
196
290
|
state,
|
|
197
291
|
});
|
|
198
292
|
}
|
|
199
|
-
return matches;
|
|
293
|
+
return { candidates: matches, abandonedCount };
|
|
200
294
|
}
|
|
201
295
|
|
|
202
296
|
export async function reconcileRalphSessionResume({
|
|
@@ -228,6 +322,18 @@ export async function reconcileRalphSessionResume({
|
|
|
228
322
|
const nowIso = new Date().toISOString();
|
|
229
323
|
|
|
230
324
|
if (currentRalphState && currentRalphState.active === true) {
|
|
325
|
+
const freshness = await readRalphStateFreshness(currentRalphPath, currentRalphState, env);
|
|
326
|
+
if (freshness.stale) {
|
|
327
|
+
await markRalphStateAbandoned(currentRalphPath, currentRalphState, freshness);
|
|
328
|
+
return {
|
|
329
|
+
currentOmxSessionId,
|
|
330
|
+
resumed: false,
|
|
331
|
+
updatedCurrentOwner: false,
|
|
332
|
+
reason: 'current_ralph_abandoned_stale',
|
|
333
|
+
targetPath: currentRalphPath,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
231
337
|
let changed = false;
|
|
232
338
|
const updated: Record<string, unknown> = { ...currentRalphState };
|
|
233
339
|
const normalizedPayloadThreadId = safeString(payloadThreadId).trim();
|
|
@@ -293,18 +399,21 @@ export async function reconcileRalphSessionResume({
|
|
|
293
399
|
};
|
|
294
400
|
}
|
|
295
401
|
|
|
296
|
-
const candidates = await scanMatchingRalphCandidates(
|
|
402
|
+
const { candidates, abandonedCount } = await scanMatchingRalphCandidates(
|
|
297
403
|
stateDir,
|
|
298
404
|
currentOmxSessionId,
|
|
299
405
|
normalizedPayloadSessionId,
|
|
300
406
|
normalizedPayloadThreadId,
|
|
407
|
+
env,
|
|
301
408
|
);
|
|
302
409
|
if (candidates.length !== 1) {
|
|
303
410
|
return {
|
|
304
411
|
currentOmxSessionId,
|
|
305
412
|
resumed: false,
|
|
306
413
|
updatedCurrentOwner: false,
|
|
307
|
-
reason: candidates.length === 0
|
|
414
|
+
reason: candidates.length === 0
|
|
415
|
+
? (abandonedCount > 0 ? 'matching_prior_ralph_abandoned_stale' : 'no_matching_prior_ralph')
|
|
416
|
+
: 'multiple_matching_prior_ralphs',
|
|
308
417
|
};
|
|
309
418
|
}
|
|
310
419
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
|
6
6
|
import { dirname, join, resolve } from 'path';
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
|
-
import {
|
|
8
|
+
import { isSessionStateUsable } from '../../hooks/session.js';
|
|
9
9
|
import { asNumber, safeString } from './utils.js';
|
|
10
10
|
|
|
11
11
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
@@ -43,7 +43,9 @@ export async function readCurrentSessionId(baseStateDir: string): Promise<string
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const cwd = resolve(baseStateDir, '..', '..');
|
|
46
|
-
const session = await
|
|
46
|
+
const session = await readJsonIfExists(join(baseStateDir, 'session.json'), null);
|
|
47
|
+
if (!session || typeof session !== 'object') return undefined;
|
|
48
|
+
if (!isSessionStateUsable(session, cwd)) return undefined;
|
|
47
49
|
const sessionId = safeString(session?.session_id);
|
|
48
50
|
return SESSION_ID_PATTERN.test(sessionId) ? sessionId : undefined;
|
|
49
51
|
}
|
|
@@ -38,7 +38,7 @@ function extractJsonCandidates(rawMessage: any): string[] {
|
|
|
38
38
|
return candidates;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async function maybePersistRuntimeVisualFeedback({ cwd, output, sessionId }: any): Promise<void> {
|
|
41
|
+
async function maybePersistRuntimeVisualFeedback({ cwd, output, sessionId, stateDir }: any): Promise<void> {
|
|
42
42
|
if (!cwd || !output) return;
|
|
43
43
|
|
|
44
44
|
const candidates = extractJsonCandidates(output);
|
|
@@ -51,7 +51,7 @@ async function maybePersistRuntimeVisualFeedback({ cwd, output, sessionId }: any
|
|
|
51
51
|
try {
|
|
52
52
|
const parsed = JSON.parse(candidate);
|
|
53
53
|
const feedback = buildVisualLoopFeedback(parsed);
|
|
54
|
-
await recordRalphVisualFeedback(cwd, feedback, sessionId || undefined);
|
|
54
|
+
await recordRalphVisualFeedback(cwd, feedback, sessionId || undefined, stateDir || undefined);
|
|
55
55
|
return;
|
|
56
56
|
} catch {
|
|
57
57
|
// Try next candidate
|
|
@@ -93,7 +93,7 @@ export async function maybePersistVisualVerdict({ cwd, payload, stateDir, logsDi
|
|
|
93
93
|
// Runtime visual feedback (JSON/fenced JSON) for ralph-progress persistence.
|
|
94
94
|
// Non-fatal and observable via warn-level structured logging.
|
|
95
95
|
try {
|
|
96
|
-
await maybePersistRuntimeVisualFeedback({ cwd, output, sessionId });
|
|
96
|
+
await maybePersistRuntimeVisualFeedback({ cwd, output, sessionId, stateDir });
|
|
97
97
|
} catch (err: any) {
|
|
98
98
|
await logNotifyHookEvent(logsDir, {
|
|
99
99
|
timestamp: new Date().toISOString(),
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { writeFile, appendFile, mkdir, readFile } from 'fs/promises';
|
|
22
22
|
import { existsSync } from 'fs';
|
|
23
23
|
import { dirname, join, resolve } from 'path';
|
|
24
|
+
import { isSessionStateUsable } from '../hooks/session.js';
|
|
24
25
|
|
|
25
26
|
import { safeString, asNumber } from './notify-hook/utils.js';
|
|
26
27
|
import {
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
maybeNotifyLeaderWorkerIdle,
|
|
66
67
|
} from './notify-hook/team-worker.js';
|
|
67
68
|
import { DEFAULT_MARKER } from './tmux-hook-engine.js';
|
|
69
|
+
import { sameFilePath } from '../utils/paths.js';
|
|
68
70
|
|
|
69
71
|
const RALPH_ACTIVE_PROGRESS_PHASES = new Set([
|
|
70
72
|
'start',
|
|
@@ -82,6 +84,117 @@ const RALPH_ACTIVE_PROGRESS_PHASES = new Set([
|
|
|
82
84
|
|
|
83
85
|
const IDLE_NOTIFICATION_SUMMARY_MAX_LENGTH = 240;
|
|
84
86
|
|
|
87
|
+
async function readJsonFileIfObject(path: string): Promise<Record<string, unknown> | null> {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await readFile(path, 'utf-8');
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
92
|
+
? parsed as Record<string, unknown>
|
|
93
|
+
: null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasOmxRuntimeStateMarker(value: Record<string, unknown> | null): boolean {
|
|
100
|
+
if (!value) return false;
|
|
101
|
+
return typeof value.active === 'boolean'
|
|
102
|
+
|| typeof value.team_name === 'string'
|
|
103
|
+
|| typeof value.current_phase === 'string'
|
|
104
|
+
|| typeof value.lifecycle_outcome === 'string'
|
|
105
|
+
|| typeof value.run_outcome === 'string';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function hasManagedTeamStateTree(cwd: string): Promise<boolean> {
|
|
109
|
+
const teamStateRoot = join(cwd, '.omx', 'state', 'team');
|
|
110
|
+
if (!existsSync(teamStateRoot)) return false;
|
|
111
|
+
let entries: string[] = [];
|
|
112
|
+
try {
|
|
113
|
+
entries = await readdir(teamStateRoot);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.startsWith('.')) continue;
|
|
119
|
+
const teamDir = join(teamStateRoot, entry);
|
|
120
|
+
if (existsSync(join(teamDir, 'manifest.v2.json')) || existsSync(join(teamDir, 'config.json'))) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function isOmxManagedCwd(cwd: string): Promise<boolean> {
|
|
128
|
+
const trustedInternalCwd = safeString(process.env.OMX_NOTIFY_HOOK_TRUSTED_MANAGED_CWD || '').trim();
|
|
129
|
+
if (trustedInternalCwd && sameFilePath(trustedInternalCwd, cwd)) return true;
|
|
130
|
+
if (existsSync(join(cwd, '.omx', 'setup-scope.json'))) return true;
|
|
131
|
+
if (existsSync(join(cwd, '.omx', 'managed'))) return true;
|
|
132
|
+
const sessionStatePath = join(cwd, '.omx', 'state', 'session.json');
|
|
133
|
+
if (existsSync(sessionStatePath)) {
|
|
134
|
+
try {
|
|
135
|
+
const sessionState = JSON.parse(await readFile(sessionStatePath, 'utf-8'));
|
|
136
|
+
if (isSessionStateUsable(sessionState, cwd)) return true;
|
|
137
|
+
} catch {
|
|
138
|
+
// Continue checking other managed markers.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const teamState = await readJsonFileIfObject(join(cwd, '.omx', 'state', 'team-state.json'));
|
|
142
|
+
if (hasOmxRuntimeStateMarker(teamState)) return true;
|
|
143
|
+
const hudState = await readJsonFileIfObject(join(cwd, '.omx', 'state', 'hud-state.json'));
|
|
144
|
+
if (hudState && (typeof hudState.last_turn_at === 'string' || typeof hudState.turn_count === 'number')) return true;
|
|
145
|
+
if (await hasManagedTeamStateTree(cwd)) return true;
|
|
146
|
+
const teamWorkerEnv = safeString(process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER || '').trim();
|
|
147
|
+
if (teamWorkerEnv) {
|
|
148
|
+
const [teamName = '', workerName = ''] = teamWorkerEnv.split('/');
|
|
149
|
+
if (teamName && workerName) {
|
|
150
|
+
const candidateStateRoots = [
|
|
151
|
+
safeString(process.env.OMX_TEAM_STATE_ROOT || '').trim(),
|
|
152
|
+
safeString(process.env.OMX_TEAM_LEADER_CWD || '').trim()
|
|
153
|
+
? join(resolve(cwd, safeString(process.env.OMX_TEAM_LEADER_CWD || '').trim()), '.omx', 'state')
|
|
154
|
+
: '',
|
|
155
|
+
join(cwd, '.omx', 'state'),
|
|
156
|
+
].filter((value, index, values) => value && values.indexOf(value) === index);
|
|
157
|
+
for (const candidateStateRoot of candidateStateRoots) {
|
|
158
|
+
const identityPath = join(candidateStateRoot, 'team', teamName, 'workers', workerName, 'identity.json');
|
|
159
|
+
if (!existsSync(identityPath)) continue;
|
|
160
|
+
try {
|
|
161
|
+
const raw = await readFile(identityPath, 'utf-8');
|
|
162
|
+
const identity = JSON.parse(raw);
|
|
163
|
+
const worktreePath = safeString(identity?.worktree_path || '').trim();
|
|
164
|
+
const stateRoot = safeString(identity?.team_state_root || '').trim();
|
|
165
|
+
if (
|
|
166
|
+
(!worktreePath || sameFilePath(worktreePath, cwd))
|
|
167
|
+
&& (!stateRoot || sameFilePath(stateRoot, candidateStateRoot))
|
|
168
|
+
) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// A worker notify hook with an explicit runtime root hint is OMX-scoped
|
|
176
|
+
// even when the hint fails validation. Let the main worker path log the
|
|
177
|
+
// unresolved-root warning and fail closed without inventing local state.
|
|
178
|
+
if (
|
|
179
|
+
safeString(process.env.OMX_TEAM_STATE_ROOT || '').trim()
|
|
180
|
+
|| safeString(process.env.OMX_TEAM_LEADER_CWD || '').trim()
|
|
181
|
+
) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const hooksPath = join(cwd, '.codex', 'hooks.json');
|
|
187
|
+
if (existsSync(hooksPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const raw = await readFile(hooksPath, 'utf-8');
|
|
190
|
+
return /(?:^|[\\/])codex-native-hook\.js(?:["'\s]|$)/.test(raw);
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
85
198
|
function summarizeIdleNotificationMessage(message: unknown): string {
|
|
86
199
|
const source = safeString(message)
|
|
87
200
|
.split('\n')
|
|
@@ -172,6 +285,9 @@ async function main() {
|
|
|
172
285
|
}
|
|
173
286
|
|
|
174
287
|
const cwd = payload.cwd || payload['cwd'] || process.cwd();
|
|
288
|
+
if (!(await isOmxManagedCwd(cwd))) {
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
175
291
|
const payloadSessionId = safeString(payload.session_id || payload['session-id'] || '');
|
|
176
292
|
const payloadThreadId = safeString(payload['thread-id'] || payload.thread_id || '');
|
|
177
293
|
const inputMessages = normalizeInputMessages(payload);
|