claude-code-session-manager 0.111.0 → 0.113.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/.claude-plugin/marketplace.json +2 -2
- package/dist/assets/{AgentLibrary-rBO1daqM.js → AgentLibrary-wvrcLGGS.js} +2 -2
- package/dist/assets/{Button-DsEYRZg0.js → Button-B6h0F0XT.js} +1 -1
- package/dist/assets/{DataModel-D8sbvvBC.js → DataModel-C5pW9E3u.js} +1 -1
- package/dist/assets/{History-BEHLtxia.js → History-BV4e8YOM.js} +2 -2
- package/dist/assets/{Hooks-CKpvlXrg.js → Hooks-CrFSv4x1.js} +3 -3
- package/dist/assets/{Library-CeF4qh9d.js → Library-D-GIt3EY.js} +5 -5
- package/dist/assets/{ListDetail-DcfmxPFk.js → ListDetail-Bco_wfs6.js} +1 -1
- package/dist/assets/{MarkdownEditor-CvK1pyto.js → MarkdownEditor-C-yHSXNV.js} +1 -1
- package/dist/assets/{McpServers-D-h2ihzJ.js → McpServers-CfDekadG.js} +2 -2
- package/dist/assets/{Memory-BnF89x8v.js → Memory-CGGLexkR.js} +4 -4
- package/dist/assets/{Panel-CQY5zano.js → Panel-DGMEuYYG.js} +1 -1
- package/dist/assets/{Permissions-DLukmmsz.js → Permissions-Cx2zjtYZ.js} +3 -3
- package/dist/assets/{Plugins-0Pf0OZ4l.js → Plugins-Bc_W3IyR.js} +2 -2
- package/dist/assets/ProvenanceBadge-DiKAUR1u.js +1 -0
- package/dist/assets/RemoteMachines-8_77-WPD.js +3 -0
- package/dist/assets/{SaveBar-DjijiUb0.js → SaveBar-Blpzdq1R.js} +1 -1
- package/dist/assets/{Scheduler-Cm3EXZvE.js → Scheduler-Bd70AM4X.js} +9 -9
- package/dist/assets/{ScopeSwitcher-B-P-kSfq.js → ScopeSwitcher-wxN0h4YW.js} +1 -1
- package/dist/assets/Settings-CQ7DA3zW.js +4 -0
- package/dist/assets/{SkillReferenceGraph-Epv2GJmj.js → SkillReferenceGraph-DH1NLOiS.js} +1 -1
- package/dist/assets/Skills-CmyRyu4_.js +2 -0
- package/dist/assets/SystemPrompt-KAc3UMCd.js +1 -0
- package/dist/assets/TagLibrary-C8EvHnpg.js +1 -0
- package/dist/assets/{TiptapBody-DAMO3DhO.js → TiptapBody-d423mH6D.js} +1 -1
- package/dist/assets/{Toggle-Ba_f7q_m.js → Toggle-D1wsGE9S.js} +1 -1
- package/dist/assets/{ViewTabs-CGX6MAhc.js → ViewTabs-CjHw3kGY.js} +1 -1
- package/dist/assets/{historyTotals-vgfk8EOL.js → historyTotals-DFHzqLt7.js} +1 -1
- package/dist/assets/{index-CPieAFsl.js → index-Ci1u018F.js} +266 -266
- package/dist/assets/{settingsSchema-D0FxNrTy.js → settingsSchema-Ctf3pZVI.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-DCu6BNYN.js → useAgentsDirWatch-B1NBmOAe.js} +1 -1
- package/dist/assets/{useRemotePeers-D4cdJDyh.js → useRemotePeers-BHjSJs9D.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +3 -2
- package/plugins/CLAUDE.md +5 -8
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -6
- package/plugins/session-manager-dev/skills/builder/1-classify-and-bump/SKILL.md +2 -2
- package/plugins/session-manager-dev/skills/builder/SKILL.md +2 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/develop/standards.md +3 -3
- package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +13 -0
- package/scripts/README.md +3 -3
- package/scripts/audit-ops-hygiene.cjs +4 -4
- package/scripts/hooks/guard-destructive-git.cjs +4 -4
- package/scripts/hooks/guard-inline-implementation.cjs +1 -1
- package/scripts/hooks/guard-prd-writes.cjs +2 -2
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -2
- package/scripts/scheduler-mcp-server.cjs +27 -9
- package/src/main/agentLibrary.cjs +5 -5
- package/src/main/agentsIpc.cjs +76 -0
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +30 -13
- package/src/main/config.cjs +20 -3
- package/src/main/crashDiagnostics.cjs +2 -2
- package/src/main/docEdit.cjs +5 -5
- package/src/main/files.cjs +2 -2
- package/src/main/git.cjs +1 -1
- package/src/main/health.cjs +20 -18
- package/src/main/heapSnapshot.cjs +1 -1
- package/src/main/historyAggregator.cjs +2 -2
- package/src/main/index.cjs +50 -132
- package/src/main/ipcSchemas.cjs +13 -10
- package/src/main/lib/CLAUDE.md +48 -0
- package/src/main/lib/activeSessions.cjs +14 -1
- package/src/main/lib/classifyPromptTicket.cjs +2 -2
- package/src/main/lib/{agentModelResolve.cjs → claude/agentModelResolve.cjs} +7 -7
- package/src/main/lib/{claudeBin.cjs → claude/claudeBin.cjs} +4 -4
- package/src/main/lib/{effectiveModelInfo.cjs → claude/effectiveModelInfo.cjs} +9 -9
- package/src/main/lib/{modelCatalog.cjs → claude/modelCatalog.cjs} +3 -3
- package/src/main/lib/claude/rateLimitDetect.cjs +54 -0
- package/src/main/lib/{rateLimitWindow.cjs → claude/rateLimitWindow.cjs} +24 -12
- package/src/main/lib/{runClaudeP.cjs → claude/runClaudeP.cjs} +1 -1
- package/src/main/lib/credentials.cjs +18 -92
- package/src/main/lib/cwdClassify.cjs +1 -1
- package/src/main/lib/{activeIndexMerge.cjs → epic/activeIndexMerge.cjs} +3 -3
- package/src/main/lib/{activeIndexRebuild.cjs → epic/activeIndexRebuild.cjs} +1 -1
- package/src/main/lib/{delegationReadiness.cjs → epic/delegationReadiness.cjs} +21 -29
- package/src/main/lib/{epicContextDigest.cjs → epic/epicContextDigest.cjs} +1 -1
- package/src/main/lib/{epicDelegationStats.cjs → epic/epicDelegationStats.cjs} +2 -2
- package/src/main/lib/{epicMint.cjs → epic/epicMint.cjs} +11 -11
- package/src/main/lib/{epicSpawnPlan.cjs → epic/epicSpawnPlan.cjs} +3 -3
- package/src/main/lib/{epicStatusMirror.cjs → epic/epicStatusMirror.cjs} +2 -2
- package/src/main/lib/{epicTranscriptDiagnostic.cjs → epic/epicTranscriptDiagnostic.cjs} +2 -2
- package/src/main/lib/{epicTranscriptPath.cjs → epic/epicTranscriptPath.cjs} +2 -2
- package/src/main/lib/{epicValidationHook.cjs → epic/epicValidationHook.cjs} +3 -3
- package/src/main/lib/{epicWorktreeBoot.cjs → epic/epicWorktreeBoot.cjs} +1 -1
- package/src/main/lib/{epicWorktreeMerge.cjs → epic/epicWorktreeMerge.cjs} +3 -3
- package/src/main/lib/{epicWorktreeMint.cjs → epic/epicWorktreeMint.cjs} +3 -3
- package/src/main/lib/{epicWorktreeProjectConfig.cjs → epic/epicWorktreeProjectConfig.cjs} +4 -4
- package/src/main/lib/{promptSessionsCreateEpic.cjs → epic/promptSessionsCreateEpic.cjs} +4 -4
- package/src/main/lib/{crossProjectFeedback.cjs → feedback-remote/crossProjectFeedback.cjs} +114 -16
- package/src/main/lib/{customerFeedbackClient.cjs → feedback-remote/customerFeedbackClient.cjs} +3 -3
- package/src/main/lib/{customerFeedbackInbox.cjs → feedback-remote/customerFeedbackInbox.cjs} +2 -2
- package/src/main/lib/{customerFeedbackIpc.cjs → feedback-remote/customerFeedbackIpc.cjs} +1 -1
- package/src/main/lib/feedback-remote/humanPromptLedger.cjs +93 -0
- package/src/main/lib/{localAdminHttp.cjs → feedback-remote/localAdminHttp.cjs} +2 -2
- package/src/main/lib/{macroAdminRoutes.cjs → feedback-remote/macroAdminRoutes.cjs} +1 -1
- package/src/main/lib/{projectHomeAdminRoutes.cjs → feedback-remote/projectHomeAdminRoutes.cjs} +4 -4
- package/src/main/lib/{relayClientAuth.cjs → feedback-remote/relayClientAuth.cjs} +2 -2
- package/src/main/lib/{remoteClientSessions.cjs → feedback-remote/remoteClientSessions.cjs} +1 -1
- package/src/main/lib/{gitWorktree.cjs → git/gitWorktree.cjs} +7 -7
- package/src/main/lib/historyRollup.cjs +1 -1
- package/src/main/lib/importReferences.cjs +1 -1
- package/src/main/lib/machineProfile.cjs +1 -1
- package/src/main/lib/mcpToolCatalog.cjs +9 -3
- package/src/main/lib/openExternalApp.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +3 -3
- package/src/main/lib/{agentPersonaSchema.cjs → persona-macro/agentPersonaSchema.cjs} +2 -2
- package/src/main/lib/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
- package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
- package/src/main/lib/{depSlugResolve.cjs → prd/depSlugResolve.cjs} +16 -1
- package/src/main/lib/{prdAdminRoutes.cjs → prd/prdAdminRoutes.cjs} +7 -7
- package/src/main/lib/{prdAgentType.cjs → prd/prdAgentType.cjs} +3 -3
- package/src/main/lib/{prdCreate.cjs → prd/prdCreate.cjs} +12 -12
- package/src/main/lib/{prdDeclaredPaths.cjs → prd/prdDeclaredPaths.cjs} +1 -1
- package/src/main/lib/{prdGateFiles.cjs → prd/prdGateFiles.cjs} +1 -1
- package/src/main/lib/{prdLocations.cjs → prd/prdLocations.cjs} +2 -2
- package/src/main/lib/{prdMigration.cjs → prd/prdMigration.cjs} +3 -3
- package/src/main/lib/prereqs.cjs +2 -2
- package/src/main/lib/{instanceLock.cjs → process/instanceLock.cjs} +4 -11
- package/src/main/lib/{pidAlive.cjs → process/pidAlive.cjs} +1 -1
- package/src/main/lib/{procName.cjs → process/procName.cjs} +2 -2
- package/src/main/lib/{timeoutShim.cjs → process/timeoutShim.cjs} +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/lib/{definitionOfDone.cjs → scheduler/definitionOfDone.cjs} +6 -12
- package/src/main/lib/scheduler/finalizeReason.cjs +30 -0
- package/src/main/lib/{fixChainDepth.cjs → scheduler/fixChainDepth.cjs} +1 -1
- package/src/main/lib/scheduler/flatSweepThrottle.cjs +17 -0
- package/src/main/lib/{jobDirtFilter.cjs → scheduler/jobDirtFilter.cjs} +1 -1
- package/src/main/lib/{jobSupervisorRecord.cjs → scheduler/jobSupervisorRecord.cjs} +1 -1
- package/src/main/lib/{launchFailure.cjs → scheduler/launchFailure.cjs} +2 -2
- package/src/main/lib/{queueStore.cjs → scheduler/queueStore.cjs} +6 -6
- package/src/main/lib/{quietMachineLease.cjs → scheduler/quietMachineLease.cjs} +1 -1
- package/src/main/lib/{rcaReport.cjs → scheduler/rcaReport.cjs} +4 -4
- package/src/main/lib/{reaperHelpers.cjs → scheduler/reaperHelpers.cjs} +3 -3
- package/src/main/lib/{reservationExpiry.cjs → scheduler/reservationExpiry.cjs} +4 -11
- package/src/main/lib/{reviewNotice.cjs → scheduler/reviewNotice.cjs} +1 -1
- package/src/main/lib/{runLogRetention.cjs → scheduler/runLogRetention.cjs} +34 -13
- package/src/main/lib/{scheduleJobTransitions.cjs → scheduler/scheduleJobTransitions.cjs} +3 -3
- package/src/main/lib/{schedulerBatch.cjs → scheduler/schedulerBatch.cjs} +9 -13
- package/src/main/lib/{schedulerConfig.cjs → scheduler/schedulerConfig.cjs} +1 -1
- package/src/main/lib/{schedulerRuntimeState.cjs → scheduler/schedulerRuntimeState.cjs} +1 -1
- package/src/main/lib/{sessionSlots.cjs → scheduler/sessionSlots.cjs} +2 -2
- package/src/main/lib/{upgradeDrain.cjs → scheduler/upgradeDrain.cjs} +8 -11
- package/src/main/lib/{watchdogHelpers.cjs → scheduler/watchdogHelpers.cjs} +5 -19
- package/src/main/lib/{auditLog.cjs → telemetry/auditLog.cjs} +1 -1
- package/src/main/lib/{opsErrorLog.cjs → telemetry/opsErrorLog.cjs} +4 -4
- package/src/main/lib/{telemetryBacklog.cjs → telemetry/telemetryBacklog.cjs} +4 -4
- package/src/main/lib/{telemetryBoot.cjs → telemetry/telemetryBoot.cjs} +1 -1
- package/src/main/lib/{telemetryClient.cjs → telemetry/telemetryClient.cjs} +12 -8
- package/src/main/lib/{telemetrySettings.cjs → telemetry/telemetrySettings.cjs} +1 -1
- package/src/main/lib/{atomicFs.cjs → util/atomicFs.cjs} +42 -0
- package/src/main/lib/{loopDelay.cjs → util/loopDelay.cjs} +1 -1
- package/src/main/logs.cjs +1 -1
- package/src/main/mcpStatus.cjs +2 -2
- package/src/main/memoryAggregate.cjs +3 -3
- package/src/main/memoryTool.cjs +1 -1
- package/src/main/otel.cjs +2 -2
- package/src/main/pluginInstall.cjs +29 -3
- package/src/main/projectBrief.cjs +6 -6
- package/src/main/projectPages.cjs +1 -1
- package/src/main/promptSessionEvents.cjs +2 -2
- package/src/main/pty.cjs +7 -7
- package/src/main/queueOps.cjs +6 -6
- package/src/main/remoteClient.cjs +4 -4
- package/src/main/remoteClientIpc.cjs +1 -1
- package/src/main/runVerify.cjs +6 -3
- package/src/main/scheduler/CLAUDE.md +26 -0
- package/src/main/scheduler/budget.cjs +576 -0
- package/src/main/scheduler/gitTreeGuard.cjs +672 -0
- package/src/main/scheduler/prdParser.cjs +4 -4
- package/src/main/scheduler/prdPaths.cjs +487 -0
- package/src/main/scheduler/reviewLadder.cjs +1011 -0
- package/src/main/scheduler/starvation.cjs +390 -0
- package/src/main/scheduler.cjs +331 -3320
- package/src/main/seedAgentPersonas.cjs +5 -5
- package/src/main/seedDevPlugin.cjs +70 -3
- package/src/main/seedSchedulerMcp.cjs +2 -2
- package/src/main/supervisor.cjs +50 -27
- package/src/main/telemetryIpc.cjs +51 -0
- package/src/main/transcripts.cjs +3 -3
- package/src/main/updaterIpc.cjs +1 -1
- package/src/main/usage.cjs +1 -1
- package/src/main/watchers.cjs +1 -1
- package/src/main/webRemote.cjs +22 -7
- package/src/preload/api.d.ts +14 -8
- package/src/preload/index.cjs +9 -3
- package/dist/assets/ProvenanceBadge-vC7Tj-JP.js +0 -1
- package/dist/assets/RemoteMachines-CWnKbFHc.js +0 -3
- package/dist/assets/Settings-DYDlp2m_.js +0 -4
- package/dist/assets/Skills-DHO0N0C2.js +0 -2
- package/dist/assets/SystemPrompt-DxeNofL9.js +0 -1
- package/dist/assets/TagLibrary-QOAs7ISO.js +0 -1
- package/plugins/session-manager-dev/skills/blog-for-project-feature/0-select/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/blog-for-project-feature/1-research/SKILL.md +0 -37
- package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +0 -64
- package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +0 -52
- package/plugins/session-manager-dev/skills/blog-for-project-feature/4-draft-copy/SKILL.md +0 -37
- package/plugins/session-manager-dev/skills/blog-for-project-feature/5-illustrate/SKILL.md +0 -50
- package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +0 -51
- package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +0 -54
- package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +0 -144
- package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +0 -90
- package/plugins/session-manager-dev/skills/discover-features/SKILL.md +0 -95
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +0 -205
- package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +0 -163
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +0 -83
- package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +0 -38
- package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +0 -64
- package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +0 -71
- package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +0 -75
- package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +0 -57
- package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +0 -40
- package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +0 -33
- package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +0 -40
- package/plugins/session-manager-dev/skills/issue-address/SKILL.md +0 -120
- package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +0 -58
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +0 -147
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +0 -137
- package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +0 -41
- package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +0 -56
- package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +0 -49
- package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +0 -57
- package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +0 -60
- package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +0 -124
- package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +0 -67
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +0 -243
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +0 -98
- package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +0 -146
- package/src/main/lib/rateLimitDetect.cjs +0 -35
- /package/src/main/lib/{agentEffortResolve.cjs → claude/agentEffortResolve.cjs} +0 -0
- /package/src/main/lib/{claudeCliCaps.cjs → claude/claudeCliCaps.cjs} +0 -0
- /package/src/main/lib/{usageCircuit.cjs → claude/usageCircuit.cjs} +0 -0
- /package/src/main/lib/{epicSpawnCwd.cjs → epic/epicSpawnCwd.cjs} +0 -0
- /package/src/main/lib/{customerFeedbackAdminRoutes.cjs → feedback-remote/customerFeedbackAdminRoutes.cjs} +0 -0
- /package/src/main/lib/{remoteE2eCrypto.cjs → feedback-remote/remoteE2eCrypto.cjs} +0 -0
- /package/src/main/lib/{branchSweep.cjs → git/branchSweep.cjs} +0 -0
- /package/src/main/lib/{gitExec.cjs → git/gitExec.cjs} +0 -0
- /package/src/main/lib/{jobWorktree.cjs → git/jobWorktree.cjs} +0 -0
- /package/src/main/lib/{jobWorktreeBootLive.cjs → git/jobWorktreeBootLive.cjs} +0 -0
- /package/src/main/lib/{jobWorktreeTerminalOrphanLive.cjs → git/jobWorktreeTerminalOrphanLive.cjs} +0 -0
- /package/src/main/lib/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +0 -0
- /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
- /package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.cjs} +0 -0
- /package/src/main/lib/{planValidator.cjs → prd/planValidator.cjs} +0 -0
- /package/src/main/lib/{prdAuthoringSeed.cjs → prd/prdAuthoringSeed.cjs} +0 -0
- /package/src/main/lib/{prdDisposition.cjs → prd/prdDisposition.cjs} +0 -0
- /package/src/main/lib/{prdFrontmatter.cjs → prd/prdFrontmatter.cjs} +0 -0
- /package/src/main/lib/{prdSizing.cjs → prd/prdSizing.cjs} +0 -0
- /package/src/main/lib/{workTypeLibrary.cjs → prd/workTypeLibrary.cjs} +0 -0
- /package/src/main/lib/{childWithLog.cjs → process/childWithLog.cjs} +0 -0
- /package/src/main/lib/{cleanEnv.cjs → process/cleanEnv.cjs} +0 -0
- /package/src/main/lib/{defaultShell.cjs → process/defaultShell.cjs} +0 -0
- /package/src/main/lib/{killTree.cjs → process/killTree.cjs} +0 -0
- /package/src/main/lib/{loginShellPath.cjs → process/loginShellPath.cjs} +0 -0
- /package/src/main/lib/{procIdentity.cjs → process/procIdentity.cjs} +0 -0
- /package/src/main/lib/{singleInstanceGuard.cjs → process/singleInstanceGuard.cjs} +0 -0
- /package/src/main/lib/{smProcNames.cjs → process/smProcNames.cjs} +0 -0
- /package/src/main/lib/{timeoutShimScript.cjs → process/timeoutShimScript.cjs} +0 -0
- /package/src/main/lib/{winSpawn.cjs → process/winSpawn.cjs} +0 -0
- /package/src/main/lib/{adoptedRunSupervisor.cjs → scheduler/adoptedRunSupervisor.cjs} +0 -0
- /package/src/main/lib/{dispatchLoop.cjs → scheduler/dispatchLoop.cjs} +0 -0
- /package/src/main/lib/{dodDrainHook.cjs → scheduler/dodDrainHook.cjs} +0 -0
- /package/src/main/lib/{fixPlanSlug.cjs → scheduler/fixPlanSlug.cjs} +0 -0
- /package/src/main/lib/{gateAuthority.cjs → scheduler/gateAuthority.cjs} +0 -0
- /package/src/main/lib/{landedSinceRun.cjs → scheduler/landedSinceRun.cjs} +0 -0
- /package/src/main/lib/{loadGate.cjs → scheduler/loadGate.cjs} +0 -0
- /package/src/main/lib/{needsReviewLedger.cjs → scheduler/needsReviewLedger.cjs} +0 -0
- /package/src/main/lib/{queueHealth.cjs → scheduler/queueHealth.cjs} +0 -0
- /package/src/main/lib/{queueHistory.cjs → scheduler/queueHistory.cjs} +0 -0
- /package/src/main/lib/{scheduleJobSchema.cjs → scheduler/scheduleJobSchema.cjs} +0 -0
- /package/src/main/lib/{schedulerPaths.cjs → scheduler/schedulerPaths.cjs} +0 -0
- /package/src/main/lib/{terminalRunOutcome.cjs → scheduler/terminalRunOutcome.cjs} +0 -0
- /package/src/main/lib/{validationSentinels.cjs → scheduler/validationSentinels.cjs} +0 -0
- /package/src/main/lib/{crashTelemetry.cjs → telemetry/crashTelemetry.cjs} +0 -0
- /package/src/main/lib/{otlpTraceExporter.cjs → telemetry/otlpTraceExporter.cjs} +0 -0
- /package/src/main/lib/{telemetryConsent.cjs → telemetry/telemetryConsent.cjs} +0 -0
- /package/src/main/lib/{telemetryCounters.cjs → telemetry/telemetryCounters.cjs} +0 -0
- /package/src/main/lib/{encodeCwd.cjs → util/encodeCwd.cjs} +0 -0
- /package/src/main/lib/{expandHome.cjs → util/expandHome.cjs} +0 -0
- /package/src/main/lib/{extractJson.cjs → util/extractJson.cjs} +0 -0
- /package/src/main/lib/{fileTail.cjs → util/fileTail.cjs} +0 -0
- /package/src/main/lib/{headTailBuffer.cjs → util/headTailBuffer.cjs} +0 -0
- /package/src/main/lib/{kebabCase.cjs → util/kebabCase.cjs} +0 -0
- /package/src/main/lib/{lruCache.cjs → util/lruCache.cjs} +0 -0
- /package/src/main/lib/{withTimeout.cjs → util/withTimeout.cjs} +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler/starvation.cjs — pure queue starvation/health classifiers and the
|
|
3
|
+
* dispatch-idle clock. Extracted verbatim from scheduler.cjs; the watchdog that
|
|
4
|
+
* arms timers / forces ticks stays there and calls in. No IO, no timers.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const launchFailure = require('../lib/scheduler/launchFailure.cjs');
|
|
9
|
+
const { STARVE_ESCALATION_MS } = require('../lib/scheduler/schedulerConfig.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* computeBlockedChains(jobs) → [{ cwd, blockedBy, blocked }]
|
|
13
|
+
*
|
|
14
|
+
* Pure, no IO. The gap computeStallSummary above cannot see.
|
|
15
|
+
*
|
|
16
|
+
* `stalled` is defined as `running === 0 && pending === 0`, which encodes an
|
|
17
|
+
* assumption that a PENDING row is healthy in-progress work. It is not: a
|
|
18
|
+
* pending row whose `dependsOn` chain terminates in a TERMINAL non-completed
|
|
19
|
+
* status (`failed`/`skipped`) can never be dispatched by pickForProject, and
|
|
20
|
+
* never will be, but it still counts toward `pending` and so reads as a
|
|
21
|
+
* healthy queue to every monitor in the app.
|
|
22
|
+
*
|
|
23
|
+
* On 2026-09-05 starry-night-ships held 42 such rows behind one `failed`
|
|
24
|
+
* job for three hours. Machine-wide `stalled` was false (42 pending),
|
|
25
|
+
* per-project `stalled` was false (42 pending), the queue-health sweep
|
|
26
|
+
* doesn't count `failed` at all, and the supervisor only probes `running` —
|
|
27
|
+
* so nothing anywhere reported a problem while nothing could ever run.
|
|
28
|
+
*
|
|
29
|
+
* Reported per project as { blockedBy: [terminal slugs], blocked: count }.
|
|
30
|
+
* Transitive by construction: a row blocked by a row that is itself blocked
|
|
31
|
+
* resolves through the same walk.
|
|
32
|
+
*/
|
|
33
|
+
function computeBlockedChains(jobs) {
|
|
34
|
+
const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
|
|
35
|
+
const byCwd = new Map();
|
|
36
|
+
for (const j of rows) {
|
|
37
|
+
const key = j.cwd || '(unknown)';
|
|
38
|
+
if (!byCwd.has(key)) byCwd.set(key, []);
|
|
39
|
+
byCwd.get(key).push(j);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const [cwd, projectJobs] of byCwd) {
|
|
44
|
+
// Reuse the picker's OWN dep resolution so this can never disagree with
|
|
45
|
+
// what the scheduler will actually dispatch (bare-name fallback included).
|
|
46
|
+
const rowBySlug = new Map(projectJobs.map((j) => [j.slug, j]));
|
|
47
|
+
const rowsByBareSlug = new Map();
|
|
48
|
+
for (const j of projectJobs) {
|
|
49
|
+
const bare = String(j.slug ?? '').replace(/^\d+-/, '');
|
|
50
|
+
if (!rowsByBareSlug.has(bare)) rowsByBareSlug.set(bare, []);
|
|
51
|
+
rowsByBareSlug.get(bare).push(j);
|
|
52
|
+
}
|
|
53
|
+
const rowsForDep = (slug) => {
|
|
54
|
+
const exact = rowBySlug.get(slug);
|
|
55
|
+
if (exact) return [exact];
|
|
56
|
+
return rowsByBareSlug.get(String(slug ?? '').replace(/^\d+-/, '')) ?? [];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Memoised walk: does this row's dep closure hit a terminally-stuck row?
|
|
60
|
+
const TERMINAL_STUCK = new Set(['failed', 'skipped']);
|
|
61
|
+
const verdicts = new Map(); // slug -> Set of terminal blocker slugs
|
|
62
|
+
const visiting = new Set();
|
|
63
|
+
const blockersFor = (job) => {
|
|
64
|
+
if (!job) return new Set();
|
|
65
|
+
if (verdicts.has(job.slug)) return verdicts.get(job.slug);
|
|
66
|
+
if (visiting.has(job.slug)) return new Set(); // dependsOn cycle — not our problem here
|
|
67
|
+
visiting.add(job.slug);
|
|
68
|
+
const found = new Set();
|
|
69
|
+
for (const depSlug of job.dependsOn ?? []) {
|
|
70
|
+
for (const dep of rowsForDep(depSlug)) {
|
|
71
|
+
if (TERMINAL_STUCK.has(dep.status)) found.add(dep.slug);
|
|
72
|
+
else if (dep.status !== 'completed') for (const b of blockersFor(dep)) found.add(b);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
visiting.delete(job.slug);
|
|
76
|
+
verdicts.set(job.slug, found);
|
|
77
|
+
return found;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const blockedBy = new Set();
|
|
81
|
+
let blocked = 0;
|
|
82
|
+
for (const j of projectJobs) {
|
|
83
|
+
if (j.status !== 'pending') continue;
|
|
84
|
+
const bs = blockersFor(j);
|
|
85
|
+
if (bs.size === 0) continue;
|
|
86
|
+
blocked += 1;
|
|
87
|
+
for (const b of bs) blockedBy.add(b);
|
|
88
|
+
}
|
|
89
|
+
if (blocked > 0) out.push({ cwd, blockedBy: [...blockedBy].sort(), blocked });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** How long a queue may hold ready work with nothing running before the
|
|
95
|
+
* starvation watchdog forces a tick. Deliberately longer than the poll
|
|
96
|
+
* loop's own cadence + backoff, so this only ever fires when the normal
|
|
97
|
+
* path has genuinely stopped driving the queue — it is a safety net, not a
|
|
98
|
+
* second scheduler. */
|
|
99
|
+
const QUEUE_STARVATION_MS = 10 * 60_000;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now }) → ms
|
|
103
|
+
*
|
|
104
|
+
* Pure. The ONE dispatch-idleness clock: `now - max(lastRunAt, lastPauseClearedAt,
|
|
105
|
+
* schedulerBootedAt)`. `lastRunAt` has exactly one writer (tickQueue, right
|
|
106
|
+
* before the spawn loop) so it already means "a batch actually launched";
|
|
107
|
+
* a pause clear and a scheduler boot are the other two moments the queue
|
|
108
|
+
* legitimately (re)starts. Deliberately NOT `lastDispatchAttemptAt`, which
|
|
109
|
+
* tickQueue stamps before every gate — a queue that ticks every 30 s and
|
|
110
|
+
* launches nothing (leaked slot, stuck hold) would refresh that stamp
|
|
111
|
+
* forever and never look idle (the structural repeat of the f18e161 bug
|
|
112
|
+
* where lastRunAt was refreshed every poll). No finite input → Infinity.
|
|
113
|
+
*/
|
|
114
|
+
function dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now } = {}) {
|
|
115
|
+
const stamps = [lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs].filter(Number.isFinite);
|
|
116
|
+
return stamps.length ? now - Math.max(...stamps) : Infinity;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* launchBlockedSlugs(jobs, launchBlocks) → Set<slug>
|
|
121
|
+
*
|
|
122
|
+
* Pure, sync (health.cjs runs as a cold process). Pending rows whose persona
|
|
123
|
+
* has ANY launch-breaker entry — a superset of computeLaunchHolds, which
|
|
124
|
+
* additionally lets one half-open probe row through per persona.
|
|
125
|
+
*/
|
|
126
|
+
function launchBlockedSlugs(jobs, launchBlocks) {
|
|
127
|
+
const out = new Set();
|
|
128
|
+
if (!launchBlocks || !Object.keys(launchBlocks).length) return out;
|
|
129
|
+
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
130
|
+
if (j && j.status === 'pending' && launchBlocks[launchFailure.launchBlockKeyFor(j)]) out.add(j.slug);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* classifyQueueStarvation({ jobs, paused, runningCount, lastRunAtMs, now, thresholdMs })
|
|
137
|
+
* → null | { kind: 'starved' | 'blocked', pending, dispatchable, blockedChains, idleMs }
|
|
138
|
+
*
|
|
139
|
+
* Pure, no IO. Answers the one question the user's invariant reduces to:
|
|
140
|
+
* "there are PRDs in a queue — is anything actually going to run them?"
|
|
141
|
+
*
|
|
142
|
+
* Every stall this codebase has seen was a DIFFERENT cause with the SAME
|
|
143
|
+
* shape: ready rows, nothing running, nobody ticking. A rate-limited exit
|
|
144
|
+
* stamped terminal `failed` (2026-09-05, 42 rows); a spin loop past the
|
|
145
|
+
* manual-clear cooldown; a worktree merge-back that left the project on a
|
|
146
|
+
* job branch; a job parked `needs_review` with no fix plan; app churn
|
|
147
|
+
* counted as unfinished work. Guarding each cause individually will always
|
|
148
|
+
* lag the next one, so this guards the SHAPE instead.
|
|
149
|
+
*
|
|
150
|
+
* Two outcomes, deliberately distinguished — they need opposite responses:
|
|
151
|
+
* 'starved' — at least one pending row is dispatchable RIGHT NOW and
|
|
152
|
+
* nothing is running. Whatever should have ticked, didn't.
|
|
153
|
+
* Forcing a tick is safe and fixes it.
|
|
154
|
+
* 'blocked' — every pending row is behind a terminal/parked dependency.
|
|
155
|
+
* A tick cannot help; this needs a human (or a heal pass) to
|
|
156
|
+
* resolve the blocker, and must be reported as such rather
|
|
157
|
+
* than silently re-ticking forever.
|
|
158
|
+
*
|
|
159
|
+
* Returns null when the queue is healthy (work running, nothing pending,
|
|
160
|
+
* paused on purpose, or simply not idle long enough yet).
|
|
161
|
+
*/
|
|
162
|
+
function classifyQueueStarvation({ jobs, paused, runningCount, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, heldSlugs, now, thresholdMs = QUEUE_STARVATION_MS } = {}) {
|
|
163
|
+
if (paused) return null; // paused is a DECISION, not a stall
|
|
164
|
+
if (runningCount > 0) return null; // work is flowing
|
|
165
|
+
const rows = Array.isArray(jobs) ? jobs : [];
|
|
166
|
+
const pending = rows.filter((j) => j && j.status === 'pending');
|
|
167
|
+
if (pending.length === 0) return null; // nothing to run — not a stall
|
|
168
|
+
|
|
169
|
+
const idleMs = dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now });
|
|
170
|
+
if (idleMs < thresholdMs) return null; // give the normal path its chance first
|
|
171
|
+
|
|
172
|
+
// Which pending rows could actually dispatch? Anything NOT named by a
|
|
173
|
+
// blocked chain (computeBlockedChains walks dependsOn with the picker's own
|
|
174
|
+
// resolution, so the two can never disagree) and NOT held by an open launch
|
|
175
|
+
// breaker / the quietMachine lease (`heldSlugs` — a separate input, never
|
|
176
|
+
// folded into the dependsOn walker). Held rows can't launch no matter how
|
|
177
|
+
// often we tick, so they read as 'blocked' (needs a human), not 'starved'.
|
|
178
|
+
const blockedChains = computeBlockedChains(rows);
|
|
179
|
+
const held = heldSlugs?.has ? heldSlugs : new Set(heldSlugs ?? []);
|
|
180
|
+
const open = held.size > 0 ? rows.filter((j) => !(j.status === 'pending' && held.has(j.slug))) : rows;
|
|
181
|
+
const openBlocked = held.size > 0 ? computeBlockedChains(open) : blockedChains;
|
|
182
|
+
const openPending = open.filter((j) => j.status === 'pending').length;
|
|
183
|
+
const dispatchable = openPending - openBlocked.reduce((n, c) => n + c.blocked, 0);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
kind: dispatchable > 0 ? 'starved' : 'blocked',
|
|
187
|
+
pending: pending.length,
|
|
188
|
+
dispatchable,
|
|
189
|
+
blockedChains,
|
|
190
|
+
idleMs,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* classifyQueueStarvationByProject({ jobs, paused, runningSet, lastRunAtMs, now, thresholdMs })
|
|
196
|
+
* → [{ cwd, kind: 'starved' | 'blocked', pending, dispatchable, blockedChains, idleMs }]
|
|
197
|
+
*
|
|
198
|
+
* Per-project driver around classifyQueueStarvation's pure single-project
|
|
199
|
+
* core. `runningCount > 0` inside that core used to be fed the MACHINE-WIDE
|
|
200
|
+
* `runningSet.size`, which meant one long-lived job in ANY project disarmed
|
|
201
|
+
* the watchdog for EVERY other project on the box — observed live
|
|
202
|
+
* 2026-09-12: a job in starry-night-ships ran 80+ minutes while two other
|
|
203
|
+
* projects sat starved/blocked for hours, and the watchdog never fired once
|
|
204
|
+
* because "work is flowing" was true somewhere else. Partitioning by cwd
|
|
205
|
+
* (the same grouping computeBlockedChains already does) fixes DETECTION only
|
|
206
|
+
* — the idle clock (see dispatchIdleMs) stays machine-wide, since
|
|
207
|
+
* `lastRunAt` is machine-level state, and only one tick is ever
|
|
208
|
+
* forced per watchdog pass regardless of how many cwds are starved.
|
|
209
|
+
*
|
|
210
|
+
* Pure, no IO. Returns [] when paused (a DECISION, not a stall) or when no
|
|
211
|
+
* project has a verdict.
|
|
212
|
+
*/
|
|
213
|
+
function classifyQueueStarvationByProject({ jobs, paused, runningSet: runningSlugs, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, heldSlugs, now, thresholdMs = QUEUE_STARVATION_MS } = {}) {
|
|
214
|
+
if (paused) return [];
|
|
215
|
+
const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
|
|
216
|
+
const byCwd = new Map();
|
|
217
|
+
for (const j of rows) {
|
|
218
|
+
const key = j.cwd || '(unknown)';
|
|
219
|
+
if (!byCwd.has(key)) byCwd.set(key, []);
|
|
220
|
+
byCwd.get(key).push(j);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const verdicts = [];
|
|
224
|
+
for (const [cwd, projectJobs] of byCwd) {
|
|
225
|
+
// Same source of truth tickQueue itself uses for "is anything running":
|
|
226
|
+
// the in-process runningSet OR a row already stamped status:'running'.
|
|
227
|
+
const projRunningCount = projectJobs.filter(
|
|
228
|
+
(j) => j.status === 'running' || runningSlugs?.has?.(j.slug),
|
|
229
|
+
).length;
|
|
230
|
+
const verdict = classifyQueueStarvation({
|
|
231
|
+
jobs: projectJobs,
|
|
232
|
+
paused: false,
|
|
233
|
+
runningCount: projRunningCount,
|
|
234
|
+
lastRunAtMs,
|
|
235
|
+
lastPauseClearedAtMs,
|
|
236
|
+
schedulerBootedAtMs,
|
|
237
|
+
heldSlugs,
|
|
238
|
+
now,
|
|
239
|
+
thresholdMs,
|
|
240
|
+
});
|
|
241
|
+
if (verdict) verdicts.push({ cwd, ...verdict });
|
|
242
|
+
}
|
|
243
|
+
return verdicts;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* classifyQueueHealth({ jobs, paused, launchBlocks, runningSet, freeSlots,
|
|
248
|
+
* totalSlots, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, cwd, thresholdMs })
|
|
249
|
+
* → { kind, cwd, pending, dispatchable, blockedChains, needsReviewCount, runningCount, ... }
|
|
250
|
+
*
|
|
251
|
+
* Single source of truth for the Scheduler page's queue-health header: the
|
|
252
|
+
* one thing a human staring at a stale-looking queue needs is "which of the
|
|
253
|
+
* genuinely different causes is this" (all slots busy? every pending row
|
|
254
|
+
* blocked on a dependency? the dispatch driver itself never ticked?) — this
|
|
255
|
+
* function names that cause instead of leaving the renderer to re-derive it.
|
|
256
|
+
*
|
|
257
|
+
* Reuses classifyQueueStarvation for the blocked/stalled read so the header
|
|
258
|
+
* can never disagree with runQueueStarvationWatchdog's own decision to force
|
|
259
|
+
* a tick: both are handed the same launch-keyed idle clock (dispatchIdleMs) and
|
|
260
|
+
* the same computeBlockedChains walk under the hood. Called here with
|
|
261
|
+
* `thresholdMs: 0` first (a live header must say "blocked" the instant every
|
|
262
|
+
* pending row is dependency-stuck, not wait out the watchdog's own 10-minute
|
|
263
|
+
* grace period) — the returned `idleMs` is then compared against the REAL
|
|
264
|
+
* `thresholdMs` to decide 'stalled' vs the healthy 'running' default, which
|
|
265
|
+
* is exactly the comparison classifyQueueStarvation would make internally.
|
|
266
|
+
*
|
|
267
|
+
* `pending`/`dispatchable`/`blockedChains`/`needsReviewCount` are always
|
|
268
|
+
* populated (via computeBlockedChains — the exact primitive
|
|
269
|
+
* classifyQueueStarvation itself calls) regardless of kind, so a 'saturated'
|
|
270
|
+
* or 'running' header can still say how much of the backlog is dependency-
|
|
271
|
+
* blocked, not just the kinds where that's the headline cause.
|
|
272
|
+
*
|
|
273
|
+
* Kinds, in the priority order they're checked (paused is a decision, not a
|
|
274
|
+
* stall; an open launch breaker explains an otherwise-inexplicable
|
|
275
|
+
* non-dispatch before slot/dependency causes are even considered):
|
|
276
|
+
* 'paused' — the scheduler itself is paused.
|
|
277
|
+
* 'launch-blocked' — a pending row's persona has an active circuit-breaker
|
|
278
|
+
* entry (lib/scheduler/launchFailure.cjs).
|
|
279
|
+
* 'idle' — nothing pending in this scope.
|
|
280
|
+
* 'saturated' — pending work exists but every session slot is in use.
|
|
281
|
+
* 'blocked' — nothing running, slots free, every pending row's
|
|
282
|
+
* dependsOn chain terminates in a non-completed row.
|
|
283
|
+
* 'stalled' — nothing running, slots free, at least one row is
|
|
284
|
+
* dispatchable right now, and the dispatch driver has
|
|
285
|
+
* been idle >= thresholdMs (agrees with the watchdog).
|
|
286
|
+
* 'running' — the healthy default: work is flowing, or the driver
|
|
287
|
+
* hasn't been idle long enough to call a stall yet.
|
|
288
|
+
*
|
|
289
|
+
* Pure, no IO. `cwd` scopes jobs/pending/blocked/needsReview to one project
|
|
290
|
+
* (the Scheduler nav row is PROJECT-face — see CLAUDE.md); `freeSlots` /
|
|
291
|
+
* `totalSlots` / `launchBlocks` stay machine-wide inputs by design, same as
|
|
292
|
+
* WindowStrip's existing scopeCwd split.
|
|
293
|
+
*/
|
|
294
|
+
function classifyQueueHealth({
|
|
295
|
+
jobs, paused, launchBlocks, runningSet: runningSlugs, freeSlots, totalSlots,
|
|
296
|
+
lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, cwd = null, thresholdMs = QUEUE_STARVATION_MS,
|
|
297
|
+
} = {}) {
|
|
298
|
+
const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
|
|
299
|
+
const projectJobs = cwd ? rows.filter((j) => j.cwd === cwd) : rows;
|
|
300
|
+
const pendingRows = projectJobs.filter((j) => j.status === 'pending');
|
|
301
|
+
const runningRows = projectJobs.filter((j) => j.status === 'running' || runningSlugs?.has?.(j.slug));
|
|
302
|
+
const needsReviewCount = projectJobs.filter((j) => j.status === 'needs_review').length;
|
|
303
|
+
|
|
304
|
+
// computeBlockedChains is the SAME primitive classifyQueueStarvation calls
|
|
305
|
+
// internally, computed once here so EVERY kind (not just 'blocked'/
|
|
306
|
+
// 'stalled') carries real dispatchable-vs-blocked counts instead of a null.
|
|
307
|
+
const blockedChains = computeBlockedChains(projectJobs);
|
|
308
|
+
const blockedTotal = blockedChains.reduce((n, c) => n + c.blocked, 0);
|
|
309
|
+
const dispatchable = Math.max(0, pendingRows.length - blockedTotal);
|
|
310
|
+
const base = {
|
|
311
|
+
cwd, pending: pendingRows.length, dispatchable, blockedChains, needsReviewCount, runningCount: runningRows.length,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (paused) return { ...base, kind: 'paused', reason: paused.reason ?? null };
|
|
315
|
+
|
|
316
|
+
// launch-blocked: only a persona a PENDING row in this scope actually uses
|
|
317
|
+
// — a breaker open for a persona nothing here needs is not this scope's
|
|
318
|
+
// problem (matches WindowStrip's own unconditional-banner-per-block read).
|
|
319
|
+
const neededAgentTypes = new Set(pendingRows.map((j) => launchFailure.launchBlockKeyFor(j)));
|
|
320
|
+
for (const [key, block] of Object.entries(launchBlocks ?? {})) {
|
|
321
|
+
if (block && neededAgentTypes.has(key)) {
|
|
322
|
+
return { ...base, kind: 'launch-blocked', agentType: key, block };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (pendingRows.length === 0) return { ...base, kind: 'idle' };
|
|
327
|
+
|
|
328
|
+
// classifyQueueStarvation only ever classifies while nothing is running
|
|
329
|
+
// (its own runningCount > 0 guard) — that boundary is also exactly where
|
|
330
|
+
// slot saturation, not dependency shape, is the honest cause.
|
|
331
|
+
if (runningRows.length > 0) {
|
|
332
|
+
if (Number.isFinite(freeSlots) && freeSlots <= 0) {
|
|
333
|
+
return { ...base, kind: 'saturated', totalSlots: totalSlots ?? null };
|
|
334
|
+
}
|
|
335
|
+
return { ...base, kind: 'running' };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Nothing running: hand the SAME rows + idle clock to classifyQueueStarvation
|
|
339
|
+
// (thresholdMs: 0 — a live header must say "blocked" the instant every
|
|
340
|
+
// pending row is dependency-stuck, not wait out the watchdog's own grace
|
|
341
|
+
// period) purely for its idleMs reading; its own dispatchable/blockedChains
|
|
342
|
+
// are mathematically identical to `base`'s (same computeBlockedChains walk
|
|
343
|
+
// over the same rows), so `base` already carries them.
|
|
344
|
+
const immediate = classifyQueueStarvation({
|
|
345
|
+
jobs: projectJobs, paused: false, runningCount: 0,
|
|
346
|
+
lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, thresholdMs: 0,
|
|
347
|
+
});
|
|
348
|
+
// pending.length is already > 0 above, so `immediate` can only be null when
|
|
349
|
+
// the clock stamp is itself in the future (clock skew) — fall back to
|
|
350
|
+
// computing idleMs the same way rather than asserting a kind we can't
|
|
351
|
+
// back up with a real number.
|
|
352
|
+
const idleMs = immediate ? immediate.idleMs
|
|
353
|
+
: dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now });
|
|
354
|
+
if (dispatchable === 0) return { ...base, kind: 'blocked', idleMs };
|
|
355
|
+
const kind = idleMs >= thresholdMs ? 'stalled' : 'running';
|
|
356
|
+
return { ...base, kind, idleMs };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* selectStarveEscalations(starvedProjects, escalatedCwds, thresholdMs)
|
|
361
|
+
* → { toEscalate: [...sp], toClear: [cwd, ...] }
|
|
362
|
+
*
|
|
363
|
+
* Pure. `starvedProjects` is this sweep's findStarvedProjects() output (the
|
|
364
|
+
* per-cwd STARVED verdict — cwd, pendingCount, oldestPendingSlug, ageMs);
|
|
365
|
+
* `escalatedCwds` is the Set already latched from a prior sweep.
|
|
366
|
+
*
|
|
367
|
+
* toEscalate: rows crossing thresholdMs for the FIRST time this stretch —
|
|
368
|
+
* i.e. old enough AND not already latched.
|
|
369
|
+
* toClear: previously-latched cwds no longer reported as starved at all this
|
|
370
|
+
* sweep, so a LATER starve on that project escalates again instead of being
|
|
371
|
+
* silently suppressed forever by a stale latch.
|
|
372
|
+
*/
|
|
373
|
+
function selectStarveEscalations(starvedProjects, escalatedCwds, thresholdMs = STARVE_ESCALATION_MS) {
|
|
374
|
+
const stillStarved = new Set(starvedProjects.map((sp) => sp.cwd));
|
|
375
|
+
const toClear = [...escalatedCwds].filter((cwd) => !stillStarved.has(cwd));
|
|
376
|
+
const toEscalate = starvedProjects.filter((sp) => sp.ageMs >= thresholdMs && !escalatedCwds.has(sp.cwd));
|
|
377
|
+
return { toEscalate, toClear };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
computeBlockedChains,
|
|
382
|
+
QUEUE_STARVATION_MS,
|
|
383
|
+
STARVE_ESCALATION_MS,
|
|
384
|
+
dispatchIdleMs,
|
|
385
|
+
launchBlockedSlugs,
|
|
386
|
+
classifyQueueStarvation,
|
|
387
|
+
classifyQueueStarvationByProject,
|
|
388
|
+
classifyQueueHealth,
|
|
389
|
+
selectStarveEscalations,
|
|
390
|
+
};
|