oh-my-codex 0.12.3 → 0.12.4
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 -0
- package/dist/cli/__tests__/index.test.js +73 -12
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +8 -27
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.d.ts +2 -0
- package/dist/cli/__tests__/mcp-parity.test.d.ts.map +1 -0
- package/dist/cli/__tests__/mcp-parity.test.js +111 -0
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js +13 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +6 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +189 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -0
- package/dist/cli/__tests__/setup-scope.test.js +48 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.d.ts +2 -0
- package/dist/cli/__tests__/state.test.d.ts.map +1 -0
- package/dist/cli/__tests__/state.test.js +46 -0
- package/dist/cli/__tests__/state.test.js.map +1 -0
- package/dist/cli/__tests__/team.test.js +238 -2
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +37 -2
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/index.d.ts +6 -13
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +47 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.d.ts +22 -0
- package/dist/cli/mcp-parity.d.ts.map +1 -0
- package/dist/cli/mcp-parity.js +227 -0
- package/dist/cli/mcp-parity.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +5 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/state.d.ts +8 -0
- package/dist/cli/state.d.ts.map +1 -0
- package/dist/cli/state.js +71 -0
- package/dist/cli/state.js.map +1 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +6 -5
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +18 -4
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.d.ts +2 -0
- package/dist/config/__tests__/codex-hooks.test.d.ts.map +1 -0
- package/dist/config/__tests__/codex-hooks.test.js +53 -0
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -0
- package/dist/config/codex-hooks.d.ts +16 -7
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +134 -2
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +6 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +6 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.d.ts +2 -0
- package/dist/hud/__tests__/reconcile.test.d.ts.map +1 -0
- package/dist/hud/__tests__/reconcile.test.js +83 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -0
- package/dist/hud/__tests__/render.test.js +43 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/constants.d.ts +2 -1
- package/dist/hud/constants.d.ts.map +1 -1
- package/dist/hud/constants.js +2 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.d.ts +4 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +11 -5
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +23 -0
- package/dist/hud/reconcile.d.ts.map +1 -0
- package/dist/hud/reconcile.js +71 -0
- package/dist/hud/reconcile.js.map +1 -0
- package/dist/hud/render.d.ts +6 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +77 -3
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/tmux.d.ts +26 -0
- package/dist/hud/tmux.d.ts.map +1 -0
- package/dist/hud/tmux.js +126 -0
- package/dist/hud/tmux.js.map +1 -0
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +16 -6
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +298 -0
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +9 -5
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +195 -1
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js +9 -5
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/trace-server.d.ts +51 -0
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js +9 -5
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +455 -8
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +159 -52
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +5 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +86 -0
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-hook/operational-events.d.ts.map +1 -1
- package/dist/scripts/notify-hook/operational-events.js +7 -2
- package/dist/scripts/notify-hook/operational-events.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts +2 -0
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +1 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js +82 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -0
- package/dist/state/__tests__/operations.test.d.ts +2 -0
- package/dist/state/__tests__/operations.test.d.ts.map +1 -0
- package/dist/state/__tests__/operations.test.js +200 -0
- package/dist/state/__tests__/operations.test.js.map +1 -0
- package/dist/state/__tests__/path-traversal.test.d.ts +2 -0
- package/dist/state/__tests__/path-traversal.test.d.ts.map +1 -0
- package/dist/state/__tests__/path-traversal.test.js +49 -0
- package/dist/state/__tests__/path-traversal.test.js.map +1 -0
- package/dist/state/operations.d.ts +11 -0
- package/dist/state/operations.d.ts.map +1 -0
- package/dist/state/operations.js +233 -0
- package/dist/state/operations.js.map +1 -0
- package/dist/team/__tests__/api-interop.test.js +24 -2
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +9 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +45 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +191 -66
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +33 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +2 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +21 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +8 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +179 -78
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +9 -0
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/tmux-session.js +3 -3
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts +2 -0
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +7 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +76 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +59 -11
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/ralph-persistence-gate.test.js +1 -4
- package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +1 -1
- package/package.json +6 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +600 -8
- package/src/scripts/codex-native-hook.ts +236 -60
- package/src/scripts/codex-native-pre-post.ts +104 -0
- package/src/scripts/notify-hook/operational-events.ts +6 -2
package/dist/team/runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
|
|
4
4
|
import { performance } from 'perf_hooks';
|
|
5
5
|
import { spawn, spawnSync } from 'child_process';
|
|
6
|
-
import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, resolveTeamWorkerCli, resolveTeamWorkerCliPlan, resolveTeamWorkerLaunchMode, waitForWorkerReady, dismissTrustPromptIfPresent,
|
|
6
|
+
import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, resolveTeamWorkerCli, resolveTeamWorkerCliPlan, resolveTeamWorkerLaunchMode, waitForWorkerReady, dismissTrustPromptIfPresent, sleepFractionalSeconds, sendToWorker, sendToWorkerStdin, isWorkerAlive, getWorkerPanePid, killWorkerByPaneIdAsync, restoreStandaloneHudPane, teardownWorkerPanes, unregisterResizeHook, destroyTeamSession, listPaneIds, listTeamSessions, } from './tmux-session.js';
|
|
7
7
|
import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, } from './team-ops.js';
|
|
8
8
|
import { queueInboxInstruction, queueDirectMailboxMessage, queueBroadcastMailboxMessage, waitForDispatchReceipt, } from './mcp-comm.js';
|
|
9
9
|
import { appendTeamDeliveryLogForCwd } from './delivery-log.js';
|
|
@@ -20,7 +20,7 @@ import { hasStructuredVerificationEvidence } from '../verification/verifier.js';
|
|
|
20
20
|
import { buildRebalanceDecisions } from './rebalance-policy.js';
|
|
21
21
|
import { readModeState, updateModeState } from '../modes/base.js';
|
|
22
22
|
import { appendTeamCommitHygieneEntries, buildTeamCommitHygieneContext, writeTeamCommitHygieneContext, } from './commit-hygiene.js';
|
|
23
|
-
import { assertCleanLeaderWorkspaceForWorkerWorktrees, ensureWorktree, isGitRepository, planWorktreeTarget, rollbackProvisionedWorktrees, } from './worktree.js';
|
|
23
|
+
import { assertCleanLeaderWorkspaceForWorkerWorktrees, ensureWorktree, isGitRepository, isWorktreeDirty, planWorktreeTarget, removeWorktreeForce, rollbackProvisionedWorktrees, } from './worktree.js';
|
|
24
24
|
async function syncRootTeamModeStateOnTerminalPhase(teamName, phase, cwd) {
|
|
25
25
|
if (phase !== 'complete' && phase !== 'failed' && phase !== 'cancelled')
|
|
26
26
|
return;
|
|
@@ -109,9 +109,15 @@ function collectShutdownPaneIds(params) {
|
|
|
109
109
|
return [...paneIds];
|
|
110
110
|
}
|
|
111
111
|
export function shouldPrekillInteractiveShutdownProcessTrees(sessionName) {
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
112
|
+
// Shared-window tmux sessions can expose overlapping ancestry around the
|
|
113
|
+
// invoking leader client. Rely on pane-targeted teardown there so shutdown
|
|
114
|
+
// does not signal the leader while tearing down worker panes.
|
|
115
|
+
if (sessionName.includes(':'))
|
|
116
|
+
return false;
|
|
117
|
+
// Detached session teardown still benefits from process-tree prekill,
|
|
118
|
+
// including native Windows prompt-worker ancestry where pane-targeted
|
|
119
|
+
// teardown alone is insufficient.
|
|
120
|
+
return true;
|
|
115
121
|
}
|
|
116
122
|
async function logRuntimeDispatchOutcome(params) {
|
|
117
123
|
const { cwd, teamName, workerName, requestId, messageId, intent, outcome, source = 'team.runtime' } = params;
|
|
@@ -789,6 +795,44 @@ async function prepareWorkerWorktreeShutdownReports(config, leaderCwd) {
|
|
|
789
795
|
}
|
|
790
796
|
return reports;
|
|
791
797
|
}
|
|
798
|
+
function listDirtyShutdownWorkers(config) {
|
|
799
|
+
const dirtyWorkers = [];
|
|
800
|
+
for (const worker of config.workers) {
|
|
801
|
+
if (!worker.worktree_repo_root || !worker.worktree_path || !existsSync(worker.worktree_path))
|
|
802
|
+
continue;
|
|
803
|
+
const worktreePath = resolve(worker.worktree_path);
|
|
804
|
+
const repoRoot = resolve(worker.worktree_repo_root);
|
|
805
|
+
const status = runGitCommand(repoRoot, ['status', '--porcelain'], worktreePath);
|
|
806
|
+
if (!status.ok || status.stdout.trim().length > 0) {
|
|
807
|
+
dirtyWorkers.push(worker.name);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return dirtyWorkers;
|
|
811
|
+
}
|
|
812
|
+
async function classifyShutdown(params) {
|
|
813
|
+
const { teamName, cwd, config, governance, confirmIssues } = params;
|
|
814
|
+
const allTasks = await listTasks(teamName, cwd);
|
|
815
|
+
const gate = {
|
|
816
|
+
total: allTasks.length,
|
|
817
|
+
pending: allTasks.filter((t) => t.status === 'pending').length,
|
|
818
|
+
blocked: allTasks.filter((t) => t.status === 'blocked').length,
|
|
819
|
+
in_progress: allTasks.filter((t) => t.status === 'in_progress').length,
|
|
820
|
+
completed: allTasks.filter((t) => t.status === 'completed').length,
|
|
821
|
+
failed: allTasks.filter((t) => t.status === 'failed').length,
|
|
822
|
+
allowed: false,
|
|
823
|
+
};
|
|
824
|
+
const dirtyWorkers = listDirtyShutdownWorkers(config);
|
|
825
|
+
const hasBlockingBacklog = gate.pending > 0 || gate.blocked > 0 || gate.in_progress > 0;
|
|
826
|
+
const requiresIssueConfirmation = gate.failed > 0 && dirtyWorkers.length === 0 && !confirmIssues;
|
|
827
|
+
gate.allowed = governance.cleanup_requires_all_workers_inactive !== true
|
|
828
|
+
|| (!hasBlockingBacklog && !requiresIssueConfirmation);
|
|
829
|
+
return {
|
|
830
|
+
gate,
|
|
831
|
+
dirtyWorkers,
|
|
832
|
+
requiresIssueConfirmation,
|
|
833
|
+
useCleanFastPath: dirtyWorkers.length === 0 && !hasBlockingBacklog && (gate.failed === 0 || confirmIssues),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
792
836
|
function resolveEffectiveTeamWorktreeMode(leaderCwd, requestedMode) {
|
|
793
837
|
if (!isGitRepository(leaderCwd)) {
|
|
794
838
|
return { enabled: false };
|
|
@@ -1319,6 +1363,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
1319
1363
|
for (let i = 1; i <= workerCount; i++) {
|
|
1320
1364
|
workerWorkspaceByName.set(`worker-${i}`, { cwd: leaderCwd });
|
|
1321
1365
|
}
|
|
1366
|
+
await detectAndCleanStaleTeam(sanitized, leaderCwd, workerCount, options.confirmStaleCleanup);
|
|
1322
1367
|
if (activeWorktreeMode) {
|
|
1323
1368
|
assertCleanLeaderWorkspaceForWorkerWorktrees(leaderCwd);
|
|
1324
1369
|
for (let i = 1; i <= workerCount; i++) {
|
|
@@ -2060,6 +2105,8 @@ export async function reassignTask(teamName, taskId, _fromWorker, toWorker, cwd)
|
|
|
2060
2105
|
*/
|
|
2061
2106
|
export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
2062
2107
|
const force = options.force === true;
|
|
2108
|
+
const confirmIssues = options.confirmIssues === true;
|
|
2109
|
+
let skipWorkerAcks = false;
|
|
2063
2110
|
const sanitized = sanitizeTeamName(teamName);
|
|
2064
2111
|
const config = await readTeamConfig(sanitized, cwd);
|
|
2065
2112
|
if (!config) {
|
|
@@ -2077,26 +2124,26 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2077
2124
|
const manifest = await readTeamManifestV2(sanitized, cwd);
|
|
2078
2125
|
const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
|
|
2079
2126
|
if (!force) {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
allowed: false,
|
|
2089
|
-
};
|
|
2090
|
-
gate.allowed = governance.cleanup_requires_all_workers_inactive !== true
|
|
2091
|
-
|| (gate.pending === 0 && gate.blocked === 0 && gate.in_progress === 0 && gate.failed === 0);
|
|
2127
|
+
const classification = await classifyShutdown({
|
|
2128
|
+
teamName: sanitized,
|
|
2129
|
+
cwd,
|
|
2130
|
+
config,
|
|
2131
|
+
governance,
|
|
2132
|
+
confirmIssues,
|
|
2133
|
+
});
|
|
2134
|
+
const { gate, dirtyWorkers, requiresIssueConfirmation, useCleanFastPath } = classification;
|
|
2092
2135
|
await appendTeamEvent(sanitized, {
|
|
2093
2136
|
type: 'shutdown_gate',
|
|
2094
2137
|
worker: 'leader-fixed',
|
|
2095
|
-
reason: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed} cleanup_requires_all_workers_inactive=${governance.cleanup_requires_all_workers_inactive}`,
|
|
2138
|
+
reason: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed} cleanup_requires_all_workers_inactive=${governance.cleanup_requires_all_workers_inactive} dirty_workers=${dirtyWorkers.join('|') || 'none'} confirm_issues=${confirmIssues} clean_fast_path=${useCleanFastPath}`,
|
|
2096
2139
|
}, cwd).catch(() => { });
|
|
2097
2140
|
if (!gate.allowed) {
|
|
2141
|
+
if (requiresIssueConfirmation) {
|
|
2142
|
+
throw new Error(`shutdown_confirm_issues_required:failed=${gate.failed}:rerun=omx team shutdown ${sanitized} --confirm-issues`);
|
|
2143
|
+
}
|
|
2098
2144
|
throw new Error(`shutdown_gate_blocked:pending=${gate.pending},blocked=${gate.blocked},in_progress=${gate.in_progress},failed=${gate.failed}`);
|
|
2099
2145
|
}
|
|
2146
|
+
skipWorkerAcks = useCleanFastPath;
|
|
2100
2147
|
}
|
|
2101
2148
|
if (force) {
|
|
2102
2149
|
await appendTeamEvent(sanitized, {
|
|
@@ -2105,73 +2152,81 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2105
2152
|
reason: 'force_bypass',
|
|
2106
2153
|
}, cwd).catch(() => { });
|
|
2107
2154
|
}
|
|
2155
|
+
if (force && config.worker_launch_mode === 'prompt') {
|
|
2156
|
+
// Prompt-mode workers are raw CLI children, not team-runtime workers that
|
|
2157
|
+
// participate in the shutdown-ack handshake. Waiting the full ack window
|
|
2158
|
+
// before force-killing them only adds deterministic suite slowness.
|
|
2159
|
+
skipWorkerAcks = true;
|
|
2160
|
+
}
|
|
2108
2161
|
const sessionName = config.tmux_session;
|
|
2109
2162
|
const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
|
|
2110
2163
|
const shutdownRequestTimes = new Map();
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
try {
|
|
2114
|
-
const requestedAt = new Date().toISOString();
|
|
2115
|
-
await writeShutdownRequest(sanitized, w.name, 'leader-fixed', cwd);
|
|
2116
|
-
shutdownRequestTimes.set(w.name, requestedAt);
|
|
2117
|
-
const triggerDirective = buildTriggerDirective(w.name, sanitized, resolveInstructionStateRoot(w.worktree_path));
|
|
2118
|
-
await dispatchCriticalInboxInstruction({
|
|
2119
|
-
teamName: sanitized,
|
|
2120
|
-
config,
|
|
2121
|
-
workerName: w.name,
|
|
2122
|
-
workerIndex: w.index,
|
|
2123
|
-
paneId: w.pane_id,
|
|
2124
|
-
inbox: generateShutdownInbox(sanitized, w.name),
|
|
2125
|
-
triggerMessage: triggerDirective.text,
|
|
2126
|
-
intent: triggerDirective.intent,
|
|
2127
|
-
cwd,
|
|
2128
|
-
dispatchPolicy,
|
|
2129
|
-
inboxCorrelationKey: `shutdown:${w.name}`,
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
catch (err) {
|
|
2133
|
-
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
// 2. Wait up to 15s for workers to exit and collect acks
|
|
2137
|
-
const deadline = Date.now() + 15_000;
|
|
2138
|
-
const rejected = [];
|
|
2139
|
-
const ackedWorkers = new Set();
|
|
2140
|
-
while (Date.now() < deadline) {
|
|
2164
|
+
if (!skipWorkerAcks) {
|
|
2165
|
+
// 1. Send shutdown inbox to each worker
|
|
2141
2166
|
for (const w of config.workers) {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2167
|
+
try {
|
|
2168
|
+
const requestedAt = new Date().toISOString();
|
|
2169
|
+
await writeShutdownRequest(sanitized, w.name, 'leader-fixed', cwd);
|
|
2170
|
+
shutdownRequestTimes.set(w.name, requestedAt);
|
|
2171
|
+
const triggerDirective = buildTriggerDirective(w.name, sanitized, resolveInstructionStateRoot(w.worktree_path));
|
|
2172
|
+
await dispatchCriticalInboxInstruction({
|
|
2173
|
+
teamName: sanitized,
|
|
2174
|
+
config,
|
|
2175
|
+
workerName: w.name,
|
|
2176
|
+
workerIndex: w.index,
|
|
2177
|
+
paneId: w.pane_id,
|
|
2178
|
+
inbox: generateShutdownInbox(sanitized, w.name),
|
|
2179
|
+
triggerMessage: triggerDirective.text,
|
|
2180
|
+
intent: triggerDirective.intent,
|
|
2181
|
+
cwd,
|
|
2182
|
+
dispatchPolicy,
|
|
2183
|
+
inboxCorrelationKey: `shutdown:${w.name}`,
|
|
2184
|
+
});
|
|
2150
2185
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
rejected.push({ worker: w.name, reason: ack.reason || 'no_reason' });
|
|
2154
|
-
}
|
|
2186
|
+
catch (err) {
|
|
2187
|
+
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
2155
2188
|
}
|
|
2156
2189
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2190
|
+
// 2. Wait up to 15s for workers to exit and collect acks
|
|
2191
|
+
const deadline = Date.now() + 15_000;
|
|
2192
|
+
const rejected = [];
|
|
2193
|
+
const ackedWorkers = new Set();
|
|
2194
|
+
while (Date.now() < deadline) {
|
|
2195
|
+
for (const w of config.workers) {
|
|
2196
|
+
const ack = await readShutdownAck(sanitized, w.name, cwd, shutdownRequestTimes.get(w.name));
|
|
2197
|
+
if (ack && !ackedWorkers.has(w.name)) {
|
|
2198
|
+
ackedWorkers.add(w.name);
|
|
2199
|
+
await appendTeamEvent(sanitized, {
|
|
2200
|
+
type: 'shutdown_ack',
|
|
2201
|
+
worker: w.name,
|
|
2202
|
+
reason: ack.status === 'reject' ? `reject:${ack.reason || 'no_reason'}` : 'accept',
|
|
2203
|
+
}, cwd);
|
|
2204
|
+
}
|
|
2205
|
+
if (ack?.status === 'reject') {
|
|
2206
|
+
if (!rejected.some((r) => r.worker === w.name)) {
|
|
2207
|
+
rejected.push({ worker: w.name, reason: ack.reason || 'no_reason' });
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (rejected.length > 0 && !force) {
|
|
2212
|
+
const detail = rejected.map(r => `${r.worker}:${r.reason}`).join(',');
|
|
2213
|
+
throw new Error(`shutdown_rejected:${detail}`);
|
|
2214
|
+
}
|
|
2215
|
+
const anyAlive = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2216
|
+
? isPromptWorkerAlive(config, w)
|
|
2217
|
+
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
2218
|
+
if (!anyAlive)
|
|
2219
|
+
break;
|
|
2220
|
+
// Sleep 2s
|
|
2221
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
2160
2222
|
}
|
|
2161
|
-
const
|
|
2223
|
+
const anyAliveAfterWait = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2162
2224
|
? isPromptWorkerAlive(config, w)
|
|
2163
2225
|
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
2164
|
-
if (!
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
}
|
|
2169
|
-
const anyAliveAfterWait = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2170
|
-
? isPromptWorkerAlive(config, w)
|
|
2171
|
-
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
2172
|
-
if (anyAliveAfterWait && !force) {
|
|
2173
|
-
// Workers may have accepted shutdown but not exited (Codex TUI requires explicit exit).
|
|
2174
|
-
// In this case, proceed to force kill panes (next step) rather than failing and leaving state around.
|
|
2226
|
+
if (anyAliveAfterWait && !force) {
|
|
2227
|
+
// Workers may have accepted shutdown but not exited (Codex TUI requires explicit exit).
|
|
2228
|
+
// In this case, proceed to force kill panes (next step) rather than failing and leaving state around.
|
|
2229
|
+
}
|
|
2175
2230
|
}
|
|
2176
2231
|
// 3. Force kill remaining workers
|
|
2177
2232
|
const leaderPaneId = config.leader_pane_id;
|
|
@@ -2413,6 +2468,52 @@ async function findActiveTeams(cwd, leaderSessionId) {
|
|
|
2413
2468
|
}
|
|
2414
2469
|
return active;
|
|
2415
2470
|
}
|
|
2471
|
+
async function detectAndCleanStaleTeam(teamName, leaderCwd, workerCount, confirmFn) {
|
|
2472
|
+
const stateDir = join(leaderCwd, '.omx', 'state', 'team', teamName);
|
|
2473
|
+
if (!existsSync(stateDir))
|
|
2474
|
+
return;
|
|
2475
|
+
const sessions = new Set(listTeamSessions());
|
|
2476
|
+
if (sessions.has(`omx-team-${teamName}`))
|
|
2477
|
+
return;
|
|
2478
|
+
const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
2479
|
+
cwd: leaderCwd, encoding: 'utf-8', windowsHide: true,
|
|
2480
|
+
});
|
|
2481
|
+
if (repoRootResult.status !== 0)
|
|
2482
|
+
return;
|
|
2483
|
+
const repoRoot = repoRootResult.stdout.trim();
|
|
2484
|
+
const worktreePaths = [];
|
|
2485
|
+
for (let i = 1; i <= workerCount; i++) {
|
|
2486
|
+
const wtPath = join(repoRoot, '.omx', 'team', teamName, 'worktrees', `worker-${i}`);
|
|
2487
|
+
if (existsSync(wtPath))
|
|
2488
|
+
worktreePaths.push(wtPath);
|
|
2489
|
+
}
|
|
2490
|
+
if (worktreePaths.length === 0) {
|
|
2491
|
+
await cleanupTeamState(teamName, leaderCwd);
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
const hasDirtyWorktrees = worktreePaths.some((p) => {
|
|
2495
|
+
try {
|
|
2496
|
+
return isWorktreeDirty(p);
|
|
2497
|
+
}
|
|
2498
|
+
catch {
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
const summary = { teamName, worktreePaths, statePath: stateDir, hasDirtyWorktrees };
|
|
2503
|
+
if (!confirmFn) {
|
|
2504
|
+
throw new Error(`stale_team_artifacts:${teamName}:${worktreePaths.length}_worktrees:` +
|
|
2505
|
+
'pass_confirmStaleCleanup_or_manually_remove');
|
|
2506
|
+
}
|
|
2507
|
+
const confirmed = await confirmFn(summary);
|
|
2508
|
+
if (!confirmed) {
|
|
2509
|
+
throw new Error(`stale_team_cleanup_declined:${teamName}:` +
|
|
2510
|
+
'manually_remove_worktrees_and_state_before_retrying');
|
|
2511
|
+
}
|
|
2512
|
+
for (const wtPath of worktreePaths) {
|
|
2513
|
+
await removeWorktreeForce(repoRoot, wtPath);
|
|
2514
|
+
}
|
|
2515
|
+
await cleanupTeamState(teamName, leaderCwd);
|
|
2516
|
+
}
|
|
2416
2517
|
async function resolveLeaderSessionId(cwd) {
|
|
2417
2518
|
const fromEnv = process.env.OMX_SESSION_ID || process.env.CODEX_SESSION_ID || process.env.SESSION_ID;
|
|
2418
2519
|
if (fromEnv && fromEnv.trim() !== '')
|
|
@@ -2632,7 +2733,7 @@ async function dispatchCriticalInboxInstruction(params) {
|
|
|
2632
2733
|
if (receipt?.status === 'failed') {
|
|
2633
2734
|
const fallback = await notifyWorkerOutcome(config, workerIndex, triggerMessage, paneId);
|
|
2634
2735
|
if (fallback.ok) {
|
|
2635
|
-
await transitionDispatchRequest(teamName, queued.request_id, '
|
|
2736
|
+
await transitionDispatchRequest(teamName, queued.request_id, 'pending', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, cwd).catch(() => { });
|
|
2636
2737
|
return {
|
|
2637
2738
|
ok: true,
|
|
2638
2739
|
transport: fallback.transport,
|
|
@@ -2658,7 +2759,7 @@ async function dispatchCriticalInboxInstruction(params) {
|
|
|
2658
2759
|
if (fallback.ok) {
|
|
2659
2760
|
const marked = await markDispatchRequestNotified(teamName, queued.request_id, { last_reason: `fallback_confirmed:${fallback.reason}` }, cwd);
|
|
2660
2761
|
if (!marked) {
|
|
2661
|
-
await transitionDispatchRequest(teamName, queued.request_id, '
|
|
2762
|
+
await transitionDispatchRequest(teamName, queued.request_id, 'pending', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, cwd).catch(() => { });
|
|
2662
2763
|
}
|
|
2663
2764
|
return {
|
|
2664
2765
|
ok: true,
|
|
@@ -2700,7 +2801,7 @@ async function finalizeHookPreferredMailboxDispatch(params) {
|
|
|
2700
2801
|
if (receipt?.status === 'failed') {
|
|
2701
2802
|
if (fallback.ok) {
|
|
2702
2803
|
await markMessageNotified(teamName, workerName, messageId, cwd).catch(() => false);
|
|
2703
|
-
await transitionDispatchRequest(teamName, requestId, 'failed', 'failed', { message_id: messageId, last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, cwd).catch(() =>
|
|
2804
|
+
await transitionDispatchRequest(teamName, requestId, 'failed', 'failed', { message_id: messageId, last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, cwd).catch(() => null);
|
|
2704
2805
|
const outcome = {
|
|
2705
2806
|
ok: true,
|
|
2706
2807
|
transport: fallback.transport,
|