mirror of
https://github.com/Yeachan-Heo/oh-my-claudecode.git
synced 2026-09-02 22:14:43 +08:00
* fix(team): add durable worker launch acknowledgement
* test(team): prove revoked launches cannot spawn providers
* fix(team): close worker launch review gaps
* ci: request GitHub-signed exact head for PR #3588
* fix(team): bind startup proof to launch attempts
* fix(team): fence launch ownership before provider start
* test(team): assert durable provider-start marker
* fix(team): close recovery ownership races
* fix(team): bind recovery activation to launch identity
* test(team): enforce ownership-only shutdown
* fix(team): fence recovery side effects and Windows specs
* test(team): follow ownership-proven shutdown path
* fix(team): preserve exact recovery identities
* fix(team): prove recovered provider liveness
* fix(team): terminate untracked recovery providers
* test(team): harden terminal ownership regressions
* fix(team): close Windows worker shell with provider
* fix(team): tear down unverified worker launches
* fix(team): fence initial launch input and process trees
* test(team): cover pane membership loss
* test(team): make revocation terminality deterministic
* test(team): harden idle recovery under load
* fix(team): retire provider processes before pane cleanup
* fix(team): close durable worker lifecycle cleanup gaps
* fix(team): preserve panes without provider identity
* fix(team): surface unverified shutdown cleanup
* fix(team): require accepted cleanup ownership
* fix(team): harden shutdown and Windows containment
* fix(team): fence cleanup ownership and shutdown
* fix(team): close PID-reuse gaps in worker lifecycle and hook runner
Recovery owner: finish #3584 Windows psmux/team lifecycle repair.
Identity-safe process termination:
- terminateOwnedProcessTree (Windows): replace raw taskkill with a single
System.Diagnostics.Process handle that suspends the tree, verifies
StartTime ticks against the expected start identity, kills deepest-first,
and WaitForExit before returning — so a reused numeric PID is never handed
to taskkill.
- worker-launch-ack: require terminal/termination-complete records to carry
a positive pid and valid process_start_identity before accepting cleanup
as verified; write durable termination-request/termination-complete files
so external (POSIX) supervision completion is proven, not assumed.
- scripts/run.cjs: capture the child's durable start identity at spawn time;
reapTree verifies the PID still matches before killing the process group,
failing closed (skip kill, rely on child.unref) if the PID was reused.
Consolidated cleanup ownership:
- retireAndCleanupCurrentWorkerLaunchAttempt now writes a cleanup-complete
marker so a successor owner can reuse exact durable evidence without
touching the pane or provider again.
- spawnV2Worker, shutdownTeamV2, scaleUp/scaleDown all route through the
consolidated path, accepting a proven-dead pane without redundant killing.
Boolean shutdown contract:
- killTeamSession, killOwnedTeamSession (runtime.ts), and shutdownTeam now
return boolean verified cleanup; session-end records a failed team when
legacy shutdown cannot prove cleanup instead of silently deleting state.
Test repairs:
- recovery-pane-rollback: terminal fixtures now carry pid + start identity.
- scaling: dead-pane short-circuit assertions aligned with consolidated path.
- session-end-process-exit: richer timeout diagnostic, higher ceiling.
- worker-launch-ack: POSIX supervision, malformed-record rejection, reuse-
proof cleanup, external termination completion regressions.
Generated artifacts rebuilt from source (dist + bridge).
* fix(team): persist provider start identity in terminal records
Codex exact-head review (r3679252756) found that all three
worker_launch_provider_terminal record constructors in runWorkerLaunchBootstrap
omitted process_start_identity, so readWorkerLaunchCleanupProof rejected valid
terminal proof during shutdown/recovery even when the provider exited cleanly.
Fix: add process_start_identity: providerStartIdentity to the exit handler,
error handler, and supervisor-timeout terminal records.
Also regenerate dist/platform/process-utils.d.ts whose doc comment was stale
(failed npm-package-bin-surface byte parity: expected 1f2008... got a003c7...).
Generated artifacts: dist/team/worker-launch-ack.js, dist/platform/process-utils.d.ts,
bridge/runtime-cli.cjs.
* fix(team): propagate shutdown result, harden action-runner, remove NUL bytes
P1 (r3679396257): shutdownTeam() now returns false on cleanup failure but
legacy callers discarded it. Propagate the boolean at all four callsites:
- src/cli/team.ts: shutdown field reflects cleaned result, adds error field
- src/cli/commands/team.ts: throw on incomplete cleanup
- src/team/api-interop.ts: throw on incomplete legacy cleanup
- src/team/runtime-cli.ts: throw on incomplete legacy cleanup
P2#1 (r3679396259): action-runner identity null fallback used child.kill()
which only kills the direct child and can orphan detached descendants.
Replace with process group kill (-child.pid) with fallback to child.kill().
P2#2 (r3679396263): worker-bootstrap overlay contained literal NUL bytes
around launch metadata. Replaced with escaped backtick-delimited references.
Advisories:
- Guard invocation.cleanup() in worker-launch-ack exit/error/supervisor
handlers and activation-gate finish with .catch(() => undefined) so a
cleanup rejection cannot suppress terminal evidence.
- Recovery liveness unknown already treated as runtime_owner_unavailable
consistently — no change needed.
Test fix: cli/team.test.ts mock now returns true for shutdownTeam.
* fix(team): cmux scale-down provider, Windows resume safety, action-runner sync identity
P1 cmux scale-down (r3679582324): scaling.ts hardcoded provider:'tmux' at
both scale-up (line 625) and scale-down (line 971) adoptWorkerPaneOwnership
calls. Native cmux surface IDs were rejected as malformed, persisting
active_scale_down.phase='failed' and blocking lifecycle. Now uses the same
canonical pane_id.startsWith('%') ? 'tmux' : 'cmux' logic as shutdownTeamV2.
P1 Windows suspended provider (r3679582329): terminateOwnedProcessTree Windows
PowerShell path could leave processes frozen after NtSuspendProcess if
enumeration, kill, deadline, or outer exec failed. Added NtResumeProcess
import, Suspended tracking on every owned process, and try/catch/finally
blocks that guarantee every suspended process is either killed (Suspended=false)
or resumed before exit. Kill failure resumes the process and exits 4. Deadline
after suspension resumes all and exits 5. Outer failure resumes all and exits 6.
The finally block is a final safety net. Caller always gets 'unknown' (not
'success') when any resume was needed.
P2 action-runner redesign: replaced async getProcessStartIdentity (which left
a PID-reuse window) with synchronous getProcessStartIdentitySync from
process-utils.ts. Identity is captured in the same tick as spawn, before
child.unref() or any async work. When identity is null, fail closed WITHOUT
signalling any PID or process group — the child has its own deadline timer
and self-exits. Never uses raw process.kill(-pid) or child.kill() on identity
failure.
P1 shutdown propagation tests: cli/team.test.ts now asserts shutdownTeam
returning false produces shutdown:false with error field.
Overlay control byte test: worker-bootstrap.test.ts now scans generated
overlay for all C0 control characters (except HT/LF/CR) and rejects them.
Generated artifacts: dist + bridge rebuilt from source.
* fix(team): cross-platform sync identity, Node-20 canonical bridge closure
P1 (r3679780731): getProcessStartIdentitySync was Linux-only, so macOS
and Windows SessionEnd actions would always fail closed before publishing
control/arm records. Added synchronous spawnSync-based identity capture
for macOS (ps lstart) and Windows (PowerShell Get-Process StartTime ticks).
The action-runner identity-null test now also includes a real platform-
branch regression proving that getProcessStartIdentitySync returns a valid
identity for the current process on Linux and macOS.
Canonical generated closure: rebuilt bridge/mcp-server.cjs, bridge/team-bridge.cjs,
bridge/cli.cjs, bridge/team.js, and dist/platform/process-utils.{js,d.ts} with
Node 20 to match CI's deterministic build environment. Fixes npm-package-bin-surface
byte-parity failure (bridge/mcp-server.cjs packed f7464f... vs committed 920818...).
* fix(team): committed fence reconciliation, shutdown retry, legacy pane cleanup
P1 scaling.ts 722-723: post-commit scale-up fence release failure left
active_scale_up stuck in 'effects'. Added explicit 'committed' phase written
atomically with worker config at the commit boundary. A 'committed' fence
is ownership-safe to reclaim: workers are provably durable. Historical
'effects' without commit proof remains fail-closed. scaleUpFenceBlocks()
and all scale-down/shutdown gates updated to allow 'committed' reconciliation.
P1 runtime-v2.ts 3925: all-dead recovery expiry wrote shutting_down without
shutdown_attempt. fenceAllDeadRecoveryExpiry now writes a shutdown_attempt
with all-dead-expiry nonce and atomic state_revision. shutdownTeamV2 adopts
it only when: (1) nonce starts with all-dead-expiry:, (2) attempt
state_revision equals config state_revision (atomic write proof), (3) owner
is dead. Arbitrary dead-owner attempts without this proof are rejected.
P1 runtime-v2.ts 4163: nonterminal cleanup left live shutdown_attempt that
blocked same-process retries. Added rollbackShutdownForRetry() called before
every nonterminal return's finalizeAutoMerge. Rolls back lifecycle_state to
'active' and clears shutdown_attempt via CAS if the same owner still holds
the fence.
P1 runtime-v2.ts 4100: legacy v2 workers with pane_id but no
launch_attempt_id went directly to providerCleanupFailures. Added
ownership-safe pane cleanup path: adopt ownership, kill pane, verify
liveness. No raw PID guesses or destructive cleanup of unrelated processes.
P2 process-utils.ts 263: macOS sync identity returned 'mac:<ms>' while async
returned bare decimal. Normalized sync to String(time) matching async format.
Generated artifacts rebuilt with Node 20 for CI byte parity.
* fix(team): same-owner all-dead adoption, retry preserves orchestration
P1 (r3680208765): all-dead expiry writes a shutdown_attempt owned by the
live process that calls shutdown. The live-owner rejection fired before
provenance adoption. Fix: check all-dead-expiry provenance first, then
allow same-owner (same pid + start identity) to adopt their own attempt.
Other live owners, stale/forged attempts still rejected.
P2 (r3680208770): retryable rollback restored lifecycle=active and then
finalizeAutoMerge drained/unregistered/stopped coordination, leaving an
active team without orchestration. Fix: rollbackShutdownForRetry returns
boolean; when rollback succeeds (team back to active), finalizeAutoMerge
is skipped, preserving orchestrator/cadence/worker registrations for
retry. Only terminal success paths call finalizeAutoMerge directly.
Test updates: dispatch test expectations adjusted for retry-preserves-
orchestration behavior.
* fix(team): sync provider identity, dedicated-window absence proof, CI parity
P1 (r3680299907): provider identity captured asynchronously after spawn;
child could settle and PID be reused before publication. Fixed: capture
synchronously via getProcessStartIdentitySync immediately after spawn.
Fall back to async only when sync unavailable, with liveness recheck
after async lookup to detect PID reuse.
P1 (r3680299913): killTeamSession dedicated-window kill failure returned
false forever even when the window was already absent. Fixed: after
kill-window fails, verify exact window absence via list-windows. Only a
successful list-windows that does NOT list the canonical window index
(line-start regex match, not substring) is proof. Command failure returns
false (unknown, not success). Regressions: absent window, present window,
list-windows failure, empty output, substring collision (3 vs 30/13),
wrong session, malformed/ambiguous output.
CI Test: shutdown-pane-cleanup provider_cleanup_unverified under CI load
resolved by synchronous identity capture eliminating the timing window
where providerStartIdentity was null when terminal record was written.
Generated artifacts rebuilt with Node 20 for CI byte parity.
* fix(team): sync identity capture in recovery activation gate
P1 (r3680565482): recovery gate still used async getProcessStartIdentity
after its last settled check. If the supervised process exited during
the async lookup (especially on Windows PowerShell latency), its PID
could be reused, isProcessAlive would accept the replacement, and the
gate would publish the wrong process identity in .launched — later
cleanup could then terminate an unrelated process tree.
Fix: use synchronous getProcessStartIdentitySync. Fall back to async
only when sync unavailable, with settled + liveness recheck after async
lookup to detect PID reuse.
* fix(team): startup rollback provider retirement, termination retry, legacy cleanup
P1-A: rollbackStartedNativeWorktreeStartup now retires each exact launched
provider before killing the team session or removing state. Tracks
launchedWorkers through the startup loop and passes them to both rollback
call sites (loop exception + final config save failure). No provider,
pane, worktree, or state orphan on partial multi-worker failure.
P1-B: termination completion write-failure retry. After owned termination
succeeds but .termination-complete write fails, verifies process is dead
via identity/liveness check and re-attempts the write. Never infers from
PID absence without request identity. Never converts identity-mismatch
alone into success.
P1-C: legacy pre-upgrade workers with pane_id but no launch_attempt_id
get ownership-safe pane cleanup in scale-down (consistent with shutdown):
adopt ownership, kill pane, verify liveness. Fail-closed on unknown
liveness. No raw PID guessing, no unrelated pane cleanup.
P2: wrapper temp-dir leak fixed. mkdtemp failure or writeFile failure
after mkdtemp now cleans up wrapperDir before re-throwing. Covers both
POSIX launch.sh and Windows launch.cmd paths.
Generated artifacts rebuilt with Node 20 for CI byte parity.
* fix(team): ownership-safe legacy recovery pane cleanup without launch_attempt_id
Residual REQUEST_CHANGES on c3: recovery still wedged pre-upgrade workers
that have pane_id + launch_descriptor but no launch_attempt_id (field did
not exist on base). Shutdown and scale-down already had ownership-safe
legacy pane cleanup; recovery returned worker_cleanup_incomplete.
spawnGatedPane now, when launch_attempt_id is absent:
- If currentLaunch already covers the exact pane, prior-launch retirement
handles cleanup
- Otherwise adopt exact pane ownership with leader/reserved protections
- Kill and verify liveness; fail closed on unknown liveness, foreign/alias
ownership, or kill that cannot prove death
- Never raw-PID signals; preserve worker/state on uncertainty
Regressions: dead-pane continue, live owned kill, unknown liveness,
leader_alias reject, kill-cannot-prove-death. All fail-closed variants
preserve pane_id and active_recovery evidence.
* fix(team): recovery uses scaleUpFenceBlocks so committed scale-up is non-blocking
P1 REQUEST_CHANGES on c370: after scale-up durably commits workers,
releaseScaleUpReservation may fail and leave active_scale_up.phase=
'committed'. The fence contract treats committed as reconcilable and
non-blocking (scaleUpFenceBlocks / shutdown), but recovery entry gates
rejected any active_scale_up and returned team_mutation_busy forever.
Export scaleUpFenceBlocks and use it at recovery election (beforeOwner),
post-election owner check, and ensureFence — matching shutdown/scale-down.
Reserved/effects/failed remain blocking/fail-closed.
Regressions (runtime-owner-busy):
- committed fence alone does not yield team_mutation_busy
- reserved/effects/failed remain team_mutation_busy
- non-committed durable-looking effects fence still blocks
Preserve all c370 legacy recovery cases and prior invariants.
Generated closure rebuilt with Node 20.
* fix(team): canonical committed-fence schema/revision, identity fail-closed, scale-down reclaim
P1 batch on exact head 109e from independent review:
1) Schema: isScaleUpAttempt now accepts phase 'committed' so real
saveTeamConfigAtRevision no longer throws invalid_persisted_state on
scale-up commit (mocks had hidden this). Malformed phases still rejected.
2) Revision policy: saveTeamConfigAtRevision aligns all active fence
state_revision fields to config.state_revision before validate/write
(alignActiveFenceRevisions). Recovery election, shutdown, and any
concurrent write that retains a committed scale-up fence stay valid.
3) Service reconciliation: reconcileCommittedTeamServices uses
scaleUpFenceBlocks — only non-committed scale-up blocks repair.
4) Scale-down reclaim: failed phase is resumable when owner is dead OR
same live owner; draining+dead still reclaimable; effects remains
fail-closed even if owner dead.
5) Provider identity: sync-only capture + rebind before publication in
worker-launch-ack and worker-activation-gate. No async-only identity
publish (PID reuse window closed).
6) Windows identity: WMIC DMTF creation dates convert to ticks: at
capture; terminate also accepts legacy dmtf: by converting. Single
format for verify/kill.
Regressions: committed save/load + revision align, malformed phase,
committed service repair, failed scale-down reclaim matrix, DMTF→ticks.
Preserve reserved/effects/failed blocking and c370 legacy recovery cases.
Generated closure rebuilt with Node 20. No artifact authorization.
* saveTeamConfigAtRevision trust boundary: assertActiveFenceOwnershipTransition
before alignActiveFenceRevisions. Same-owner phase/revision only; foreign
install requires reclaim; clear requires release. Covers scale_up/down,
recovery, shutdown_attempt, all_dead_recovery.
2) Committed scale-up: services reconcile while fence held; release requires
lifecycle active (no teardown race restart).
3) Failed scale-down resumes exact operation_id + workers (no retarget).
4) Launch paths: identity bound immediately after spawn; creation-bound
ChildProcess containment when sync identity unavailable (no identity-less
terminateProvider false-return leak).
5) DMTF: exact 100ns precision, strict calendar; dmtf: normalized in
isProcessIdentityLive before compare.
6) Legacy classification: agentTypes preserved in teamReadConfig; legacy
checked before workers[] in API/session-end.
7) Empty split-pane pane list fail-closed (not cleanup success).
8) launch_attempt_id: reject empty/null; accept non-empty strings.
Regressions: fence ownership matrix, DMTF precision/calendar, real on-disk
legacy agentTypes, empty split-pane, scale-down resume identity.
Node 20 rebuilt dist/bridge. No authorization/merge.
EOF
)
* fix(team): allow failed→draining scale-down resume; lifecycle-guard service reconcile
P1 from independent exact-head review on c9e323ba (issuecomment-5133415568):
1) assertActiveFenceOwnershipTransition: same-owner scale-down may move
failed→draining when operation_id/workers are unchanged. Foreign owner,
retarget, and other backward phases stay fail-closed.
2) reconcileCommittedTeamServices: re-read authoritative lifecycle before
side effects and immediately before startMergeOrchestrator / cadence
install; return repair_required when lifecycle is not active.
Regressions: pure transition + real CAS for failed→draining; lifecycle
flip aborts orchestrator/cadence. Test fixtures use TeamConfig lifecycle
union typing. Node 20 dist/bridge rebuild. No self-review/merge.
* fix(team): launch Windows workers through attempt wrapper
* fix(team): bind Windows worker launch authority
* chore: restore authorized generated closure
* test(team): stabilize early provider exit observation
* fix(team): close reconciled Windows lifecycle gaps
* build: regenerate reconciled Node 20 closure
* ci: bind signed exact head for PR #3588
* ci: stage PR 3588 signature anchor
* ci: remove PR 3588 signature anchor
* Fix worker launch cleanup retries
* Remove committed build artifacts
* fix(team): stabilize idle gemini recovery packaging
28 lines
897 B
JavaScript
Executable File
28 lines
897 B
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { readSessionEndFrame } from './lib/stdin.mjs';
|
|
import { isMainThread } from 'node:worker_threads';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { resolve } from 'node:path';
|
|
|
|
const fallback = { continue: true, suppressOutput: true };
|
|
|
|
export async function runSessionEndHook() {
|
|
const frame = await readSessionEndFrame();
|
|
|
|
if (frame.status !== 'ok') {
|
|
console.log(JSON.stringify(fallback));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { processSessionEnd } = await import('../dist/hooks/session-end/index.js');
|
|
const result = await processSessionEnd(frame.value);
|
|
console.log(JSON.stringify(result));
|
|
} catch (error) {
|
|
console.error('[session-end] Error:', error.message);
|
|
console.log(JSON.stringify(fallback));
|
|
}
|
|
}
|
|
|
|
if (!isMainThread || (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))) void runSessionEndHook();
|