claude-code-session-manager 0.87.0 → 0.88.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.md +26 -53
- package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-BZ6IY2g1.js} +1 -1
- package/dist/assets/{DataModel--mISIJ6h.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-C2ahUXTg.js → History-CF157HyX.js} +1 -1
- package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CCPPipBk.js} +1 -1
- package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-Dc8Qst1R.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-DTg3JlOT.js} +1 -1
- package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-DdjY1sA3.js} +1 -1
- package/dist/assets/{Memory-CW62MXlh.js → Memory-BJ3Jlw0C.js} +1 -1
- package/dist/assets/{Panel-Bw1FhRuF.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-m0KAnccF.js} +1 -1
- package/dist/assets/{Plugins-BnKx9flD.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-D2ppxSfy.js} +7 -7
- package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/{Settings-DluB-vN1.js → Settings-BHSGGFEc.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-gNdo_HNK.js → Skills-Dl3q75QV.js} +1 -1
- package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-fc_JjdxL.js → index-B3emy2oI.js} +356 -356
- package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-CdzxaqwM.js} +1 -1
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +10 -13
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +28 -71
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queueHistory.test.cjs +2 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +4 -2
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +267 -51
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +1 -1
- package/src/main/index.cjs +61 -10
- package/src/main/ipcSchemas.cjs +6 -17
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/src/main/lib/activeSessions.cjs +20 -88
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +33 -1
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +9 -10
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/gitWorktree.cjs +95 -8
- package/src/main/lib/guardShims.cjs +3 -3
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +29 -77
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdLocations.cjs +48 -1
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/queueHistory.cjs +8 -7
- package/src/main/lib/queueStore.cjs +57 -37
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +2 -2
- package/src/main/lib/scheduleJobTransitions.cjs +29 -2
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +53 -6
- package/src/main/lib/watchdogHelpers.cjs +70 -37
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler.cjs +1598 -381
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/usage.cjs +126 -62
- package/src/preload/api.d.ts +18 -64
- package/src/preload/index.cjs +2 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
- package/web/README.md +0 -41
- package/web/project-pages/logic/dist/logic.cjs +0 -4709
- package/web/project-pages/render.cjs +0 -70
- package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
- package/web/project-pages/validate-summary.cjs +0 -62
|
@@ -31,14 +31,13 @@
|
|
|
31
31
|
const fs = require('node:fs');
|
|
32
32
|
const fsp = require('node:fs/promises');
|
|
33
33
|
const path = require('node:path');
|
|
34
|
-
const os = require('node:os');
|
|
35
34
|
const crypto = require('node:crypto');
|
|
36
35
|
const { allProjectCwds, activeProjectCwds, bustProjectCwdCache } = require('./activeSessions.cjs');
|
|
36
|
+
const { classifyCwd } = require('./cwdClassify.cjs');
|
|
37
37
|
const { assertOpsWrite, resolveOpsRoot, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
38
38
|
const { ScheduleJobSchema } = require('./scheduleJobSchema.cjs');
|
|
39
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
39
40
|
|
|
40
|
-
const MACHINE_STATE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduler-machine.json');
|
|
41
|
-
const LEGACY_QUEUE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'queue.json');
|
|
42
41
|
const OPS_DIRNAME = OPS_ROOT_DIR;
|
|
43
42
|
const STATE_SUBPATH = ['scheduler', 'state'];
|
|
44
43
|
|
|
@@ -118,7 +117,12 @@ function projectHistoryPath(cwd) {
|
|
|
118
117
|
*/
|
|
119
118
|
function queuePathOrSkip(cwd, context) {
|
|
120
119
|
try {
|
|
121
|
-
|
|
120
|
+
const file = projectQueuePath(cwd);
|
|
121
|
+
// Unprovable project identity: the row stays dispatchable and is never
|
|
122
|
+
// dropped or emptied — the write itself is refused by assertOpsWrite.
|
|
123
|
+
const { kind, reason } = classifyCwd(cwd);
|
|
124
|
+
if (kind === 'unknown') console.warn(`[queueStore] ${context}: project identity unprovable for "${cwd}" (${reason}) — reads allowed, writes refused`);
|
|
125
|
+
return file;
|
|
122
126
|
} catch (e) {
|
|
123
127
|
if (e?.ephemeral) {
|
|
124
128
|
console.warn(`[queueStore] ${context}: refusing ephemeral cwd "${cwd}" — ${e.message}`);
|
|
@@ -190,8 +194,10 @@ async function writeJsonAtomic(file, value) {
|
|
|
190
194
|
// list briefly. Correctness fallback: a brand-new project appears at worst
|
|
191
195
|
// CACHE_MS late, and its first write goes through writeSplit which busts the
|
|
192
196
|
// cache.
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
// Above the 60 s dispatch-loop interval so one pass never re-scans cold; the
|
|
198
|
+
// cached array is frozen (shared, never mutated).
|
|
199
|
+
const CACHE_MS = 120_000;
|
|
200
|
+
let cwdCache = { at: 0, cwds: Object.freeze([]) };
|
|
195
201
|
|
|
196
202
|
function stateCwds(opts) {
|
|
197
203
|
const now = Date.now();
|
|
@@ -205,12 +211,13 @@ function stateCwds(opts) {
|
|
|
205
211
|
}
|
|
206
212
|
// ...and active projects are included even before their first write.
|
|
207
213
|
for (const cwd of activeProjectCwds(undefined, opts)) add(cwd);
|
|
214
|
+
Object.freeze(cwds);
|
|
208
215
|
if (!opts) cwdCache = { at: now, cwds };
|
|
209
216
|
return cwds;
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
function bustCwdCache() {
|
|
213
|
-
cwdCache = { at: 0, cwds: [] };
|
|
220
|
+
cwdCache = { at: 0, cwds: Object.freeze([]) };
|
|
214
221
|
// Without this chain, activeSessions' own scan cache hands stateCwds a
|
|
215
222
|
// stale list even after this bust — turning a caller's "I just wrote a
|
|
216
223
|
// brand-new project's first state file, make it visible now" into a
|
|
@@ -231,6 +238,10 @@ function shapeMachine(data) {
|
|
|
231
238
|
// classifyQueueStarvation's header for why the two must never merge.
|
|
232
239
|
lastDispatchAttemptAt: data.lastDispatchAttemptAt ?? null,
|
|
233
240
|
paused: data.paused ?? null,
|
|
241
|
+
// Upgrade-drain state (lib/upgradeDrain.cjs): deliberately NOT part of
|
|
242
|
+
// `paused`, so a rate-limit pause can neither overwrite it nor have the
|
|
243
|
+
// manual-clear cooldown swallow it.
|
|
244
|
+
drain: data.drain && typeof data.drain === 'object' ? data.drain : null,
|
|
234
245
|
// Launch circuit breaker (lib/launchFailure.cjs): per-persona blocks and
|
|
235
246
|
// the degraded-mode env a persona is currently launching with. Machine
|
|
236
247
|
// state, not per-project: the broken thing is the installed CLI.
|
|
@@ -291,7 +302,6 @@ function findLongestValidJsonPrefix(raw) {
|
|
|
291
302
|
// project's own ops root is the natural home for a machine-level log entry,
|
|
292
303
|
// mirroring scheduler.cjs's own `job.cwd || DEFAULT_PROJECT_CWD` fallback for
|
|
293
304
|
// cwd-less machine errors (schedulerBatch.cjs's DEFAULT_PROJECT_CWD).
|
|
294
|
-
const MACHINE_STATE_LOG_CWD = path.join(os.homedir(), 'Projects', 'session-manager');
|
|
295
305
|
|
|
296
306
|
// The truncated tail in the real 2026-09-11 tear contained an orphaned
|
|
297
307
|
// `resumeAt` fragment with no way to reconstruct the `paused` object it
|
|
@@ -306,7 +316,7 @@ const PAUSED_HINT_RE = /"paused"|resumeAt/;
|
|
|
306
316
|
function logMachineStateRecovery({ level, message, meta }) {
|
|
307
317
|
try {
|
|
308
318
|
const { appendError } = require('./opsErrorLog.cjs');
|
|
309
|
-
appendError({ cwd:
|
|
319
|
+
appendError({ cwd: schedulerPaths.machineStateLogCwd(), scope: 'scheduler', level, message, meta });
|
|
310
320
|
} catch { /* durable logging must never block recovery */ }
|
|
311
321
|
console.error(`[queueStore] ${message}`);
|
|
312
322
|
}
|
|
@@ -331,7 +341,7 @@ function buildMachineStateRecovery(raw, parseError) {
|
|
|
331
341
|
? '; the discarded tail looks like it contained paused/resumeAt data that could not '
|
|
332
342
|
+ 'be reconstructed — verify pause status manually'
|
|
333
343
|
: ''),
|
|
334
|
-
meta: { path:
|
|
344
|
+
meta: { path: schedulerPaths.machineStatePath(), prefixLength: prefix.prefixLength, totalLength: raw.length, pausedHint },
|
|
335
345
|
};
|
|
336
346
|
}
|
|
337
347
|
// No valid JSON prefix at all: fall back to defaults (shapeMachine({}) —
|
|
@@ -344,7 +354,7 @@ function buildMachineStateRecovery(raw, parseError) {
|
|
|
344
354
|
message:
|
|
345
355
|
`scheduler-machine.json unrecoverable (${parseError?.message}) — falling back to defaults `
|
|
346
356
|
+ 'so dispatch does not silently halt',
|
|
347
|
-
meta: { path:
|
|
357
|
+
meta: { path: schedulerPaths.machineStatePath(), totalLength: raw.length },
|
|
348
358
|
};
|
|
349
359
|
}
|
|
350
360
|
|
|
@@ -352,7 +362,7 @@ function recoverTornMachineStateSync(raw, parseError) {
|
|
|
352
362
|
const recovery = buildMachineStateRecovery(raw, parseError);
|
|
353
363
|
logMachineStateRecovery(recovery);
|
|
354
364
|
try {
|
|
355
|
-
writeJsonAtomicSync(
|
|
365
|
+
writeJsonAtomicSync(schedulerPaths.machineStatePath(), recovery.value);
|
|
356
366
|
} catch (e) {
|
|
357
367
|
console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
|
|
358
368
|
}
|
|
@@ -363,7 +373,7 @@ async function recoverTornMachineState(raw, parseError) {
|
|
|
363
373
|
const recovery = buildMachineStateRecovery(raw, parseError);
|
|
364
374
|
logMachineStateRecovery(recovery);
|
|
365
375
|
try {
|
|
366
|
-
await writeJsonAtomic(
|
|
376
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), recovery.value);
|
|
367
377
|
} catch (e) {
|
|
368
378
|
console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
|
|
369
379
|
}
|
|
@@ -378,10 +388,10 @@ async function recoverTornMachineState(raw, parseError) {
|
|
|
378
388
|
function loadMachineStateSync() {
|
|
379
389
|
let raw;
|
|
380
390
|
try {
|
|
381
|
-
raw = fs.readFileSync(
|
|
391
|
+
raw = fs.readFileSync(schedulerPaths.machineStatePath(), 'utf8');
|
|
382
392
|
} catch (e) {
|
|
383
393
|
if (e?.code === 'ENOENT') return {};
|
|
384
|
-
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath:
|
|
394
|
+
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
|
|
385
395
|
}
|
|
386
396
|
try {
|
|
387
397
|
return { shaped: shapeMachine(JSON.parse(raw)) };
|
|
@@ -394,10 +404,10 @@ function loadMachineStateSync() {
|
|
|
394
404
|
async function loadMachineState() {
|
|
395
405
|
let raw;
|
|
396
406
|
try {
|
|
397
|
-
raw = await fsp.readFile(
|
|
407
|
+
raw = await fsp.readFile(schedulerPaths.machineStatePath(), 'utf8');
|
|
398
408
|
} catch (e) {
|
|
399
409
|
if (e?.code === 'ENOENT') return {};
|
|
400
|
-
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath:
|
|
410
|
+
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
|
|
401
411
|
}
|
|
402
412
|
try {
|
|
403
413
|
return { shaped: shapeMachine(JSON.parse(raw)) };
|
|
@@ -442,15 +452,17 @@ function shapeJobs(raw, file) {
|
|
|
442
452
|
|
|
443
453
|
/**
|
|
444
454
|
* readMergedSync(opts?) → { config, jobs, scheduledFor, lastRunAt, paused,
|
|
445
|
-
* unreadable?, unreadablePath?, sourceCwds }.
|
|
455
|
+
* unreadable?, unreadablePath?, unreadableCwds, sourceCwds }.
|
|
446
456
|
*
|
|
447
|
-
* `unreadable`
|
|
448
|
-
* exists but fails to parse
|
|
449
|
-
*
|
|
450
|
-
*
|
|
457
|
+
* `unreadable` is reserved for the machine-runtime file alone (a machine-wide
|
|
458
|
+
* halt). A project shard that exists but fails to read/parse is quarantined
|
|
459
|
+
* per cwd in `unreadableCwds: [{cwd, file, error}]` — other projects keep
|
|
460
|
+
* dispatching, and the torn shard is never treated as empty, recovered, or
|
|
461
|
+
* written. `sourceCwds` records every healthy project file consulted so
|
|
462
|
+
* writeSplit can persist "this project now has zero jobs".
|
|
451
463
|
*/
|
|
452
464
|
function readMergedSync(opts) {
|
|
453
|
-
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
|
|
465
|
+
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
|
|
454
466
|
const sourceCwds = [];
|
|
455
467
|
const machine = loadMachineStateSync();
|
|
456
468
|
if (machine.shaped) {
|
|
@@ -473,8 +485,9 @@ function readMergedSync(opts) {
|
|
|
473
485
|
sourceCwds.push(cwd);
|
|
474
486
|
} catch (e) {
|
|
475
487
|
if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
|
|
476
|
-
|
|
477
|
-
|
|
488
|
+
// Quarantine this ONE shard; never recover or rewrite it (a job shard is a
|
|
489
|
+
// ledger — a prefix could time-travel a running row back to pending).
|
|
490
|
+
out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
|
|
478
491
|
}
|
|
479
492
|
}
|
|
480
493
|
defineSources(out, sourceCwds);
|
|
@@ -483,7 +496,7 @@ function readMergedSync(opts) {
|
|
|
483
496
|
|
|
484
497
|
/** Async twin of readMergedSync for IPC hot paths. */
|
|
485
498
|
async function readMerged(opts) {
|
|
486
|
-
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
|
|
499
|
+
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
|
|
487
500
|
const sourceCwds = [];
|
|
488
501
|
const machine = await loadMachineState();
|
|
489
502
|
if (machine.shaped) {
|
|
@@ -506,8 +519,9 @@ async function readMerged(opts) {
|
|
|
506
519
|
sourceCwds.push(cwd);
|
|
507
520
|
} catch (e) {
|
|
508
521
|
if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
|
|
509
|
-
|
|
510
|
-
|
|
522
|
+
// Quarantine this ONE shard; never recover or rewrite it (a job shard is a
|
|
523
|
+
// ledger — a prefix could time-travel a running row back to pending).
|
|
524
|
+
out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
|
|
511
525
|
}
|
|
512
526
|
}
|
|
513
527
|
defineSources(out, sourceCwds);
|
|
@@ -531,21 +545,25 @@ function defineSources(state, sourceCwds) {
|
|
|
531
545
|
* deletions stick.
|
|
532
546
|
*/
|
|
533
547
|
async function writeSplit(state, defaultCwd) {
|
|
534
|
-
await writeJsonAtomic(
|
|
548
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), {
|
|
535
549
|
config: state.config,
|
|
536
550
|
scheduledFor: state.scheduledFor ?? null,
|
|
537
551
|
lastRunAt: state.lastRunAt ?? null,
|
|
538
552
|
lastDispatchAttemptAt: state.lastDispatchAttemptAt ?? null,
|
|
539
553
|
paused: state.paused ?? null,
|
|
554
|
+
drain: state.drain ?? null,
|
|
540
555
|
launchBlocks: state.launchBlocks ?? {},
|
|
541
556
|
launchMitigations: state.launchMitigations ?? {},
|
|
542
557
|
});
|
|
543
558
|
|
|
559
|
+
// A quarantined shard is left byte-identical: never re-created or clobbered,
|
|
560
|
+
// even if merged rows carry that cwd.
|
|
561
|
+
const quarantined = new Set((state.unreadableCwds ?? []).map((u) => u.cwd));
|
|
544
562
|
const byCwd = new Map();
|
|
545
|
-
for (const cwd of state.sourceCwds ?? []) byCwd.set(cwd, []);
|
|
563
|
+
for (const cwd of state.sourceCwds ?? []) if (!quarantined.has(cwd)) byCwd.set(cwd, []);
|
|
546
564
|
for (const job of state.jobs ?? []) {
|
|
547
565
|
const cwd = job.cwd || defaultCwd;
|
|
548
|
-
if (!cwd) continue; // nowhere to put it; job is dropped from persistence rather than crashing
|
|
566
|
+
if (!cwd || quarantined.has(cwd)) continue; // nowhere to put it; job is dropped from persistence rather than crashing
|
|
549
567
|
if (!byCwd.has(cwd)) byCwd.set(cwd, []);
|
|
550
568
|
byCwd.get(cwd).push(job);
|
|
551
569
|
}
|
|
@@ -576,7 +594,7 @@ async function writeSplit(state, defaultCwd) {
|
|
|
576
594
|
async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
577
595
|
let raw;
|
|
578
596
|
try {
|
|
579
|
-
raw = await fsp.readFile(
|
|
597
|
+
raw = await fsp.readFile(schedulerPaths.legacyQueuePath(), 'utf8');
|
|
580
598
|
} catch {
|
|
581
599
|
return { migrated: false };
|
|
582
600
|
}
|
|
@@ -588,8 +606,8 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
|
588
606
|
return { migrated: false, error: e?.message };
|
|
589
607
|
}
|
|
590
608
|
|
|
591
|
-
if (!fs.existsSync(
|
|
592
|
-
await writeJsonAtomic(
|
|
609
|
+
if (!fs.existsSync(schedulerPaths.machineStatePath())) {
|
|
610
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), {
|
|
593
611
|
config: legacy.config || {},
|
|
594
612
|
scheduledFor: legacy.scheduledFor ?? null,
|
|
595
613
|
lastRunAt: legacy.lastRunAt ?? null,
|
|
@@ -622,14 +640,12 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
|
622
640
|
}
|
|
623
641
|
}
|
|
624
642
|
|
|
625
|
-
await fsp.rename(
|
|
643
|
+
await fsp.rename(schedulerPaths.legacyQueuePath(), `${schedulerPaths.legacyQueuePath()}.retired-${Date.now()}`);
|
|
626
644
|
bustCwdCache();
|
|
627
645
|
return { migrated: true, moved, projects: byCwd.size };
|
|
628
646
|
}
|
|
629
647
|
|
|
630
648
|
module.exports = {
|
|
631
|
-
MACHINE_STATE_PATH,
|
|
632
|
-
LEGACY_QUEUE_PATH,
|
|
633
649
|
STATE_SUBPATH,
|
|
634
650
|
projectStateDir,
|
|
635
651
|
projectQueuePath,
|
|
@@ -645,3 +661,7 @@ module.exports = {
|
|
|
645
661
|
writeJsonAtomicSync,
|
|
646
662
|
findLongestValidJsonPrefix,
|
|
647
663
|
};
|
|
664
|
+
|
|
665
|
+
// Lazy path getters (SM_SCHEDULER_HOME resolved at read, never at require).
|
|
666
|
+
Object.defineProperty(module.exports, 'MACHINE_STATE_PATH', { get: schedulerPaths.machineStatePath, enumerable: true });
|
|
667
|
+
Object.defineProperty(module.exports, 'LEGACY_QUEUE_PATH', { get: schedulerPaths.legacyQueuePath, enumerable: true });
|
|
@@ -12,17 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
+
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
16
|
+
const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
|
|
17
|
+
|
|
15
18
|
// slug of the job currently holding the lease, or null.
|
|
16
19
|
let heldBySlug = null;
|
|
20
|
+
let heldClaimedAt = 0;
|
|
17
21
|
|
|
18
22
|
function isHeld() {
|
|
19
23
|
return heldBySlug !== null;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** acquire(slug) → true if the lease was free and is now held by `slug`. */
|
|
23
|
-
function acquire(slug) {
|
|
27
|
+
function acquire(slug, { claimedAt = Date.now() } = {}) {
|
|
24
28
|
if (heldBySlug !== null) return false;
|
|
25
29
|
heldBySlug = String(slug);
|
|
30
|
+
heldClaimedAt = claimedAt;
|
|
26
31
|
return true;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -36,6 +41,17 @@ function release(slug) {
|
|
|
36
41
|
return true;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
/** expireDead — free a lease whose holder has no running row and is past the grace window
|
|
45
|
+
* (the lease never carries a pid; the row-liveness + grace proof is the whole test). */
|
|
46
|
+
function expireDead({ liveSlugs, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
|
|
47
|
+
if (heldBySlug === null) return false;
|
|
48
|
+
const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
|
|
49
|
+
if (!isProvablyDead({ claimedAt: heldClaimedAt, pid: null }, { live: live.has(heldBySlug), now, graceMs })) return false;
|
|
50
|
+
appendAuditEvent('quiet_lease_expired', { slug: heldBySlug });
|
|
51
|
+
heldBySlug = null;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
function holder() {
|
|
40
56
|
return heldBySlug;
|
|
41
57
|
}
|
|
@@ -43,6 +59,7 @@ function holder() {
|
|
|
43
59
|
/** Test hook: force the lease back to free. */
|
|
44
60
|
function __resetForTests() {
|
|
45
61
|
heldBySlug = null;
|
|
62
|
+
heldClaimedAt = 0;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
module.exports = { isHeld, acquire, release, holder, __resetForTests };
|
|
65
|
+
module.exports = { isHeld, acquire, release, expireDead, holder, __resetForTests };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reservationExpiry.cjs — the single fail-CLOSED "is this reservation's owner
|
|
3
|
+
* provably dead?" predicate shared by sessionSlots, quietMachineLease, the
|
|
4
|
+
* investigation set and gitWorktree's accounting. A reservation is released
|
|
5
|
+
* only on proof: no live row for its slug AND (a stamped pid that is dead, or
|
|
6
|
+
* no pid ever stamped and claimedAt older than graceMs). Anything ambiguous
|
|
7
|
+
* (live row, live pid, inside the grace window, claimed after the pass began)
|
|
8
|
+
* is kept. Never signals a process.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_GRACE_MS = 10 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function defaultPidAlive(pid) {
|
|
15
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e?.code === 'EPERM'; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ claimedAt: number, pid: number|null }} res
|
|
20
|
+
* @param {{ live: boolean, pidAlive?: (pid:number)=>boolean, now: number, graceMs?: number }} ctx
|
|
21
|
+
*/
|
|
22
|
+
function isProvablyDead(res, { live, pidAlive = defaultPidAlive, now, graceMs = DEFAULT_GRACE_MS }) {
|
|
23
|
+
if (live) return false;
|
|
24
|
+
// Made after the pass began (i.e. this tick) — never expire it.
|
|
25
|
+
if (!Number.isFinite(res.claimedAt) || res.claimedAt > now) return false;
|
|
26
|
+
if (Number.isInteger(res.pid) && res.pid > 0) return !pidAlive(res.pid);
|
|
27
|
+
return now - res.claimedAt > graceMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { isProvablyDead, defaultPidAlive, DEFAULT_GRACE_MS };
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { spawn } = require('node:child_process');
|
|
18
|
-
const { resolveClaudeBin } = require('./claudeBin.cjs');
|
|
18
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
|
|
19
|
+
const { withProcRole } = require('./cleanEnv.cjs');
|
|
19
20
|
|
|
20
21
|
const MAX_OUT_BYTES = 8 * 1024 * 1024;
|
|
21
22
|
|
|
@@ -31,7 +32,8 @@ function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, systemPromp
|
|
|
31
32
|
'--output-format', 'text',
|
|
32
33
|
];
|
|
33
34
|
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
34
|
-
const
|
|
35
|
+
const target = claudeSpawnTarget('aux', 'runclaudep', bin);
|
|
36
|
+
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
35
37
|
let out = '';
|
|
36
38
|
let err = '';
|
|
37
39
|
let killedForSize = false;
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Directory shape: one subdirectory per tick (`RUNS_DIR/<iso-ts>/`, minted by
|
|
14
14
|
* scheduler.cjs's pickRunDir), which commonly holds MANY different PRD
|
|
15
|
-
* slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`,
|
|
15
|
+
* slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`, the
|
|
16
|
+
* `<slug>.supervisor.json` dispatch record (jobSupervisorRecord.cjs), and an
|
|
16
17
|
* optional `root-cause-<slug>.md` (rcaReport.cjs) per slug. Retention is
|
|
17
18
|
* therefore computed per (runId, slug) ENTRY, not per directory: a directory
|
|
18
19
|
* is only "fully removable" once every entry it holds is independently
|
|
@@ -131,7 +132,7 @@ function scanRunEntries(runsDir) {
|
|
|
131
132
|
for (const metaFile of metaFiles) {
|
|
132
133
|
const slug = metaFile.slice(0, -META_SUFFIX.length);
|
|
133
134
|
const relatedNames = files.filter(
|
|
134
|
-
(f) => f === metaFile || f === `${slug}.log` || f === `root-cause-${slug}.md`
|
|
135
|
+
(f) => f === metaFile || f === `${slug}.log` || f === `${slug}.supervisor.json` || f === `root-cause-${slug}.md`
|
|
135
136
|
);
|
|
136
137
|
|
|
137
138
|
let sizeBytes = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canonical runtime schema for a ScheduleJob record, mirroring the TS
|
|
3
|
-
* `ScheduleJob` interface
|
|
3
|
+
* `ScheduleJob` interface (`ScheduleJob` in src/preload/api.d.ts) field-for-field.
|
|
4
4
|
*
|
|
5
5
|
* Modelled directly on promptSessionSchema.cjs's "zod schema asserted at the
|
|
6
6
|
* main-process boundary" pattern: the Epic entity has had this guarantee
|
|
@@ -52,7 +52,7 @@ const ScheduleJobStatusHistoryEntrySchema = z
|
|
|
52
52
|
.passthrough();
|
|
53
53
|
|
|
54
54
|
// Only `slug` and `status` are required. Every other ScheduleJob field
|
|
55
|
-
// (src/preload/api.d.ts
|
|
55
|
+
// (`ScheduleJob` in src/preload/api.d.ts) is declared here for documentation and type
|
|
56
56
|
// checking when present, but kept optional: a job row moves through several
|
|
57
57
|
// legitimate partial shapes across its life (freshly minted with nulls,
|
|
58
58
|
// mid-run with runtime fields, terminal with exitCode/error) and plenty of
|
|
@@ -207,7 +207,23 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
|
|
|
207
207
|
// Fire-and-forget, exactly like every other best-effort side-channel write
|
|
208
208
|
// in this codebase (rcaReport.cjs, auditLog.cjs's own posture) — a ledger
|
|
209
209
|
// write failure must never fail or delay the transition it's recording.
|
|
210
|
-
|
|
210
|
+
// 'investigating' is a PAUSE, not a resolution, when it's reached FROM
|
|
211
|
+
// needs_review: spawnInvestigation always restores the job to exactly the
|
|
212
|
+
// status it left (a snapshot taken before the pause), so
|
|
213
|
+
// needs_review -> investigating -> needs_review is one continuous episode,
|
|
214
|
+
// not two. Without this check, the pause mints a spurious 'auto-fix'
|
|
215
|
+
// resolution mid-episode, then the resume mints a spurious fresh entry
|
|
216
|
+
// that overwrites the original episode's runId/enteredAt/reason in the
|
|
217
|
+
// ledger's by-key maps (keyed on slug+runId) — corrupting byLadderRung and
|
|
218
|
+
// understating dwell time. Detected via the stamped episode fields
|
|
219
|
+
// themselves rather than the transition's source string, since the same
|
|
220
|
+
// 'investigating' status is also reached fresh from 'failed' (a job that
|
|
221
|
+
// was never parked needs_review at all, and has no episode to pause).
|
|
222
|
+
const isNeedsReviewPause = from === 'needs_review' && toStatus === 'investigating';
|
|
223
|
+
const pausedEpisodeOpen = job.needsReviewEntryRunId !== undefined;
|
|
224
|
+
const isNeedsReviewResume = from === 'investigating' && pausedEpisodeOpen;
|
|
225
|
+
|
|
226
|
+
if (toStatus === 'needs_review' && from !== 'needs_review' && !isNeedsReviewResume) {
|
|
211
227
|
// ENTRY: stamp the episode's runId/enteredAt on the job itself so the
|
|
212
228
|
// RESOLUTION line (below) can reference the SAME runId even if the job's
|
|
213
229
|
// own `runId` is reassigned in between (e.g. a resume-recovery
|
|
@@ -217,9 +233,17 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
|
|
|
217
233
|
queueHistory.appendHistory([buildNeedsReviewEntryLine(job, entry)]).catch((e) => {
|
|
218
234
|
console.error('[scheduleJobTransitions] failed to append needs_review entry', e?.message ?? String(e));
|
|
219
235
|
});
|
|
220
|
-
} else if (
|
|
236
|
+
} else if (
|
|
237
|
+
(from === 'needs_review' && toStatus !== 'needs_review' && !isNeedsReviewPause)
|
|
238
|
+
|| (isNeedsReviewResume && toStatus !== 'needs_review')
|
|
239
|
+
) {
|
|
221
240
|
// RESOLUTION: read the episode stamped at entry, then clear it — the
|
|
222
241
|
// episode is over regardless of what this resolution line looks like.
|
|
242
|
+
// The `isNeedsReviewResume` arm covers an episode that paused for
|
|
243
|
+
// investigation and is now resolving to something other than
|
|
244
|
+
// needs_review (e.g. the investigation actually fixed it) — the episode
|
|
245
|
+
// stamps were deliberately left untouched across the pause, so this
|
|
246
|
+
// still references the ORIGINAL entry time, not the pause's.
|
|
223
247
|
const episode = { runId: job.needsReviewEntryRunId ?? null, enteredAt: job.needsReviewEnteredAt ?? null };
|
|
224
248
|
delete job.needsReviewEntryRunId;
|
|
225
249
|
delete job.needsReviewEnteredAt;
|
|
@@ -227,6 +251,9 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
|
|
|
227
251
|
console.error('[scheduleJobTransitions] failed to append needs_review resolution', e?.message ?? String(e));
|
|
228
252
|
});
|
|
229
253
|
}
|
|
254
|
+
// else: isNeedsReviewPause (needs_review -> investigating), or
|
|
255
|
+
// isNeedsReviewResume && toStatus === 'needs_review' (investigating ->
|
|
256
|
+
// needs_review, still the same open episode) — intentionally a no-op.
|
|
230
257
|
|
|
231
258
|
return true;
|
|
232
259
|
}
|
|
@@ -75,7 +75,7 @@ function dedupeCycles(cycles) {
|
|
|
75
75
|
return out;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function
|
|
78
|
+
function makeDepResolver(projectJobs, satisfiedSlugs = new Set()) {
|
|
79
79
|
const rowBySlug = new Map(projectJobs.map((j) => [j.slug, j]));
|
|
80
80
|
const rowsByBareSlug = new Map();
|
|
81
81
|
for (const j of projectJobs) {
|
|
@@ -102,6 +102,31 @@ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
|
102
102
|
}
|
|
103
103
|
return satisfiedBareSlugs.has(bareSlug(slug));
|
|
104
104
|
};
|
|
105
|
+
return { rowsForDep, isKnownSatisfied };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* findUnresolvableDepRoots(projectJobs, satisfiedSlugs) → string[]
|
|
110
|
+
*
|
|
111
|
+
* Pure. The dep slugs that name NO live row and NO completion/archive record
|
|
112
|
+
* — the roots findBlockingDep holds as 'unresolved'. Unlike a failed/pending
|
|
113
|
+
* dep row, nothing in the queue will ever change one of these; only a human
|
|
114
|
+
* (or a restored record) can. Sorted, de-duplicated. O(jobs + deps).
|
|
115
|
+
*/
|
|
116
|
+
function findUnresolvableDepRoots(projectJobs, satisfiedSlugs = new Set()) {
|
|
117
|
+
const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
|
|
118
|
+
const roots = new Set();
|
|
119
|
+
for (const j of projectJobs) {
|
|
120
|
+
if (j.status !== 'pending') continue;
|
|
121
|
+
for (const dep of j.dependsOn ?? []) {
|
|
122
|
+
if (rowsForDep(dep).length === 0 && !isKnownSatisfied(dep)) roots.add(dep);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [...roots].sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
129
|
+
const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
|
|
105
130
|
// A dep row is blocking unless it's 'completed', OR it's a 'skipped' row
|
|
106
131
|
// stamped `needsReviewAutoResolvedSkip` — the bounded needs_review
|
|
107
132
|
// auto-resolve terminal decision (scheduler.cjs's
|
|
@@ -658,6 +683,6 @@ function findStarvedProjects(jobs, now, thresholdMs) {
|
|
|
658
683
|
}
|
|
659
684
|
|
|
660
685
|
module.exports = {
|
|
661
|
-
pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, DEFAULT_PROJECT_CWD,
|
|
686
|
+
pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD,
|
|
662
687
|
DEP_HISTORY_FAIL_OPEN,
|
|
663
688
|
};
|