oh-my-codex 0.7.5 → 0.8.0
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/README.de.md +314 -0
- package/README.es.md +295 -17
- package/README.fr.md +314 -0
- package/README.it.md +314 -0
- package/README.ja.md +296 -18
- package/README.ko.md +295 -17
- package/README.md +68 -3
- package/README.pt.md +295 -17
- package/README.ru.md +295 -17
- package/README.tr.md +314 -0
- package/README.vi.md +296 -18
- package/README.zh.md +292 -17
- package/dist/catalog/__tests__/generator.test.js +2 -0
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/catalog/__tests__/schema.test.js +7 -0
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.d.ts +2 -0
- package/dist/cli/__tests__/ask.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ask.test.js +236 -0
- package/dist/cli/__tests__/ask.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +45 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -0
- package/dist/cli/__tests__/index.test.js +43 -1
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +2 -0
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +15 -0
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -0
- package/dist/cli/__tests__/ralph.test.d.ts +2 -0
- package/dist/cli/__tests__/ralph.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralph.test.js +40 -0
- package/dist/cli/__tests__/ralph.test.js.map +1 -0
- package/dist/cli/__tests__/setup-scope.test.js +2 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/team-decompose.test.d.ts +2 -0
- package/dist/cli/__tests__/team-decompose.test.d.ts.map +1 -0
- package/dist/cli/__tests__/team-decompose.test.js +67 -0
- package/dist/cli/__tests__/team-decompose.test.js.map +1 -0
- package/dist/cli/__tests__/version.test.d.ts +2 -0
- package/dist/cli/__tests__/version.test.d.ts.map +1 -0
- package/dist/cli/__tests__/version.test.js +21 -0
- package/dist/cli/__tests__/version.test.js.map +1 -0
- package/dist/cli/ask.d.ts +13 -0
- package/dist/cli/ask.d.ts.map +1 -0
- package/dist/cli/ask.js +174 -0
- package/dist/cli/ask.js.map +1 -0
- package/dist/cli/constants.d.ts +10 -0
- package/dist/cli/constants.d.ts.map +1 -0
- package/dist/cli/constants.js +10 -0
- package/dist/cli/constants.js.map +1 -0
- package/dist/cli/doctor.js +16 -5
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +117 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ralph.d.ts +4 -0
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +89 -13
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/team.d.ts +18 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +108 -16
- package/dist/cli/team.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +8 -0
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js +55 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/emulator.test.js +6 -0
- package/dist/hooks/__tests__/emulator.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +44 -22
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +59 -0
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +88 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +199 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts +11 -0
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +266 -0
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -0
- package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js +51 -0
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +2 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +34 -0
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -0
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +36 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +2 -0
- package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/visual-verdict-loop.test.js +35 -0
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -0
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +18 -16
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/codebase-map.d.ts.map +1 -1
- package/dist/hooks/codebase-map.js +6 -2
- package/dist/hooks/codebase-map.js.map +1 -1
- package/dist/hooks/emulator.d.ts.map +1 -1
- package/dist/hooks/emulator.js +2 -0
- package/dist/hooks/emulator.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +2 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +6 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +2 -24
- package/dist/hud/index.js.map +1 -1
- package/dist/mcp/__tests__/team-server-cleanup.test.d.ts +2 -0
- package/dist/mcp/__tests__/team-server-cleanup.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/team-server-cleanup.test.js +219 -0
- package/dist/mcp/__tests__/team-server-cleanup.test.js.map +1 -0
- package/dist/mcp/bootstrap.d.ts +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +1 -0
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +18 -8
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/memory-server.js +72 -11
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +4 -1
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +18 -5
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/mcp/team-server.d.ts +24 -0
- package/dist/mcp/team-server.d.ts.map +1 -0
- package/dist/mcp/team-server.js +425 -0
- package/dist/mcp/team-server.js.map +1 -0
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js +8 -3
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js +35 -0
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +12 -3
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/dispatcher.d.ts.map +1 -1
- package/dist/notifications/dispatcher.js +4 -4
- package/dist/notifications/dispatcher.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +6 -2
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.d.ts.map +1 -1
- package/dist/notifications/session-registry.js +2 -2
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/tmux.d.ts.map +1 -1
- package/dist/notifications/tmux.js +13 -4
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/notifications/types.d.ts +4 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/index.test.js +40 -0
- package/dist/openclaw/__tests__/index.test.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +5 -2
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/openclaw/index.d.ts.map +1 -1
- package/dist/openclaw/index.js +1 -0
- package/dist/openclaw/index.js.map +1 -1
- package/dist/openclaw/types.d.ts +2 -0
- package/dist/openclaw/types.d.ts.map +1 -1
- package/dist/ralph/__tests__/persistence.test.js +28 -1
- package/dist/ralph/__tests__/persistence.test.js.map +1 -1
- package/dist/ralph/persistence.d.ts +21 -0
- package/dist/ralph/persistence.d.ts.map +1 -1
- package/dist/ralph/persistence.js +85 -2
- package/dist/ralph/persistence.js.map +1 -1
- package/dist/state/paths.d.ts +3 -0
- package/dist/state/paths.d.ts.map +1 -0
- package/dist/state/paths.js +2 -0
- package/dist/state/paths.js.map +1 -0
- package/dist/team/__tests__/idle-nudge.test.d.ts +2 -0
- package/dist/team/__tests__/idle-nudge.test.d.ts.map +1 -0
- package/dist/team/__tests__/idle-nudge.test.js +225 -0
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -0
- package/dist/team/__tests__/role-router.test.d.ts +2 -0
- package/dist/team/__tests__/role-router.test.d.ts.map +1 -0
- package/dist/team/__tests__/role-router.test.js +204 -0
- package/dist/team/__tests__/role-router.test.js.map +1 -0
- package/dist/team/__tests__/runtime-cli.test.d.ts +2 -0
- package/dist/team/__tests__/runtime-cli.test.d.ts.map +1 -0
- package/dist/team/__tests__/runtime-cli.test.js +72 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -0
- package/dist/team/__tests__/runtime.test.js +195 -9
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +132 -2
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.d.ts +2 -0
- package/dist/team/__tests__/state-root.test.d.ts.map +1 -0
- package/dist/team/__tests__/state-root.test.js +9 -0
- package/dist/team/__tests__/state-root.test.js.map +1 -0
- package/dist/team/__tests__/state.test.js +52 -17
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.d.ts +2 -0
- package/dist/team/__tests__/team-ops-contract.test.d.ts.map +1 -0
- package/dist/team/__tests__/team-ops-contract.test.js +90 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.test.js +94 -7
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +59 -0
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +81 -2
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/idle-nudge.d.ts +53 -0
- package/dist/team/idle-nudge.d.ts.map +1 -0
- package/dist/team/idle-nudge.js +140 -0
- package/dist/team/idle-nudge.js.map +1 -0
- package/dist/team/mcp-comm.d.ts +1 -1
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +6 -2
- package/dist/team/mcp-comm.js.map +1 -1
- package/dist/team/orchestrator.d.ts +1 -10
- package/dist/team/orchestrator.d.ts.map +1 -1
- package/dist/team/orchestrator.js +8 -0
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/role-router.d.ts +32 -0
- package/dist/team/role-router.d.ts.map +1 -0
- package/dist/team/role-router.js +137 -0
- package/dist/team/role-router.js.map +1 -0
- package/dist/team/runtime-cli.d.ts +18 -0
- package/dist/team/runtime-cli.d.ts.map +1 -0
- package/dist/team/runtime-cli.js +244 -0
- package/dist/team/runtime-cli.js.map +1 -0
- package/dist/team/runtime.d.ts +6 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +148 -60
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +1 -0
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +74 -32
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/approvals.d.ts +25 -0
- package/dist/team/state/approvals.d.ts.map +1 -0
- package/dist/team/state/approvals.js +31 -0
- package/dist/team/state/approvals.js.map +1 -0
- package/dist/team/state/config.d.ts +2 -0
- package/dist/team/state/config.d.ts.map +1 -0
- package/dist/team/state/config.js +2 -0
- package/dist/team/state/config.js.map +1 -0
- package/dist/team/state/dispatch-lock.d.ts +3 -0
- package/dist/team/state/dispatch-lock.d.ts.map +1 -0
- package/dist/team/state/dispatch-lock.js +81 -0
- package/dist/team/state/dispatch-lock.js.map +1 -0
- package/dist/team/state/dispatch.d.ts +61 -0
- package/dist/team/state/dispatch.d.ts.map +1 -0
- package/dist/team/state/dispatch.js +158 -0
- package/dist/team/state/dispatch.js.map +1 -0
- package/dist/team/state/events.d.ts +2 -0
- package/dist/team/state/events.d.ts.map +1 -0
- package/dist/team/state/events.js +2 -0
- package/dist/team/state/events.js.map +1 -0
- package/dist/team/state/index.d.ts +11 -0
- package/dist/team/state/index.d.ts.map +1 -0
- package/dist/team/state/index.js +11 -0
- package/dist/team/state/index.js.map +1 -0
- package/dist/team/state/io.d.ts +2 -0
- package/dist/team/state/io.d.ts.map +1 -0
- package/dist/team/state/io.js +2 -0
- package/dist/team/state/io.js.map +1 -0
- package/dist/team/state/locks.d.ts +16 -0
- package/dist/team/state/locks.d.ts.map +1 -0
- package/dist/team/state/locks.js +201 -0
- package/dist/team/state/locks.js.map +1 -0
- package/dist/team/state/mailbox.d.ts +39 -0
- package/dist/team/state/mailbox.d.ts.map +1 -0
- package/dist/team/state/mailbox.js +58 -0
- package/dist/team/state/mailbox.js.map +1 -0
- package/dist/team/state/monitor.d.ts +96 -0
- package/dist/team/state/monitor.d.ts.map +1 -0
- package/dist/team/state/monitor.js +163 -0
- package/dist/team/state/monitor.js.map +1 -0
- package/dist/team/state/shutdown.d.ts +2 -0
- package/dist/team/state/shutdown.d.ts.map +1 -0
- package/dist/team/state/shutdown.js +2 -0
- package/dist/team/state/shutdown.js.map +1 -0
- package/dist/team/state/summary.d.ts +2 -0
- package/dist/team/state/summary.d.ts.map +1 -0
- package/dist/team/state/summary.js +2 -0
- package/dist/team/state/summary.js.map +1 -0
- package/dist/team/state/tasks.d.ts +49 -0
- package/dist/team/state/tasks.d.ts.map +1 -0
- package/dist/team/state/tasks.js +182 -0
- package/dist/team/state/tasks.js.map +1 -0
- package/dist/team/state/types.d.ts +281 -0
- package/dist/team/state/types.d.ts.map +1 -0
- package/dist/team/state/types.js +3 -0
- package/dist/team/state/types.js.map +1 -0
- package/dist/team/state/workers.d.ts +2 -0
- package/dist/team/state/workers.d.ts.map +1 -0
- package/dist/team/state/workers.js +2 -0
- package/dist/team/state/workers.js.map +1 -0
- package/dist/team/state-root.d.ts +5 -0
- package/dist/team/state-root.d.ts.map +1 -0
- package/dist/team/state-root.js +8 -0
- package/dist/team/state-root.js.map +1 -0
- package/dist/team/state.d.ts +6 -2
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +200 -881
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +42 -2
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +229 -74
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts +2 -0
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +47 -20
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.d.ts +5 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +71 -17
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/safe-json.d.ts +3 -0
- package/dist/utils/safe-json.d.ts.map +1 -0
- package/dist/utils/safe-json.js +19 -0
- package/dist/utils/safe-json.js.map +1 -0
- package/dist/utils/sleep.d.ts +3 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +15 -0
- package/dist/utils/sleep.js.map +1 -0
- package/dist/visual/__tests__/verdict.test.d.ts +2 -0
- package/dist/visual/__tests__/verdict.test.d.ts.map +1 -0
- package/dist/visual/__tests__/verdict.test.js +81 -0
- package/dist/visual/__tests__/verdict.test.js.map +1 -0
- package/dist/visual/constants.d.ts +4 -0
- package/dist/visual/constants.d.ts.map +1 -0
- package/dist/visual/constants.js +3 -0
- package/dist/visual/constants.js.map +1 -0
- package/dist/visual/verdict.d.ts +17 -0
- package/dist/visual/verdict.d.ts.map +1 -0
- package/dist/visual/verdict.js +61 -0
- package/dist/visual/verdict.js.map +1 -0
- package/package.json +10 -3
- package/scripts/ask-claude.sh +17 -0
- package/scripts/ask-gemini.sh +14 -0
- package/scripts/fixtures/ask-advisor-stub.js +12 -0
- package/scripts/notify-hook/log.js +5 -0
- package/scripts/notify-hook/team-dispatch.js +56 -1
- package/scripts/notify-hook/tmux-injection.js +45 -4
- package/scripts/notify-hook/visual-verdict.js +158 -0
- package/scripts/notify-hook.js +27 -0
- package/scripts/run-provider-advisor.js +179 -0
- package/scripts/tmux-hook-engine.js +24 -0
- package/skills/ask-claude/SKILL.md +61 -0
- package/skills/ask-gemini/SKILL.md +61 -0
- package/skills/autopilot/SKILL.md +34 -4
- package/skills/configure-notifications/SKILL.md +1 -1
- package/skills/configure-openclaw/SKILL.md +154 -157
- package/skills/deep-interview/SKILL.md +247 -0
- package/skills/doctor/SKILL.md +1 -1
- package/skills/help/SKILL.md +3 -3
- package/skills/ralph/SKILL.md +42 -11
- package/skills/ralplan/SKILL.md +17 -0
- package/skills/skill/SKILL.md +32 -32
- package/skills/team/SKILL.md +60 -0
- package/skills/visual-verdict/SKILL.md +76 -0
- package/skills/web-clone/SKILL.md +366 -0
- package/skills/worker/SKILL.md +5 -4
- package/templates/AGENTS.md +9 -0
- package/templates/catalog-manifest.json +39 -2
package/dist/team/state.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir
|
|
1
|
+
import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname, resolve, sep } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
-
import { performance } from 'perf_hooks';
|
|
6
5
|
import { omxStateDir } from '../utils/paths.js';
|
|
6
|
+
import { computeTaskReadiness as computeTaskReadinessImpl, claimTask as claimTaskImpl, transitionTaskStatus as transitionTaskStatusImpl, releaseTaskClaim as releaseTaskClaimImpl, listTasks as listTasksImpl, } from './state/tasks.js';
|
|
7
|
+
import { sendDirectMessage as sendDirectMessageImpl, broadcastMessage as broadcastMessageImpl, markMessageDelivered as markMessageDeliveredImpl, markMessageNotified as markMessageNotifiedImpl, listMailboxMessages as listMailboxMessagesImpl, } from './state/mailbox.js';
|
|
8
|
+
import { enqueueDispatchRequest as enqueueDispatchRequestImpl, listDispatchRequests as listDispatchRequestsImpl, readDispatchRequest as readDispatchRequestImpl, transitionDispatchRequest as transitionDispatchRequestImpl, markDispatchRequestNotified as markDispatchRequestNotifiedImpl, markDispatchRequestDelivered as markDispatchRequestDeliveredImpl, normalizeDispatchRequest as normalizeDispatchRequestImpl, } from './state/dispatch.js';
|
|
9
|
+
import { resolveDispatchLockTimeoutMs as resolveDispatchLockTimeoutMsImpl, withDispatchLock as withDispatchLockImpl, } from './state/dispatch-lock.js';
|
|
10
|
+
import { writeTaskApproval as writeTaskApprovalImpl, readTaskApproval as readTaskApprovalImpl, } from './state/approvals.js';
|
|
11
|
+
import { getTeamSummary as getTeamSummaryImpl, readMonitorSnapshot as readMonitorSnapshotImpl, writeMonitorSnapshot as writeMonitorSnapshotImpl, readTeamPhase as readTeamPhaseImpl, writeTeamPhase as writeTeamPhaseImpl, } from './state/monitor.js';
|
|
12
|
+
import { withScalingLock as withScalingLockImpl, withTeamLock as withTeamLockImpl, withTaskClaimLock as withTaskClaimLockImpl, withMailboxLock as withMailboxLockImpl, } from './state/locks.js';
|
|
7
13
|
import { TEAM_NAME_SAFE_PATTERN, WORKER_NAME_SAFE_PATTERN, TASK_ID_SAFE_PATTERN, TEAM_TASK_STATUSES, canTransitionTeamTaskStatus, isTerminalTeamTaskStatus, } from './contracts.js';
|
|
14
|
+
let renameForAtomicWrite = rename;
|
|
15
|
+
export function setWriteAtomicRenameForTests(fn) {
|
|
16
|
+
renameForAtomicWrite = fn;
|
|
17
|
+
}
|
|
18
|
+
export function resetWriteAtomicRenameForTests() {
|
|
19
|
+
renameForAtomicWrite = rename;
|
|
20
|
+
}
|
|
8
21
|
export const DEFAULT_MAX_WORKERS = 20;
|
|
9
22
|
export const ABSOLUTE_MAX_WORKERS = 20;
|
|
10
|
-
const DEFAULT_CLAIM_LEASE_MS = 15 * 60 * 1000;
|
|
11
23
|
const LOCK_STALE_MS = 5 * 60 * 1000;
|
|
12
24
|
const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 800;
|
|
13
25
|
const MIN_DISPATCH_ACK_TIMEOUT_MS = 100;
|
|
14
26
|
const MAX_DISPATCH_ACK_TIMEOUT_MS = 10_000;
|
|
15
|
-
const OMX_DISPATCH_LOCK_TIMEOUT_ENV = 'OMX_DISPATCH_LOCK_TIMEOUT_MS';
|
|
16
|
-
const DEFAULT_DISPATCH_LOCK_TIMEOUT_MS = 15_000;
|
|
17
|
-
const MIN_DISPATCH_LOCK_TIMEOUT_MS = 1_000;
|
|
18
|
-
const MAX_DISPATCH_LOCK_TIMEOUT_MS = 120_000;
|
|
19
|
-
const DISPATCH_LOCK_INITIAL_POLL_MS = 25;
|
|
20
|
-
const DISPATCH_LOCK_MAX_POLL_MS = 500;
|
|
21
27
|
function isTerminalTaskStatus(status) {
|
|
22
28
|
return isTerminalTeamTaskStatus(status);
|
|
23
29
|
}
|
|
@@ -41,9 +47,6 @@ function validateTaskId(taskId) {
|
|
|
41
47
|
throw new Error(`Invalid task ID: "${taskId}". Must be a positive integer (digits only, max 20 digits).`);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
|
-
async function writeTaskClaimLockOwnerToken(ownerPath, ownerToken) {
|
|
45
|
-
await writeFile(ownerPath, ownerToken, 'utf8');
|
|
46
|
-
}
|
|
47
50
|
function defaultLeader() {
|
|
48
51
|
return {
|
|
49
52
|
session_id: '',
|
|
@@ -320,12 +323,19 @@ export async function writeAtomic(filePath, data) {
|
|
|
320
323
|
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
321
324
|
await writeFile(tmpPath, data, 'utf8');
|
|
322
325
|
try {
|
|
323
|
-
await
|
|
326
|
+
await renameForAtomicWrite(tmpPath, filePath);
|
|
324
327
|
}
|
|
325
328
|
catch (error) {
|
|
326
329
|
const err = error;
|
|
327
330
|
if (err.code === 'ENOENT' && existsSync(filePath)) {
|
|
328
|
-
|
|
331
|
+
try {
|
|
332
|
+
const existing = await readFile(filePath, 'utf8');
|
|
333
|
+
if (existing === data)
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Preserve original ENOENT below if destination cannot be read.
|
|
338
|
+
}
|
|
329
339
|
}
|
|
330
340
|
throw error;
|
|
331
341
|
}
|
|
@@ -636,15 +646,15 @@ export async function writeWorkerIdentity(teamName, workerName, identity, cwd) {
|
|
|
636
646
|
}
|
|
637
647
|
// Read worker heartbeat (returns null on missing/malformed)
|
|
638
648
|
export async function readWorkerHeartbeat(teamName, workerName, cwd) {
|
|
649
|
+
const p = join(workerDir(teamName, workerName, cwd), 'heartbeat.json');
|
|
639
650
|
try {
|
|
640
|
-
const p = join(workerDir(teamName, workerName, cwd), 'heartbeat.json');
|
|
641
|
-
if (!existsSync(p))
|
|
642
|
-
return null;
|
|
643
651
|
const raw = await readFile(p, 'utf8');
|
|
644
652
|
const parsed = JSON.parse(raw);
|
|
645
653
|
return isWorkerHeartbeat(parsed) ? parsed : null;
|
|
646
654
|
}
|
|
647
|
-
catch {
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (error.code === 'ENOENT')
|
|
657
|
+
return null;
|
|
648
658
|
return null;
|
|
649
659
|
}
|
|
650
660
|
}
|
|
@@ -656,11 +666,8 @@ export async function updateWorkerHeartbeat(teamName, workerName, heartbeat, cwd
|
|
|
656
666
|
// Read worker status (returns {state:'unknown'} on missing/malformed)
|
|
657
667
|
export async function readWorkerStatus(teamName, workerName, cwd) {
|
|
658
668
|
const unknownStatus = { state: 'unknown', updated_at: '1970-01-01T00:00:00.000Z' };
|
|
669
|
+
const p = join(workerDir(teamName, workerName, cwd), 'status.json');
|
|
659
670
|
try {
|
|
660
|
-
const p = join(workerDir(teamName, workerName, cwd), 'status.json');
|
|
661
|
-
if (!existsSync(p)) {
|
|
662
|
-
return unknownStatus;
|
|
663
|
-
}
|
|
664
671
|
const raw = await readFile(p, 'utf8');
|
|
665
672
|
const parsed = JSON.parse(raw);
|
|
666
673
|
if (!isWorkerStatus(parsed)) {
|
|
@@ -668,7 +675,9 @@ export async function readWorkerStatus(teamName, workerName, cwd) {
|
|
|
668
675
|
}
|
|
669
676
|
return parsed;
|
|
670
677
|
}
|
|
671
|
-
catch {
|
|
678
|
+
catch (error) {
|
|
679
|
+
if (error.code === 'ENOENT')
|
|
680
|
+
return unknownStatus;
|
|
672
681
|
return unknownStatus;
|
|
673
682
|
}
|
|
674
683
|
}
|
|
@@ -679,59 +688,7 @@ export async function writeWorkerStatus(teamName, workerName, status, cwd) {
|
|
|
679
688
|
}
|
|
680
689
|
// File-based scaling lock to prevent concurrent scale_up/scale_down operations
|
|
681
690
|
export async function withScalingLock(teamName, cwd, fn) {
|
|
682
|
-
|
|
683
|
-
const ownerPath = join(lockDir, 'owner');
|
|
684
|
-
const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
685
|
-
const deadline = Date.now() + 10_000;
|
|
686
|
-
// Ensure parent directory exists before entering spin loop
|
|
687
|
-
await mkdir(dirname(lockDir), { recursive: true });
|
|
688
|
-
while (true) {
|
|
689
|
-
try {
|
|
690
|
-
await mkdir(lockDir);
|
|
691
|
-
try {
|
|
692
|
-
await writeFile(ownerPath, ownerToken, 'utf8');
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
696
|
-
throw error;
|
|
697
|
-
}
|
|
698
|
-
break;
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
const err = error;
|
|
702
|
-
if (err.code !== 'EEXIST')
|
|
703
|
-
throw error;
|
|
704
|
-
try {
|
|
705
|
-
const info = await stat(lockDir);
|
|
706
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
707
|
-
if (ageMs > LOCK_STALE_MS) {
|
|
708
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch {
|
|
713
|
-
// best effort
|
|
714
|
-
}
|
|
715
|
-
if (Date.now() > deadline) {
|
|
716
|
-
throw new Error(`Timed out acquiring scaling lock for team ${teamName}`);
|
|
717
|
-
}
|
|
718
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
try {
|
|
722
|
-
return await fn();
|
|
723
|
-
}
|
|
724
|
-
finally {
|
|
725
|
-
try {
|
|
726
|
-
const currentOwner = await readFile(ownerPath, 'utf8');
|
|
727
|
-
if (currentOwner.trim() === ownerToken) {
|
|
728
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
// best effort
|
|
733
|
-
}
|
|
734
|
-
}
|
|
691
|
+
return await withScalingLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
|
|
735
692
|
}
|
|
736
693
|
// Write prompt to worker's inbox.md (atomic)
|
|
737
694
|
export async function writeWorkerInbox(teamName, workerName, prompt, cwd) {
|
|
@@ -745,170 +702,13 @@ function taskFilePath(teamName, taskId, cwd) {
|
|
|
745
702
|
return p;
|
|
746
703
|
}
|
|
747
704
|
async function withTeamLock(teamName, cwd, fn) {
|
|
748
|
-
|
|
749
|
-
const ownerPath = join(lockDir, 'owner');
|
|
750
|
-
const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
751
|
-
const deadline = Date.now() + 5000;
|
|
752
|
-
while (true) {
|
|
753
|
-
try {
|
|
754
|
-
await mkdir(lockDir);
|
|
755
|
-
try {
|
|
756
|
-
await writeFile(ownerPath, ownerToken, 'utf8');
|
|
757
|
-
}
|
|
758
|
-
catch (error) {
|
|
759
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
760
|
-
throw error;
|
|
761
|
-
}
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
catch (error) {
|
|
765
|
-
const err = error;
|
|
766
|
-
if (err.code !== 'EEXIST')
|
|
767
|
-
throw error;
|
|
768
|
-
// Best-effort stale lock recovery for crashed processes.
|
|
769
|
-
try {
|
|
770
|
-
const info = await stat(lockDir);
|
|
771
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
772
|
-
if (ageMs > LOCK_STALE_MS) {
|
|
773
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
catch {
|
|
778
|
-
// best effort
|
|
779
|
-
}
|
|
780
|
-
if (Date.now() > deadline) {
|
|
781
|
-
throw new Error(`Timed out acquiring team task lock for ${teamName}`);
|
|
782
|
-
}
|
|
783
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
try {
|
|
787
|
-
return await fn();
|
|
788
|
-
}
|
|
789
|
-
finally {
|
|
790
|
-
try {
|
|
791
|
-
const currentOwner = await readFile(ownerPath, 'utf8');
|
|
792
|
-
if (currentOwner.trim() === ownerToken) {
|
|
793
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
catch {
|
|
797
|
-
// best effort
|
|
798
|
-
}
|
|
799
|
-
}
|
|
705
|
+
return await withTeamLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
|
|
800
706
|
}
|
|
801
707
|
async function withTaskClaimLock(teamName, taskId, cwd, fn) {
|
|
802
|
-
|
|
803
|
-
const ownerPath = join(lockDir, 'owner');
|
|
804
|
-
const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
805
|
-
const staleLockMs = LOCK_STALE_MS;
|
|
806
|
-
const deadline = Date.now() + 5000;
|
|
807
|
-
while (true) {
|
|
808
|
-
try {
|
|
809
|
-
await mkdir(lockDir);
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
const err = error;
|
|
814
|
-
if (err.code !== 'EEXIST')
|
|
815
|
-
throw error;
|
|
816
|
-
// Best-effort stale lock recovery for abandoned claim locks.
|
|
817
|
-
try {
|
|
818
|
-
const info = await stat(lockDir);
|
|
819
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
820
|
-
if (ageMs > staleLockMs) {
|
|
821
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
catch {
|
|
826
|
-
// If stat/remove fails, fall through to conflict.
|
|
827
|
-
}
|
|
828
|
-
if (Date.now() > deadline)
|
|
829
|
-
return { ok: false };
|
|
830
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
try {
|
|
834
|
-
try {
|
|
835
|
-
await writeTaskClaimLockOwnerToken(ownerPath, ownerToken);
|
|
836
|
-
}
|
|
837
|
-
catch (error) {
|
|
838
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
839
|
-
throw error;
|
|
840
|
-
}
|
|
841
|
-
return { ok: true, value: await fn() };
|
|
842
|
-
}
|
|
843
|
-
finally {
|
|
844
|
-
try {
|
|
845
|
-
const currentOwner = await readFile(ownerPath, 'utf8');
|
|
846
|
-
if (currentOwner.trim() === ownerToken) {
|
|
847
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
catch {
|
|
851
|
-
// best effort
|
|
852
|
-
}
|
|
853
|
-
}
|
|
708
|
+
return await withTaskClaimLockImpl(teamName, taskId, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
|
|
854
709
|
}
|
|
855
710
|
async function withMailboxLock(teamName, workerName, cwd, fn) {
|
|
856
|
-
|
|
857
|
-
if (!existsSync(root)) {
|
|
858
|
-
throw new Error(`Team ${teamName} not found`);
|
|
859
|
-
}
|
|
860
|
-
const lockDir = mailboxLockDir(teamName, workerName, cwd);
|
|
861
|
-
const ownerPath = join(lockDir, 'owner');
|
|
862
|
-
const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
863
|
-
const deadline = Date.now() + 5000;
|
|
864
|
-
await mkdir(dirname(lockDir), { recursive: true });
|
|
865
|
-
while (true) {
|
|
866
|
-
try {
|
|
867
|
-
await mkdir(lockDir, { recursive: false });
|
|
868
|
-
try {
|
|
869
|
-
await writeFile(ownerPath, ownerToken, 'utf8');
|
|
870
|
-
}
|
|
871
|
-
catch (error) {
|
|
872
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
873
|
-
throw error;
|
|
874
|
-
}
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
catch (error) {
|
|
878
|
-
const err = error;
|
|
879
|
-
if (err.code !== 'EEXIST')
|
|
880
|
-
throw error;
|
|
881
|
-
try {
|
|
882
|
-
const info = await stat(lockDir);
|
|
883
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
884
|
-
if (ageMs > LOCK_STALE_MS) {
|
|
885
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
catch {
|
|
890
|
-
// best effort
|
|
891
|
-
}
|
|
892
|
-
if (Date.now() > deadline) {
|
|
893
|
-
throw new Error(`Timed out acquiring mailbox lock for ${teamName}/${workerName}`);
|
|
894
|
-
}
|
|
895
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
try {
|
|
899
|
-
return await fn();
|
|
900
|
-
}
|
|
901
|
-
finally {
|
|
902
|
-
try {
|
|
903
|
-
const currentOwner = await readFile(ownerPath, 'utf8');
|
|
904
|
-
if (currentOwner.trim() === ownerToken) {
|
|
905
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
catch {
|
|
909
|
-
// best effort
|
|
910
|
-
}
|
|
911
|
-
}
|
|
711
|
+
return await withMailboxLockImpl(teamName, workerName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
|
|
912
712
|
}
|
|
913
713
|
// Create a task (auto-increment ID)
|
|
914
714
|
export async function createTask(teamName, task, cwd) {
|
|
@@ -979,223 +779,57 @@ export async function updateTask(teamName, taskId, updates, cwd) {
|
|
|
979
779
|
}
|
|
980
780
|
// List all tasks sorted by numeric ID
|
|
981
781
|
export async function listTasks(teamName, cwd) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const matched = files.flatMap((entry) => {
|
|
987
|
-
if (!entry.isFile())
|
|
988
|
-
return [];
|
|
989
|
-
const m = /^task-(\d+)\.json$/.exec(entry.name);
|
|
990
|
-
if (!m)
|
|
991
|
-
return [];
|
|
992
|
-
return [{ id: m[1], fileName: entry.name }];
|
|
782
|
+
return await listTasksImpl(teamName, cwd, {
|
|
783
|
+
teamDir,
|
|
784
|
+
isTeamTask,
|
|
785
|
+
normalizeTask,
|
|
993
786
|
});
|
|
994
|
-
const results = await Promise.all(matched.map(async ({ id, fileName }) => {
|
|
995
|
-
try {
|
|
996
|
-
const raw = await readFile(join(tasksRoot, fileName), 'utf8');
|
|
997
|
-
const parsed = JSON.parse(raw);
|
|
998
|
-
if (!isTeamTask(parsed))
|
|
999
|
-
return null;
|
|
1000
|
-
const normalized = normalizeTask(parsed);
|
|
1001
|
-
// Ignore corrupt task files whose internal id mismatches filename.
|
|
1002
|
-
if (normalized.id !== id)
|
|
1003
|
-
return null;
|
|
1004
|
-
return normalized;
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
}));
|
|
1010
|
-
const tasks = [];
|
|
1011
|
-
for (const task of results) {
|
|
1012
|
-
if (task)
|
|
1013
|
-
tasks.push(task);
|
|
1014
|
-
}
|
|
1015
|
-
tasks.sort((a, b) => Number(a.id) - Number(b.id));
|
|
1016
|
-
return tasks;
|
|
1017
787
|
}
|
|
1018
788
|
export async function computeTaskReadiness(teamName, taskId, cwd) {
|
|
1019
|
-
|
|
1020
|
-
if (!task)
|
|
1021
|
-
return { ready: false, reason: 'blocked_dependency', dependencies: [] };
|
|
1022
|
-
const deps = task.depends_on ?? task.blocked_by ?? [];
|
|
1023
|
-
if (deps.length === 0)
|
|
1024
|
-
return { ready: true };
|
|
1025
|
-
const depTasks = await Promise.all(deps.map((d) => readTask(teamName, d, cwd)));
|
|
1026
|
-
const incomplete = deps.filter((_, idx) => {
|
|
1027
|
-
const t = depTasks[idx];
|
|
1028
|
-
return !t || t.status !== 'completed';
|
|
1029
|
-
});
|
|
1030
|
-
if (incomplete.length > 0)
|
|
1031
|
-
return { ready: false, reason: 'blocked_dependency', dependencies: incomplete };
|
|
1032
|
-
return { ready: true };
|
|
789
|
+
return await computeTaskReadinessImpl(teamName, taskId, cwd, { readTask });
|
|
1033
790
|
}
|
|
1034
791
|
export async function claimTask(teamName, taskId, workerName, expectedVersion, cwd) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const readiness = await computeTaskReadiness(teamName, taskId, cwd);
|
|
1046
|
-
if (!readiness.ready) {
|
|
1047
|
-
return { ok: false, error: 'blocked_dependency', dependencies: readiness.dependencies };
|
|
1048
|
-
}
|
|
1049
|
-
const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
|
|
1050
|
-
const current = await readTask(teamName, taskId, cwd);
|
|
1051
|
-
if (!current)
|
|
1052
|
-
return { ok: false, error: 'task_not_found' };
|
|
1053
|
-
const v = normalizeTask(current);
|
|
1054
|
-
if (expectedVersion !== null && v.version !== expectedVersion) {
|
|
1055
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1056
|
-
}
|
|
1057
|
-
const readinessAfterLock = await computeTaskReadiness(teamName, taskId, cwd);
|
|
1058
|
-
if (!readinessAfterLock.ready) {
|
|
1059
|
-
return {
|
|
1060
|
-
ok: false,
|
|
1061
|
-
error: 'blocked_dependency',
|
|
1062
|
-
dependencies: readinessAfterLock.dependencies,
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
if (isTerminalTaskStatus(v.status)) {
|
|
1066
|
-
return { ok: false, error: 'already_terminal' };
|
|
1067
|
-
}
|
|
1068
|
-
if (v.status === 'in_progress') {
|
|
1069
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1070
|
-
}
|
|
1071
|
-
if (v.status === 'pending' || v.status === 'blocked') {
|
|
1072
|
-
if (v.claim) {
|
|
1073
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1074
|
-
}
|
|
1075
|
-
if (v.owner && v.owner !== workerName) {
|
|
1076
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
const claimToken = randomUUID();
|
|
1080
|
-
const leasedUntil = new Date(Date.now() + DEFAULT_CLAIM_LEASE_MS).toISOString();
|
|
1081
|
-
const updated = {
|
|
1082
|
-
...v,
|
|
1083
|
-
status: 'in_progress',
|
|
1084
|
-
owner: workerName,
|
|
1085
|
-
claim: { owner: workerName, token: claimToken, leased_until: leasedUntil },
|
|
1086
|
-
version: v.version + 1,
|
|
1087
|
-
};
|
|
1088
|
-
await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
|
|
1089
|
-
return { ok: true, task: updated, claimToken };
|
|
792
|
+
return await claimTaskImpl(taskId, workerName, expectedVersion, {
|
|
793
|
+
teamName,
|
|
794
|
+
cwd,
|
|
795
|
+
readTask,
|
|
796
|
+
readTeamConfig,
|
|
797
|
+
withTaskClaimLock,
|
|
798
|
+
normalizeTask,
|
|
799
|
+
isTerminalTaskStatus,
|
|
800
|
+
taskFilePath,
|
|
801
|
+
writeAtomic,
|
|
1090
802
|
});
|
|
1091
|
-
if (!lock.ok)
|
|
1092
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1093
|
-
return lock.value;
|
|
1094
803
|
}
|
|
1095
804
|
export async function transitionTaskStatus(teamName, taskId, from, to, claimToken, cwd) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (v.status !== from)
|
|
1111
|
-
return { ok: false, error: 'invalid_transition' };
|
|
1112
|
-
if (!v.owner || !v.claim || v.claim.owner !== v.owner || v.claim.token !== claimToken) {
|
|
1113
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1114
|
-
}
|
|
1115
|
-
if (new Date(v.claim.leased_until) <= new Date())
|
|
1116
|
-
return { ok: false, error: 'lease_expired' };
|
|
1117
|
-
const completedAt = new Date().toISOString();
|
|
1118
|
-
const updated = {
|
|
1119
|
-
...v,
|
|
1120
|
-
status: to,
|
|
1121
|
-
completed_at: completedAt,
|
|
1122
|
-
claim: undefined,
|
|
1123
|
-
version: v.version + 1,
|
|
1124
|
-
};
|
|
1125
|
-
await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
|
|
1126
|
-
if (to === 'completed') {
|
|
1127
|
-
await appendTeamEvent(teamName, {
|
|
1128
|
-
type: 'task_completed',
|
|
1129
|
-
worker: updated.owner || 'unknown',
|
|
1130
|
-
task_id: updated.id,
|
|
1131
|
-
message_id: null,
|
|
1132
|
-
reason: undefined,
|
|
1133
|
-
}, cwd);
|
|
1134
|
-
}
|
|
1135
|
-
else if (to === 'failed') {
|
|
1136
|
-
await appendTeamEvent(teamName, {
|
|
1137
|
-
type: 'task_failed',
|
|
1138
|
-
worker: updated.owner || 'unknown',
|
|
1139
|
-
task_id: updated.id,
|
|
1140
|
-
message_id: null,
|
|
1141
|
-
reason: updated.error || 'task_failed',
|
|
1142
|
-
}, cwd);
|
|
1143
|
-
}
|
|
1144
|
-
return { ok: true, task: updated };
|
|
805
|
+
return await transitionTaskStatusImpl(taskId, from, to, claimToken, {
|
|
806
|
+
teamName,
|
|
807
|
+
cwd,
|
|
808
|
+
readTask,
|
|
809
|
+
readTeamConfig,
|
|
810
|
+
withTaskClaimLock,
|
|
811
|
+
normalizeTask,
|
|
812
|
+
isTerminalTaskStatus,
|
|
813
|
+
canTransitionTaskStatus,
|
|
814
|
+
taskFilePath,
|
|
815
|
+
writeAtomic,
|
|
816
|
+
appendTeamEvent,
|
|
817
|
+
readMonitorSnapshot,
|
|
818
|
+
writeMonitorSnapshot,
|
|
1145
819
|
});
|
|
1146
|
-
if (!lock.ok)
|
|
1147
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1148
|
-
// If a task_completed event was emitted via this claim-safe path, record the task ID in
|
|
1149
|
-
// the monitor snapshot so that emitMonitorDerivedEvents does not emit a duplicate event
|
|
1150
|
-
// on the next monitorTeam poll (issue #161).
|
|
1151
|
-
if (to === 'completed') {
|
|
1152
|
-
const existingSnap = await readMonitorSnapshot(teamName, cwd);
|
|
1153
|
-
const updatedSnap = existingSnap
|
|
1154
|
-
? { ...existingSnap, completedEventTaskIds: { ...(existingSnap.completedEventTaskIds ?? {}), [taskId]: true } }
|
|
1155
|
-
: {
|
|
1156
|
-
taskStatusById: {},
|
|
1157
|
-
workerAliveByName: {},
|
|
1158
|
-
workerStateByName: {},
|
|
1159
|
-
workerTurnCountByName: {},
|
|
1160
|
-
workerTaskIdByName: {},
|
|
1161
|
-
mailboxNotifiedByMessageId: {},
|
|
1162
|
-
completedEventTaskIds: { [taskId]: true },
|
|
1163
|
-
};
|
|
1164
|
-
await writeMonitorSnapshot(teamName, updatedSnap, cwd);
|
|
1165
|
-
}
|
|
1166
|
-
return lock.value;
|
|
1167
820
|
}
|
|
1168
821
|
export async function releaseTaskClaim(teamName, taskId, claimToken, workerName, cwd) {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
const leaseActive = Boolean(v.claim && new Date(v.claim.leased_until) > new Date());
|
|
1181
|
-
const tokenMatches = Boolean(v.claim && v.claim.token === claimToken && leaseActive);
|
|
1182
|
-
const ownerMatches = v.status === 'in_progress' && v.owner === workerName;
|
|
1183
|
-
if (!tokenMatches && !ownerMatches) {
|
|
1184
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1185
|
-
}
|
|
1186
|
-
const updated = {
|
|
1187
|
-
...v,
|
|
1188
|
-
status: 'pending',
|
|
1189
|
-
owner: undefined,
|
|
1190
|
-
claim: undefined,
|
|
1191
|
-
version: v.version + 1,
|
|
1192
|
-
};
|
|
1193
|
-
await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
|
|
1194
|
-
return { ok: true, task: updated };
|
|
822
|
+
return await releaseTaskClaimImpl(taskId, claimToken, workerName, {
|
|
823
|
+
teamName,
|
|
824
|
+
cwd,
|
|
825
|
+
readTask,
|
|
826
|
+
readTeamConfig,
|
|
827
|
+
withTaskClaimLock,
|
|
828
|
+
normalizeTask,
|
|
829
|
+
isTerminalTaskStatus,
|
|
830
|
+
taskFilePath,
|
|
831
|
+
writeAtomic,
|
|
1195
832
|
});
|
|
1196
|
-
if (!lock.ok)
|
|
1197
|
-
return { ok: false, error: 'claim_conflict' };
|
|
1198
|
-
return lock.value;
|
|
1199
833
|
}
|
|
1200
834
|
export async function appendTeamEvent(teamName, event, cwd) {
|
|
1201
835
|
const full = {
|
|
@@ -1231,44 +865,6 @@ async function writeMailbox(teamName, mailbox, cwd) {
|
|
|
1231
865
|
const p = mailboxPath(teamName, mailbox.worker, cwd);
|
|
1232
866
|
await writeAtomic(p, JSON.stringify(mailbox, null, 2));
|
|
1233
867
|
}
|
|
1234
|
-
function isDispatchKind(value) {
|
|
1235
|
-
return value === 'inbox' || value === 'mailbox' || value === 'nudge';
|
|
1236
|
-
}
|
|
1237
|
-
function isDispatchStatus(value) {
|
|
1238
|
-
return value === 'pending' || value === 'notified' || value === 'delivered' || value === 'failed';
|
|
1239
|
-
}
|
|
1240
|
-
function normalizeDispatchRequest(teamName, raw, nowIso = new Date().toISOString()) {
|
|
1241
|
-
if (!isDispatchKind(raw.kind))
|
|
1242
|
-
return null;
|
|
1243
|
-
if (typeof raw.to_worker !== 'string' || raw.to_worker.trim() === '')
|
|
1244
|
-
return null;
|
|
1245
|
-
if (typeof raw.trigger_message !== 'string' || raw.trigger_message.trim() === '')
|
|
1246
|
-
return null;
|
|
1247
|
-
const status = isDispatchStatus(raw.status) ? raw.status : 'pending';
|
|
1248
|
-
return {
|
|
1249
|
-
request_id: typeof raw.request_id === 'string' && raw.request_id.trim() !== '' ? raw.request_id : randomUUID(),
|
|
1250
|
-
kind: raw.kind,
|
|
1251
|
-
team_name: teamName,
|
|
1252
|
-
to_worker: raw.to_worker,
|
|
1253
|
-
worker_index: typeof raw.worker_index === 'number' ? raw.worker_index : undefined,
|
|
1254
|
-
pane_id: typeof raw.pane_id === 'string' && raw.pane_id !== '' ? raw.pane_id : undefined,
|
|
1255
|
-
trigger_message: raw.trigger_message,
|
|
1256
|
-
message_id: typeof raw.message_id === 'string' && raw.message_id !== '' ? raw.message_id : undefined,
|
|
1257
|
-
inbox_correlation_key: typeof raw.inbox_correlation_key === 'string' && raw.inbox_correlation_key !== '' ? raw.inbox_correlation_key : undefined,
|
|
1258
|
-
transport_preference: raw.transport_preference === 'transport_direct' || raw.transport_preference === 'prompt_stdin'
|
|
1259
|
-
? raw.transport_preference
|
|
1260
|
-
: 'hook_preferred_with_fallback',
|
|
1261
|
-
fallback_allowed: raw.fallback_allowed !== false,
|
|
1262
|
-
status,
|
|
1263
|
-
attempt_count: Number.isFinite(raw.attempt_count) ? Math.max(0, Math.floor(raw.attempt_count)) : 0,
|
|
1264
|
-
created_at: typeof raw.created_at === 'string' && raw.created_at !== '' ? raw.created_at : nowIso,
|
|
1265
|
-
updated_at: typeof raw.updated_at === 'string' && raw.updated_at !== '' ? raw.updated_at : nowIso,
|
|
1266
|
-
notified_at: typeof raw.notified_at === 'string' && raw.notified_at !== '' ? raw.notified_at : undefined,
|
|
1267
|
-
delivered_at: typeof raw.delivered_at === 'string' && raw.delivered_at !== '' ? raw.delivered_at : undefined,
|
|
1268
|
-
failed_at: typeof raw.failed_at === 'string' && raw.failed_at !== '' ? raw.failed_at : undefined,
|
|
1269
|
-
last_reason: typeof raw.last_reason === 'string' && raw.last_reason !== '' ? raw.last_reason : undefined,
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
868
|
async function readDispatchRequests(teamName, cwd) {
|
|
1273
869
|
const path = dispatchRequestsPath(teamName, cwd);
|
|
1274
870
|
try {
|
|
@@ -1280,7 +876,7 @@ async function readDispatchRequests(teamName, cwd) {
|
|
|
1280
876
|
return [];
|
|
1281
877
|
const nowIso = new Date().toISOString();
|
|
1282
878
|
return parsed
|
|
1283
|
-
.map((entry) =>
|
|
879
|
+
.map((entry) => normalizeDispatchRequestImpl(teamName, (entry ?? {}), nowIso))
|
|
1284
880
|
.filter((entry) => entry !== null);
|
|
1285
881
|
}
|
|
1286
882
|
catch {
|
|
@@ -1291,383 +887,158 @@ async function writeDispatchRequests(teamName, requests, cwd) {
|
|
|
1291
887
|
await writeAtomic(dispatchRequestsPath(teamName, cwd), JSON.stringify(requests, null, 2));
|
|
1292
888
|
}
|
|
1293
889
|
export function resolveDispatchLockTimeoutMs(env = process.env) {
|
|
1294
|
-
|
|
1295
|
-
if (raw === undefined || raw === '')
|
|
1296
|
-
return DEFAULT_DISPATCH_LOCK_TIMEOUT_MS;
|
|
1297
|
-
const parsed = Number(raw);
|
|
1298
|
-
if (!Number.isFinite(parsed))
|
|
1299
|
-
return DEFAULT_DISPATCH_LOCK_TIMEOUT_MS;
|
|
1300
|
-
return Math.max(MIN_DISPATCH_LOCK_TIMEOUT_MS, Math.min(MAX_DISPATCH_LOCK_TIMEOUT_MS, Math.floor(parsed)));
|
|
890
|
+
return resolveDispatchLockTimeoutMsImpl(env);
|
|
1301
891
|
}
|
|
1302
892
|
async function withDispatchLock(teamName, cwd, fn) {
|
|
1303
|
-
|
|
1304
|
-
if (!existsSync(root))
|
|
1305
|
-
throw new Error(`Team ${teamName} not found`);
|
|
1306
|
-
const lockDir = dispatchLockDir(teamName, cwd);
|
|
1307
|
-
const ownerPath = join(lockDir, 'owner');
|
|
1308
|
-
const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
1309
|
-
const timeoutMs = resolveDispatchLockTimeoutMs(process.env);
|
|
1310
|
-
const deadline = Date.now() + timeoutMs;
|
|
1311
|
-
let pollMs = DISPATCH_LOCK_INITIAL_POLL_MS;
|
|
1312
|
-
await mkdir(dirname(lockDir), { recursive: true });
|
|
1313
|
-
while (true) {
|
|
1314
|
-
try {
|
|
1315
|
-
await mkdir(lockDir, { recursive: false });
|
|
1316
|
-
try {
|
|
1317
|
-
await writeFile(ownerPath, ownerToken, 'utf8');
|
|
1318
|
-
}
|
|
1319
|
-
catch (error) {
|
|
1320
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
1321
|
-
throw error;
|
|
1322
|
-
}
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
|
-
catch (error) {
|
|
1326
|
-
const err = error;
|
|
1327
|
-
if (err.code !== 'EEXIST')
|
|
1328
|
-
throw error;
|
|
1329
|
-
try {
|
|
1330
|
-
const info = await stat(lockDir);
|
|
1331
|
-
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
1332
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
catch {
|
|
1337
|
-
// best effort
|
|
1338
|
-
}
|
|
1339
|
-
if (Date.now() > deadline) {
|
|
1340
|
-
throw new Error(`Timed out acquiring dispatch lock for ${teamName} after ${timeoutMs}ms. ` +
|
|
1341
|
-
`Set ${OMX_DISPATCH_LOCK_TIMEOUT_ENV} to increase (current: ${timeoutMs}ms, max: ${MAX_DISPATCH_LOCK_TIMEOUT_MS}ms).`);
|
|
1342
|
-
}
|
|
1343
|
-
// Exponential backoff with jitter to reduce thundering herd
|
|
1344
|
-
const jitter = 0.5 + Math.random() * 0.5;
|
|
1345
|
-
await new Promise((resolve) => setTimeout(resolve, Math.floor(pollMs * jitter)));
|
|
1346
|
-
pollMs = Math.min(pollMs * 2, DISPATCH_LOCK_MAX_POLL_MS);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
try {
|
|
1350
|
-
return await fn();
|
|
1351
|
-
}
|
|
1352
|
-
finally {
|
|
1353
|
-
try {
|
|
1354
|
-
const currentOwner = await readFile(ownerPath, 'utf8');
|
|
1355
|
-
if (currentOwner.trim() === ownerToken) {
|
|
1356
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
catch {
|
|
1360
|
-
// best effort
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
function equivalentPendingDispatch(existing, input) {
|
|
1365
|
-
if (existing.status !== 'pending')
|
|
1366
|
-
return false;
|
|
1367
|
-
if (existing.kind !== input.kind)
|
|
1368
|
-
return false;
|
|
1369
|
-
if (existing.to_worker !== input.to_worker)
|
|
1370
|
-
return false;
|
|
1371
|
-
if (input.kind === 'mailbox') {
|
|
1372
|
-
return Boolean(input.message_id) && existing.message_id === input.message_id;
|
|
1373
|
-
}
|
|
1374
|
-
if (input.kind === 'inbox' && input.inbox_correlation_key) {
|
|
1375
|
-
return existing.inbox_correlation_key === input.inbox_correlation_key;
|
|
1376
|
-
}
|
|
1377
|
-
return existing.trigger_message === input.trigger_message;
|
|
893
|
+
return await withDispatchLockImpl(teamName, cwd, teamDir, dispatchLockDir, fn);
|
|
1378
894
|
}
|
|
1379
895
|
export async function enqueueDispatchRequest(teamName, requestInput, cwd) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const requests = await readDispatchRequests(teamName, cwd);
|
|
1388
|
-
const existing = requests.find((req) => equivalentPendingDispatch(req, requestInput));
|
|
1389
|
-
if (existing)
|
|
1390
|
-
return { request: existing, deduped: true };
|
|
1391
|
-
const nowIso = new Date().toISOString();
|
|
1392
|
-
const request = normalizeDispatchRequest(teamName, {
|
|
1393
|
-
request_id: randomUUID(),
|
|
1394
|
-
...requestInput,
|
|
1395
|
-
status: 'pending',
|
|
1396
|
-
attempt_count: 0,
|
|
1397
|
-
created_at: nowIso,
|
|
1398
|
-
updated_at: nowIso,
|
|
1399
|
-
}, nowIso);
|
|
1400
|
-
if (!request)
|
|
1401
|
-
throw new Error('failed_to_normalize_dispatch_request');
|
|
1402
|
-
requests.push(request);
|
|
1403
|
-
await writeDispatchRequests(teamName, requests, cwd);
|
|
1404
|
-
return { request, deduped: false };
|
|
896
|
+
return await enqueueDispatchRequestImpl(requestInput, {
|
|
897
|
+
teamName,
|
|
898
|
+
cwd,
|
|
899
|
+
validateWorkerName,
|
|
900
|
+
withDispatchLock,
|
|
901
|
+
readDispatchRequests,
|
|
902
|
+
writeDispatchRequests,
|
|
1405
903
|
});
|
|
1406
904
|
}
|
|
1407
905
|
export async function listDispatchRequests(teamName, cwd, opts = {}) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
if (typeof opts.limit === 'number' && opts.limit > 0)
|
|
1417
|
-
filtered = filtered.slice(0, opts.limit);
|
|
1418
|
-
return filtered;
|
|
906
|
+
return await listDispatchRequestsImpl(opts, {
|
|
907
|
+
teamName,
|
|
908
|
+
cwd,
|
|
909
|
+
validateWorkerName,
|
|
910
|
+
withDispatchLock,
|
|
911
|
+
readDispatchRequests,
|
|
912
|
+
writeDispatchRequests,
|
|
913
|
+
});
|
|
1419
914
|
}
|
|
1420
915
|
export async function readDispatchRequest(teamName, requestId, cwd) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (from === 'notified' && (to === 'delivered' || to === 'failed'))
|
|
1430
|
-
return true;
|
|
1431
|
-
return false;
|
|
916
|
+
return await readDispatchRequestImpl(requestId, {
|
|
917
|
+
teamName,
|
|
918
|
+
cwd,
|
|
919
|
+
validateWorkerName,
|
|
920
|
+
withDispatchLock,
|
|
921
|
+
readDispatchRequests,
|
|
922
|
+
writeDispatchRequests,
|
|
923
|
+
});
|
|
1432
924
|
}
|
|
1433
925
|
export async function transitionDispatchRequest(teamName, requestId, from, to, patch = {}, cwd) {
|
|
1434
|
-
return await
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
return null;
|
|
1442
|
-
if (!canTransitionDispatchStatus(existing.status, to))
|
|
1443
|
-
return null;
|
|
1444
|
-
const nowIso = new Date().toISOString();
|
|
1445
|
-
const nextAttemptCount = Math.max(existing.attempt_count, Number.isFinite(patch.attempt_count)
|
|
1446
|
-
? Math.floor(patch.attempt_count)
|
|
1447
|
-
: (existing.status === to ? existing.attempt_count : existing.attempt_count + 1));
|
|
1448
|
-
const next = {
|
|
1449
|
-
...existing,
|
|
1450
|
-
...patch,
|
|
1451
|
-
status: to,
|
|
1452
|
-
attempt_count: Math.max(0, nextAttemptCount),
|
|
1453
|
-
updated_at: nowIso,
|
|
1454
|
-
};
|
|
1455
|
-
if (to === 'notified')
|
|
1456
|
-
next.notified_at = patch.notified_at ?? nowIso;
|
|
1457
|
-
if (to === 'delivered')
|
|
1458
|
-
next.delivered_at = patch.delivered_at ?? nowIso;
|
|
1459
|
-
if (to === 'failed')
|
|
1460
|
-
next.failed_at = patch.failed_at ?? nowIso;
|
|
1461
|
-
requests[index] = next;
|
|
1462
|
-
await writeDispatchRequests(teamName, requests, cwd);
|
|
1463
|
-
return next;
|
|
926
|
+
return await transitionDispatchRequestImpl(requestId, from, to, patch, {
|
|
927
|
+
teamName,
|
|
928
|
+
cwd,
|
|
929
|
+
validateWorkerName,
|
|
930
|
+
withDispatchLock,
|
|
931
|
+
readDispatchRequests,
|
|
932
|
+
writeDispatchRequests,
|
|
1464
933
|
});
|
|
1465
934
|
}
|
|
1466
935
|
export async function markDispatchRequestNotified(teamName, requestId, patch = {}, cwd) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
936
|
+
return await markDispatchRequestNotifiedImpl(requestId, patch, {
|
|
937
|
+
teamName,
|
|
938
|
+
cwd,
|
|
939
|
+
validateWorkerName,
|
|
940
|
+
withDispatchLock,
|
|
941
|
+
readDispatchRequests,
|
|
942
|
+
writeDispatchRequests,
|
|
943
|
+
});
|
|
1473
944
|
}
|
|
1474
945
|
export async function markDispatchRequestDelivered(teamName, requestId, patch = {}, cwd) {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
946
|
+
return await markDispatchRequestDeliveredImpl(requestId, patch, {
|
|
947
|
+
teamName,
|
|
948
|
+
cwd,
|
|
949
|
+
validateWorkerName,
|
|
950
|
+
withDispatchLock,
|
|
951
|
+
readDispatchRequests,
|
|
952
|
+
writeDispatchRequests,
|
|
953
|
+
});
|
|
1481
954
|
}
|
|
1482
955
|
export async function sendDirectMessage(teamName, fromWorker, toWorker, body, cwd) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const mailbox = await readMailbox(teamName, toWorker, cwd);
|
|
1492
|
-
mailbox.messages.push(msg);
|
|
1493
|
-
await writeMailbox(teamName, mailbox, cwd);
|
|
956
|
+
return await sendDirectMessageImpl(fromWorker, toWorker, body, {
|
|
957
|
+
teamName,
|
|
958
|
+
cwd,
|
|
959
|
+
withMailboxLock,
|
|
960
|
+
readMailbox,
|
|
961
|
+
writeMailbox,
|
|
962
|
+
appendTeamEvent,
|
|
963
|
+
readTeamConfig,
|
|
1494
964
|
});
|
|
1495
|
-
await appendTeamEvent(teamName, { type: 'message_received', worker: toWorker, task_id: undefined, message_id: msg.message_id, reason: undefined }, cwd);
|
|
1496
|
-
return msg;
|
|
1497
965
|
}
|
|
1498
966
|
export async function broadcastMessage(teamName, fromWorker, body, cwd) {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
return delivered;
|
|
967
|
+
return await broadcastMessageImpl(fromWorker, body, {
|
|
968
|
+
teamName,
|
|
969
|
+
cwd,
|
|
970
|
+
withMailboxLock,
|
|
971
|
+
readMailbox,
|
|
972
|
+
writeMailbox,
|
|
973
|
+
appendTeamEvent,
|
|
974
|
+
readTeamConfig,
|
|
975
|
+
});
|
|
1510
976
|
}
|
|
1511
977
|
export async function markMessageDelivered(teamName, workerName, messageId, cwd) {
|
|
1512
|
-
return
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
return true;
|
|
978
|
+
return await markMessageDeliveredImpl(workerName, messageId, {
|
|
979
|
+
teamName,
|
|
980
|
+
cwd,
|
|
981
|
+
withMailboxLock,
|
|
982
|
+
readMailbox,
|
|
983
|
+
writeMailbox,
|
|
984
|
+
appendTeamEvent,
|
|
985
|
+
readTeamConfig,
|
|
1522
986
|
});
|
|
1523
987
|
}
|
|
1524
988
|
export async function markMessageNotified(teamName, workerName, messageId, cwd) {
|
|
1525
|
-
return
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
989
|
+
return await markMessageNotifiedImpl(workerName, messageId, {
|
|
990
|
+
teamName,
|
|
991
|
+
cwd,
|
|
992
|
+
withMailboxLock,
|
|
993
|
+
readMailbox,
|
|
994
|
+
writeMailbox,
|
|
995
|
+
appendTeamEvent,
|
|
996
|
+
readTeamConfig,
|
|
1533
997
|
});
|
|
1534
998
|
}
|
|
1535
999
|
export async function listMailboxMessages(teamName, workerName, cwd) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1000
|
+
return await listMailboxMessagesImpl(workerName, {
|
|
1001
|
+
teamName,
|
|
1002
|
+
cwd,
|
|
1003
|
+
withMailboxLock,
|
|
1004
|
+
readMailbox,
|
|
1005
|
+
writeMailbox,
|
|
1006
|
+
appendTeamEvent,
|
|
1007
|
+
readTeamConfig,
|
|
1008
|
+
});
|
|
1538
1009
|
}
|
|
1539
1010
|
export async function writeTaskApproval(teamName, approval, cwd) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
reason: `${approval.status}:${approval.decision_reason}`,
|
|
1548
|
-
}, cwd);
|
|
1011
|
+
await writeTaskApprovalImpl(approval, {
|
|
1012
|
+
teamName,
|
|
1013
|
+
cwd,
|
|
1014
|
+
approvalPath,
|
|
1015
|
+
writeAtomic,
|
|
1016
|
+
appendTeamEvent,
|
|
1017
|
+
});
|
|
1549
1018
|
}
|
|
1550
1019
|
export async function readTaskApproval(teamName, taskId, cwd) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
return null;
|
|
1559
|
-
if (!['pending', 'approved', 'rejected'].includes(parsed.status))
|
|
1560
|
-
return null;
|
|
1561
|
-
return parsed;
|
|
1562
|
-
}
|
|
1563
|
-
catch {
|
|
1564
|
-
return null;
|
|
1565
|
-
}
|
|
1020
|
+
return await readTaskApprovalImpl(taskId, {
|
|
1021
|
+
teamName,
|
|
1022
|
+
cwd,
|
|
1023
|
+
approvalPath,
|
|
1024
|
+
writeAtomic,
|
|
1025
|
+
appendTeamEvent,
|
|
1026
|
+
});
|
|
1566
1027
|
}
|
|
1567
1028
|
// Get team summary with aggregation and non-reporting worker detection
|
|
1568
1029
|
export async function getTeamSummary(teamName, cwd) {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
blocked: 0,
|
|
1582
|
-
in_progress: 0,
|
|
1583
|
-
completed: 0,
|
|
1584
|
-
failed: 0,
|
|
1585
|
-
};
|
|
1586
|
-
for (const t of tasks) {
|
|
1587
|
-
if (t.status === 'pending')
|
|
1588
|
-
counts.pending++;
|
|
1589
|
-
else if (t.status === 'blocked')
|
|
1590
|
-
counts.blocked++;
|
|
1591
|
-
else if (t.status === 'in_progress')
|
|
1592
|
-
counts.in_progress++;
|
|
1593
|
-
else if (t.status === 'completed')
|
|
1594
|
-
counts.completed++;
|
|
1595
|
-
else if (t.status === 'failed')
|
|
1596
|
-
counts.failed++;
|
|
1597
|
-
}
|
|
1598
|
-
const workers = cfg.workers || [];
|
|
1599
|
-
const workerSummaries = [];
|
|
1600
|
-
const nonReportingWorkers = [];
|
|
1601
|
-
const nextSnapshot = {
|
|
1602
|
-
workerTurnCountByName: {},
|
|
1603
|
-
workerTaskByName: {},
|
|
1604
|
-
};
|
|
1605
|
-
const workerPollStartMs = performance.now();
|
|
1606
|
-
const workerSignals = await Promise.all(workers.map(async (worker) => {
|
|
1607
|
-
const [hb, status] = await Promise.all([
|
|
1608
|
-
readWorkerHeartbeat(teamName, worker.name, cwd),
|
|
1609
|
-
readWorkerStatus(teamName, worker.name, cwd),
|
|
1610
|
-
]);
|
|
1611
|
-
return { worker, hb, status };
|
|
1612
|
-
}));
|
|
1613
|
-
const workersPolledMs = performance.now() - workerPollStartMs;
|
|
1614
|
-
for (const { worker: w, hb, status } of workerSignals) {
|
|
1615
|
-
const alive = hb?.alive ?? false;
|
|
1616
|
-
const lastTurnAt = hb?.last_turn_at ?? null;
|
|
1617
|
-
const currentTaskId = status.current_task_id ?? '';
|
|
1618
|
-
const prevTaskId = previousSnapshot?.workerTaskByName[w.name] ?? '';
|
|
1619
|
-
const prevTurnCount = previousSnapshot?.workerTurnCountByName[w.name] ?? 0;
|
|
1620
|
-
const currentTask = currentTaskId ? taskById.get(currentTaskId) ?? null : null;
|
|
1621
|
-
const turnsWithoutProgress = hb &&
|
|
1622
|
-
status.state === 'working' &&
|
|
1623
|
-
currentTask &&
|
|
1624
|
-
(currentTask.status === 'pending' || currentTask.status === 'in_progress') &&
|
|
1625
|
-
currentTaskId === prevTaskId
|
|
1626
|
-
? Math.max(0, hb.turn_count - prevTurnCount)
|
|
1627
|
-
: 0;
|
|
1628
|
-
if (alive && status.state === 'working' && turnsWithoutProgress > 5) {
|
|
1629
|
-
nonReportingWorkers.push(w.name);
|
|
1630
|
-
}
|
|
1631
|
-
workerSummaries.push({ name: w.name, alive, lastTurnAt, turnsWithoutProgress });
|
|
1632
|
-
nextSnapshot.workerTurnCountByName[w.name] = hb?.turn_count ?? 0;
|
|
1633
|
-
nextSnapshot.workerTaskByName[w.name] = currentTaskId;
|
|
1634
|
-
}
|
|
1635
|
-
await writeSummarySnapshot(teamName, nextSnapshot, cwd);
|
|
1636
|
-
return {
|
|
1637
|
-
teamName: cfg.name,
|
|
1638
|
-
workerCount: cfg.worker_count,
|
|
1639
|
-
tasks: counts,
|
|
1640
|
-
workers: workerSummaries,
|
|
1641
|
-
nonReportingWorkers,
|
|
1642
|
-
performance: {
|
|
1643
|
-
total_ms: Number((performance.now() - summaryStartMs).toFixed(2)),
|
|
1644
|
-
tasks_loaded_ms: Number(tasksLoadedMs.toFixed(2)),
|
|
1645
|
-
workers_polled_ms: Number(workersPolledMs.toFixed(2)),
|
|
1646
|
-
task_count: tasks.length,
|
|
1647
|
-
worker_count: workers.length,
|
|
1648
|
-
},
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
async function readSummarySnapshot(teamName, cwd) {
|
|
1652
|
-
const p = summarySnapshotPath(teamName, cwd);
|
|
1653
|
-
if (!existsSync(p))
|
|
1654
|
-
return null;
|
|
1655
|
-
try {
|
|
1656
|
-
const raw = await readFile(p, 'utf8');
|
|
1657
|
-
const parsed = JSON.parse(raw);
|
|
1658
|
-
if (!parsed || typeof parsed !== 'object')
|
|
1659
|
-
return null;
|
|
1660
|
-
return {
|
|
1661
|
-
workerTurnCountByName: parsed.workerTurnCountByName ?? {},
|
|
1662
|
-
workerTaskByName: parsed.workerTaskByName ?? {},
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
catch {
|
|
1666
|
-
return null;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
async function writeSummarySnapshot(teamName, snapshot, cwd) {
|
|
1670
|
-
await writeAtomic(summarySnapshotPath(teamName, cwd), JSON.stringify(snapshot, null, 2));
|
|
1030
|
+
return await getTeamSummaryImpl({
|
|
1031
|
+
teamName,
|
|
1032
|
+
cwd,
|
|
1033
|
+
readTeamConfig,
|
|
1034
|
+
listTasks,
|
|
1035
|
+
readWorkerHeartbeat,
|
|
1036
|
+
readWorkerStatus,
|
|
1037
|
+
summarySnapshotPath,
|
|
1038
|
+
monitorSnapshotPath,
|
|
1039
|
+
teamPhasePath,
|
|
1040
|
+
writeAtomic,
|
|
1041
|
+
});
|
|
1671
1042
|
}
|
|
1672
1043
|
export async function writeShutdownRequest(teamName, workerName, requestedBy, cwd) {
|
|
1673
1044
|
const p = join(workerDir(teamName, workerName, cwd), 'shutdown-request.json');
|
|
@@ -1675,8 +1046,6 @@ export async function writeShutdownRequest(teamName, workerName, requestedBy, cw
|
|
|
1675
1046
|
}
|
|
1676
1047
|
export async function readShutdownAck(teamName, workerName, cwd, minUpdatedAt) {
|
|
1677
1048
|
const ackPath = join(workerDir(teamName, workerName, cwd), 'shutdown-ack.json');
|
|
1678
|
-
if (!existsSync(ackPath))
|
|
1679
|
-
return null;
|
|
1680
1049
|
try {
|
|
1681
1050
|
const raw = await readFile(ackPath, 'utf-8');
|
|
1682
1051
|
const parsed = JSON.parse(raw);
|
|
@@ -1690,7 +1059,9 @@ export async function readShutdownAck(teamName, workerName, cwd, minUpdatedAt) {
|
|
|
1690
1059
|
}
|
|
1691
1060
|
return parsed;
|
|
1692
1061
|
}
|
|
1693
|
-
catch {
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
if (error.code === 'ENOENT')
|
|
1064
|
+
return null;
|
|
1694
1065
|
return null;
|
|
1695
1066
|
}
|
|
1696
1067
|
}
|
|
@@ -1701,69 +1072,17 @@ function monitorSnapshotPath(teamName, cwd) {
|
|
|
1701
1072
|
return join(teamDir(teamName, cwd), 'monitor-snapshot.json');
|
|
1702
1073
|
}
|
|
1703
1074
|
export async function readMonitorSnapshot(teamName, cwd) {
|
|
1704
|
-
|
|
1705
|
-
if (!existsSync(p))
|
|
1706
|
-
return null;
|
|
1707
|
-
try {
|
|
1708
|
-
const raw = await readFile(p, 'utf-8');
|
|
1709
|
-
const parsed = JSON.parse(raw);
|
|
1710
|
-
if (!parsed || typeof parsed !== 'object')
|
|
1711
|
-
return null;
|
|
1712
|
-
const monitorTimings = (() => {
|
|
1713
|
-
const candidate = parsed.monitorTimings;
|
|
1714
|
-
if (!candidate || typeof candidate !== 'object')
|
|
1715
|
-
return undefined;
|
|
1716
|
-
if (typeof candidate.list_tasks_ms !== 'number' ||
|
|
1717
|
-
typeof candidate.worker_scan_ms !== 'number' ||
|
|
1718
|
-
typeof candidate.mailbox_delivery_ms !== 'number' ||
|
|
1719
|
-
typeof candidate.total_ms !== 'number' ||
|
|
1720
|
-
typeof candidate.updated_at !== 'string') {
|
|
1721
|
-
return undefined;
|
|
1722
|
-
}
|
|
1723
|
-
return candidate;
|
|
1724
|
-
})();
|
|
1725
|
-
return {
|
|
1726
|
-
taskStatusById: parsed.taskStatusById ?? {},
|
|
1727
|
-
workerAliveByName: parsed.workerAliveByName ?? {},
|
|
1728
|
-
workerStateByName: parsed.workerStateByName ?? {},
|
|
1729
|
-
workerTurnCountByName: parsed.workerTurnCountByName ?? {},
|
|
1730
|
-
workerTaskIdByName: parsed.workerTaskIdByName ?? {},
|
|
1731
|
-
mailboxNotifiedByMessageId: parsed.mailboxNotifiedByMessageId ?? {},
|
|
1732
|
-
completedEventTaskIds: parsed.completedEventTaskIds ?? {},
|
|
1733
|
-
monitorTimings,
|
|
1734
|
-
};
|
|
1735
|
-
}
|
|
1736
|
-
catch {
|
|
1737
|
-
return null;
|
|
1738
|
-
}
|
|
1075
|
+
return await readMonitorSnapshotImpl(teamName, cwd, monitorSnapshotPath);
|
|
1739
1076
|
}
|
|
1740
1077
|
export async function writeMonitorSnapshot(teamName, snapshot, cwd) {
|
|
1741
|
-
await
|
|
1078
|
+
await writeMonitorSnapshotImpl(teamName, snapshot, cwd, monitorSnapshotPath, writeAtomic);
|
|
1742
1079
|
}
|
|
1743
1080
|
export async function readTeamPhase(teamName, cwd) {
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
return null;
|
|
1747
|
-
try {
|
|
1748
|
-
const raw = await readFile(p, 'utf-8');
|
|
1749
|
-
const parsed = JSON.parse(raw);
|
|
1750
|
-
if (!parsed || typeof parsed !== 'object')
|
|
1751
|
-
return null;
|
|
1752
|
-
const currentPhase = typeof parsed.current_phase === 'string' ? parsed.current_phase : 'team-exec';
|
|
1753
|
-
return {
|
|
1754
|
-
current_phase: currentPhase,
|
|
1755
|
-
max_fix_attempts: typeof parsed.max_fix_attempts === 'number' ? parsed.max_fix_attempts : 3,
|
|
1756
|
-
current_fix_attempt: typeof parsed.current_fix_attempt === 'number' ? parsed.current_fix_attempt : 0,
|
|
1757
|
-
transitions: Array.isArray(parsed.transitions) ? parsed.transitions : [],
|
|
1758
|
-
updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : new Date().toISOString(),
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
catch {
|
|
1762
|
-
return null;
|
|
1763
|
-
}
|
|
1081
|
+
const phase = await readTeamPhaseImpl(teamName, cwd, teamPhasePath);
|
|
1082
|
+
return phase;
|
|
1764
1083
|
}
|
|
1765
1084
|
export async function writeTeamPhase(teamName, phaseState, cwd) {
|
|
1766
|
-
await
|
|
1085
|
+
await writeTeamPhaseImpl(teamName, phaseState, cwd, teamPhasePath, writeAtomic);
|
|
1767
1086
|
}
|
|
1768
1087
|
// === Config persistence (public wrapper) ===
|
|
1769
1088
|
export async function saveTeamConfig(config, cwd) {
|