@agimon-ai/doompi-team 0.0.1-alpha.2 → 0.0.1-alpha.21
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 +33 -17
- package/dist/adapters/asyncJobTracker.cjs +2 -0
- package/dist/adapters/asyncJobTracker.cjs.map +1 -0
- package/dist/adapters/asyncJobTracker.mjs +2 -0
- package/dist/adapters/asyncJobTracker.mjs.map +1 -0
- package/dist/adapters/atomicJson.cjs +2 -0
- package/dist/adapters/atomicJson.cjs.map +1 -0
- package/dist/adapters/atomicJson.mjs +2 -0
- package/dist/adapters/atomicJson.mjs.map +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/adapters/pollScheduler.cjs +2 -0
- package/dist/adapters/pollScheduler.cjs.map +1 -0
- package/dist/adapters/pollScheduler.mjs +2 -0
- package/dist/adapters/pollScheduler.mjs.map +1 -0
- package/dist/adapters/process/childTranscript.cjs +5 -0
- package/dist/adapters/process/childTranscript.cjs.map +1 -0
- package/dist/adapters/process/childTranscript.mjs +5 -0
- package/dist/adapters/process/childTranscript.mjs.map +1 -0
- package/dist/adapters/process/env.cjs +2 -0
- package/dist/adapters/process/env.cjs.map +1 -0
- package/dist/adapters/process/env.d.cts +69 -0
- package/dist/adapters/process/env.d.cts.map +1 -0
- package/dist/adapters/process/env.d.mts +69 -0
- package/dist/adapters/process/env.d.mts.map +1 -0
- package/dist/adapters/process/env.mjs +2 -0
- package/dist/adapters/process/env.mjs.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.cjs +2 -0
- package/dist/adapters/process/sdkRunnerEntry.cjs.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.d.cts +5 -0
- package/dist/adapters/process/sdkRunnerEntry.d.cts.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.d.mts +5 -0
- package/dist/adapters/process/sdkRunnerEntry.d.mts.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.mjs +2 -0
- package/dist/adapters/process/sdkRunnerEntry.mjs.map +1 -0
- package/dist/adapters/processTerminal.cjs +2 -0
- package/dist/adapters/processTerminal.cjs.map +1 -0
- package/dist/adapters/processTerminal.mjs +2 -0
- package/dist/adapters/processTerminal.mjs.map +1 -0
- package/dist/adapters/resultWatcher.cjs +2 -0
- package/dist/adapters/resultWatcher.cjs.map +1 -0
- package/dist/adapters/resultWatcher.mjs +2 -0
- package/dist/adapters/resultWatcher.mjs.map +1 -0
- package/dist/adapters/runIdResolver.cjs +2 -0
- package/dist/adapters/runIdResolver.cjs.map +1 -0
- package/dist/adapters/runIdResolver.mjs +2 -0
- package/dist/adapters/runIdResolver.mjs.map +1 -0
- package/dist/adapters/runRegistry.cjs +2 -0
- package/dist/adapters/runRegistry.cjs.map +1 -0
- package/dist/adapters/runRegistry.mjs +2 -0
- package/dist/adapters/runRegistry.mjs.map +1 -0
- package/dist/adapters/scopeOwner.cjs +2 -0
- package/dist/adapters/scopeOwner.cjs.map +1 -0
- package/dist/adapters/scopeOwner.mjs +2 -0
- package/dist/adapters/scopeOwner.mjs.map +1 -0
- package/dist/adapters/staleRunReconciler.cjs +2 -0
- package/dist/adapters/staleRunReconciler.cjs.map +1 -0
- package/dist/adapters/staleRunReconciler.mjs +2 -0
- package/dist/adapters/staleRunReconciler.mjs.map +1 -0
- package/dist/adapters/statusReader.cjs +2 -0
- package/dist/adapters/statusReader.cjs.map +1 -0
- package/dist/adapters/statusReader.mjs +2 -0
- package/dist/adapters/statusReader.mjs.map +1 -0
- package/dist/adapters/suspendedRuns.cjs +6 -0
- package/dist/adapters/suspendedRuns.cjs.map +1 -0
- package/dist/adapters/suspendedRuns.mjs +6 -0
- package/dist/adapters/suspendedRuns.mjs.map +1 -0
- package/dist/api/backgroundWork.cjs +1 -1
- package/dist/api/backgroundWork.d.cts +2 -2
- package/dist/api/backgroundWork.d.mts +2 -2
- package/dist/api/backgroundWork.mjs +1 -1
- package/dist/api/capabilityCeiling.cjs +1 -1
- package/dist/api/capabilityCeiling.d.cts +3 -3
- package/dist/api/capabilityCeiling.d.mts +3 -3
- package/dist/api/capabilityCeiling.mjs +1 -2
- package/dist/api/delegation.cjs +1 -1
- package/dist/api/delegation.d.cts +1 -1
- package/dist/api/delegation.d.mts +1 -1
- package/dist/api/delegation.mjs +1 -1
- package/dist/api/teamSnapshot.cjs +1 -1
- package/dist/api/teamSnapshot.d.cts +2 -2
- package/dist/api/teamSnapshot.d.mts +2 -2
- package/dist/api/teamSnapshot.mjs +1 -2
- package/dist/commands/slash/chainExpression.cjs.map +1 -0
- package/dist/commands/slash/chainExpression.mjs.map +1 -0
- package/dist/commands/slash/doctor.cjs +5 -0
- package/dist/commands/slash/doctor.cjs.map +1 -0
- package/dist/commands/slash/doctor.mjs +5 -0
- package/dist/{slash/doctor.cjs.map → commands/slash/doctor.mjs.map} +1 -1
- package/dist/{slash → commands/slash}/slashCommands.cjs +1 -1
- package/dist/commands/slash/slashCommands.cjs.map +1 -0
- package/dist/{slash → commands/slash}/slashCommands.mjs +2 -2
- package/dist/commands/slash/slashCommands.mjs.map +1 -0
- package/dist/commands/slash/spawnRequestMapping.cjs.map +1 -0
- package/dist/commands/slash/spawnRequestMapping.mjs.map +1 -0
- package/dist/commands/slash/subagentLaunch.cjs +2 -0
- package/dist/commands/slash/subagentLaunch.cjs.map +1 -0
- package/dist/commands/slash/subagentLaunch.mjs +2 -0
- package/dist/commands/slash/subagentLaunch.mjs.map +1 -0
- package/dist/container/globalBridge.cjs.map +1 -1
- package/dist/container/globalBridge.d.cts +1 -1
- package/dist/container/globalBridge.d.mts +1 -1
- package/dist/container/globalBridge.mjs.map +1 -1
- package/dist/container/index.cjs +1 -1
- package/dist/container/index.cjs.map +1 -1
- package/dist/container/index.d.cts +2 -1
- package/dist/container/index.d.cts.map +1 -1
- package/dist/container/index.d.mts +2 -1
- package/dist/container/index.d.mts.map +1 -1
- package/dist/container/index.mjs +1 -1
- package/dist/container/index.mjs.map +1 -1
- package/dist/container/modules/agents.cjs +1 -1
- package/dist/container/modules/agents.cjs.map +1 -1
- package/dist/container/modules/agents.mjs +1 -1
- package/dist/container/modules/agents.mjs.map +1 -1
- package/dist/container/modules/core.cjs +1 -1
- package/dist/container/modules/core.cjs.map +1 -1
- package/dist/container/modules/core.mjs +1 -1
- package/dist/container/modules/core.mjs.map +1 -1
- package/dist/container/modules/extensions.cjs +1 -1
- package/dist/container/modules/extensions.cjs.map +1 -1
- package/dist/container/modules/extensions.mjs +1 -1
- package/dist/container/modules/extensions.mjs.map +1 -1
- package/dist/container/modules/intercom.cjs +1 -1
- package/dist/container/modules/intercom.cjs.map +1 -1
- package/dist/container/modules/intercom.mjs +1 -1
- package/dist/container/modules/intercom.mjs.map +1 -1
- package/dist/container/modules/runs.cjs +1 -1
- package/dist/container/modules/runs.cjs.map +1 -1
- package/dist/container/modules/runs.mjs +1 -1
- package/dist/container/modules/runs.mjs.map +1 -1
- package/dist/container/types.cjs +1 -1
- package/dist/container/types.cjs.map +1 -1
- package/dist/container/types.d.cts +1 -0
- package/dist/container/types.d.cts.map +1 -1
- package/dist/container/types.d.mts +1 -0
- package/dist/container/types.d.mts.map +1 -1
- package/dist/container/types.mjs +1 -1
- package/dist/container/types.mjs.map +1 -1
- package/dist/env.cjs +1 -2
- package/dist/env.d.cts +2 -65
- package/dist/env.d.mts +2 -65
- package/dist/env.mjs +1 -1
- package/dist/env.mjs.map +1 -1
- package/dist/extensions/config.cjs.map +1 -1
- package/dist/extensions/config.mjs.map +1 -1
- package/dist/extensions/delegationBridge.cjs.map +1 -1
- package/dist/extensions/delegationBridge.mjs.map +1 -1
- package/dist/extensions/doom.cjs +1 -1
- package/dist/extensions/doom.cjs.map +1 -1
- package/dist/extensions/doom.mjs +1 -1
- package/dist/extensions/doom.mjs.map +1 -1
- package/dist/extensions/fablePlanBridge.cjs +1 -1
- package/dist/extensions/fablePlanBridge.cjs.map +1 -1
- package/dist/extensions/fablePlanBridge.mjs +1 -1
- package/dist/extensions/fablePlanBridge.mjs.map +1 -1
- package/dist/extensions/managementActions.cjs +1 -1
- package/dist/extensions/managementActions.cjs.map +1 -1
- package/dist/extensions/managementActions.mjs +1 -1
- package/dist/extensions/managementActions.mjs.map +1 -1
- package/dist/extensions/operationJournal.cjs +1 -1
- package/dist/extensions/operationJournal.cjs.map +1 -1
- package/dist/extensions/operationJournal.mjs +1 -1
- package/dist/extensions/operationJournal.mjs.map +1 -1
- package/dist/extensions/orchestratorPrompt.cjs +5 -6
- package/dist/extensions/orchestratorPrompt.cjs.map +1 -1
- package/dist/extensions/orchestratorPrompt.mjs +5 -6
- package/dist/extensions/orchestratorPrompt.mjs.map +1 -1
- package/dist/extensions/schemas.mjs.map +1 -1
- package/dist/extensions/spawnPlan.cjs +3 -2
- package/dist/extensions/spawnPlan.cjs.map +1 -1
- package/dist/extensions/spawnPlan.mjs +3 -2
- package/dist/extensions/spawnPlan.mjs.map +1 -1
- package/dist/extensions/standard.cjs +1 -1
- package/dist/extensions/standard.cjs.map +1 -1
- package/dist/extensions/standard.mjs +1 -1
- package/dist/extensions/standard.mjs.map +1 -1
- package/dist/extensions/statusViews.cjs +1 -1
- package/dist/extensions/statusViews.cjs.map +1 -1
- package/dist/extensions/statusViews.mjs +2 -2
- package/dist/extensions/statusViews.mjs.map +1 -1
- package/dist/extensions/subagentPromptRuntime.cjs +11 -11
- package/dist/extensions/subagentPromptRuntime.cjs.map +1 -1
- package/dist/extensions/subagentPromptRuntime.d.cts.map +1 -1
- package/dist/extensions/subagentPromptRuntime.d.mts.map +1 -1
- package/dist/extensions/subagentPromptRuntime.mjs +5 -5
- package/dist/extensions/subagentPromptRuntime.mjs.map +1 -1
- package/dist/extensions/subagentTool.cjs +5 -6
- package/dist/extensions/subagentTool.cjs.map +1 -1
- package/dist/extensions/subagentTool.mjs +5 -6
- package/dist/extensions/subagentTool.mjs.map +1 -1
- package/dist/extensions/toolDescription.cjs +1 -1
- package/dist/extensions/toolDescription.cjs.map +1 -1
- package/dist/extensions/toolDescription.mjs +1 -1
- package/dist/extensions/toolDescription.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/runs/background/cliRunnerEntry.cjs +1 -2
- package/dist/runs/background/cliRunnerEntry.mjs +1 -2
- package/dist/runs/sdkRunnerEntry.cjs +1 -2
- package/dist/runs/sdkRunnerEntry.d.cts +2 -1
- package/dist/runs/sdkRunnerEntry.d.mts +2 -1
- package/dist/runs/sdkRunnerEntry.mjs +1 -2
- package/dist/schemas/team/backgroundWork.cjs +2 -0
- package/dist/schemas/team/backgroundWork.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/backgroundWork.d.cts +2 -2
- package/dist/schemas/team/backgroundWork.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/backgroundWork.d.mts +2 -2
- package/dist/schemas/team/backgroundWork.d.mts.map +1 -0
- package/dist/schemas/team/backgroundWork.mjs +2 -0
- package/dist/{protocol/backgroundWork.cjs.map → schemas/team/backgroundWork.mjs.map} +1 -1
- package/dist/schemas/team/capabilityCeiling.cjs +2 -0
- package/dist/schemas/team/capabilityCeiling.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/capabilityCeiling.d.cts +3 -3
- package/dist/schemas/team/capabilityCeiling.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/capabilityCeiling.d.mts +3 -3
- package/dist/schemas/team/capabilityCeiling.d.mts.map +1 -0
- package/dist/schemas/team/capabilityCeiling.mjs +2 -0
- package/dist/schemas/team/capabilityCeiling.mjs.map +1 -0
- package/dist/schemas/team/delegation.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/delegation.d.cts +1 -1
- package/dist/schemas/team/delegation.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/delegation.d.mts +1 -1
- package/dist/schemas/team/delegation.d.mts.map +1 -0
- package/dist/schemas/team/delegation.mjs.map +1 -0
- package/dist/schemas/team/packageConfig.cjs +2 -0
- package/dist/schemas/team/packageConfig.cjs.map +1 -0
- package/dist/schemas/team/packageConfig.mjs +2 -0
- package/dist/schemas/team/packageConfig.mjs.map +1 -0
- package/dist/services/agents/discovery.cjs +2 -0
- package/dist/services/agents/discovery.cjs.map +1 -0
- package/dist/services/agents/discovery.mjs +2 -0
- package/dist/services/agents/discovery.mjs.map +1 -0
- package/dist/services/agents/frontmatter.cjs.map +1 -0
- package/dist/{agents → services/agents}/frontmatter.mjs +1 -1
- package/dist/services/agents/frontmatter.mjs.map +1 -0
- package/dist/services/agents/loader.cjs +2 -0
- package/dist/services/agents/loader.cjs.map +1 -0
- package/dist/services/agents/loader.mjs +2 -0
- package/dist/services/agents/loader.mjs.map +1 -0
- package/dist/services/agents/memory.cjs +3 -0
- package/dist/services/agents/memory.cjs.map +1 -0
- package/dist/{agents → services/agents}/memory.mjs +1 -1
- package/dist/services/agents/memory.mjs.map +1 -0
- package/dist/services/agents/projectRoot.cjs +2 -0
- package/dist/services/agents/projectRoot.cjs.map +1 -0
- package/dist/services/agents/projectRoot.mjs +2 -0
- package/dist/services/agents/projectRoot.mjs.map +1 -0
- package/dist/services/agents/settings.cjs +2 -0
- package/dist/services/agents/settings.cjs.map +1 -0
- package/dist/services/agents/settings.mjs +2 -0
- package/dist/services/agents/settings.mjs.map +1 -0
- package/dist/services/agents/skills.cjs +0 -0
- package/dist/services/agents/skills.cjs.map +1 -0
- package/dist/services/agents/skills.mjs +5 -0
- package/dist/services/agents/skills.mjs.map +1 -0
- package/dist/{agents → services/agents}/types.d.cts +4 -4
- package/dist/services/agents/types.d.cts.map +1 -0
- package/dist/{agents → services/agents}/types.d.mts +4 -4
- package/dist/services/agents/types.d.mts.map +1 -0
- package/dist/services/api/backgroundWork.cjs +1 -0
- package/dist/services/api/backgroundWork.d.cts +1 -0
- package/dist/services/api/backgroundWork.d.mts +1 -0
- package/dist/services/api/backgroundWork.mjs +1 -0
- package/dist/services/api/capabilityCeiling.cjs +1 -0
- package/dist/services/api/capabilityCeiling.d.cts +2 -0
- package/dist/services/api/capabilityCeiling.d.mts +2 -0
- package/dist/services/api/capabilityCeiling.mjs +2 -0
- package/dist/services/api/capabilityCeiling.mjs.map +1 -0
- package/dist/services/api/teamSnapshot.cjs +1 -0
- package/dist/services/api/teamSnapshot.d.cts +1 -0
- package/dist/services/api/teamSnapshot.d.mts +1 -0
- package/dist/services/api/teamSnapshot.mjs +2 -0
- package/dist/services/api/teamSnapshot.mjs.map +1 -0
- package/dist/services/intercom/intercomBridge.cjs.map +1 -0
- package/dist/services/intercom/intercomBridge.mjs.map +1 -0
- package/dist/services/intercom/nativeTeamChannel.cjs +5 -0
- package/dist/services/intercom/nativeTeamChannel.cjs.map +1 -0
- package/dist/{intercom → services/intercom}/nativeTeamChannel.d.cts +1 -1
- package/dist/services/intercom/nativeTeamChannel.d.cts.map +1 -0
- package/dist/{intercom → services/intercom}/nativeTeamChannel.d.mts +1 -1
- package/dist/services/intercom/nativeTeamChannel.d.mts.map +1 -0
- package/dist/services/intercom/nativeTeamChannel.mjs +5 -0
- package/dist/services/intercom/nativeTeamChannel.mjs.map +1 -0
- package/dist/services/intercom/supervisorControlChannel.cjs +2 -0
- package/dist/services/intercom/supervisorControlChannel.cjs.map +1 -0
- package/dist/{intercom → services/intercom}/supervisorControlChannel.mjs +1 -1
- package/dist/services/intercom/supervisorControlChannel.mjs.map +1 -0
- package/dist/services/runs/background/asyncExecution.cjs +2 -0
- package/dist/services/runs/background/asyncExecution.cjs.map +1 -0
- package/dist/services/runs/background/asyncExecution.mjs +2 -0
- package/dist/services/runs/background/asyncExecution.mjs.map +1 -0
- package/dist/services/runs/background/asyncJobTracker.cjs +1 -0
- package/dist/services/runs/background/asyncJobTracker.mjs +2 -0
- package/dist/services/runs/background/asyncJobTracker.mjs.map +1 -0
- package/dist/services/runs/background/cliRunnerEntry.cjs +2 -0
- package/dist/services/runs/background/cliRunnerEntry.cjs.map +1 -0
- package/dist/services/runs/background/cliRunnerEntry.mjs +2 -0
- package/dist/services/runs/background/cliRunnerEntry.mjs.map +1 -0
- package/dist/services/runs/background/completionBatcher.cjs.map +1 -0
- package/dist/services/runs/background/completionBatcher.mjs +2 -0
- package/dist/services/runs/background/completionBatcher.mjs.map +1 -0
- package/dist/{runs → services/runs}/background/notify.cjs +1 -1
- package/dist/services/runs/background/notify.cjs.map +1 -0
- package/dist/{runs → services/runs}/background/notify.mjs +1 -1
- package/dist/services/runs/background/notify.mjs.map +1 -0
- package/dist/services/runs/background/parentWatchdog.cjs.map +1 -0
- package/dist/services/runs/background/parentWatchdog.mjs.map +1 -0
- package/dist/services/runs/background/processTerminal.cjs +1 -0
- package/dist/services/runs/background/processTerminal.mjs +2 -0
- package/dist/services/runs/background/processTerminal.mjs.map +1 -0
- package/dist/services/runs/background/resultWatcher.cjs +1 -0
- package/dist/services/runs/background/resultWatcher.mjs +2 -0
- package/dist/services/runs/background/resultWatcher.mjs.map +1 -0
- package/dist/services/runs/background/runIdResolver.cjs +1 -0
- package/dist/services/runs/background/runIdResolver.mjs +2 -0
- package/dist/services/runs/background/runIdResolver.mjs.map +1 -0
- package/dist/services/runs/background/runnerBootstrap.cjs +2 -0
- package/dist/services/runs/background/runnerBootstrap.cjs.map +1 -0
- package/dist/services/runs/background/runnerBootstrap.mjs +2 -0
- package/dist/services/runs/background/runnerBootstrap.mjs.map +1 -0
- package/dist/services/runs/background/runnerExecution.cjs +2 -0
- package/dist/services/runs/background/runnerExecution.cjs.map +1 -0
- package/dist/services/runs/background/runnerExecution.mjs +2 -0
- package/dist/services/runs/background/runnerExecution.mjs.map +1 -0
- package/dist/{runs → services/runs}/background/runnerReporting.cjs +2 -2
- package/dist/services/runs/background/runnerReporting.cjs.map +1 -0
- package/dist/{runs → services/runs}/background/runnerReporting.mjs +2 -2
- package/dist/services/runs/background/runnerReporting.mjs.map +1 -0
- package/dist/services/runs/background/runnerTeamMembership.cjs +2 -0
- package/dist/services/runs/background/runnerTeamMembership.cjs.map +1 -0
- package/dist/services/runs/background/runnerTeamMembership.mjs +2 -0
- package/dist/services/runs/background/runnerTeamMembership.mjs.map +1 -0
- package/dist/services/runs/background/spawnHandshake.cjs +2 -0
- package/dist/services/runs/background/spawnHandshake.cjs.map +1 -0
- package/dist/services/runs/background/spawnHandshake.mjs +2 -0
- package/dist/services/runs/background/spawnHandshake.mjs.map +1 -0
- package/dist/services/runs/background/staleRunReconciler.cjs +1 -0
- package/dist/services/runs/background/staleRunReconciler.mjs +2 -0
- package/dist/services/runs/background/staleRunReconciler.mjs.map +1 -0
- package/dist/services/runs/background/statusReader.cjs +1 -0
- package/dist/services/runs/background/statusReader.mjs +2 -0
- package/dist/services/runs/background/statusReader.mjs.map +1 -0
- package/dist/services/runs/background/statusWriter.cjs +2 -0
- package/dist/services/runs/background/statusWriter.cjs.map +1 -0
- package/dist/services/runs/background/statusWriter.mjs +2 -0
- package/dist/services/runs/background/statusWriter.mjs.map +1 -0
- package/dist/services/runs/background/subagentWait.cjs +2 -0
- package/dist/services/runs/background/subagentWait.cjs.map +1 -0
- package/dist/services/runs/background/subagentWait.mjs +2 -0
- package/dist/services/runs/background/subagentWait.mjs.map +1 -0
- package/dist/{runs/background/terminalPersistence.mjs → services/runs/background/terminalPersistence.cjs} +2 -2
- package/dist/services/runs/background/terminalPersistence.cjs.map +1 -0
- package/dist/services/runs/background/terminalPersistence.mjs +2 -0
- package/dist/services/runs/background/terminalPersistence.mjs.map +1 -0
- package/dist/services/runs/extensionErrorTelemetry.cjs +2 -0
- package/dist/services/runs/extensionErrorTelemetry.cjs.map +1 -0
- package/dist/services/runs/extensionErrorTelemetry.mjs +2 -0
- package/dist/services/runs/extensionErrorTelemetry.mjs.map +1 -0
- package/dist/services/runs/registry/processGroup.cjs.map +1 -0
- package/dist/services/runs/registry/processGroup.mjs.map +1 -0
- package/dist/services/runs/registry/runRegistry.cjs +1 -0
- package/dist/services/runs/registry/runRegistry.mjs +2 -0
- package/dist/services/runs/registry/runRegistry.mjs.map +1 -0
- package/dist/services/runs/registry/sessionLifecycle.cjs +2 -0
- package/dist/services/runs/registry/sessionLifecycle.cjs.map +1 -0
- package/dist/services/runs/registry/sessionLifecycle.mjs +2 -0
- package/dist/services/runs/registry/sessionLifecycle.mjs.map +1 -0
- package/dist/services/runs/registry/suspendedRuns.cjs +1 -0
- package/dist/services/runs/registry/suspendedRuns.mjs +2 -0
- package/dist/services/runs/registry/suspendedRuns.mjs.map +1 -0
- package/dist/services/runs/sdkRunnerEntry.cjs +1 -0
- package/dist/services/runs/sdkRunnerEntry.mjs +1 -0
- package/dist/services/runs/shared/claudeFableProfile.cjs +4 -0
- package/dist/services/runs/shared/claudeFableProfile.cjs.map +1 -0
- package/dist/services/runs/shared/claudeFableProfile.mjs +4 -0
- package/dist/services/runs/shared/claudeFableProfile.mjs.map +1 -0
- package/dist/services/runs/shared/depthGuard.cjs +2 -0
- package/dist/services/runs/shared/depthGuard.cjs.map +1 -0
- package/dist/services/runs/shared/depthGuard.mjs +2 -0
- package/dist/services/runs/shared/depthGuard.mjs.map +1 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs +2 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs.map +1 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs +2 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs.map +1 -0
- package/dist/services/runs/shared/modelFallback.cjs +2 -0
- package/dist/services/runs/shared/modelFallback.cjs.map +1 -0
- package/dist/services/runs/shared/modelFallback.mjs +2 -0
- package/dist/services/runs/shared/modelFallback.mjs.map +1 -0
- package/dist/services/runs/shared/modelScope.cjs +2 -0
- package/dist/services/runs/shared/modelScope.cjs.map +1 -0
- package/dist/services/runs/shared/modelScope.mjs +2 -0
- package/dist/services/runs/shared/modelScope.mjs.map +1 -0
- package/dist/services/runs/shared/nestedPath.cjs +2 -0
- package/dist/services/runs/shared/nestedPath.cjs.map +1 -0
- package/dist/services/runs/shared/nestedPath.mjs +2 -0
- package/dist/services/runs/shared/nestedPath.mjs.map +1 -0
- package/dist/services/runs/shared/piArgs.cjs +2 -0
- package/dist/services/runs/shared/piArgs.cjs.map +1 -0
- package/dist/services/runs/shared/piArgs.mjs +2 -0
- package/dist/services/runs/shared/piArgs.mjs.map +1 -0
- package/dist/services/runs/shared/piSpawn.cjs +2 -0
- package/dist/services/runs/shared/piSpawn.cjs.map +1 -0
- package/dist/services/runs/shared/piSpawn.mjs +2 -0
- package/dist/services/runs/shared/piSpawn.mjs.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.cjs +2 -0
- package/dist/services/runs/shared/runWithConcurrency.cjs.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.d.cts +25 -0
- package/dist/services/runs/shared/runWithConcurrency.d.cts.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.d.mts +25 -0
- package/dist/services/runs/shared/runWithConcurrency.d.mts.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.mjs +2 -0
- package/dist/services/runs/shared/runWithConcurrency.mjs.map +1 -0
- package/dist/services/runs/shared/runtimeRegistry.cjs +2 -0
- package/dist/services/runs/shared/runtimeRegistry.cjs.map +1 -0
- package/dist/services/runs/shared/runtimeRegistry.mjs +2 -0
- package/dist/services/runs/shared/runtimeRegistry.mjs.map +1 -0
- package/dist/services/runs/shared/structuredOutput.cjs +2 -0
- package/dist/{runs → services/runs}/shared/structuredOutput.cjs.map +1 -1
- package/dist/services/runs/shared/structuredOutput.mjs +2 -0
- package/dist/{runs → services/runs}/shared/structuredOutput.mjs.map +1 -1
- package/dist/services/runs/shared/toolAvailability.cjs +2 -0
- package/dist/services/runs/shared/toolAvailability.cjs.map +1 -0
- package/dist/services/runs/shared/toolAvailability.mjs +2 -0
- package/dist/services/runs/shared/toolAvailability.mjs.map +1 -0
- package/dist/services/runs/shared/toolBudget.cjs +2 -0
- package/dist/services/runs/shared/toolBudget.cjs.map +1 -0
- package/dist/services/runs/shared/toolBudget.mjs +2 -0
- package/dist/services/runs/shared/toolBudget.mjs.map +1 -0
- package/dist/shared/artifacts.cjs.map +1 -1
- package/dist/shared/artifacts.mjs.map +1 -1
- package/dist/shared/atomicJson.cjs +1 -2
- package/dist/shared/atomicJson.mjs +1 -1
- package/dist/shared/atomicJson.mjs.map +1 -1
- package/dist/shared/childTranscript.cjs +1 -5
- package/dist/shared/childTranscript.mjs +1 -4
- package/dist/shared/childTranscript.mjs.map +1 -1
- package/dist/shared/fileSystemRetry.cjs +1 -1
- package/dist/shared/fileSystemRetry.cjs.map +1 -1
- package/dist/shared/fileSystemRetry.mjs +1 -1
- package/dist/shared/fileSystemRetry.mjs.map +1 -1
- package/dist/shared/formatters.cjs.map +1 -1
- package/dist/shared/formatters.mjs.map +1 -1
- package/dist/shared/paths.cjs +1 -1
- package/dist/shared/paths.cjs.map +1 -1
- package/dist/shared/paths.mjs +1 -1
- package/dist/shared/paths.mjs.map +1 -1
- package/dist/shared/pollScheduler.cjs +1 -2
- package/dist/shared/pollScheduler.mjs +1 -1
- package/dist/shared/pollScheduler.mjs.map +1 -1
- package/dist/shared/scopeOwner.cjs +1 -2
- package/dist/shared/scopeOwner.mjs +1 -1
- package/dist/shared/scopeOwner.mjs.map +1 -1
- package/dist/tui/agentCatalog.cjs +1 -1
- package/dist/tui/agentCatalog.cjs.map +1 -1
- package/dist/tui/agentCatalog.mjs +1 -1
- package/dist/tui/agentCatalog.mjs.map +1 -1
- package/dist/tui/agentResourceProjection.cjs +1 -1
- package/dist/tui/agentResourceProjection.cjs.map +1 -1
- package/dist/tui/agentResourceProjection.mjs +1 -1
- package/dist/tui/agentResourceProjection.mjs.map +1 -1
- package/dist/tui/completionNotice.cjs +1 -1
- package/dist/tui/completionNotice.cjs.map +1 -1
- package/dist/tui/completionNotice.mjs +1 -1
- package/dist/tui/completionNotice.mjs.map +1 -1
- package/dist/tui/fleet.cjs +2 -2
- package/dist/tui/fleet.cjs.map +1 -1
- package/dist/tui/fleet.mjs +2 -2
- package/dist/tui/fleet.mjs.map +1 -1
- package/dist/tui/fleetAgentView.cjs.map +1 -1
- package/dist/tui/fleetAgentView.mjs.map +1 -1
- package/dist/tui/fleetStatus.cjs +1 -1
- package/dist/tui/fleetStatus.cjs.map +1 -1
- package/dist/tui/fleetStatus.mjs +1 -1
- package/dist/tui/fleetStatus.mjs.map +1 -1
- package/dist/tui/fleetTranscript.cjs +1 -1
- package/dist/tui/fleetTranscript.cjs.map +1 -1
- package/dist/tui/fleetTranscript.mjs +1 -1
- package/dist/tui/fleetTranscript.mjs.map +1 -1
- package/dist/tui/register.cjs +1 -1
- package/dist/tui/register.cjs.map +1 -1
- package/dist/tui/register.mjs +1 -1
- package/dist/tui/register.mjs.map +1 -1
- package/dist/tui/subagentToolRender.cjs +3 -0
- package/dist/tui/subagentToolRender.cjs.map +1 -0
- package/dist/tui/subagentToolRender.mjs +3 -0
- package/dist/tui/subagentToolRender.mjs.map +1 -0
- package/dist/{types.d.cts → types/index.d.cts} +2 -2
- package/dist/types/index.d.cts.map +1 -0
- package/dist/{types.d.mts → types/index.d.mts} +2 -2
- package/dist/types/index.d.mts.map +1 -0
- package/package.json +23 -16
- package/dist/agents/discovery.cjs +0 -2
- package/dist/agents/discovery.cjs.map +0 -1
- package/dist/agents/discovery.mjs +0 -2
- package/dist/agents/discovery.mjs.map +0 -1
- package/dist/agents/frontmatter.cjs.map +0 -1
- package/dist/agents/frontmatter.mjs.map +0 -1
- package/dist/agents/loader.cjs +0 -2
- package/dist/agents/loader.cjs.map +0 -1
- package/dist/agents/loader.mjs +0 -2
- package/dist/agents/loader.mjs.map +0 -1
- package/dist/agents/memory.cjs +0 -3
- package/dist/agents/memory.cjs.map +0 -1
- package/dist/agents/memory.mjs.map +0 -1
- package/dist/agents/projectRoot.cjs +0 -2
- package/dist/agents/projectRoot.cjs.map +0 -1
- package/dist/agents/projectRoot.mjs +0 -2
- package/dist/agents/projectRoot.mjs.map +0 -1
- package/dist/agents/settings.cjs +0 -2
- package/dist/agents/settings.cjs.map +0 -1
- package/dist/agents/settings.mjs +0 -2
- package/dist/agents/settings.mjs.map +0 -1
- package/dist/agents/skills.cjs +0 -0
- package/dist/agents/skills.cjs.map +0 -1
- package/dist/agents/skills.mjs +0 -5
- package/dist/agents/skills.mjs.map +0 -1
- package/dist/agents/types.d.cts.map +0 -1
- package/dist/agents/types.d.mts.map +0 -1
- package/dist/api/capabilityCeiling.mjs.map +0 -1
- package/dist/api/teamSnapshot.mjs.map +0 -1
- package/dist/env.cjs.map +0 -1
- package/dist/env.d.cts.map +0 -1
- package/dist/env.d.mts.map +0 -1
- package/dist/extension.cjs +0 -2
- package/dist/extension.cjs.map +0 -1
- package/dist/extension.mjs +0 -2
- package/dist/extension.mjs.map +0 -1
- package/dist/intercom/intercomBridge.cjs.map +0 -1
- package/dist/intercom/intercomBridge.mjs.map +0 -1
- package/dist/intercom/nativeTeamChannel.cjs +0 -5
- package/dist/intercom/nativeTeamChannel.cjs.map +0 -1
- package/dist/intercom/nativeTeamChannel.d.cts.map +0 -1
- package/dist/intercom/nativeTeamChannel.d.mts.map +0 -1
- package/dist/intercom/nativeTeamChannel.mjs +0 -5
- package/dist/intercom/nativeTeamChannel.mjs.map +0 -1
- package/dist/intercom/supervisorControlChannel.cjs +0 -2
- package/dist/intercom/supervisorControlChannel.cjs.map +0 -1
- package/dist/intercom/supervisorControlChannel.mjs.map +0 -1
- package/dist/protocol/backgroundWork.cjs +0 -2
- package/dist/protocol/backgroundWork.d.cts.map +0 -1
- package/dist/protocol/backgroundWork.d.mts.map +0 -1
- package/dist/protocol/backgroundWork.mjs +0 -2
- package/dist/protocol/backgroundWork.mjs.map +0 -1
- package/dist/protocol/capabilityCeiling.cjs +0 -2
- package/dist/protocol/capabilityCeiling.cjs.map +0 -1
- package/dist/protocol/capabilityCeiling.d.cts.map +0 -1
- package/dist/protocol/capabilityCeiling.d.mts.map +0 -1
- package/dist/protocol/capabilityCeiling.mjs +0 -2
- package/dist/protocol/capabilityCeiling.mjs.map +0 -1
- package/dist/protocol/delegation.cjs.map +0 -1
- package/dist/protocol/delegation.d.cts.map +0 -1
- package/dist/protocol/delegation.d.mts.map +0 -1
- package/dist/protocol/delegation.mjs.map +0 -1
- package/dist/runs/background/asyncExecution.cjs +0 -2
- package/dist/runs/background/asyncExecution.cjs.map +0 -1
- package/dist/runs/background/asyncExecution.mjs +0 -2
- package/dist/runs/background/asyncExecution.mjs.map +0 -1
- package/dist/runs/background/asyncJobTracker.cjs +0 -2
- package/dist/runs/background/asyncJobTracker.cjs.map +0 -1
- package/dist/runs/background/asyncJobTracker.mjs +0 -2
- package/dist/runs/background/asyncJobTracker.mjs.map +0 -1
- package/dist/runs/background/cliRunnerEntry.cjs.map +0 -1
- package/dist/runs/background/cliRunnerEntry.mjs.map +0 -1
- package/dist/runs/background/completionBatcher.cjs.map +0 -1
- package/dist/runs/background/completionBatcher.mjs +0 -2
- package/dist/runs/background/completionBatcher.mjs.map +0 -1
- package/dist/runs/background/notify.cjs.map +0 -1
- package/dist/runs/background/notify.mjs.map +0 -1
- package/dist/runs/background/parentWatchdog.cjs.map +0 -1
- package/dist/runs/background/parentWatchdog.mjs.map +0 -1
- package/dist/runs/background/processTerminal.cjs +0 -2
- package/dist/runs/background/processTerminal.cjs.map +0 -1
- package/dist/runs/background/processTerminal.mjs +0 -2
- package/dist/runs/background/processTerminal.mjs.map +0 -1
- package/dist/runs/background/resultWatcher.cjs +0 -2
- package/dist/runs/background/resultWatcher.cjs.map +0 -1
- package/dist/runs/background/resultWatcher.mjs +0 -2
- package/dist/runs/background/resultWatcher.mjs.map +0 -1
- package/dist/runs/background/runIdResolver.cjs +0 -2
- package/dist/runs/background/runIdResolver.cjs.map +0 -1
- package/dist/runs/background/runIdResolver.mjs +0 -2
- package/dist/runs/background/runIdResolver.mjs.map +0 -1
- package/dist/runs/background/runnerBootstrap.cjs +0 -2
- package/dist/runs/background/runnerBootstrap.cjs.map +0 -1
- package/dist/runs/background/runnerBootstrap.mjs +0 -2
- package/dist/runs/background/runnerBootstrap.mjs.map +0 -1
- package/dist/runs/background/runnerExecution.cjs +0 -2
- package/dist/runs/background/runnerExecution.cjs.map +0 -1
- package/dist/runs/background/runnerExecution.mjs +0 -2
- package/dist/runs/background/runnerExecution.mjs.map +0 -1
- package/dist/runs/background/runnerReporting.cjs.map +0 -1
- package/dist/runs/background/runnerReporting.mjs.map +0 -1
- package/dist/runs/background/runnerTeamMembership.cjs +0 -2
- package/dist/runs/background/runnerTeamMembership.cjs.map +0 -1
- package/dist/runs/background/runnerTeamMembership.mjs +0 -2
- package/dist/runs/background/runnerTeamMembership.mjs.map +0 -1
- package/dist/runs/background/spawnHandshake.cjs +0 -2
- package/dist/runs/background/spawnHandshake.cjs.map +0 -1
- package/dist/runs/background/spawnHandshake.mjs +0 -2
- package/dist/runs/background/spawnHandshake.mjs.map +0 -1
- package/dist/runs/background/staleRunReconciler.cjs +0 -2
- package/dist/runs/background/staleRunReconciler.cjs.map +0 -1
- package/dist/runs/background/staleRunReconciler.mjs +0 -2
- package/dist/runs/background/staleRunReconciler.mjs.map +0 -1
- package/dist/runs/background/statusReader.cjs +0 -2
- package/dist/runs/background/statusReader.cjs.map +0 -1
- package/dist/runs/background/statusReader.mjs +0 -2
- package/dist/runs/background/statusReader.mjs.map +0 -1
- package/dist/runs/background/statusWriter.cjs +0 -2
- package/dist/runs/background/statusWriter.cjs.map +0 -1
- package/dist/runs/background/statusWriter.mjs +0 -2
- package/dist/runs/background/statusWriter.mjs.map +0 -1
- package/dist/runs/background/subagentWait.cjs +0 -2
- package/dist/runs/background/subagentWait.cjs.map +0 -1
- package/dist/runs/background/subagentWait.mjs +0 -2
- package/dist/runs/background/subagentWait.mjs.map +0 -1
- package/dist/runs/background/terminalPersistence.cjs +0 -2
- package/dist/runs/background/terminalPersistence.cjs.map +0 -1
- package/dist/runs/background/terminalPersistence.mjs.map +0 -1
- package/dist/runs/registry/processGroup.cjs.map +0 -1
- package/dist/runs/registry/processGroup.mjs.map +0 -1
- package/dist/runs/registry/runRegistry.cjs +0 -2
- package/dist/runs/registry/runRegistry.cjs.map +0 -1
- package/dist/runs/registry/runRegistry.mjs +0 -2
- package/dist/runs/registry/runRegistry.mjs.map +0 -1
- package/dist/runs/registry/sessionLifecycle.cjs +0 -2
- package/dist/runs/registry/sessionLifecycle.cjs.map +0 -1
- package/dist/runs/registry/sessionLifecycle.mjs +0 -2
- package/dist/runs/registry/sessionLifecycle.mjs.map +0 -1
- package/dist/runs/registry/suspendedRuns.cjs +0 -4
- package/dist/runs/registry/suspendedRuns.cjs.map +0 -1
- package/dist/runs/registry/suspendedRuns.mjs +0 -4
- package/dist/runs/registry/suspendedRuns.mjs.map +0 -1
- package/dist/runs/sdkRunnerEntry.cjs.map +0 -1
- package/dist/runs/sdkRunnerEntry.mjs.map +0 -1
- package/dist/runs/shared/claudeFableProfile.cjs +0 -4
- package/dist/runs/shared/claudeFableProfile.cjs.map +0 -1
- package/dist/runs/shared/claudeFableProfile.mjs +0 -4
- package/dist/runs/shared/claudeFableProfile.mjs.map +0 -1
- package/dist/runs/shared/depthGuard.cjs +0 -2
- package/dist/runs/shared/depthGuard.cjs.map +0 -1
- package/dist/runs/shared/depthGuard.mjs +0 -2
- package/dist/runs/shared/depthGuard.mjs.map +0 -1
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs +0 -2
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs.map +0 -1
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs +0 -2
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs.map +0 -1
- package/dist/runs/shared/modelFallback.cjs +0 -2
- package/dist/runs/shared/modelFallback.cjs.map +0 -1
- package/dist/runs/shared/modelFallback.mjs +0 -2
- package/dist/runs/shared/modelFallback.mjs.map +0 -1
- package/dist/runs/shared/modelScope.cjs +0 -2
- package/dist/runs/shared/modelScope.cjs.map +0 -1
- package/dist/runs/shared/modelScope.mjs +0 -2
- package/dist/runs/shared/modelScope.mjs.map +0 -1
- package/dist/runs/shared/nestedPath.cjs +0 -2
- package/dist/runs/shared/nestedPath.cjs.map +0 -1
- package/dist/runs/shared/nestedPath.mjs +0 -2
- package/dist/runs/shared/nestedPath.mjs.map +0 -1
- package/dist/runs/shared/piArgs.cjs +0 -2
- package/dist/runs/shared/piArgs.cjs.map +0 -1
- package/dist/runs/shared/piArgs.mjs +0 -2
- package/dist/runs/shared/piArgs.mjs.map +0 -1
- package/dist/runs/shared/piSpawn.cjs +0 -2
- package/dist/runs/shared/piSpawn.cjs.map +0 -1
- package/dist/runs/shared/piSpawn.mjs +0 -2
- package/dist/runs/shared/piSpawn.mjs.map +0 -1
- package/dist/runs/shared/runWithConcurrency.cjs +0 -2
- package/dist/runs/shared/runWithConcurrency.cjs.map +0 -1
- package/dist/runs/shared/runWithConcurrency.mjs +0 -2
- package/dist/runs/shared/runWithConcurrency.mjs.map +0 -1
- package/dist/runs/shared/runtimeRegistry.cjs +0 -2
- package/dist/runs/shared/runtimeRegistry.cjs.map +0 -1
- package/dist/runs/shared/runtimeRegistry.mjs +0 -2
- package/dist/runs/shared/runtimeRegistry.mjs.map +0 -1
- package/dist/runs/shared/structuredOutput.cjs +0 -2
- package/dist/runs/shared/structuredOutput.mjs +0 -2
- package/dist/runs/shared/toolAvailability.cjs +0 -2
- package/dist/runs/shared/toolAvailability.cjs.map +0 -1
- package/dist/runs/shared/toolAvailability.mjs +0 -2
- package/dist/runs/shared/toolAvailability.mjs.map +0 -1
- package/dist/runs/shared/toolBudget.cjs +0 -2
- package/dist/runs/shared/toolBudget.cjs.map +0 -1
- package/dist/runs/shared/toolBudget.mjs +0 -2
- package/dist/runs/shared/toolBudget.mjs.map +0 -1
- package/dist/shared/atomicJson.cjs.map +0 -1
- package/dist/shared/childTranscript.cjs.map +0 -1
- package/dist/shared/pollScheduler.cjs.map +0 -1
- package/dist/shared/scopeOwner.cjs.map +0 -1
- package/dist/slash/chainExpression.cjs.map +0 -1
- package/dist/slash/chainExpression.mjs.map +0 -1
- package/dist/slash/doctor.cjs +0 -5
- package/dist/slash/doctor.mjs +0 -5
- package/dist/slash/doctor.mjs.map +0 -1
- package/dist/slash/slashCommands.cjs.map +0 -1
- package/dist/slash/slashCommands.mjs.map +0 -1
- package/dist/slash/spawnRequestMapping.cjs.map +0 -1
- package/dist/slash/spawnRequestMapping.mjs.map +0 -1
- package/dist/slash/subagentLaunch.cjs +0 -2
- package/dist/slash/subagentLaunch.cjs.map +0 -1
- package/dist/slash/subagentLaunch.mjs +0 -2
- package/dist/slash/subagentLaunch.mjs.map +0 -1
- package/dist/types.d.cts.map +0 -1
- package/dist/types.d.mts.map +0 -1
- /package/dist/{slash → commands/slash}/chainExpression.cjs +0 -0
- /package/dist/{slash → commands/slash}/chainExpression.mjs +0 -0
- /package/dist/{slash → commands/slash}/spawnRequestMapping.cjs +0 -0
- /package/dist/{slash → commands/slash}/spawnRequestMapping.mjs +0 -0
- /package/dist/{protocol → schemas/team}/delegation.cjs +0 -0
- /package/dist/{protocol → schemas/team}/delegation.mjs +0 -0
- /package/dist/{agents → services/agents}/frontmatter.cjs +0 -0
- /package/dist/{intercom → services/intercom}/intercomBridge.cjs +0 -0
- /package/dist/{intercom → services/intercom}/intercomBridge.mjs +0 -0
- /package/dist/{runs → services/runs}/background/completionBatcher.cjs +0 -0
- /package/dist/{runs → services/runs}/background/parentWatchdog.cjs +0 -0
- /package/dist/{runs → services/runs}/background/parentWatchdog.mjs +0 -0
- /package/dist/{runs → services/runs}/registry/processGroup.cjs +0 -0
- /package/dist/{runs → services/runs}/registry/processGroup.mjs +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"staleRunReconciler.mjs","names":[],"sources":["../../../src/runs/background/staleRunReconciler.ts"],"sourcesContent":["/**\n * Repairs a run's `status.json` from `running` to `failed` when, and only\n * when, its owning process is provably dead.\n *\n * WHY THIS EXISTS:\n * A runner process can die without ever reaching\n * `TerminalPersistenceService.finalize()` - `SIGKILL`, a host crash, an `OOM`\n * kill. `status.json` is then stuck reporting `running` forever: nothing else\n * in this package ever revisits it, because nothing else has a reason to.\n * This is that reason. It is read-only reconnaissance until it has actual\n * proof, and exactly one narrow write when it does.\n *\n * WHY THIS DOES NOT TOUCH THE RESULT FILE (the TOCTOU the predecessor had):\n * `doom-pi-subagents/src/runs/background/staleRunReconciler.ts:436-450`\n * checks `fs.existsSync(resultPath)`, then separately reads and parses that\n * same path to decide how to repair `status.json` from the result's content.\n * Between those two steps, `ResultWatcher.claimResult()` on the other side of\n * this same package can rename that exact path away as part of its own\n * atomic claim (`resultWatcher.ts`'s fix 4) - so the existence check and the\n * read do not necessarily observe the same file. This module never reads or\n * interprets a result file at all. It asks `RunIdResolver`, which already\n * knows about both the pending and the claimed location, only ONE question:\n * \"does a result exist anywhere for this run yet, in either form?\" If the\n * answer is yes, this module does nothing - a result existing at all means\n * the run finished, which is `ResultWatcher` and whatever consumes its\n * delivery's job to turn into a status update, not a staleness this module\n * needs to repair. This module only ever acts when NO result exists anywhere\n * and the owning process is provably gone, which is a state `ResultWatcher`\n * has no reason to ever contend for.\n *\n * WHY THIS NEVER CALLS `writeAtomicJson(statusPath, ...)`:\n * `CoalescedStatusWriter` is the only thing in this package permitted to write\n * `status.json` (see that module's header). This class is not exempt: a\n * repair opens its OWN, dedicated `CoalescedStatusWriter` instance - never the\n * process-wide singleton, which may simultaneously be the live write path for\n * a run this process is actually running - seeds it from the on-disk status\n * that was just read, and applies the repair through `updateSync()`, the same\n * as any other terminal transition in this package. The write is still, in\n * every observable way, `CoalescedStatusWriter` writing `status.json`; only\n * the instance's *lifetime* is scoped to this one repair.\n *\n * WHAT \"PROVABLE\" MEANS HERE:\n * Only `ProcessTerminalInspector.inspect()` returning `'crashed'` - an\n * OS-confirmed `ESRCH` on the crash marker's pid - authorizes a repair. The\n * predecessor also repaired a run whose pid was still alive (or unverifiable)\n * purely because `status.json` had not updated in 24 hours\n * (`staleAlivePidMs`). That branch is deliberately NOT ported: an\n * unresponsive-but-alive pid is still alive, and treating a long silence as\n * death is a guess, not proof. A run that is merely slow survives this module\n * untouched, by design.\n *\n * ORPHANED CLAIMS (steer-requests, append-requests, and any future queue):\n * `control-channel.ts` claims a queued steer request by renaming it to\n * `<name>.claim-<claimant>` before dispatch, then `commit()`s (deletes) it on\n * success or `release()`s (renames it back) on a throw. `chain-append.ts`\n * does the identical thing for append-requests. A hard kill between claim and\n * release leaves the `.claim-<claimant>` file behind with neither outcome\n * ever happening - the request just sits there, invisible, forever.\n * `sweepOrphanedClaims()` is the same kind of repair as `reconcile()`, for a\n * different piece of state: it puts every orphaned claim back under its\n * original queued name so the next scan retries it, on the same \"provable\n * death, never a guess\" standard.\n *\n * Per `port-steering`, confirmed against the actual code for both queues\n * separately rather than assumed to carry over from one to the other: the\n * two queues do NOT construct their queued filenames the same way (steer\n * zero-pads a timestamp and base64url-encodes an id; append uses a raw\n * `Date.now()` integer and a raw `randomUUID()`), but both still guarantee no\n * literal `.` before the trailing `.json`, so a last-occurrence split on\n * `.claim-` is unambiguous for either. For claimant identity: steer's only\n * production claimant is `` `${process.pid}-${randomUUID()}` ``, built by the\n * run's own runner process. Append-requests has no production claimant yet -\n * nothing in this package claims from it in production, only tests - so its\n * eventual format is undecided. What DOES hold for append-requests today,\n * independent of whatever claimant format is eventually chosen, is that\n * `append-requests/` lives under `currentRunsDir()/<runId>/`, scoped to exactly one\n * run, and nothing in this package's architecture has a reason for any\n * process other than that run's own runner to ever claim from its own queue.\n * So for every queue this module knows about, \"is this claim orphaned\" and\n * \"is this run's owning process provably dead\" are the same question,\n * answered by the same evidence - for steer-requests because of what its one\n * real claimant is, for append-requests because of what the directory can\n * structurally ever contain - which is why this method takes no claimant and\n * gates purely on `ProcessTerminalInspector.inspect(runId)` the same as\n * `reconcile()` does - once that says `'crashed'`, every claim under every\n * one of the run's claim-queue directories is orphaned, full stop; there is\n * nothing left to correlate a specific claim against.\n *\n * STRUCTURAL DISCOVERY, NOT A REGISTRY (revised from an earlier version of\n * this module that kept a `CLAIM_QUEUES` list of known queue directories):\n * A registry means a third queue added later is swept only if whoever adds it\n * remembers to add an entry here too - the exact \"someone has to remember\"\n * failure mode this whole feature exists to close for orphaned claims\n * themselves. So `sweepOrphanedClaims()` does not enumerate queues at all: it\n * walks every directory under the run's own folder (`currentRunsDir()/<runId>/`,\n * bounded by `MAX_CLAIM_SCAN_DEPTH` as insurance, not because any real queue\n * nests that deep) and recovers whatever it finds containing `.claim-`. A\n * queue added anywhere under a run's directory is covered from the day it\n * exists, with no change to this file.\n *\n * WHAT THIS GIVES UP, DELIBERATELY: there is no longer a single list a reader\n * can check to see \"which queues does this module know about\" - the queues\n * ARE whatever is on disk, discovered structurally. It also makes the\n * `.claim-<claimant>` suffix a load-bearing, package-wide contract rather\n * than an internal detail of `control-channel.ts` and `chain-append.ts`: any\n * future file under a run's directory whose name happens to contain that\n * substring is treated as an orphaned claim and unconditionally recovered by\n * stripping the suffix, on the assumption that anything shaped like this\n * package's one established claim-by-rename convention behaves like it. That\n * assumption held for both queues that exist today (see the `port-steering`\n * paragraph above) and is judged worth the coverage guarantee in exchange.\n *\n * `sweepOrphanedClaims()` is a separate public method rather than a step\n * folded into `reconcile()`, because it has an independent gating condition\n * (it is not related to `status.json`'s state, or to whether a result\n * exists) and an independent, testable outcome.\n *\n * AVOID:\n * - Synthesizing a status for a run whose `status.json` does not exist yet.\n * `CoalescedStatusWriter.open()` writes it synchronously before returning\n * (see that module's header), so a genuinely started run already has one by\n * the time anything could observe it as missing; a missing file is either a\n * run that has not started yet or an id that does not exist, and this\n * module cannot tell those apart, so it reports nothing for either\n * - Treating `'unknown'` liveness the same as `'crashed'`; see\n * `processTerminal.ts` for why they are kept distinct\n * - Parsing a pid out of a claimant string. It is `ControlChannelWatcher`'s\n * implementation detail, not a documented wire format (`port-steering`'s\n * own words); the crash marker is already the load-bearing evidence\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport { appendJsonl } from '../../shared/artifacts.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { TERMINAL_ASYNC_JOB_STATES } from './asyncJobTracker.ts';\nimport type { IProcessTerminalInspector } from './processTerminal.ts';\nimport type { IRunIdResolver } from './runIdResolver.ts';\nimport {\n CoalescedStatusWriter,\n STATUS_FILE_NAME,\n type ICoalescedStatusWriter,\n type StatusWithRecentEntries,\n} from './statusWriter.ts';\n\nconst EVENTS_FILE_NAME = 'events.jsonl';\n\n/**\n * Matches `control-channel.ts`'s and `chain-append.ts`'s private\n * `CLAIM_SUFFIX_PREFIX` exactly (the two are identical to each other); not\n * exported from either module, and now the one thing that makes a directory\n * entry \"a claim\" for the purposes of the structural scan below.\n */\nconst CLAIM_SUFFIX_PREFIX = '.claim-';\n\n/**\n * Insurance, not a modeled limit: no real claim queue nests anywhere near\n * this deep under a run's directory. Bounds `walkForClaims()` so a pathological\n * or symlink-cycle directory tree cannot make the scan unbounded.\n */\nconst MAX_CLAIM_SCAN_DEPTH = 8;\n\n/**\n * The minimum shape this module reads and repairs. A run's actual status\n * carries far more than this - this package has not settled a shared status\n * type yet - so everything else on disk is preserved verbatim by round\n * tripping the whole parsed object through `open()`, never reconstructed here.\n */\nexport interface ReconcilableStatus extends StatusWithRecentEntries {\n runId?: string;\n state?: string;\n error?: string;\n startedAt?: number;\n lastUpdate?: number;\n endedAt?: number;\n [key: string]: unknown;\n}\n\nexport interface ReconcileOutcome {\n repaired: boolean;\n /** Why nothing happened, or what was repaired - for logging, never parsed. */\n reason: string;\n}\n\n/** One orphaned claim recovered back to its original queued name. */\nexport interface RecoveredClaim {\n /**\n * The queue's directory, relative to the run's own directory and\n * forward-slash-normalized (e.g. `'append-requests'`, `'control/steer-requests'`).\n * Discovered structurally by `walkForClaims()`, not looked up in a registry.\n */\n queue: string;\n /** The original (recovered) filename, in that queue. */\n fileName: string;\n}\n\nexport interface SweepOutcome {\n recovered: RecoveredClaim[];\n}\n\n/** Both repairs from one pass over a run. See `IStaleRunReconciler.reconcileAndSweep`. */\nexport interface ReconcileSweepOutcome {\n reconcile: ReconcileOutcome;\n sweep: SweepOutcome;\n}\n\nexport interface IStaleRunReconciler {\n /** Repair `runId`'s status if, and only if, its process is provably dead. */\n reconcile(runId: string): ReconcileOutcome;\n /** Recover every one of `runId`'s orphaned claims, discovered structurally under its run directory, if and only if its process is provably dead. */\n sweepOrphanedClaims(runId: string): SweepOutcome;\n /**\n * Both repairs, sharing ONE `ProcessTerminalInspector.inspect()` call.\n *\n * Calling `reconcile()` and `sweepOrphanedClaims()` separately - which is\n * what the parent's poll subscriber used to do - inspects the same run\n * twice per tick, and each inspect is a crash-marker read, a JSON parse and\n * a `kill(pid, 0)`. Both gate on the identical question (\"is this run's\n * owning process provably dead?\"), so the second answer is always the first\n * answer; only the cost is new. For a healthy child, which is the\n * overwhelmingly common case, every bit of that work is discarded.\n *\n * The two methods stay public and independently callable: they have\n * genuinely independent gating conditions (see `sweepOrphanedClaims`'s own\n * doc) and independent tests. This is a shared-work wrapper over them, not\n * a replacement.\n *\n * The inspect is lazy and `reconcile`'s cheap file gates run before it, so\n * a terminal or already-delivered run never pays for liveness on the\n * reconcile side. It still pays once on the sweep side: orphaned claims are\n * gated on process death alone, deliberately independent of `status.json`\n * (see `sweepOrphanedClaims`), and folding them into the status gate would\n * quietly stop recovering claims for a run that finished. The guarantee here\n * is exactly one probe per pass, not zero.\n *\n * In practice the parent's subscriber skips terminal runs before calling\n * this at all, so that case costs nothing in the hot path either.\n */\n reconcileAndSweep(runId: string): ReconcileSweepOutcome;\n}\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n@injectable()\nexport class StaleRunReconciler implements IStaleRunReconciler {\n constructor(\n @inject(DOOM_TEAM_TYPES.ProcessTerminalInspector) private readonly processTerminal: IProcessTerminalInspector,\n @inject(DOOM_TEAM_TYPES.RunIdResolver) private readonly runIdResolver: IRunIdResolver,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n /**\n * A fresh writer per repair, deliberately never the container's singleton -\n * see the class doc for why sharing it would be unsafe. Protected so a test\n * can substitute a recording fake instead of touching disk.\n */\n protected createStatusWriter(): ICoalescedStatusWriter<ReconcilableStatus> {\n return new CoalescedStatusWriter<ReconcilableStatus>();\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return undefined;\n throw error;\n }\n }\n\n protected readStatus(runId: string): ReconcilableStatus | undefined {\n const raw = this.readFile(statusPathFor(runId));\n if (raw === undefined) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A status file mid-write (CoalescedStatusWriter's rename not yet\n // visible, or a torn read) is not evidence of anything; retried later.\n return undefined;\n }\n return isRecord(parsed) ? (parsed as ReconcilableStatus) : undefined;\n }\n\n /** Best-effort diagnostic trail; a failing event log must never fail a repair. */\n protected appendEvent(runId: string, payload: object): void {\n try {\n appendJsonl(path.join(currentRunsDir(), runId, EVENTS_FILE_NAME), JSON.stringify(payload));\n } catch {\n // See method doc.\n }\n }\n\n /**\n * Walks `dir` and every subdirectory beneath it (bounded by\n * `MAX_CLAIM_SCAN_DEPTH`), returning every entry whose name contains\n * `.claim-`, wherever it is found. Replaces a flat, single-directory\n * listing keyed off a registry: see the class doc's \"STRUCTURAL DISCOVERY\"\n * section for why. Recursing only on `entry.isDirectory()` means a symlink\n * (`isSymbolicLink()`, not `isDirectory()`) is never followed, so this\n * needs no separate cycle-detection.\n */\n protected walkForClaims(dir: string, depth = 0): Array<{ dir: string; name: string }> {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === 'ENOENT') return [];\n throw error;\n }\n\n const found: Array<{ dir: string; name: string }> = [];\n for (const entry of entries) {\n if (entry.name.includes(CLAIM_SUFFIX_PREFIX)) {\n found.push({ dir, name: entry.name });\n } else if (entry.isDirectory() && depth < MAX_CLAIM_SCAN_DEPTH) {\n found.push(...this.walkForClaims(path.join(dir, entry.name), depth + 1));\n }\n }\n return found;\n }\n\n /**\n * Put a claimed file back under its original queued name. Returns `true`\n * only when the rename actually happened. Mirrors `control-channel.ts`'s\n * own `release()`: on any failure (original name occupied - astronomically\n * unlikely, the directory gone), there is nothing left to preserve by\n * retrying, so the orphaned claim is dropped rather than left unreachable.\n */\n protected recoverClaim(claimedPath: string, originalPath: string): boolean {\n try {\n fs.renameSync(claimedPath, originalPath);\n return true;\n } catch {\n try {\n fs.unlinkSync(claimedPath);\n } catch {\n // Best effort; see method doc.\n }\n return false;\n }\n }\n\n /**\n * A memoizing wrapper over `inspect()`, so one tick's pair of repairs asks\n * the OS about a pid once instead of twice. Each public entry point makes\n * its own, so a standalone `reconcile()` still inspects exactly when it\n * always did.\n */\n private inspectOnce(runId: string): () => ReturnType<IProcessTerminalInspector['inspect']> {\n let verdict: ReturnType<IProcessTerminalInspector['inspect']> | undefined;\n return () => (verdict ??= this.processTerminal.inspect(runId));\n }\n\n reconcile(runId: string): ReconcileOutcome {\n return this.reconcileWith(runId, this.inspectOnce(runId));\n }\n\n sweepOrphanedClaims(runId: string): SweepOutcome {\n return this.sweepWith(runId, this.inspectOnce(runId));\n }\n\n reconcileAndSweep(runId: string): ReconcileSweepOutcome {\n const inspect = this.inspectOnce(runId);\n // Order matters: `reconcileWith` runs its cheap file gates before ever\n // calling `inspect`, so a terminal or already-delivered run pays nothing\n // extra here. Whichever of the two asks first funds the single call.\n return { reconcile: this.reconcileWith(runId, inspect), sweep: this.sweepWith(runId, inspect) };\n }\n\n private reconcileWith(\n runId: string,\n inspect: () => ReturnType<IProcessTerminalInspector['inspect']>,\n ): ReconcileOutcome {\n const status = this.readStatus(runId);\n if (!status) return { repaired: false, reason: 'No status file for this run.' };\n\n const state = status.state;\n if (state !== undefined && TERMINAL_ASYNC_JOB_STATES.has(state)) {\n return { repaired: false, reason: `Run already terminal (${state}).` };\n }\n\n const location = this.runIdResolver.resolve(runId);\n if (location?.resultPath) {\n return { repaired: false, reason: \"A result already exists; delivery is ResultWatcher's job, not a repair.\" };\n }\n\n const verdict = inspect();\n if (verdict.state !== 'crashed') {\n return {\n repaired: false,\n reason: `Owning process liveness is '${verdict.state}', which does not prove death; leaving the run as is.`,\n };\n }\n\n return this.repair(runId, status, verdict.marker.pid);\n }\n\n private sweepWith(runId: string, inspect: () => ReturnType<IProcessTerminalInspector['inspect']>): SweepOutcome {\n const verdict = inspect();\n if (verdict.state !== 'crashed') return { recovered: [] };\n\n const runDir = path.join(currentRunsDir(), runId);\n const recovered: RecoveredClaim[] = [];\n for (const { dir, name: entry } of this.walkForClaims(runDir)) {\n const claimIndex = entry.lastIndexOf(CLAIM_SUFFIX_PREFIX);\n // Filtered by walkForClaims() already; defensive, not reachable.\n if (claimIndex === -1) continue;\n const fileName = entry.slice(0, claimIndex);\n const wasRecovered = this.recoverClaim(path.join(dir, entry), path.join(dir, fileName));\n if (wasRecovered) {\n const queue = path.relative(runDir, dir).split(path.sep).join('/');\n recovered.push({ queue, fileName });\n this.appendEvent(runId, {\n type: 'subagent.run.claim_recovered',\n ts: this.now(),\n runId,\n pid: verdict.marker.pid,\n queue,\n recovered: fileName,\n });\n }\n }\n return { recovered };\n }\n\n private repair(runId: string, status: ReconcilableStatus, deadPid: number): ReconcileOutcome {\n const now = this.now();\n const message = `Runner process ${deadPid} exited without finalizing this run. Marked failed by stale-run reconciliation.`;\n\n const writer = this.createStatusWriter();\n writer.open(runId, status);\n writer.updateSync((current) => {\n current.state = 'failed';\n current.error = current.error ?? message;\n current.lastUpdate = now;\n current.endedAt = current.endedAt ?? now;\n });\n\n this.appendEvent(runId, { type: 'subagent.run.repaired_stale', ts: now, runId, pid: deadPid, message });\n\n return { repaired: true, reason: message };\n }\n}\n"],"mappings":"28BAoPA,SAAS,EAAc,EAAuB,CAC5C,OAAO,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAiB,CAG7D,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAGN,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,0BAxHtB,IACC,IACG,IACM,IAQtC,aAErB,EAAmB,eAQnB,EAAsB,UAOtB,EAAuB,IAgGtB,KAAwD,CAC7D,YACE,EACA,EACA,CAFmE,KAAA,gBAAA,EACX,KAAA,cAAA,EAG1D,KAAwB,CACtB,OAAO,KAAK,KAAK,CAQnB,oBAA2E,CACzE,OAAO,IAAI,EAGb,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAO,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,GAIV,WAAqB,EAA+C,CAClE,IAAM,EAAM,KAAK,SAAS,EAAc,EAAM,CAAC,CAC/C,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CAGN,OAEF,OAAO,EAAS,EAAO,CAAI,EAAgC,IAAA,GAI7D,YAAsB,EAAe,EAAuB,CAC1D,GAAI,CACF,EAAY,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAiB,CAAE,KAAK,UAAU,EAAQ,CAAC,MACpF,GAcV,cAAwB,EAAa,EAAQ,EAAyC,CACpF,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,EAAK,CAAE,cAAe,GAAM,CAAC,OAC/C,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,MAAO,EAAE,CAC5C,MAAM,EAGR,IAAM,EAA8C,EAAE,CACtD,IAAK,IAAM,KAAS,EACd,EAAM,KAAK,SAAS,EAAoB,CAC1C,EAAM,KAAK,CAAE,MAAK,KAAM,EAAM,KAAM,CAAC,CAC5B,EAAM,aAAa,EAAI,EAAQ,GACxC,EAAM,KAAK,GAAG,KAAK,cAAc,EAAK,KAAK,EAAK,EAAM,KAAK,CAAE,EAAQ,EAAE,CAAC,CAG5E,OAAO,EAUT,aAAuB,EAAqB,EAA+B,CACzE,GAAI,CAEF,OADA,EAAG,WAAW,EAAa,EAAa,CACjC,QACD,CACN,GAAI,CACF,EAAG,WAAW,EAAY,MACpB,EAGR,MAAO,IAUX,YAAoB,EAAuE,CACzF,IAAI,EACJ,UAAc,IAAY,KAAK,gBAAgB,QAAQ,EAAM,CAG/D,UAAU,EAAiC,CACzC,OAAO,KAAK,cAAc,EAAO,KAAK,YAAY,EAAM,CAAC,CAG3D,oBAAoB,EAA6B,CAC/C,OAAO,KAAK,UAAU,EAAO,KAAK,YAAY,EAAM,CAAC,CAGvD,kBAAkB,EAAsC,CACtD,IAAM,EAAU,KAAK,YAAY,EAAM,CAIvC,MAAO,CAAE,UAAW,KAAK,cAAc,EAAO,EAAQ,CAAE,MAAO,KAAK,UAAU,EAAO,EAAQ,CAAE,CAGjG,cACE,EACA,EACkB,CAClB,IAAM,EAAS,KAAK,WAAW,EAAM,CACrC,GAAI,CAAC,EAAQ,MAAO,CAAE,SAAU,GAAO,OAAQ,+BAAgC,CAE/E,IAAM,EAAQ,EAAO,MACrB,GAAI,IAAU,IAAA,IAAa,EAA0B,IAAI,EAAM,CAC7D,MAAO,CAAE,SAAU,GAAO,OAAQ,yBAAyB,EAAM,IAAK,CAIxE,GADiB,KAAK,cAAc,QAAQ,EAChC,EAAE,WACZ,MAAO,CAAE,SAAU,GAAO,OAAQ,0EAA2E,CAG/G,IAAM,EAAU,GAAS,CAQzB,OAPI,EAAQ,QAAU,UAOf,KAAK,OAAO,EAAO,EAAQ,EAAQ,OAAO,IAAI,CAN5C,CACL,SAAU,GACV,OAAQ,+BAA+B,EAAQ,MAAM,uDACtD,CAML,UAAkB,EAAe,EAA+E,CAC9G,IAAM,EAAU,GAAS,CACzB,GAAI,EAAQ,QAAU,UAAW,MAAO,CAAE,UAAW,EAAE,CAAE,CAEzD,IAAM,EAAS,EAAK,KAAK,GAAgB,CAAE,EAAM,CAC3C,EAA8B,EAAE,CACtC,IAAK,GAAM,CAAE,MAAK,KAAM,KAAW,KAAK,cAAc,EAAO,CAAE,CAC7D,IAAM,EAAa,EAAM,YAAY,EAAoB,CAEzD,GAAI,IAAe,GAAI,SACvB,IAAM,EAAW,EAAM,MAAM,EAAG,EAAW,CAE3C,GADqB,KAAK,aAAa,EAAK,KAAK,EAAK,EAAM,CAAE,EAAK,KAAK,EAAK,EAAS,CACtE,CAAE,CAChB,IAAM,EAAQ,EAAK,SAAS,EAAQ,EAAI,CAAC,MAAM,EAAK,IAAI,CAAC,KAAK,IAAI,CAClE,EAAU,KAAK,CAAE,QAAO,WAAU,CAAC,CACnC,KAAK,YAAY,EAAO,CACtB,KAAM,+BACN,GAAI,KAAK,KAAK,CACd,QACA,IAAK,EAAQ,OAAO,IACpB,QACA,UAAW,EACZ,CAAC,EAGN,MAAO,CAAE,YAAW,CAGtB,OAAe,EAAe,EAA4B,EAAmC,CAC3F,IAAM,EAAM,KAAK,KAAK,CAChB,EAAU,kBAAkB,EAAQ,iFAEpC,EAAS,KAAK,oBAAoB,CAWxC,OAVA,EAAO,KAAK,EAAO,EAAO,CAC1B,EAAO,WAAY,GAAY,CAC7B,EAAQ,MAAQ,SAChB,EAAQ,MAAQ,EAAQ,OAAS,EACjC,EAAQ,WAAa,EACrB,EAAQ,QAAU,EAAQ,SAAW,GACrC,CAEF,KAAK,YAAY,EAAO,CAAE,KAAM,8BAA+B,GAAI,EAAK,QAAO,IAAK,EAAS,UAAS,CAAC,CAEhG,CAAE,SAAU,GAAM,OAAQ,EAAS,QAxM7C,GAAY,KAGR,EAAO,EAAgB,yBAAyB,CAAA,KAChD,EAAO,EAAgB,cAAc,CAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/paths.cjs`),n=require(`./statusWriter.cjs`);let r=require(`node:fs`);r=e.__toESM(r,1);let i=require(`node:path`);i=e.__toESM(i,1);function a(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function o(e,t=`<memory>`){let n;try{n=JSON.parse(e)}catch(e){return{kind:`malformed`,path:t,message:e instanceof Error?e.message:String(e)}}return!a(n)||typeof n.runId!=`string`||typeof n.agent!=`string`||typeof n.state!=`string`?{kind:`malformed`,path:t,message:`Expected runId, agent and state fields.`}:n.version!==void 0&&n.version!==1?{kind:`incompatible`,path:t,version:n.version}:(n.state===`complete`&&(n.state=`completed`),typeof n.reason==`string`&&n.attentionReason===void 0&&(n.attentionReason=n.reason),{kind:`ok`,path:t,status:n})}function s(e){let t;try{t=r.readFileSync(e,`utf8`)}catch(t){return t instanceof Error&&`code`in t&&t.code===`ENOENT`?{kind:`missing`,path:e}:{kind:`io_error`,path:e,message:t instanceof Error?t.message:String(t)}}return o(t,e)}function c(e){return s(i.join(t.currentRunsDir(),e,n.STATUS_FILE_NAME))}function l(e){let t=c(e);return t.kind===`ok`?t.status:void 0}exports.parseAsyncRunStatus=o,exports.readAsyncRunStatus=l,exports.readAsyncRunStatusResultAt=s;
|
|
2
|
-
//# sourceMappingURL=statusReader.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"statusReader.cjs","names":["fs","path","currentRunsDir","STATUS_FILE_NAME"],"sources":["../../../src/runs/background/statusReader.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { STATUS_FILE_NAME } from './statusWriter.ts';\n\nexport type StatusReadFailure =\n | { kind: 'missing'; path: string }\n | { kind: 'malformed'; path: string; message: string }\n | { kind: 'incompatible'; path: string; version: unknown }\n | { kind: 'io_error'; path: string; message: string };\n\nexport type StatusReadResult = { kind: 'ok'; path: string; status: AsyncRunStatus } | StatusReadFailure;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function parseAsyncRunStatus(raw: string, statusPath = '<memory>'): StatusReadResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n return { kind: 'malformed', path: statusPath, message: error instanceof Error ? error.message : String(error) };\n }\n if (\n !isRecord(parsed) ||\n typeof parsed.runId !== 'string' ||\n typeof parsed.agent !== 'string' ||\n typeof parsed.state !== 'string'\n ) {\n return { kind: 'malformed', path: statusPath, message: 'Expected runId, agent and state fields.' };\n }\n if (parsed.version !== undefined && parsed.version !== 1) {\n return { kind: 'incompatible', path: statusPath, version: parsed.version };\n }\n\n // Read-only compatibility for statuses created before the canonical state\n // name was adopted. New writers emit only `completed`.\n if (parsed.state === 'complete') parsed.state = 'completed';\n if (typeof parsed.reason === 'string' && parsed.attentionReason === undefined) {\n parsed.attentionReason = parsed.reason;\n }\n return { kind: 'ok', path: statusPath, status: parsed as unknown as AsyncRunStatus };\n}\n\n/** Canonical status reader. It preserves the reason a status could not be read. */\nexport function readAsyncRunStatusResultAt(statusPath: string): StatusReadResult {\n let raw: string;\n try {\n raw = fs.readFileSync(statusPath, 'utf8');\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT')\n return { kind: 'missing', path: statusPath };\n return { kind: 'io_error', path: statusPath, message: error instanceof Error ? error.message : String(error) };\n }\n return parseAsyncRunStatus(raw, statusPath);\n}\n\nexport function readAsyncRunStatusResult(runId: string): StatusReadResult {\n return readAsyncRunStatusResultAt(path.join(currentRunsDir(), runId, STATUS_FILE_NAME));\n}\n\n/** Compatibility convenience for callers that intentionally treat all read failures as absence. */\nexport function readAsyncRunStatus(runId: string): AsyncRunStatus | undefined {\n const result = readAsyncRunStatusResult(runId);\n return result.kind === 'ok' ? result.status : undefined;\n}\n\nexport function readAsyncRunStatusAt(statusPath: string): AsyncRunStatus | undefined {\n const result = readAsyncRunStatusResultAt(statusPath);\n return result.kind === 'ok' ? result.status : undefined;\n}\n"],"mappings":"kNAeA,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAG7E,SAAgB,EAAoB,EAAa,EAAa,WAA8B,CAC1F,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,OACjB,EAAO,CACd,MAAO,CAAE,KAAM,YAAa,KAAM,EAAY,QAAS,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAE,CAoBjH,MAjBE,CAAC,EAAS,EAAO,EACjB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,OAAU,SAEjB,CAAE,KAAM,YAAa,KAAM,EAAY,QAAS,0CAA2C,CAEhG,EAAO,UAAY,IAAA,IAAa,EAAO,UAAY,EAC9C,CAAE,KAAM,eAAgB,KAAM,EAAY,QAAS,EAAO,QAAS,EAKxE,EAAO,QAAU,aAAY,EAAO,MAAQ,aAC5C,OAAO,EAAO,QAAW,UAAY,EAAO,kBAAoB,IAAA,KAClE,EAAO,gBAAkB,EAAO,QAE3B,CAAE,KAAM,KAAM,KAAM,EAAY,OAAQ,EAAqC,EAItF,SAAgB,EAA2B,EAAsC,CAC/E,IAAI,EACJ,GAAI,CACF,EAAMA,EAAG,aAAa,EAAY,OAAO,OAClC,EAAO,CAGd,OAFI,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,SACvD,CAAE,KAAM,UAAW,KAAM,EAAY,CACvC,CAAE,KAAM,WAAY,KAAM,EAAY,QAAS,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAE,CAEhH,OAAO,EAAoB,EAAK,EAAW,CAG7C,SAAgB,EAAyB,EAAiC,CACxE,OAAO,EAA2BC,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAOC,EAAAA,iBAAiB,CAAC,CAIzF,SAAgB,EAAmB,EAA2C,CAC5E,IAAM,EAAS,EAAyB,EAAM,CAC9C,OAAO,EAAO,OAAS,KAAO,EAAO,OAAS,IAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{currentRunsDir as t,init_paths as n}from"../../shared/paths.mjs";import{STATUS_FILE_NAME as r,init_statusWriter as i}from"./statusWriter.mjs";import*as a from"node:fs";import*as o from"node:path";function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e,t=`<memory>`){let n;try{n=JSON.parse(e)}catch(e){return{kind:`malformed`,path:t,message:e instanceof Error?e.message:String(e)}}return!s(n)||typeof n.runId!=`string`||typeof n.agent!=`string`||typeof n.state!=`string`?{kind:`malformed`,path:t,message:`Expected runId, agent and state fields.`}:n.version!==void 0&&n.version!==1?{kind:`incompatible`,path:t,version:n.version}:(n.state===`complete`&&(n.state=`completed`),typeof n.reason==`string`&&n.attentionReason===void 0&&(n.attentionReason=n.reason),{kind:`ok`,path:t,status:n})}function l(e){let t;try{t=a.readFileSync(e,`utf8`)}catch(t){return t instanceof Error&&`code`in t&&t.code===`ENOENT`?{kind:`missing`,path:e}:{kind:`io_error`,path:e,message:t instanceof Error?t.message:String(t)}}return c(t,e)}function u(e){return l(o.join(t(),e,r))}function d(e){let t=u(e);return t.kind===`ok`?t.status:void 0}var f=e((()=>{n(),i()}));f();export{f as init_statusReader,c as parseAsyncRunStatus,d as readAsyncRunStatus,l as readAsyncRunStatusResultAt};
|
|
2
|
-
//# sourceMappingURL=statusReader.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"statusReader.mjs","names":[],"sources":["../../../src/runs/background/statusReader.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { STATUS_FILE_NAME } from './statusWriter.ts';\n\nexport type StatusReadFailure =\n | { kind: 'missing'; path: string }\n | { kind: 'malformed'; path: string; message: string }\n | { kind: 'incompatible'; path: string; version: unknown }\n | { kind: 'io_error'; path: string; message: string };\n\nexport type StatusReadResult = { kind: 'ok'; path: string; status: AsyncRunStatus } | StatusReadFailure;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function parseAsyncRunStatus(raw: string, statusPath = '<memory>'): StatusReadResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n return { kind: 'malformed', path: statusPath, message: error instanceof Error ? error.message : String(error) };\n }\n if (\n !isRecord(parsed) ||\n typeof parsed.runId !== 'string' ||\n typeof parsed.agent !== 'string' ||\n typeof parsed.state !== 'string'\n ) {\n return { kind: 'malformed', path: statusPath, message: 'Expected runId, agent and state fields.' };\n }\n if (parsed.version !== undefined && parsed.version !== 1) {\n return { kind: 'incompatible', path: statusPath, version: parsed.version };\n }\n\n // Read-only compatibility for statuses created before the canonical state\n // name was adopted. New writers emit only `completed`.\n if (parsed.state === 'complete') parsed.state = 'completed';\n if (typeof parsed.reason === 'string' && parsed.attentionReason === undefined) {\n parsed.attentionReason = parsed.reason;\n }\n return { kind: 'ok', path: statusPath, status: parsed as unknown as AsyncRunStatus };\n}\n\n/** Canonical status reader. It preserves the reason a status could not be read. */\nexport function readAsyncRunStatusResultAt(statusPath: string): StatusReadResult {\n let raw: string;\n try {\n raw = fs.readFileSync(statusPath, 'utf8');\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT')\n return { kind: 'missing', path: statusPath };\n return { kind: 'io_error', path: statusPath, message: error instanceof Error ? error.message : String(error) };\n }\n return parseAsyncRunStatus(raw, statusPath);\n}\n\nexport function readAsyncRunStatusResult(runId: string): StatusReadResult {\n return readAsyncRunStatusResultAt(path.join(currentRunsDir(), runId, STATUS_FILE_NAME));\n}\n\n/** Compatibility convenience for callers that intentionally treat all read failures as absence. */\nexport function readAsyncRunStatus(runId: string): AsyncRunStatus | undefined {\n const result = readAsyncRunStatusResult(runId);\n return result.kind === 'ok' ? result.status : undefined;\n}\n\nexport function readAsyncRunStatusAt(statusPath: string): AsyncRunStatus | undefined {\n const result = readAsyncRunStatusResultAt(statusPath);\n return result.kind === 'ok' ? result.status : undefined;\n}\n"],"mappings":"2QAeA,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAG7E,SAAgB,EAAoB,EAAa,EAAa,WAA8B,CAC1F,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,OACjB,EAAO,CACd,MAAO,CAAE,KAAM,YAAa,KAAM,EAAY,QAAS,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAE,CAoBjH,MAjBE,CAAC,EAAS,EAAO,EACjB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,OAAU,SAEjB,CAAE,KAAM,YAAa,KAAM,EAAY,QAAS,0CAA2C,CAEhG,EAAO,UAAY,IAAA,IAAa,EAAO,UAAY,EAC9C,CAAE,KAAM,eAAgB,KAAM,EAAY,QAAS,EAAO,QAAS,EAKxE,EAAO,QAAU,aAAY,EAAO,MAAQ,aAC5C,OAAO,EAAO,QAAW,UAAY,EAAO,kBAAoB,IAAA,KAClE,EAAO,gBAAkB,EAAO,QAE3B,CAAE,KAAM,KAAM,KAAM,EAAY,OAAQ,EAAqC,EAItF,SAAgB,EAA2B,EAAsC,CAC/E,IAAI,EACJ,GAAI,CACF,EAAM,EAAG,aAAa,EAAY,OAAO,OAClC,EAAO,CAGd,OAFI,aAAiB,OAAS,SAAU,GAAS,EAAM,OAAS,SACvD,CAAE,KAAM,UAAW,KAAM,EAAY,CACvC,CAAE,KAAM,WAAY,KAAM,EAAY,QAAS,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAE,CAEhH,OAAO,EAAoB,EAAK,EAAW,CAG7C,SAAgB,EAAyB,EAAiC,CACxE,OAAO,EAA2B,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAiB,CAAC,CAIzF,SAAgB,EAAmB,EAA2C,CAC5E,IAAM,EAAS,EAAyB,EAAM,CAC9C,OAAO,EAAO,OAAS,KAAO,EAAO,OAAS,IAAA,oBAhEO,IAEF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/atomicJson.cjs`),n=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),r=require(`../../shared/paths.cjs`);let i=require(`node:path`);i=e.__toESM(i,1);let a=require(`inversify`);const o=`status.json`;let s=class{flushIntervalMs=75;statusPath;status;dirtyCount=0;flushTimer;open(e,n){this.flushTimer&&=(clearTimeout(this.flushTimer),void 0),this.statusPath=i.join(r.currentRunsDir(),e,o),this.status=n,this.dirtyCount=0,t.writeAtomicJson(this.statusPath,this.clone(n))}update(e){e(this.requireStatus()),this.dirtyCount++,this.scheduleFlush()}updateSync(e){e(this.requireStatus()),this.dirtyCount++,this.flush()}appendTool(e){this.update(t=>c(t,`recentTools`,e))}appendOutput(e){this.update(t=>c(t,`recentOutput`,e))}close(){this.flush()}clone(e){return structuredClone(e)}flush(){if(this.flushTimer&&=(clearTimeout(this.flushTimer),void 0),this.dirtyCount===0)return;let e=this.status,n=this.statusPath;if(!e||!n)return;let r=this.clone(e);this.dirtyCount=0,t.writeAtomicJson(n,r)}scheduleFlush(){if(this.flushTimer)return;let e=setTimeout(()=>this.flush(),this.flushIntervalMs);e.unref?.(),this.flushTimer=e}requireStatus(){if(!this.status)throw Error(`CoalescedStatusWriter used before open().`);return this.status}};s=n.__decorate([(0,a.injectable)()],s);function c(e,t,n){let r=e[t]??[];r.push(n),r.length>50&&r.splice(0,r.length-50),e[t]=r}Object.defineProperty(exports,`CoalescedStatusWriter`,{enumerable:!0,get:function(){return s}}),exports.STATUS_FILE_NAME=o;
|
|
2
|
-
//# sourceMappingURL=statusWriter.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"statusWriter.cjs","names":["path","currentRunsDir"],"sources":["../../../src/runs/background/statusWriter.ts"],"sourcesContent":["/**\n * Coalesced writer for a run's `status.json`.\n *\n * WHY THIS EXISTS:\n * In the predecessor (`doom-pi-subagents/src/runs/background/subagent-runner.ts`)\n * `status.json` was rewritten in full on every child event, and every rewrite\n * `structuredClone`d the whole run graph first. On a fan-out run that makes the\n * hot path quadratic: N events cost O(N) clones of an O(N)-sized graph. Its\n * `recentTools` and `recentOutput` arrays were also appended to and never\n * trimmed, so a long run's status file, and the cost of cloning it, grew\n * without bound.\n *\n * DESIGN PATTERNS:\n * - Coalescing: `update()` mutates the in-memory status and arms a trailing\n * ~75ms timer. A burst of events inside that window costs one clone and one\n * write, not one per event\n * - The clone is gated by a dirty counter, not by the timer firing. A flush\n * with nothing dirty since the last one performs no clone and no write at\n * all, which is the specific fix for the quadratic behaviour above\n * - `updateSync()` bypasses the timer for transitions that must not be lost if\n * the process dies right after: terminal run status, and chain step\n * transitions. Everything else can wait out the coalescing window\n * - `recentTools` and `recentOutput` are capped at 50 entries, oldest dropped,\n * on every append, so neither the in-memory status nor the cloned snapshot\n * grows across a long run\n *\n * SINGLE WRITER CONTRACT (read this before adding another write site):\n * `CoalescedStatusWriter` is the ONLY thing in this package that may write\n * `status.json`. Not \"the preferred way\" - the only way. The predecessor had\n * four unsynchronised writers to the same file, which is exactly how a run's\n * displayed status ends up torn, stale, or overwritten by a slower writer\n * racing a faster one; coalescing a single in-memory owner is what makes that\n * class of bug structurally impossible instead of merely unlikely.\n *\n * Anything else that wants to influence a run's displayed status - pause a\n * step, request a steer, ask a run to stop, report progress from a different\n * process than the one that called `open()` - must NOT open or write\n * `status.json` directly. It writes a sidecar/intent file instead, and the\n * process holding this writer observes that file and folds it into its own\n * `update()`. Readers for those intent files do not exist yet; that is a gap\n * to fill, not a license to reach for `writeAtomicJson(statusPath, ...)`\n * somewhere else. If you are about to write `status.json` from outside this\n * class, stop - that is the bug this module exists to remove.\n *\n * AVOID:\n * - Reading back `status.json` from disk to decide what to write next; the\n * in-memory status held by `open()` is the only source of truth this class\n * trusts\n * - Adding a second writer of `status.json` anywhere else in the package,\n * including \"just this once\" debug or recovery code\n */\n\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\n\n/** How long a burst of updates may be buffered before a trailing flush. */\nconst DEFAULT_FLUSH_INTERVAL_MS = 75;\n\n/** Oldest-dropped cap for `recentTools` and `recentOutput`. */\nconst MAX_RECENT_ENTRIES = 50;\n\n/**\n * The one status filename.\n *\n * Exported because the reconciler and the steering reader both have to find\n * this file without going through the writer. Two modules matching a literal\n * by eye is how a rename silently orphans a reader.\n */\nexport const STATUS_FILE_NAME = 'status.json';\n\n/**\n * The minimum shape this writer needs to know about. Everything else in a\n * concrete run's status is opaque to it: capping `recentTools` and\n * `recentOutput` is the one piece of run semantics this module owns, because\n * it is the specific fix for the unbounded-growth bug above.\n */\nexport interface StatusWithRecentEntries {\n recentTools?: unknown[];\n recentOutput?: unknown[];\n}\n\nexport interface ICoalescedStatusWriter<TStatus extends StatusWithRecentEntries = StatusWithRecentEntries> {\n /**\n * Start tracking a run's status and write it once, synchronously, so the\n * file exists before the caller does anything else.\n *\n * Calling this again, on the same or a different `runId`, is a reset, not\n * an error: it discards whatever was previously tracked, including any\n * mutation buffered by `update()` that had not flushed yet, cancels the\n * pending coalescing timer, and starts a fresh session against the new\n * status. This is deliberate so a runner that retries a bootstrap step can\n * just call `open()` again rather than having to detect and recover from a\n * half-initialized writer.\n */\n open(runId: string, initialStatus: TStatus): void;\n /** Apply an in-memory mutation and let the trailing timer coalesce the write. */\n update(mutator: (status: TStatus) => void): void;\n /** Apply an in-memory mutation and flush synchronously, for transitions that must survive a crash. */\n updateSync(mutator: (status: TStatus) => void): void;\n /** Append one entry to `recentTools`, capped at 50, coalesced like `update()`. */\n appendTool(entry: unknown): void;\n /** Append one entry to `recentOutput`, capped at 50, coalesced like `update()`. */\n appendOutput(entry: unknown): void;\n /** Cancel any pending timer and flush whatever is dirty. Safe to call more than once. */\n close(): void;\n}\n\n@injectable()\nexport class CoalescedStatusWriter<\n TStatus extends StatusWithRecentEntries = StatusWithRecentEntries,\n> implements ICoalescedStatusWriter<TStatus> {\n /**\n * Seam for tests, as a protected member rather than a constructor argument:\n * inversify cannot resolve a primitive, so configuration cannot arrive\n * through the constructor. A test subclass overrides this to avoid waiting\n * out the real window when it does exercise the timer directly instead of\n * using fake timers.\n */\n protected readonly flushIntervalMs: number = DEFAULT_FLUSH_INTERVAL_MS;\n\n private statusPath: string | undefined;\n private status: TStatus | undefined;\n private dirtyCount = 0;\n private flushTimer: NodeJS.Timeout | undefined;\n\n open(runId: string, initialStatus: TStatus): void {\n // A second open() is a reset: cancel whatever this instance was\n // previously tracking (including an unflushed dirty mutation) rather\n // than merging with it. See the doc comment on the interface method.\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n this.statusPath = path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n this.status = initialStatus;\n this.dirtyCount = 0;\n // The initial write is not a coalescing candidate: callers rely on the\n // file existing as soon as `open()` returns, so it always goes straight\n // to disk rather than waiting for a timer.\n writeAtomicJson(this.statusPath, this.clone(initialStatus));\n }\n\n update(mutator: (status: TStatus) => void): void {\n mutator(this.requireStatus());\n this.dirtyCount++;\n this.scheduleFlush();\n }\n\n updateSync(mutator: (status: TStatus) => void): void {\n mutator(this.requireStatus());\n this.dirtyCount++;\n this.flush();\n }\n\n appendTool(entry: unknown): void {\n this.update((status) => appendCapped(status, 'recentTools', entry));\n }\n\n appendOutput(entry: unknown): void {\n this.update((status) => appendCapped(status, 'recentOutput', entry));\n }\n\n close(): void {\n this.flush();\n }\n\n /**\n * The clone-and-write step. Protected so a test can override `clone()`\n * alone and assert it was never called across a run of no-op flushes,\n * which is the direct, observable proof that the dirty-counter gate works.\n */\n protected clone<T>(value: T): T {\n return structuredClone(value);\n }\n\n private flush(): void {\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n if (this.dirtyCount === 0) return;\n const status = this.status;\n const statusPath = this.statusPath;\n if (!status || !statusPath) return;\n const snapshot = this.clone(status);\n this.dirtyCount = 0;\n writeAtomicJson(statusPath, snapshot);\n }\n\n private scheduleFlush(): void {\n if (this.flushTimer) return;\n const timer = setTimeout(() => this.flush(), this.flushIntervalMs);\n // A pending flush timer must never be the reason the process stays alive.\n timer.unref?.();\n this.flushTimer = timer;\n }\n\n private requireStatus(): TStatus {\n if (!this.status) throw new Error('CoalescedStatusWriter used before open().');\n return this.status;\n }\n}\n\n/** Push `entry` onto `status[field]`, dropping the oldest until at most 50 remain. */\nfunction appendCapped(status: StatusWithRecentEntries, field: 'recentTools' | 'recentOutput', entry: unknown): void {\n const list = status[field] ?? [];\n list.push(entry);\n if (list.length > MAX_RECENT_ENTRIES) list.splice(0, list.length - MAX_RECENT_ENTRIES);\n status[field] = list;\n}\n"],"mappings":"2RA2DA,MAYa,EAAmB,cAwCzB,IAAA,EAAA,KAEsC,CAQ3C,gBAA6C,GAE7C,WACA,OACA,WAAqB,EACrB,WAEA,KAAK,EAAe,EAA8B,CAIhD,AAEE,KAAK,cADL,aAAa,KAAK,WAAW,CACX,IAAA,IAEpB,KAAK,WAAaA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAO,EAAiB,CACtE,KAAK,OAAS,EACd,KAAK,WAAa,EAIlB,EAAA,gBAAgB,KAAK,WAAY,KAAK,MAAM,EAAc,CAAC,CAG7D,OAAO,EAA0C,CAC/C,EAAQ,KAAK,eAAe,CAAC,CAC7B,KAAK,aACL,KAAK,eAAe,CAGtB,WAAW,EAA0C,CACnD,EAAQ,KAAK,eAAe,CAAC,CAC7B,KAAK,aACL,KAAK,OAAO,CAGd,WAAW,EAAsB,CAC/B,KAAK,OAAQ,GAAW,EAAa,EAAQ,cAAe,EAAM,CAAC,CAGrE,aAAa,EAAsB,CACjC,KAAK,OAAQ,GAAW,EAAa,EAAQ,eAAgB,EAAM,CAAC,CAGtE,OAAc,CACZ,KAAK,OAAO,CAQd,MAAmB,EAAa,CAC9B,OAAO,gBAAgB,EAAM,CAG/B,OAAsB,CAKpB,GAJA,AAEE,KAAK,cADL,aAAa,KAAK,WAAW,CACX,IAAA,IAEhB,KAAK,aAAe,EAAG,OAC3B,IAAM,EAAS,KAAK,OACd,EAAa,KAAK,WACxB,GAAI,CAAC,GAAU,CAAC,EAAY,OAC5B,IAAM,EAAW,KAAK,MAAM,EAAO,CACnC,KAAK,WAAa,EAClB,EAAA,gBAAgB,EAAY,EAAS,CAGvC,eAA8B,CAC5B,GAAI,KAAK,WAAY,OACrB,IAAM,EAAQ,eAAiB,KAAK,OAAO,CAAE,KAAK,gBAAgB,CAElE,EAAM,SAAS,CACf,KAAK,WAAa,EAGpB,eAAiC,CAC/B,GAAI,CAAC,KAAK,OAAQ,MAAU,MAAM,4CAA4C,CAC9E,OAAO,KAAK,2CA5FH,CAAA,CAAA,EAAA,CAiGb,SAAS,EAAa,EAAiC,EAAuC,EAAsB,CAClH,IAAM,EAAO,EAAO,IAAU,EAAE,CAChC,EAAK,KAAK,EAAM,CACZ,EAAK,OAAS,IAAoB,EAAK,OAAO,EAAG,EAAK,OAAS,GAAmB,CACtF,EAAO,GAAS"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{init_atomicJson as t,writeAtomicJson as n}from"../../shared/atomicJson.mjs";import{__decorate as r,init_decorate as i}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{currentRunsDir as a,init_paths as o}from"../../shared/paths.mjs";import*as s from"node:path";import{injectable as c}from"inversify";function l(e,t,n){let r=e[t]??[];r.push(n),r.length>d&&r.splice(0,r.length-d),e[t]=r}var u,d,f,p,m=e((()=>{t(),o(),i(),u=75,d=50,f=`status.json`,p=class{flushIntervalMs=u;statusPath;status;dirtyCount=0;flushTimer;open(e,t){this.flushTimer&&=(clearTimeout(this.flushTimer),void 0),this.statusPath=s.join(a(),e,f),this.status=t,this.dirtyCount=0,n(this.statusPath,this.clone(t))}update(e){e(this.requireStatus()),this.dirtyCount++,this.scheduleFlush()}updateSync(e){e(this.requireStatus()),this.dirtyCount++,this.flush()}appendTool(e){this.update(t=>l(t,`recentTools`,e))}appendOutput(e){this.update(t=>l(t,`recentOutput`,e))}close(){this.flush()}clone(e){return structuredClone(e)}flush(){if(this.flushTimer&&=(clearTimeout(this.flushTimer),void 0),this.dirtyCount===0)return;let e=this.status,t=this.statusPath;if(!e||!t)return;let r=this.clone(e);this.dirtyCount=0,n(t,r)}scheduleFlush(){if(this.flushTimer)return;let e=setTimeout(()=>this.flush(),this.flushIntervalMs);e.unref?.(),this.flushTimer=e}requireStatus(){if(!this.status)throw Error(`CoalescedStatusWriter used before open().`);return this.status}},p=r([c()],p)}));m();export{p as CoalescedStatusWriter,f as STATUS_FILE_NAME,m as init_statusWriter};
|
|
2
|
-
//# sourceMappingURL=statusWriter.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"statusWriter.mjs","names":[],"sources":["../../../src/runs/background/statusWriter.ts"],"sourcesContent":["/**\n * Coalesced writer for a run's `status.json`.\n *\n * WHY THIS EXISTS:\n * In the predecessor (`doom-pi-subagents/src/runs/background/subagent-runner.ts`)\n * `status.json` was rewritten in full on every child event, and every rewrite\n * `structuredClone`d the whole run graph first. On a fan-out run that makes the\n * hot path quadratic: N events cost O(N) clones of an O(N)-sized graph. Its\n * `recentTools` and `recentOutput` arrays were also appended to and never\n * trimmed, so a long run's status file, and the cost of cloning it, grew\n * without bound.\n *\n * DESIGN PATTERNS:\n * - Coalescing: `update()` mutates the in-memory status and arms a trailing\n * ~75ms timer. A burst of events inside that window costs one clone and one\n * write, not one per event\n * - The clone is gated by a dirty counter, not by the timer firing. A flush\n * with nothing dirty since the last one performs no clone and no write at\n * all, which is the specific fix for the quadratic behaviour above\n * - `updateSync()` bypasses the timer for transitions that must not be lost if\n * the process dies right after: terminal run status, and chain step\n * transitions. Everything else can wait out the coalescing window\n * - `recentTools` and `recentOutput` are capped at 50 entries, oldest dropped,\n * on every append, so neither the in-memory status nor the cloned snapshot\n * grows across a long run\n *\n * SINGLE WRITER CONTRACT (read this before adding another write site):\n * `CoalescedStatusWriter` is the ONLY thing in this package that may write\n * `status.json`. Not \"the preferred way\" - the only way. The predecessor had\n * four unsynchronised writers to the same file, which is exactly how a run's\n * displayed status ends up torn, stale, or overwritten by a slower writer\n * racing a faster one; coalescing a single in-memory owner is what makes that\n * class of bug structurally impossible instead of merely unlikely.\n *\n * Anything else that wants to influence a run's displayed status - pause a\n * step, request a steer, ask a run to stop, report progress from a different\n * process than the one that called `open()` - must NOT open or write\n * `status.json` directly. It writes a sidecar/intent file instead, and the\n * process holding this writer observes that file and folds it into its own\n * `update()`. Readers for those intent files do not exist yet; that is a gap\n * to fill, not a license to reach for `writeAtomicJson(statusPath, ...)`\n * somewhere else. If you are about to write `status.json` from outside this\n * class, stop - that is the bug this module exists to remove.\n *\n * AVOID:\n * - Reading back `status.json` from disk to decide what to write next; the\n * in-memory status held by `open()` is the only source of truth this class\n * trusts\n * - Adding a second writer of `status.json` anywhere else in the package,\n * including \"just this once\" debug or recovery code\n */\n\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\n\n/** How long a burst of updates may be buffered before a trailing flush. */\nconst DEFAULT_FLUSH_INTERVAL_MS = 75;\n\n/** Oldest-dropped cap for `recentTools` and `recentOutput`. */\nconst MAX_RECENT_ENTRIES = 50;\n\n/**\n * The one status filename.\n *\n * Exported because the reconciler and the steering reader both have to find\n * this file without going through the writer. Two modules matching a literal\n * by eye is how a rename silently orphans a reader.\n */\nexport const STATUS_FILE_NAME = 'status.json';\n\n/**\n * The minimum shape this writer needs to know about. Everything else in a\n * concrete run's status is opaque to it: capping `recentTools` and\n * `recentOutput` is the one piece of run semantics this module owns, because\n * it is the specific fix for the unbounded-growth bug above.\n */\nexport interface StatusWithRecentEntries {\n recentTools?: unknown[];\n recentOutput?: unknown[];\n}\n\nexport interface ICoalescedStatusWriter<TStatus extends StatusWithRecentEntries = StatusWithRecentEntries> {\n /**\n * Start tracking a run's status and write it once, synchronously, so the\n * file exists before the caller does anything else.\n *\n * Calling this again, on the same or a different `runId`, is a reset, not\n * an error: it discards whatever was previously tracked, including any\n * mutation buffered by `update()` that had not flushed yet, cancels the\n * pending coalescing timer, and starts a fresh session against the new\n * status. This is deliberate so a runner that retries a bootstrap step can\n * just call `open()` again rather than having to detect and recover from a\n * half-initialized writer.\n */\n open(runId: string, initialStatus: TStatus): void;\n /** Apply an in-memory mutation and let the trailing timer coalesce the write. */\n update(mutator: (status: TStatus) => void): void;\n /** Apply an in-memory mutation and flush synchronously, for transitions that must survive a crash. */\n updateSync(mutator: (status: TStatus) => void): void;\n /** Append one entry to `recentTools`, capped at 50, coalesced like `update()`. */\n appendTool(entry: unknown): void;\n /** Append one entry to `recentOutput`, capped at 50, coalesced like `update()`. */\n appendOutput(entry: unknown): void;\n /** Cancel any pending timer and flush whatever is dirty. Safe to call more than once. */\n close(): void;\n}\n\n@injectable()\nexport class CoalescedStatusWriter<\n TStatus extends StatusWithRecentEntries = StatusWithRecentEntries,\n> implements ICoalescedStatusWriter<TStatus> {\n /**\n * Seam for tests, as a protected member rather than a constructor argument:\n * inversify cannot resolve a primitive, so configuration cannot arrive\n * through the constructor. A test subclass overrides this to avoid waiting\n * out the real window when it does exercise the timer directly instead of\n * using fake timers.\n */\n protected readonly flushIntervalMs: number = DEFAULT_FLUSH_INTERVAL_MS;\n\n private statusPath: string | undefined;\n private status: TStatus | undefined;\n private dirtyCount = 0;\n private flushTimer: NodeJS.Timeout | undefined;\n\n open(runId: string, initialStatus: TStatus): void {\n // A second open() is a reset: cancel whatever this instance was\n // previously tracking (including an unflushed dirty mutation) rather\n // than merging with it. See the doc comment on the interface method.\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n this.statusPath = path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n this.status = initialStatus;\n this.dirtyCount = 0;\n // The initial write is not a coalescing candidate: callers rely on the\n // file existing as soon as `open()` returns, so it always goes straight\n // to disk rather than waiting for a timer.\n writeAtomicJson(this.statusPath, this.clone(initialStatus));\n }\n\n update(mutator: (status: TStatus) => void): void {\n mutator(this.requireStatus());\n this.dirtyCount++;\n this.scheduleFlush();\n }\n\n updateSync(mutator: (status: TStatus) => void): void {\n mutator(this.requireStatus());\n this.dirtyCount++;\n this.flush();\n }\n\n appendTool(entry: unknown): void {\n this.update((status) => appendCapped(status, 'recentTools', entry));\n }\n\n appendOutput(entry: unknown): void {\n this.update((status) => appendCapped(status, 'recentOutput', entry));\n }\n\n close(): void {\n this.flush();\n }\n\n /**\n * The clone-and-write step. Protected so a test can override `clone()`\n * alone and assert it was never called across a run of no-op flushes,\n * which is the direct, observable proof that the dirty-counter gate works.\n */\n protected clone<T>(value: T): T {\n return structuredClone(value);\n }\n\n private flush(): void {\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = undefined;\n }\n if (this.dirtyCount === 0) return;\n const status = this.status;\n const statusPath = this.statusPath;\n if (!status || !statusPath) return;\n const snapshot = this.clone(status);\n this.dirtyCount = 0;\n writeAtomicJson(statusPath, snapshot);\n }\n\n private scheduleFlush(): void {\n if (this.flushTimer) return;\n const timer = setTimeout(() => this.flush(), this.flushIntervalMs);\n // A pending flush timer must never be the reason the process stays alive.\n timer.unref?.();\n this.flushTimer = timer;\n }\n\n private requireStatus(): TStatus {\n if (!this.status) throw new Error('CoalescedStatusWriter used before open().');\n return this.status;\n }\n}\n\n/** Push `entry` onto `status[field]`, dropping the oldest until at most 50 remain. */\nfunction appendCapped(status: StatusWithRecentEntries, field: 'recentTools' | 'recentOutput', entry: unknown): void {\n const list = status[field] ?? [];\n list.push(entry);\n if (list.length > MAX_RECENT_ENTRIES) list.splice(0, list.length - MAX_RECENT_ENTRIES);\n status[field] = list;\n}\n"],"mappings":"gZA+MA,SAAS,EAAa,EAAiC,EAAuC,EAAsB,CAClH,IAAM,EAAO,EAAO,IAAU,EAAE,CAChC,EAAK,KAAK,EAAM,CACZ,EAAK,OAAS,GAAoB,EAAK,OAAO,EAAG,EAAK,OAAS,EAAmB,CACtF,EAAO,GAAS,2BA5J2C,IACN,KAGjD,EAA4B,GAG5B,EAAqB,GASd,EAAmB,gBAwCzB,KAEsC,CAQ3C,gBAA6C,EAE7C,WACA,OACA,WAAqB,EACrB,WAEA,KAAK,EAAe,EAA8B,CAIhD,AAEE,KAAK,cADL,aAAa,KAAK,WAAW,CACX,IAAA,IAEpB,KAAK,WAAa,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAiB,CACtE,KAAK,OAAS,EACd,KAAK,WAAa,EAIlB,EAAgB,KAAK,WAAY,KAAK,MAAM,EAAc,CAAC,CAG7D,OAAO,EAA0C,CAC/C,EAAQ,KAAK,eAAe,CAAC,CAC7B,KAAK,aACL,KAAK,eAAe,CAGtB,WAAW,EAA0C,CACnD,EAAQ,KAAK,eAAe,CAAC,CAC7B,KAAK,aACL,KAAK,OAAO,CAGd,WAAW,EAAsB,CAC/B,KAAK,OAAQ,GAAW,EAAa,EAAQ,cAAe,EAAM,CAAC,CAGrE,aAAa,EAAsB,CACjC,KAAK,OAAQ,GAAW,EAAa,EAAQ,eAAgB,EAAM,CAAC,CAGtE,OAAc,CACZ,KAAK,OAAO,CAQd,MAAmB,EAAa,CAC9B,OAAO,gBAAgB,EAAM,CAG/B,OAAsB,CAKpB,GAJA,AAEE,KAAK,cADL,aAAa,KAAK,WAAW,CACX,IAAA,IAEhB,KAAK,aAAe,EAAG,OAC3B,IAAM,EAAS,KAAK,OACd,EAAa,KAAK,WACxB,GAAI,CAAC,GAAU,CAAC,EAAY,OAC5B,IAAM,EAAW,KAAK,MAAM,EAAO,CACnC,KAAK,WAAa,EAClB,EAAgB,EAAY,EAAS,CAGvC,eAA8B,CAC5B,GAAI,KAAK,WAAY,OACrB,IAAM,EAAQ,eAAiB,KAAK,OAAO,CAAE,KAAK,gBAAgB,CAElE,EAAM,SAAS,CACf,KAAK,WAAa,EAGpB,eAAiC,CAC/B,GAAI,CAAC,KAAK,OAAQ,MAAU,MAAM,4CAA4C,CAC9E,OAAO,KAAK,cA5Ff,GAAY,CAAA,CAAA,EAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
require(`../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),t=require(`../../container/types.cjs`),n=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),r=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`);let i=require(`inversify`);const a=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]);function o(e,t){return t?{runId:t.runId,status:t.status,activityState:t.activityState,attentionReason:t.attentionReason,error:t.error,startedAt:t.startedAt,updatedAt:t.updatedAt}:{runId:e,status:void 0}}function s(e){return e.status!==void 0&&a.has(e.status)}function c(e){return e.activityState===`needs_attention`}function l(e,t){return t===`attention`?c(e):t===`completion`?s(e):c(e)||s(e)}function u(e){return c(e)?`attention`:`completed`}let d=class{constructor(e){this.tracker=e}pollIntervalMs=250;defaultTimeoutMs=18e5;now(){return Date.now()}sleep(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}async wait(e){let t=this.now(),n=e.timeoutMs!==void 0&&e.timeoutMs>0?e.timeoutMs:this.defaultTimeoutMs,r=e.waitFor??`any`,i=e.sessionId?this.tracker.forSession(e.sessionId):this.tracker,a=this.resolveTargetIds(e.target,i);for(let e of a)i.track(e);let s=()=>this.now()-t,c=()=>a.map(e=>o(e,i.get(e)));if(a.length===0)return{reason:`no-active-runs`,elapsedMs:0,runs:[]};let d=c();if(d.every(e=>e.status===void 0))return{reason:`no-active-runs`,elapsedMs:s(),runs:d};for(;;){let t=c(),i=t.find(e=>l(e,r));if(i)return{reason:r===`completion`?`completed`:r===`attention`?`attention`:u(i),elapsedMs:s(),runs:t};if(e.signal?.aborted)return{reason:`aborted`,elapsedMs:s(),runs:t};if(s()>=n)return{reason:`timeout`,elapsedMs:s(),runs:t};await this.sleep(Math.min(this.pollIntervalMs,Math.max(0,n-s())))}}resolveTargetIds(e,t){return`id`in e?[e.id]:`ids`in e?e.ids:t.list().map(e=>e.runId)}};d=e.__decorate([(0,i.injectable)(),r.__decorateParam(0,(0,i.inject)(t.DOOM_TEAM_TYPES.AsyncJobTracker)),n.__decorateMetadata(`design:paramtypes`,[Object])],d),Object.defineProperty(exports,`SubagentWaiter`,{enumerable:!0,get:function(){return d}});
|
|
2
|
-
//# sourceMappingURL=subagentWait.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"subagentWait.cjs","names":["DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/subagentWait.ts"],"sourcesContent":["/**\n * Blocks a turn until one or more tracked async runs finish, need attention,\n * or a timeout elapses - resolving with a status snapshot every time.\n *\n * WHY THIS IS A REDESIGN, NOT A PORT:\n * `doom-pi-subagents/src/runs/background/subagentWait.ts` is 686 lines\n * because it also waits on remembered detached FOREGROUND runs (reading\n * child transcripts byte-range-tailed off disk to render a live preview) and\n * session-scoped background-work PROVIDER items (a separate registration\n * protocol with its own wake channels), on top of async runs. This package is\n * async-only - there is no foreground run mode, and no provider protocol has\n * been ported (see `spawnHandshake.ts`'s header for the same \"async-only\"\n * framing applied elsewhere). Waiting on either would be dead code with\n * nothing to ever populate it. Only the async-run wait survives, rebuilt\n * against what this package actually has.\n *\n * COMPOSITION, NOT REIMPLEMENTATION:\n * `status.json` has one writer (`CoalescedStatusWriter`) and several\n * legitimate readers; this module is not a new one. It reads run state\n * exclusively through `AsyncJobTracker`, which already owns the poll-and-cache\n * loop against `status.json` (`PollScheduler`-driven, per its own header).\n * `wait()` never touches `node:fs` and registers no `PollScheduler`\n * subscription of its own - see \"WHY A PRIVATE POLL LOOP\" below for why it\n * still owns a short-lived timer despite that.\n *\n * TARGET SHAPE - `{ id } | { ids } | { all: true }`:\n * One run, several, or every run `AsyncJobTracker` currently has tracked\n * (`list()` IS that set - whatever the run's own spawn path called `track()`\n * for is what \"outstanding\" means here; this module does not independently\n * decide what counts as outstanding). An `id`/`ids` entry not yet tracked is\n * tracked on the caller's behalf, mirroring `track()`'s own idempotent-refresh\n * contract, so a caller does not have to track before it can wait.\n *\n * RETURN-ON-FIRST, NOT WAIT-FOR-ALL:\n * `wait()` resolves the moment ANY targeted run satisfies `waitFor`, not once\n * every targeted run does. This is a deliberate judgement call, not something\n * spelled out in the brief, and it is the more expressive of the two shapes:\n * wait-for-all composes out of return-on-first (call `wait()` again for\n * whatever remains in `WaitOutcome.runs` that has not yet satisfied\n * `waitFor`), but the reverse does not hold - if wait-for-all were the\n * primitive, a caller that wants the NEXT completion (the predecessor's own\n * rolling-replacement fleet-manager loop: wait for one, spawn its\n * replacement, wait again) could only get it by polling. Choosing the shape\n * that does not foreclose the other is why this is return-on-first rather\n * than a use case that happened to need it; the fleet-manager loop is\n * evidence the choice is usable, not the reason for it. If a real caller\n * turns up needing true wait-for-all-at-once as a single call, that is a\n * reason to add it later, built on top of this primitive.\n *\n * TIMEOUT IS A SNAPSHOT, NOT AN ERROR:\n * `wait()` always resolves; it never rejects for a mere timeout. Waiting\n * longer than asked is a normal outcome of an async system, and forcing every\n * caller into a `try`/`catch` for it is how a real error (a thrown bug) ends\n * up caught and swallowed right alongside it - the same reasoning\n * `SpawnHandshake`'s header gives for its own three-outcome, all-resolved\n * design. `WaitOutcome.reason` is `'timeout'`; the caller decides what to do\n * with `WaitOutcome.runs`, which is populated on every path, including this one.\n *\n * `'attention'` MUST WIN EARLY - THE POINT OF THIS TOOL:\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and `reason: 'missing-deliverable'` after a failed nudge, but has no\n * standing to write `status.json` itself (see its own header). Once whatever\n * calls `evaluate()` folds that result into `status.json` through\n * `CoalescedStatusWriter` - the run's own runner main loop, not yet built -\n * `AsyncJobTracker` picks the fields up on its next poll (see that module's\n * own header note on this), and THIS module is what lets a caller that is\n * blocked in `wait()` ever find out, instead of only discovering it by\n * polling `subagent({ action: \"status\" })` in a loop. `waitFor: 'attention'`\n * returns the instant any targeted run's `activityState` is\n * `'needs_attention'`, before its timeout, before it would ever go terminal.\n * `waitFor: 'completion'` deliberately does the opposite - it waits THROUGH\n * attention, for a caller (an auto-drain loop) that wants to know about\n * results, not about a mid-flight nudge. `'any'` (the default) returns on\n * whichever comes first, matching the predecessor's default behaviour.\n *\n * WHY A PRIVATE POLL LOOP, NOT A SECOND `PollScheduler` SUBSCRIPTION:\n * `PollScheduler` is for long-lived subscribers that back off toward a shared\n * idle ceiling; `wait()` is the opposite shape, a short-lived, bounded call\n * that needs a TIGHT interval for its whole (short) life, the same reasoning\n * `SpawnHandshake`'s header gives for not registering with it either. The loop\n * here only ever reads `AsyncJobTracker`'s already-cached, in-memory state\n * (no `node:fs`, no disk I/O of its own), so polling it every\n * `pollIntervalMs` costs a handful of `Map` reads, not a filesystem round trip.\n *\n * WHY NO EVENT-BUS WAKE (dropped from the predecessor):\n * The predecessor wakes early on `pi.events` firing (an `ExtensionAPI` event\n * bus this package has not built) and otherwise falls back to a fixed poll.\n * With no bus to wake on, this module is poll-only - which is exactly the\n * fallback path the predecessor already had to support for when its own bus\n * was unavailable, so there is no new failure mode here, only a narrower one.\n *\n * AVOID:\n * - Reading `status.json` directly. If a field `wait()` needs is missing from\n * `TrackedAsyncJob`, that is a reason to extend `AsyncJobTracker` (see its\n * header's note on `activityState`/`attentionReason` for the last time this\n * happened), not to open a second read path around it\n * - Treating a run id that was never tracked and has no readable `status.json`\n * the same as one that is merely slow. `wait()` resolves `'no-active-runs'`\n * immediately when NONE of the targeted ids ever resolve to a known status,\n * rather than waiting out the full timeout on nothing\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { ActivityState } from '../../types.ts';\nimport type { IAsyncJobTracker, ITrackedAsyncJobs } from './asyncJobTracker.ts';\n\n/** States `AsyncJobTracker` (mirroring `staleRunReconciler.ts`) treats as finished. */\nconst TERMINAL_STATES: ReadonlySet<string> = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\nconst DEFAULT_POLL_INTERVAL_MS = 250;\nexport type WaitTarget = { id: string } | { ids: string[] } | { all: true };\n\nexport type WaitForMode = 'completion' | 'attention' | 'any';\n\nexport interface WaitRequest {\n target: WaitTarget;\n /** Pi context that owns the targeted run collection. */\n sessionId?: string;\n /** Defaults to `'any'`. */\n waitFor?: WaitForMode;\n /** Defaults to 30 minutes. */\n timeoutMs?: number;\n /** Resolves early with `reason: 'aborted'` when this fires. */\n signal?: AbortSignal;\n}\n\nexport type WaitReason = 'completed' | 'attention' | 'timeout' | 'aborted' | 'no-active-runs';\n\n/** One targeted run's state at the moment `wait()` resolved. */\nexport interface WaitRunSnapshot {\n runId: string;\n /** `undefined` when this run has never had a readable `status.json`. */\n status: string | undefined;\n activityState?: ActivityState;\n /** Present alongside `activityState: 'needs_attention'`, e.g. `'missing-deliverable'`. */\n attentionReason?: string;\n error?: string;\n startedAt?: number;\n updatedAt?: number;\n}\n\nexport interface WaitOutcome {\n reason: WaitReason;\n elapsedMs: number;\n /** Every targeted run's snapshot, regardless of `reason` - populated even on `'timeout'`. */\n runs: WaitRunSnapshot[];\n}\n\nexport interface ISubagentWaiter {\n wait(request: WaitRequest): Promise<WaitOutcome>;\n}\n\nfunction snapshotFrom(runId: string, job: ReturnType<ITrackedAsyncJobs['get']>): WaitRunSnapshot {\n if (!job) return { runId, status: undefined };\n return {\n runId: job.runId,\n status: job.status,\n activityState: job.activityState,\n attentionReason: job.attentionReason,\n error: job.error,\n startedAt: job.startedAt,\n updatedAt: job.updatedAt,\n };\n}\n\nfunction isTerminal(snapshot: WaitRunSnapshot): boolean {\n return snapshot.status !== undefined && TERMINAL_STATES.has(snapshot.status);\n}\n\nfunction needsAttention(snapshot: WaitRunSnapshot): boolean {\n return snapshot.activityState === 'needs_attention';\n}\n\nfunction satisfies(snapshot: WaitRunSnapshot, waitFor: WaitForMode): boolean {\n if (waitFor === 'attention') return needsAttention(snapshot);\n if (waitFor === 'completion') return isTerminal(snapshot);\n return needsAttention(snapshot) || isTerminal(snapshot);\n}\n\n/** For `waitFor: 'any'` only: which condition actually satisfied it. Attention wins when both happen to be true, since it is the more actionable signal. */\nfunction reasonFor(snapshot: WaitRunSnapshot): WaitReason {\n return needsAttention(snapshot) ? 'attention' : 'completed';\n}\n\n@injectable()\nexport class SubagentWaiter implements ISubagentWaiter {\n constructor(@inject(DOOM_TEAM_TYPES.AsyncJobTracker) private readonly tracker: IAsyncJobTracker) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive. See `pollScheduler.ts` and\n * `agents/discovery.ts` for the same pattern.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly defaultTimeoutMs: number = DEFAULT_TIMEOUT_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Sleep seam so a test can drive this with fake timers without a real delay. */\n protected sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n }\n\n async wait(request: WaitRequest): Promise<WaitOutcome> {\n const startedAt = this.now();\n const timeoutMs =\n request.timeoutMs !== undefined && request.timeoutMs > 0 ? request.timeoutMs : this.defaultTimeoutMs;\n const waitFor = request.waitFor ?? 'any';\n\n const jobs = request.sessionId ? this.tracker.forSession(request.sessionId) : this.tracker;\n const runIds = this.resolveTargetIds(request.target, jobs);\n for (const runId of runIds) jobs.track(runId);\n\n const elapsed = (): number => this.now() - startedAt;\n const snapshotAll = (): WaitRunSnapshot[] => runIds.map((runId) => snapshotFrom(runId, jobs.get(runId)));\n\n if (runIds.length === 0) {\n return { reason: 'no-active-runs', elapsedMs: 0, runs: [] };\n }\n\n const initialRuns = snapshotAll();\n if (initialRuns.every((snapshot) => snapshot.status === undefined)) {\n return { reason: 'no-active-runs', elapsedMs: elapsed(), runs: initialRuns };\n }\n\n for (;;) {\n const runs = snapshotAll();\n const satisfied = runs.find((snapshot) => satisfies(snapshot, waitFor));\n if (satisfied) {\n // The label reflects what `waitFor` actually asked for, not merely\n // whatever the snapshot happens to carry: a run that is BOTH terminal\n // and (independently) still flagged needs_attention must still report\n // 'completed' for `waitFor: 'completion'`, since attention was never\n // part of what that mode was watching for.\n const reason: WaitReason =\n waitFor === 'completion' ? 'completed' : waitFor === 'attention' ? 'attention' : reasonFor(satisfied);\n return { reason, elapsedMs: elapsed(), runs };\n }\n if (request.signal?.aborted) {\n return { reason: 'aborted', elapsedMs: elapsed(), runs };\n }\n if (elapsed() >= timeoutMs) {\n return { reason: 'timeout', elapsedMs: elapsed(), runs };\n }\n await this.sleep(Math.min(this.pollIntervalMs, Math.max(0, timeoutMs - elapsed())));\n }\n }\n\n private resolveTargetIds(target: WaitTarget, jobs: ITrackedAsyncJobs): string[] {\n if ('id' in target) return [target.id];\n if ('ids' in target) return target.ids;\n return jobs.list().map((job) => job.runId);\n }\n}\n"],"mappings":"kXA6GA,MAAM,EAAuC,IAAI,IAAI,CAAC,WAAY,YAAa,SAAU,SAAU,UAAU,CAAC,CA8C9G,SAAS,EAAa,EAAe,EAA4D,CAE/F,OADK,EACE,CACL,MAAO,EAAI,MACX,OAAQ,EAAI,OACZ,cAAe,EAAI,cACnB,gBAAiB,EAAI,gBACrB,MAAO,EAAI,MACX,UAAW,EAAI,UACf,UAAW,EAAI,UAChB,CATgB,CAAE,QAAO,OAAQ,IAAA,GAAW,CAY/C,SAAS,EAAW,EAAoC,CACtD,OAAO,EAAS,SAAW,IAAA,IAAa,EAAgB,IAAI,EAAS,OAAO,CAG9E,SAAS,EAAe,EAAoC,CAC1D,OAAO,EAAS,gBAAkB,kBAGpC,SAAS,EAAU,EAA2B,EAA+B,CAG3E,OAFI,IAAY,YAAoB,EAAe,EAAS,CACxD,IAAY,aAAqB,EAAW,EAAS,CAClD,EAAe,EAAS,EAAI,EAAW,EAAS,CAIzD,SAAS,EAAU,EAAuC,CACxD,OAAO,EAAe,EAAS,CAAG,YAAc,YAI3C,IAAA,EAAA,KAAgD,CACrD,YAAY,EAAqF,CAA3B,KAAA,QAAA,EAOtE,eAA4C,IAC5C,iBAA8C,KAE9C,KAAwB,CACtB,OAAO,KAAK,KAAK,CAInB,MAAgB,EAA2B,CACzC,OAAO,IAAI,QAAS,GAAY,CAChB,WAAW,EAAS,EAC7B,CAAC,SAAS,EACf,CAGJ,MAAM,KAAK,EAA4C,CACrD,IAAM,EAAY,KAAK,KAAK,CACtB,EACJ,EAAQ,YAAc,IAAA,IAAa,EAAQ,UAAY,EAAI,EAAQ,UAAY,KAAK,iBAChF,EAAU,EAAQ,SAAW,MAE7B,EAAO,EAAQ,UAAY,KAAK,QAAQ,WAAW,EAAQ,UAAU,CAAG,KAAK,QAC7E,EAAS,KAAK,iBAAiB,EAAQ,OAAQ,EAAK,CAC1D,IAAK,IAAM,KAAS,EAAQ,EAAK,MAAM,EAAM,CAE7C,IAAM,MAAwB,KAAK,KAAK,CAAG,EACrC,MAAuC,EAAO,IAAK,GAAU,EAAa,EAAO,EAAK,IAAI,EAAM,CAAC,CAAC,CAExG,GAAI,EAAO,SAAW,EACpB,MAAO,CAAE,OAAQ,iBAAkB,UAAW,EAAG,KAAM,EAAE,CAAE,CAG7D,IAAM,EAAc,GAAa,CACjC,GAAI,EAAY,MAAO,GAAa,EAAS,SAAW,IAAA,GAAU,CAChE,MAAO,CAAE,OAAQ,iBAAkB,UAAW,GAAS,CAAE,KAAM,EAAa,CAG9E,OAAS,CACP,IAAM,EAAO,GAAa,CACpB,EAAY,EAAK,KAAM,GAAa,EAAU,EAAU,EAAQ,CAAC,CACvE,GAAI,EAQF,MAAO,CAAE,OADP,IAAY,aAAe,YAAc,IAAY,YAAc,YAAc,EAAU,EAAU,CACtF,UAAW,GAAS,CAAE,OAAM,CAE/C,GAAI,EAAQ,QAAQ,QAClB,MAAO,CAAE,OAAQ,UAAW,UAAW,GAAS,CAAE,OAAM,CAE1D,GAAI,GAAS,EAAI,EACf,MAAO,CAAE,OAAQ,UAAW,UAAW,GAAS,CAAE,OAAM,CAE1D,MAAM,KAAK,MAAM,KAAK,IAAI,KAAK,eAAgB,KAAK,IAAI,EAAG,EAAY,GAAS,CAAC,CAAC,CAAC,EAIvF,iBAAyB,EAAoB,EAAmC,CAG9E,MAFI,OAAQ,EAAe,CAAC,EAAO,GAAG,CAClC,QAAS,EAAe,EAAO,IAC5B,EAAK,MAAM,CAAC,IAAK,GAAQ,EAAI,MAAM,qCAxEjC,kCAESA,EAAAA,gBAAgB,gBAAgB,CAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{DOOM_TEAM_TYPES as r,init_types as i}from"../../container/types.mjs";import{__decorateMetadata as a,init_decorateMetadata as o}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as s,init_decorateParam as c}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{inject as l,injectable as u}from"inversify";function d(e,t){return t?{runId:t.runId,status:t.status,activityState:t.activityState,attentionReason:t.attentionReason,error:t.error,startedAt:t.startedAt,updatedAt:t.updatedAt}:{runId:e,status:void 0}}function f(e){return e.status!==void 0&&g.has(e.status)}function p(e){return e.activityState===`needs_attention`}function m(e,t){return t===`attention`?p(e):t===`completion`?f(e):p(e)||f(e)}function h(e){return p(e)?`attention`:`completed`}var g,_,v,y,b=e((()=>{i(),o(),c(),n(),g=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]),_=1800*1e3,v=250,y=class{constructor(e){this.tracker=e}pollIntervalMs=v;defaultTimeoutMs=_;now(){return Date.now()}sleep(e){return new Promise(t=>{setTimeout(t,e).unref?.()})}async wait(e){let t=this.now(),n=e.timeoutMs!==void 0&&e.timeoutMs>0?e.timeoutMs:this.defaultTimeoutMs,r=e.waitFor??`any`,i=e.sessionId?this.tracker.forSession(e.sessionId):this.tracker,a=this.resolveTargetIds(e.target,i);for(let e of a)i.track(e);let o=()=>this.now()-t,s=()=>a.map(e=>d(e,i.get(e)));if(a.length===0)return{reason:`no-active-runs`,elapsedMs:0,runs:[]};let c=s();if(c.every(e=>e.status===void 0))return{reason:`no-active-runs`,elapsedMs:o(),runs:c};for(;;){let t=s(),i=t.find(e=>m(e,r));if(i)return{reason:r===`completion`?`completed`:r===`attention`?`attention`:h(i),elapsedMs:o(),runs:t};if(e.signal?.aborted)return{reason:`aborted`,elapsedMs:o(),runs:t};if(o()>=n)return{reason:`timeout`,elapsedMs:o(),runs:t};await this.sleep(Math.min(this.pollIntervalMs,Math.max(0,n-o())))}}resolveTargetIds(e,t){return`id`in e?[e.id]:`ids`in e?e.ids:t.list().map(e=>e.runId)}},y=t([u(),s(0,l(r.AsyncJobTracker)),a(`design:paramtypes`,[Object])],y)}));b();export{y as SubagentWaiter,b as init_subagentWait};
|
|
2
|
-
//# sourceMappingURL=subagentWait.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"subagentWait.mjs","names":[],"sources":["../../../src/runs/background/subagentWait.ts"],"sourcesContent":["/**\n * Blocks a turn until one or more tracked async runs finish, need attention,\n * or a timeout elapses - resolving with a status snapshot every time.\n *\n * WHY THIS IS A REDESIGN, NOT A PORT:\n * `doom-pi-subagents/src/runs/background/subagentWait.ts` is 686 lines\n * because it also waits on remembered detached FOREGROUND runs (reading\n * child transcripts byte-range-tailed off disk to render a live preview) and\n * session-scoped background-work PROVIDER items (a separate registration\n * protocol with its own wake channels), on top of async runs. This package is\n * async-only - there is no foreground run mode, and no provider protocol has\n * been ported (see `spawnHandshake.ts`'s header for the same \"async-only\"\n * framing applied elsewhere). Waiting on either would be dead code with\n * nothing to ever populate it. Only the async-run wait survives, rebuilt\n * against what this package actually has.\n *\n * COMPOSITION, NOT REIMPLEMENTATION:\n * `status.json` has one writer (`CoalescedStatusWriter`) and several\n * legitimate readers; this module is not a new one. It reads run state\n * exclusively through `AsyncJobTracker`, which already owns the poll-and-cache\n * loop against `status.json` (`PollScheduler`-driven, per its own header).\n * `wait()` never touches `node:fs` and registers no `PollScheduler`\n * subscription of its own - see \"WHY A PRIVATE POLL LOOP\" below for why it\n * still owns a short-lived timer despite that.\n *\n * TARGET SHAPE - `{ id } | { ids } | { all: true }`:\n * One run, several, or every run `AsyncJobTracker` currently has tracked\n * (`list()` IS that set - whatever the run's own spawn path called `track()`\n * for is what \"outstanding\" means here; this module does not independently\n * decide what counts as outstanding). An `id`/`ids` entry not yet tracked is\n * tracked on the caller's behalf, mirroring `track()`'s own idempotent-refresh\n * contract, so a caller does not have to track before it can wait.\n *\n * RETURN-ON-FIRST, NOT WAIT-FOR-ALL:\n * `wait()` resolves the moment ANY targeted run satisfies `waitFor`, not once\n * every targeted run does. This is a deliberate judgement call, not something\n * spelled out in the brief, and it is the more expressive of the two shapes:\n * wait-for-all composes out of return-on-first (call `wait()` again for\n * whatever remains in `WaitOutcome.runs` that has not yet satisfied\n * `waitFor`), but the reverse does not hold - if wait-for-all were the\n * primitive, a caller that wants the NEXT completion (the predecessor's own\n * rolling-replacement fleet-manager loop: wait for one, spawn its\n * replacement, wait again) could only get it by polling. Choosing the shape\n * that does not foreclose the other is why this is return-on-first rather\n * than a use case that happened to need it; the fleet-manager loop is\n * evidence the choice is usable, not the reason for it. If a real caller\n * turns up needing true wait-for-all-at-once as a single call, that is a\n * reason to add it later, built on top of this primitive.\n *\n * TIMEOUT IS A SNAPSHOT, NOT AN ERROR:\n * `wait()` always resolves; it never rejects for a mere timeout. Waiting\n * longer than asked is a normal outcome of an async system, and forcing every\n * caller into a `try`/`catch` for it is how a real error (a thrown bug) ends\n * up caught and swallowed right alongside it - the same reasoning\n * `SpawnHandshake`'s header gives for its own three-outcome, all-resolved\n * design. `WaitOutcome.reason` is `'timeout'`; the caller decides what to do\n * with `WaitOutcome.runs`, which is populated on every path, including this one.\n *\n * `'attention'` MUST WIN EARLY - THE POINT OF THIS TOOL:\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and `reason: 'missing-deliverable'` after a failed nudge, but has no\n * standing to write `status.json` itself (see its own header). Once whatever\n * calls `evaluate()` folds that result into `status.json` through\n * `CoalescedStatusWriter` - the run's own runner main loop, not yet built -\n * `AsyncJobTracker` picks the fields up on its next poll (see that module's\n * own header note on this), and THIS module is what lets a caller that is\n * blocked in `wait()` ever find out, instead of only discovering it by\n * polling `subagent({ action: \"status\" })` in a loop. `waitFor: 'attention'`\n * returns the instant any targeted run's `activityState` is\n * `'needs_attention'`, before its timeout, before it would ever go terminal.\n * `waitFor: 'completion'` deliberately does the opposite - it waits THROUGH\n * attention, for a caller (an auto-drain loop) that wants to know about\n * results, not about a mid-flight nudge. `'any'` (the default) returns on\n * whichever comes first, matching the predecessor's default behaviour.\n *\n * WHY A PRIVATE POLL LOOP, NOT A SECOND `PollScheduler` SUBSCRIPTION:\n * `PollScheduler` is for long-lived subscribers that back off toward a shared\n * idle ceiling; `wait()` is the opposite shape, a short-lived, bounded call\n * that needs a TIGHT interval for its whole (short) life, the same reasoning\n * `SpawnHandshake`'s header gives for not registering with it either. The loop\n * here only ever reads `AsyncJobTracker`'s already-cached, in-memory state\n * (no `node:fs`, no disk I/O of its own), so polling it every\n * `pollIntervalMs` costs a handful of `Map` reads, not a filesystem round trip.\n *\n * WHY NO EVENT-BUS WAKE (dropped from the predecessor):\n * The predecessor wakes early on `pi.events` firing (an `ExtensionAPI` event\n * bus this package has not built) and otherwise falls back to a fixed poll.\n * With no bus to wake on, this module is poll-only - which is exactly the\n * fallback path the predecessor already had to support for when its own bus\n * was unavailable, so there is no new failure mode here, only a narrower one.\n *\n * AVOID:\n * - Reading `status.json` directly. If a field `wait()` needs is missing from\n * `TrackedAsyncJob`, that is a reason to extend `AsyncJobTracker` (see its\n * header's note on `activityState`/`attentionReason` for the last time this\n * happened), not to open a second read path around it\n * - Treating a run id that was never tracked and has no readable `status.json`\n * the same as one that is merely slow. `wait()` resolves `'no-active-runs'`\n * immediately when NONE of the targeted ids ever resolve to a known status,\n * rather than waiting out the full timeout on nothing\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { ActivityState } from '../../types.ts';\nimport type { IAsyncJobTracker, ITrackedAsyncJobs } from './asyncJobTracker.ts';\n\n/** States `AsyncJobTracker` (mirroring `staleRunReconciler.ts`) treats as finished. */\nconst TERMINAL_STATES: ReadonlySet<string> = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\nconst DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;\nconst DEFAULT_POLL_INTERVAL_MS = 250;\nexport type WaitTarget = { id: string } | { ids: string[] } | { all: true };\n\nexport type WaitForMode = 'completion' | 'attention' | 'any';\n\nexport interface WaitRequest {\n target: WaitTarget;\n /** Pi context that owns the targeted run collection. */\n sessionId?: string;\n /** Defaults to `'any'`. */\n waitFor?: WaitForMode;\n /** Defaults to 30 minutes. */\n timeoutMs?: number;\n /** Resolves early with `reason: 'aborted'` when this fires. */\n signal?: AbortSignal;\n}\n\nexport type WaitReason = 'completed' | 'attention' | 'timeout' | 'aborted' | 'no-active-runs';\n\n/** One targeted run's state at the moment `wait()` resolved. */\nexport interface WaitRunSnapshot {\n runId: string;\n /** `undefined` when this run has never had a readable `status.json`. */\n status: string | undefined;\n activityState?: ActivityState;\n /** Present alongside `activityState: 'needs_attention'`, e.g. `'missing-deliverable'`. */\n attentionReason?: string;\n error?: string;\n startedAt?: number;\n updatedAt?: number;\n}\n\nexport interface WaitOutcome {\n reason: WaitReason;\n elapsedMs: number;\n /** Every targeted run's snapshot, regardless of `reason` - populated even on `'timeout'`. */\n runs: WaitRunSnapshot[];\n}\n\nexport interface ISubagentWaiter {\n wait(request: WaitRequest): Promise<WaitOutcome>;\n}\n\nfunction snapshotFrom(runId: string, job: ReturnType<ITrackedAsyncJobs['get']>): WaitRunSnapshot {\n if (!job) return { runId, status: undefined };\n return {\n runId: job.runId,\n status: job.status,\n activityState: job.activityState,\n attentionReason: job.attentionReason,\n error: job.error,\n startedAt: job.startedAt,\n updatedAt: job.updatedAt,\n };\n}\n\nfunction isTerminal(snapshot: WaitRunSnapshot): boolean {\n return snapshot.status !== undefined && TERMINAL_STATES.has(snapshot.status);\n}\n\nfunction needsAttention(snapshot: WaitRunSnapshot): boolean {\n return snapshot.activityState === 'needs_attention';\n}\n\nfunction satisfies(snapshot: WaitRunSnapshot, waitFor: WaitForMode): boolean {\n if (waitFor === 'attention') return needsAttention(snapshot);\n if (waitFor === 'completion') return isTerminal(snapshot);\n return needsAttention(snapshot) || isTerminal(snapshot);\n}\n\n/** For `waitFor: 'any'` only: which condition actually satisfied it. Attention wins when both happen to be true, since it is the more actionable signal. */\nfunction reasonFor(snapshot: WaitRunSnapshot): WaitReason {\n return needsAttention(snapshot) ? 'attention' : 'completed';\n}\n\n@injectable()\nexport class SubagentWaiter implements ISubagentWaiter {\n constructor(@inject(DOOM_TEAM_TYPES.AsyncJobTracker) private readonly tracker: IAsyncJobTracker) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive. See `pollScheduler.ts` and\n * `agents/discovery.ts` for the same pattern.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly defaultTimeoutMs: number = DEFAULT_TIMEOUT_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Sleep seam so a test can drive this with fake timers without a real delay. */\n protected sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n }\n\n async wait(request: WaitRequest): Promise<WaitOutcome> {\n const startedAt = this.now();\n const timeoutMs =\n request.timeoutMs !== undefined && request.timeoutMs > 0 ? request.timeoutMs : this.defaultTimeoutMs;\n const waitFor = request.waitFor ?? 'any';\n\n const jobs = request.sessionId ? this.tracker.forSession(request.sessionId) : this.tracker;\n const runIds = this.resolveTargetIds(request.target, jobs);\n for (const runId of runIds) jobs.track(runId);\n\n const elapsed = (): number => this.now() - startedAt;\n const snapshotAll = (): WaitRunSnapshot[] => runIds.map((runId) => snapshotFrom(runId, jobs.get(runId)));\n\n if (runIds.length === 0) {\n return { reason: 'no-active-runs', elapsedMs: 0, runs: [] };\n }\n\n const initialRuns = snapshotAll();\n if (initialRuns.every((snapshot) => snapshot.status === undefined)) {\n return { reason: 'no-active-runs', elapsedMs: elapsed(), runs: initialRuns };\n }\n\n for (;;) {\n const runs = snapshotAll();\n const satisfied = runs.find((snapshot) => satisfies(snapshot, waitFor));\n if (satisfied) {\n // The label reflects what `waitFor` actually asked for, not merely\n // whatever the snapshot happens to carry: a run that is BOTH terminal\n // and (independently) still flagged needs_attention must still report\n // 'completed' for `waitFor: 'completion'`, since attention was never\n // part of what that mode was watching for.\n const reason: WaitReason =\n waitFor === 'completion' ? 'completed' : waitFor === 'attention' ? 'attention' : reasonFor(satisfied);\n return { reason, elapsedMs: elapsed(), runs };\n }\n if (request.signal?.aborted) {\n return { reason: 'aborted', elapsedMs: elapsed(), runs };\n }\n if (elapsed() >= timeoutMs) {\n return { reason: 'timeout', elapsedMs: elapsed(), runs };\n }\n await this.sleep(Math.min(this.pollIntervalMs, Math.max(0, timeoutMs - elapsed())));\n }\n }\n\n private resolveTargetIds(target: WaitTarget, jobs: ITrackedAsyncJobs): string[] {\n if ('id' in target) return [target.id];\n if ('ids' in target) return target.ids;\n return jobs.list().map((job) => job.runId);\n }\n}\n"],"mappings":"4jBA2JA,SAAS,EAAa,EAAe,EAA4D,CAE/F,OADK,EACE,CACL,MAAO,EAAI,MACX,OAAQ,EAAI,OACZ,cAAe,EAAI,cACnB,gBAAiB,EAAI,gBACrB,MAAO,EAAI,MACX,UAAW,EAAI,UACf,UAAW,EAAI,UAChB,CATgB,CAAE,QAAO,OAAQ,IAAA,GAAW,CAY/C,SAAS,EAAW,EAAoC,CACtD,OAAO,EAAS,SAAW,IAAA,IAAa,EAAgB,IAAI,EAAS,OAAO,CAG9E,SAAS,EAAe,EAAoC,CAC1D,OAAO,EAAS,gBAAkB,kBAGpC,SAAS,EAAU,EAA2B,EAA+B,CAG3E,OAFI,IAAY,YAAoB,EAAe,EAAS,CACxD,IAAY,aAAqB,EAAW,EAAS,CAClD,EAAe,EAAS,EAAI,EAAW,EAAS,CAIzD,SAAS,EAAU,EAAuC,CACxD,OAAO,EAAe,EAAS,CAAG,YAAc,qCAhFS,aAKrD,EAAuC,IAAI,IAAI,CAAC,WAAY,YAAa,SAAU,SAAU,UAAU,CAAC,CAExG,EAAqB,KAAU,IAC/B,EAA2B,MA4E1B,KAAgD,CACrD,YAAY,EAAqF,CAA3B,KAAA,QAAA,EAOtE,eAA4C,EAC5C,iBAA8C,EAE9C,KAAwB,CACtB,OAAO,KAAK,KAAK,CAInB,MAAgB,EAA2B,CACzC,OAAO,IAAI,QAAS,GAAY,CAChB,WAAW,EAAS,EAC7B,CAAC,SAAS,EACf,CAGJ,MAAM,KAAK,EAA4C,CACrD,IAAM,EAAY,KAAK,KAAK,CACtB,EACJ,EAAQ,YAAc,IAAA,IAAa,EAAQ,UAAY,EAAI,EAAQ,UAAY,KAAK,iBAChF,EAAU,EAAQ,SAAW,MAE7B,EAAO,EAAQ,UAAY,KAAK,QAAQ,WAAW,EAAQ,UAAU,CAAG,KAAK,QAC7E,EAAS,KAAK,iBAAiB,EAAQ,OAAQ,EAAK,CAC1D,IAAK,IAAM,KAAS,EAAQ,EAAK,MAAM,EAAM,CAE7C,IAAM,MAAwB,KAAK,KAAK,CAAG,EACrC,MAAuC,EAAO,IAAK,GAAU,EAAa,EAAO,EAAK,IAAI,EAAM,CAAC,CAAC,CAExG,GAAI,EAAO,SAAW,EACpB,MAAO,CAAE,OAAQ,iBAAkB,UAAW,EAAG,KAAM,EAAE,CAAE,CAG7D,IAAM,EAAc,GAAa,CACjC,GAAI,EAAY,MAAO,GAAa,EAAS,SAAW,IAAA,GAAU,CAChE,MAAO,CAAE,OAAQ,iBAAkB,UAAW,GAAS,CAAE,KAAM,EAAa,CAG9E,OAAS,CACP,IAAM,EAAO,GAAa,CACpB,EAAY,EAAK,KAAM,GAAa,EAAU,EAAU,EAAQ,CAAC,CACvE,GAAI,EAQF,MAAO,CAAE,OADP,IAAY,aAAe,YAAc,IAAY,YAAc,YAAc,EAAU,EAAU,CACtF,UAAW,GAAS,CAAE,OAAM,CAE/C,GAAI,EAAQ,QAAQ,QAClB,MAAO,CAAE,OAAQ,UAAW,UAAW,GAAS,CAAE,OAAM,CAE1D,GAAI,GAAS,EAAI,EACf,MAAO,CAAE,OAAQ,UAAW,UAAW,GAAS,CAAE,OAAM,CAE1D,MAAM,KAAK,MAAM,KAAK,IAAI,KAAK,eAAgB,KAAK,IAAI,EAAG,EAAY,GAAS,CAAC,CAAC,CAAC,EAIvF,iBAAyB,EAAoB,EAAmC,CAG9E,MAFI,OAAQ,EAAe,CAAC,EAAO,GAAG,CAClC,QAAS,EAAe,EAAO,IAC5B,EAAK,MAAM,CAAC,IAAK,GAAQ,EAAI,MAAM,QAxE7C,GAAY,KAEE,EAAO,EAAgB,gBAAgB,CAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/atomicJson.cjs`),n=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),r=require(`../../container/types.cjs`),i=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),a=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`),o=require(`../../shared/paths.cjs`);let s=require(`node:fs`);s=e.__toESM(s,1);let c=require(`node:path`);c=e.__toESM(c,1);let l=require(`inversify`),u=require(`node:os`);u=e.__toESM(u,1);const d=`runner-crash-marker.json`;function f(e){return c.join(o.currentRunsDir(),e,d)}function p(e){return 128+u.constants.signals[e]}let m=class{constructor(e){this.statusWriter=e}runId;mutateTerminalStatus;finalized=!0;handlersInstalled=!1;trackedChildPids=new Set;handleSigterm=()=>this.handleSignal(`SIGTERM`);handleSigint=()=>this.handleSignal(`SIGINT`);handleSighup=()=>this.handleSignal(`SIGHUP`);handleUncaughtException=e=>this.handleFatal({kind:`uncaughtException`,error:e});handleUnhandledRejection=e=>this.handleFatal({kind:`unhandledRejection`,reason:e});now(){return Date.now()}onSignal(e,t){process.on(e,t)}offSignal(e,t){process.off(e,t)}onUncaughtException(e){process.on(`uncaughtException`,e)}offUncaughtException(e){process.off(`uncaughtException`,e)}onUnhandledRejection(e){process.on(`unhandledRejection`,e)}offUnhandledRejection(e){process.off(`unhandledRejection`,e)}killProcess(e,t){try{process.kill(e,t)}catch{}}exitProcess(e){process.exit(e)}begin(e,t){this.dispose(),this.runId=e,this.mutateTerminalStatus=t,this.finalized=!1,this.trackedChildPids.clear(),this.writeCrashMarker(e),this.installHandlers()}trackChild(e){this.trackedChildPids.add(e)}untrackChild(e){this.trackedChildPids.delete(e)}finalize(){this.triggerFinalize(void 0)}dispose(){this.handlersInstalled&&(this.handlersInstalled=!1,this.offSignal(`SIGTERM`,this.handleSigterm),this.offSignal(`SIGINT`,this.handleSigint),this.offSignal(`SIGHUP`,this.handleSighup),this.offUncaughtException(this.handleUncaughtException),this.offUnhandledRejection(this.handleUnhandledRejection))}installHandlers(){this.handlersInstalled||(this.handlersInstalled=!0,this.onSignal(`SIGTERM`,this.handleSigterm),this.onSignal(`SIGINT`,this.handleSigint),this.onSignal(`SIGHUP`,this.handleSighup),this.onUncaughtException(this.handleUncaughtException),this.onUnhandledRejection(this.handleUnhandledRejection))}handleSignal(e){this.triggerFinalize({kind:`signal`,signal:e}),this.exitProcess(p(e))}handleFatal(e){this.triggerFinalize(e),this.exitProcess(1)}triggerFinalize(e){if(!this.finalized){this.finalized=!0,this.dispose();try{let t=this.mutateTerminalStatus;t&&this.statusWriter.updateSync(n=>t(n,e)),this.clearCrashMarker()}finally{this.killTrackedChildren()}}}killTrackedChildren(){for(let e of this.trackedChildPids)this.killProcess(e,`SIGKILL`);this.trackedChildPids.clear()}writeCrashMarker(e){let n={version:1,runId:e,pid:process.pid,startedAt:this.now()};t.writeAtomicJson(f(e),n)}clearCrashMarker(){if(this.runId)try{s.rmSync(f(this.runId),{force:!0})}catch{}}};m=n.__decorate([(0,l.injectable)(),a.__decorateParam(0,(0,l.inject)(r.DOOM_TEAM_TYPES.CoalescedStatusWriter)),i.__decorateMetadata(`design:paramtypes`,[Object])],m),exports.CRASH_MARKER_FILE_NAME=d,Object.defineProperty(exports,`TerminalPersistenceService`,{enumerable:!0,get:function(){return m}});
|
|
2
|
-
//# sourceMappingURL=terminalPersistence.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"terminalPersistence.cjs","names":["path","currentRunsDir","os","DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/terminalPersistence.ts"],"sourcesContent":["/**\n * Guarantees one runner process ends with a persisted terminal result.\n *\n * WHY THIS EXISTS:\n * The predecessor's runner finalization\n * (`doom-pi-subagents/src/runs/background/subagent-runner.ts`) is straight-line\n * code with no `try`/`finally` and no signal handling beyond `SIGUSR2`. A throw\n * anywhere in that path, or a `SIGTERM`/`SIGKILL` of the runner process, leaves\n * the run's `status.json` reporting `running` for up to 24 hours, with the\n * `pi` child it spawned still alive and still burning tokens. A user who hits\n * Ctrl-C on a foreground run gets exactly this: the terminal returns, but the\n * child and the stale \"running\" status both live on.\n *\n * DESIGN PATTERNS:\n * - One runner process runs exactly one run, so this is a singleton holding the\n * state of the one run it is guarding, the same shape as `CoalescedStatusWriter`\n * - `ICoalescedStatusWriter` is injected, not assumed: this class holds a real\n * reference to the one true `status.json` writer and calls its synchronous\n * `updateSync` itself, rather than trusting a caller-supplied closure to have\n * done so. `mutateTerminalStatus` stays a caller-supplied closure for the\n * run's own richer status SHAPE only - this class does not know what fields\n * a terminal status carries, just that writing them goes through the one\n * writer, by construction rather than by docstring\n * - Idempotency is a single boolean guard checked and set before anything else\n * happens, so a second trigger - a duplicate signal, a throw during the first\n * finalize - observes \"already finalized\" before it can do anything\n * - Signal and process-exit machinery are `protected` seams, not constructor\n * arguments: inversify cannot inject a primitive, and a unit test must never\n * deliver a real signal to, or call the real `process.exit` on, the process\n * running the test\n *\n * THE CRASH MARKER:\n * `begin()` writes a small marker file before the run does anything else, and\n * a successful `finalize()` deletes it. A run that dies so hard it never reaches\n * `finalize()` at all (`SIGKILL`, a host crash) leaves the marker behind with no\n * result file next to it - the marker's `pid` is then dead, which is what lets a\n * later reader tell \"crashed\" apart from \"still running\": a genuinely running\n * run also has a live marker, but with a live pid. This module only writes and\n * clears the marker; reading it to make that determination is a different\n * module's job.\n *\n * AVOID:\n * - Calling `mutateTerminalStatus` with anything that can be asynchronous. The\n * whole guarantee here depends on finalize running to completion before\n * another trigger can observe the `finalized` flag, which only holds if\n * every step, including `CoalescedStatusWriter.updateSync`, is synchronous\n * - Writing `status.json` from here directly with `writeAtomicJson`; the\n * injected `ICoalescedStatusWriter` is the only thing that may touch it\n * - Calling `process.removeAllListeners` for a signal; that would remove a\n * listener some other module in the same process installed, not just ours\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport type { ICoalescedStatusWriter, StatusWithRecentEntries } from './statusWriter.ts';\n\n/**\n * The one crash-marker filename.\n *\n * Exported because `processTerminal.ts` reads this marker to decide whether a\n * run died without finishing. It writes the marker, that module reads it, and\n * they must not drift.\n */\nexport const CRASH_MARKER_FILE_NAME = 'runner-crash-marker.json';\nconst CRASH_MARKER_VERSION = 1;\n\n/** Exit code used for a fatal exception, matching Node's own default for an uncaught error. */\nconst FATAL_EXCEPTION_EXIT_CODE = 1;\n\n/** What triggered a terminal finalize, so the caller's `mutateTerminalStatus` closure can record why. */\nexport type TerminalTrigger =\n | { kind: 'signal'; signal: NodeJS.Signals }\n | { kind: 'uncaughtException'; error: Error }\n | { kind: 'unhandledRejection'; reason: unknown };\n\ninterface CrashMarkerRecord {\n version: typeof CRASH_MARKER_VERSION;\n runId: string;\n pid: number;\n startedAt: number;\n}\n\nfunction crashMarkerPath(runId: string): string {\n return path.join(currentRunsDir(), runId, CRASH_MARKER_FILE_NAME);\n}\n\n/** `128 + signal number`, the shell convention for a process killed by a signal. */\nfunction signalExitCode(signal: NodeJS.Signals): number {\n return 128 + os.constants.signals[signal];\n}\n\nexport interface ITerminalPersistenceService<TStatus extends StatusWithRecentEntries = StatusWithRecentEntries> {\n /**\n * Start guarding one run: writes the crash marker and installs the signal\n * and exception handlers. Safe to call again, on the same or a different\n * run id - a second call tears down whatever this instance was previously\n * guarding first, the same reset semantics as `CoalescedStatusWriter.open`.\n *\n * `mutateTerminalStatus` is called at most once, synchronously, on the\n * already-open status passed to `CoalescedStatusWriter.updateSync`, whenever\n * the run reaches a terminal state: `trigger` is undefined for the run's own\n * call to `finalize()`, and describes the signal or exception for every\n * other path. The caller is responsible for having called\n * `ICoalescedStatusWriter.open()` before the run's terminal state can be\n * reached; this class only mutates and flushes, it does not open.\n */\n begin(runId: string, mutateTerminalStatus: (status: TStatus, trigger: TerminalTrigger | undefined) => void): void;\n /** Track a spawned `pi` child so it is killed when this run finalizes. */\n trackChild(pid: number): void;\n /** Stop tracking a child that already exited on its own. */\n untrackChild(pid: number): void;\n /**\n * The run's own terminal call: persists the result through the injected\n * status writer's synchronous flush path, kills every tracked child, and\n * clears the crash marker. Idempotent - a second call, from any source,\n * including a signal that arrives after this one returns, is a no-op.\n */\n finalize(): void;\n /** Remove the installed signal and exception handlers without finalizing. */\n dispose(): void;\n}\n\n/**\n * Owner of the one run a runner process guards.\n *\n * WHY THIS IS A SERVICE AND NOT A TOP-LEVEL FUNCTION:\n * The predecessor's finalization lived inline in the runner's main function, so\n * nothing else in that module could trigger it, and a signal handler installed\n * elsewhere had no way to reach it. Putting it on an injectable instance gives\n * every part of the runner - the main run loop, a spawn-site failure, a signal\n * handler - the same `finalize()` to call, all guarded by the same flag.\n */\n@injectable()\nexport class TerminalPersistenceService<\n TStatus extends StatusWithRecentEntries = StatusWithRecentEntries,\n> implements ITerminalPersistenceService<TStatus> {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter) private readonly statusWriter: ICoalescedStatusWriter<TStatus>,\n ) {}\n\n private runId: string | undefined;\n private mutateTerminalStatus: ((status: TStatus, trigger: TerminalTrigger | undefined) => void) | undefined;\n /**\n * Starts `true` so that a call to `finalize()` or a delivered signal before\n * `begin()` has ever run is a safe no-op rather than a crash on undefined\n * state.\n */\n private finalized = true;\n private handlersInstalled = false;\n private readonly trackedChildPids = new Set<number>();\n\n // Bound once per instance so `offSignal`/`offUncaughtException` remove the\n // exact function reference `onSignal`/`onUncaughtException` installed.\n private readonly handleSigterm = (): void => this.handleSignal('SIGTERM');\n private readonly handleSigint = (): void => this.handleSignal('SIGINT');\n private readonly handleSighup = (): void => this.handleSignal('SIGHUP');\n private readonly handleUncaughtException = (error: Error): void =>\n this.handleFatal({ kind: 'uncaughtException', error });\n private readonly handleUnhandledRejection = (reason: unknown): void =>\n this.handleFatal({ kind: 'unhandledRejection', reason });\n\n protected now(): number {\n return Date.now();\n }\n\n // ---------------------------------------------------------------------\n // Process seams. A test subclass overrides every one of these so a unit\n // test never registers a real signal handler, never signals a real pid,\n // and never calls the real `process.exit` on the process running the test.\n // ---------------------------------------------------------------------\n\n protected onSignal(event: NodeJS.Signals, handler: NodeJS.SignalsListener): void {\n process.on(event, handler);\n }\n\n protected offSignal(event: NodeJS.Signals, handler: NodeJS.SignalsListener): void {\n process.off(event, handler);\n }\n\n protected onUncaughtException(handler: (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => void): void {\n process.on('uncaughtException', handler);\n }\n\n protected offUncaughtException(handler: (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => void): void {\n process.off('uncaughtException', handler);\n }\n\n protected onUnhandledRejection(handler: (reason: unknown, promise: Promise<unknown>) => void): void {\n process.on('unhandledRejection', handler);\n }\n\n protected offUnhandledRejection(handler: (reason: unknown, promise: Promise<unknown>) => void): void {\n process.off('unhandledRejection', handler);\n }\n\n protected killProcess(pid: number, signal: NodeJS.Signals): void {\n try {\n process.kill(pid, signal);\n } catch {\n // Already exited, or never existed. Nothing left to kill, which is the\n // outcome we wanted anyway.\n }\n }\n\n protected exitProcess(code: number): void {\n // Excluded from coverage: calling the real implementation would terminate\n // the process running whichever suite exercises this class, so every\n // test overrides this seam instead of triggering it for real.\n /* v8 ignore next */\n process.exit(code);\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n begin(runId: string, mutateTerminalStatus: (status: TStatus, trigger: TerminalTrigger | undefined) => void): void {\n // A second begin() is a reset: tear down whatever this instance was\n // previously guarding before starting the new one.\n this.dispose();\n this.runId = runId;\n this.mutateTerminalStatus = mutateTerminalStatus;\n this.finalized = false;\n this.trackedChildPids.clear();\n this.writeCrashMarker(runId);\n this.installHandlers();\n }\n\n trackChild(pid: number): void {\n this.trackedChildPids.add(pid);\n }\n\n untrackChild(pid: number): void {\n this.trackedChildPids.delete(pid);\n }\n\n finalize(): void {\n this.triggerFinalize(undefined);\n }\n\n dispose(): void {\n if (!this.handlersInstalled) return;\n this.handlersInstalled = false;\n this.offSignal('SIGTERM', this.handleSigterm);\n this.offSignal('SIGINT', this.handleSigint);\n this.offSignal('SIGHUP', this.handleSighup);\n this.offUncaughtException(this.handleUncaughtException);\n this.offUnhandledRejection(this.handleUnhandledRejection);\n }\n\n // ---------------------------------------------------------------------\n // Internals\n // ---------------------------------------------------------------------\n\n private installHandlers(): void {\n if (this.handlersInstalled) return;\n this.handlersInstalled = true;\n this.onSignal('SIGTERM', this.handleSigterm);\n this.onSignal('SIGINT', this.handleSigint);\n this.onSignal('SIGHUP', this.handleSighup);\n this.onUncaughtException(this.handleUncaughtException);\n this.onUnhandledRejection(this.handleUnhandledRejection);\n }\n\n private handleSignal(signal: NodeJS.Signals): void {\n this.triggerFinalize({ kind: 'signal', signal });\n // Registering a listener for these signals suppresses Node's default\n // terminate-on-signal behaviour, so once we have finalized we must end\n // the process ourselves - otherwise the signal is effectively swallowed\n // and the process hangs instead of honouring the Ctrl-C or `kill` that\n // was sent to it.\n this.exitProcess(signalExitCode(signal));\n }\n\n private handleFatal(trigger: TerminalTrigger): void {\n this.triggerFinalize(trigger);\n this.exitProcess(FATAL_EXCEPTION_EXIT_CODE);\n }\n\n /**\n * The idempotency gate every trigger funnels through.\n *\n * FIX: checked and set before anything else runs, so a second trigger -\n * whether that is `finalize()` called twice, a signal that arrives while\n * the first finalize is still on the stack, or a signal delivered after a\n * normal finalize already ran - observes `finalized === true` and returns\n * immediately. Because every step from here down is synchronous, there is\n * no `await` between the check and the set for a second trigger to race.\n */\n private triggerFinalize(trigger: TerminalTrigger | undefined): void {\n if (this.finalized) return;\n this.finalized = true;\n // Removed before persisting, not after: if a fatal signal arrives while\n // `mutateTerminalStatus` itself is running, it must not re-enter this\n // method through a reinstalled handler while the first pass is already\n // committed to exiting.\n this.dispose();\n try {\n const mutate = this.mutateTerminalStatus;\n // Excluded from coverage: unreachable by construction, for the same\n // reason as the runId guard below - mutateTerminalStatus is always set\n // once begin() has run, which finalized guards.\n /* v8 ignore next */\n if (mutate) this.statusWriter.updateSync((status) => mutate(status, trigger));\n // Only cleared once a result has actually been persisted. If the\n // mutator throws, the marker is left in place - that is the accurate\n // state: the run started and never produced a result, which is exactly\n // what the marker exists to record.\n this.clearCrashMarker();\n } finally {\n // Killing tracked children is unconditional: an orphaned child must not\n // survive a failed persist any more than it survives a successful one.\n this.killTrackedChildren();\n }\n }\n\n private killTrackedChildren(): void {\n for (const pid of this.trackedChildPids) this.killProcess(pid, 'SIGKILL');\n this.trackedChildPids.clear();\n }\n\n private writeCrashMarker(runId: string): void {\n const marker: CrashMarkerRecord = {\n version: CRASH_MARKER_VERSION,\n runId,\n pid: process.pid,\n startedAt: this.now(),\n };\n writeAtomicJson(crashMarkerPath(runId), marker);\n }\n\n private clearCrashMarker(): void {\n // Excluded from coverage: unreachable by construction. triggerFinalize\n // only reaches here once `finalized` has been observed false, which only\n // happens after begin() has already set runId - kept as a defensive\n // check instead of a non-null assertion, not as a reachable branch.\n /* v8 ignore next */\n if (!this.runId) return;\n try {\n fs.rmSync(crashMarkerPath(this.runId), { force: true });\n } catch {\n // Best effort: a marker that resists removal still correctly reads as\n // \"this run may have crashed\" to a later reader, which is the safe\n // direction for this cleanup to fail in.\n }\n }\n}\n"],"mappings":"6jBAqEA,MAAa,EAAyB,2BAmBtC,SAAS,EAAgB,EAAuB,CAC9C,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAO,EAAuB,CAInE,SAAS,EAAe,EAAgC,CACtD,MAAO,KAAMC,EAAG,UAAU,QAAQ,GA6C7B,IAAA,EAAA,KAE2C,CAChD,YACE,EACA,CADgE,KAAA,aAAA,EAGlE,MACA,qBAMA,UAAoB,GACpB,kBAA4B,GAC5B,iBAAoC,IAAI,IAIxC,kBAA6C,KAAK,aAAa,UAAU,CACzE,iBAA4C,KAAK,aAAa,SAAS,CACvE,iBAA4C,KAAK,aAAa,SAAS,CACvE,wBAA4C,GAC1C,KAAK,YAAY,CAAE,KAAM,oBAAqB,QAAO,CAAC,CACxD,yBAA6C,GAC3C,KAAK,YAAY,CAAE,KAAM,qBAAsB,SAAQ,CAAC,CAE1D,KAAwB,CACtB,OAAO,KAAK,KAAK,CASnB,SAAmB,EAAuB,EAAuC,CAC/E,QAAQ,GAAG,EAAO,EAAQ,CAG5B,UAAoB,EAAuB,EAAuC,CAChF,QAAQ,IAAI,EAAO,EAAQ,CAG7B,oBAA8B,EAA+E,CAC3G,QAAQ,GAAG,oBAAqB,EAAQ,CAG1C,qBAA+B,EAA+E,CAC5G,QAAQ,IAAI,oBAAqB,EAAQ,CAG3C,qBAA+B,EAAqE,CAClG,QAAQ,GAAG,qBAAsB,EAAQ,CAG3C,sBAAgC,EAAqE,CACnG,QAAQ,IAAI,qBAAsB,EAAQ,CAG5C,YAAsB,EAAa,EAA8B,CAC/D,GAAI,CACF,QAAQ,KAAK,EAAK,EAAO,MACnB,GAMV,YAAsB,EAAoB,CAKxC,QAAQ,KAAK,EAAK,CAOpB,MAAM,EAAe,EAA6F,CAGhH,KAAK,SAAS,CACd,KAAK,MAAQ,EACb,KAAK,qBAAuB,EAC5B,KAAK,UAAY,GACjB,KAAK,iBAAiB,OAAO,CAC7B,KAAK,iBAAiB,EAAM,CAC5B,KAAK,iBAAiB,CAGxB,WAAW,EAAmB,CAC5B,KAAK,iBAAiB,IAAI,EAAI,CAGhC,aAAa,EAAmB,CAC9B,KAAK,iBAAiB,OAAO,EAAI,CAGnC,UAAiB,CACf,KAAK,gBAAgB,IAAA,GAAU,CAGjC,SAAgB,CACT,KAAK,oBACV,KAAK,kBAAoB,GACzB,KAAK,UAAU,UAAW,KAAK,cAAc,CAC7C,KAAK,UAAU,SAAU,KAAK,aAAa,CAC3C,KAAK,UAAU,SAAU,KAAK,aAAa,CAC3C,KAAK,qBAAqB,KAAK,wBAAwB,CACvD,KAAK,sBAAsB,KAAK,yBAAyB,EAO3D,iBAAgC,CAC1B,KAAK,oBACT,KAAK,kBAAoB,GACzB,KAAK,SAAS,UAAW,KAAK,cAAc,CAC5C,KAAK,SAAS,SAAU,KAAK,aAAa,CAC1C,KAAK,SAAS,SAAU,KAAK,aAAa,CAC1C,KAAK,oBAAoB,KAAK,wBAAwB,CACtD,KAAK,qBAAqB,KAAK,yBAAyB,EAG1D,aAAqB,EAA8B,CACjD,KAAK,gBAAgB,CAAE,KAAM,SAAU,SAAQ,CAAC,CAMhD,KAAK,YAAY,EAAe,EAAO,CAAC,CAG1C,YAAoB,EAAgC,CAClD,KAAK,gBAAgB,EAAQ,CAC7B,KAAK,YAAY,EAA0B,CAa7C,gBAAwB,EAA4C,CAC9D,SAAK,UAMT,CALA,KAAK,UAAY,GAKjB,KAAK,SAAS,CACd,GAAI,CACF,IAAM,EAAS,KAAK,qBAKhB,GAAQ,KAAK,aAAa,WAAY,GAAW,EAAO,EAAQ,EAAQ,CAAC,CAK7E,KAAK,kBAAkB,QACf,CAGR,KAAK,qBAAqB,GAI9B,qBAAoC,CAClC,IAAK,IAAM,KAAO,KAAK,iBAAkB,KAAK,YAAY,EAAK,UAAU,CACzE,KAAK,iBAAiB,OAAO,CAG/B,iBAAyB,EAAqB,CAC5C,IAAM,EAA4B,CAChC,QAAS,EACT,QACA,IAAK,QAAQ,IACb,UAAW,KAAK,KAAK,CACtB,CACD,EAAA,gBAAgB,EAAgB,EAAM,CAAE,EAAO,CAGjD,kBAAiC,CAM1B,QAAK,MACV,GAAI,CACF,EAAG,OAAO,EAAgB,KAAK,MAAM,CAAE,CAAE,MAAO,GAAM,CAAC,MACjD,uCAhNC,kCAKDC,EAAAA,gBAAgB,sBAAsB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"terminalPersistence.mjs","names":[],"sources":["../../../src/runs/background/terminalPersistence.ts"],"sourcesContent":["/**\n * Guarantees one runner process ends with a persisted terminal result.\n *\n * WHY THIS EXISTS:\n * The predecessor's runner finalization\n * (`doom-pi-subagents/src/runs/background/subagent-runner.ts`) is straight-line\n * code with no `try`/`finally` and no signal handling beyond `SIGUSR2`. A throw\n * anywhere in that path, or a `SIGTERM`/`SIGKILL` of the runner process, leaves\n * the run's `status.json` reporting `running` for up to 24 hours, with the\n * `pi` child it spawned still alive and still burning tokens. A user who hits\n * Ctrl-C on a foreground run gets exactly this: the terminal returns, but the\n * child and the stale \"running\" status both live on.\n *\n * DESIGN PATTERNS:\n * - One runner process runs exactly one run, so this is a singleton holding the\n * state of the one run it is guarding, the same shape as `CoalescedStatusWriter`\n * - `ICoalescedStatusWriter` is injected, not assumed: this class holds a real\n * reference to the one true `status.json` writer and calls its synchronous\n * `updateSync` itself, rather than trusting a caller-supplied closure to have\n * done so. `mutateTerminalStatus` stays a caller-supplied closure for the\n * run's own richer status SHAPE only - this class does not know what fields\n * a terminal status carries, just that writing them goes through the one\n * writer, by construction rather than by docstring\n * - Idempotency is a single boolean guard checked and set before anything else\n * happens, so a second trigger - a duplicate signal, a throw during the first\n * finalize - observes \"already finalized\" before it can do anything\n * - Signal and process-exit machinery are `protected` seams, not constructor\n * arguments: inversify cannot inject a primitive, and a unit test must never\n * deliver a real signal to, or call the real `process.exit` on, the process\n * running the test\n *\n * THE CRASH MARKER:\n * `begin()` writes a small marker file before the run does anything else, and\n * a successful `finalize()` deletes it. A run that dies so hard it never reaches\n * `finalize()` at all (`SIGKILL`, a host crash) leaves the marker behind with no\n * result file next to it - the marker's `pid` is then dead, which is what lets a\n * later reader tell \"crashed\" apart from \"still running\": a genuinely running\n * run also has a live marker, but with a live pid. This module only writes and\n * clears the marker; reading it to make that determination is a different\n * module's job.\n *\n * AVOID:\n * - Calling `mutateTerminalStatus` with anything that can be asynchronous. The\n * whole guarantee here depends on finalize running to completion before\n * another trigger can observe the `finalized` flag, which only holds if\n * every step, including `CoalescedStatusWriter.updateSync`, is synchronous\n * - Writing `status.json` from here directly with `writeAtomicJson`; the\n * injected `ICoalescedStatusWriter` is the only thing that may touch it\n * - Calling `process.removeAllListeners` for a signal; that would remove a\n * listener some other module in the same process installed, not just ours\n */\n\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport type { ICoalescedStatusWriter, StatusWithRecentEntries } from './statusWriter.ts';\n\n/**\n * The one crash-marker filename.\n *\n * Exported because `processTerminal.ts` reads this marker to decide whether a\n * run died without finishing. It writes the marker, that module reads it, and\n * they must not drift.\n */\nexport const CRASH_MARKER_FILE_NAME = 'runner-crash-marker.json';\nconst CRASH_MARKER_VERSION = 1;\n\n/** Exit code used for a fatal exception, matching Node's own default for an uncaught error. */\nconst FATAL_EXCEPTION_EXIT_CODE = 1;\n\n/** What triggered a terminal finalize, so the caller's `mutateTerminalStatus` closure can record why. */\nexport type TerminalTrigger =\n | { kind: 'signal'; signal: NodeJS.Signals }\n | { kind: 'uncaughtException'; error: Error }\n | { kind: 'unhandledRejection'; reason: unknown };\n\ninterface CrashMarkerRecord {\n version: typeof CRASH_MARKER_VERSION;\n runId: string;\n pid: number;\n startedAt: number;\n}\n\nfunction crashMarkerPath(runId: string): string {\n return path.join(currentRunsDir(), runId, CRASH_MARKER_FILE_NAME);\n}\n\n/** `128 + signal number`, the shell convention for a process killed by a signal. */\nfunction signalExitCode(signal: NodeJS.Signals): number {\n return 128 + os.constants.signals[signal];\n}\n\nexport interface ITerminalPersistenceService<TStatus extends StatusWithRecentEntries = StatusWithRecentEntries> {\n /**\n * Start guarding one run: writes the crash marker and installs the signal\n * and exception handlers. Safe to call again, on the same or a different\n * run id - a second call tears down whatever this instance was previously\n * guarding first, the same reset semantics as `CoalescedStatusWriter.open`.\n *\n * `mutateTerminalStatus` is called at most once, synchronously, on the\n * already-open status passed to `CoalescedStatusWriter.updateSync`, whenever\n * the run reaches a terminal state: `trigger` is undefined for the run's own\n * call to `finalize()`, and describes the signal or exception for every\n * other path. The caller is responsible for having called\n * `ICoalescedStatusWriter.open()` before the run's terminal state can be\n * reached; this class only mutates and flushes, it does not open.\n */\n begin(runId: string, mutateTerminalStatus: (status: TStatus, trigger: TerminalTrigger | undefined) => void): void;\n /** Track a spawned `pi` child so it is killed when this run finalizes. */\n trackChild(pid: number): void;\n /** Stop tracking a child that already exited on its own. */\n untrackChild(pid: number): void;\n /**\n * The run's own terminal call: persists the result through the injected\n * status writer's synchronous flush path, kills every tracked child, and\n * clears the crash marker. Idempotent - a second call, from any source,\n * including a signal that arrives after this one returns, is a no-op.\n */\n finalize(): void;\n /** Remove the installed signal and exception handlers without finalizing. */\n dispose(): void;\n}\n\n/**\n * Owner of the one run a runner process guards.\n *\n * WHY THIS IS A SERVICE AND NOT A TOP-LEVEL FUNCTION:\n * The predecessor's finalization lived inline in the runner's main function, so\n * nothing else in that module could trigger it, and a signal handler installed\n * elsewhere had no way to reach it. Putting it on an injectable instance gives\n * every part of the runner - the main run loop, a spawn-site failure, a signal\n * handler - the same `finalize()` to call, all guarded by the same flag.\n */\n@injectable()\nexport class TerminalPersistenceService<\n TStatus extends StatusWithRecentEntries = StatusWithRecentEntries,\n> implements ITerminalPersistenceService<TStatus> {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter) private readonly statusWriter: ICoalescedStatusWriter<TStatus>,\n ) {}\n\n private runId: string | undefined;\n private mutateTerminalStatus: ((status: TStatus, trigger: TerminalTrigger | undefined) => void) | undefined;\n /**\n * Starts `true` so that a call to `finalize()` or a delivered signal before\n * `begin()` has ever run is a safe no-op rather than a crash on undefined\n * state.\n */\n private finalized = true;\n private handlersInstalled = false;\n private readonly trackedChildPids = new Set<number>();\n\n // Bound once per instance so `offSignal`/`offUncaughtException` remove the\n // exact function reference `onSignal`/`onUncaughtException` installed.\n private readonly handleSigterm = (): void => this.handleSignal('SIGTERM');\n private readonly handleSigint = (): void => this.handleSignal('SIGINT');\n private readonly handleSighup = (): void => this.handleSignal('SIGHUP');\n private readonly handleUncaughtException = (error: Error): void =>\n this.handleFatal({ kind: 'uncaughtException', error });\n private readonly handleUnhandledRejection = (reason: unknown): void =>\n this.handleFatal({ kind: 'unhandledRejection', reason });\n\n protected now(): number {\n return Date.now();\n }\n\n // ---------------------------------------------------------------------\n // Process seams. A test subclass overrides every one of these so a unit\n // test never registers a real signal handler, never signals a real pid,\n // and never calls the real `process.exit` on the process running the test.\n // ---------------------------------------------------------------------\n\n protected onSignal(event: NodeJS.Signals, handler: NodeJS.SignalsListener): void {\n process.on(event, handler);\n }\n\n protected offSignal(event: NodeJS.Signals, handler: NodeJS.SignalsListener): void {\n process.off(event, handler);\n }\n\n protected onUncaughtException(handler: (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => void): void {\n process.on('uncaughtException', handler);\n }\n\n protected offUncaughtException(handler: (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => void): void {\n process.off('uncaughtException', handler);\n }\n\n protected onUnhandledRejection(handler: (reason: unknown, promise: Promise<unknown>) => void): void {\n process.on('unhandledRejection', handler);\n }\n\n protected offUnhandledRejection(handler: (reason: unknown, promise: Promise<unknown>) => void): void {\n process.off('unhandledRejection', handler);\n }\n\n protected killProcess(pid: number, signal: NodeJS.Signals): void {\n try {\n process.kill(pid, signal);\n } catch {\n // Already exited, or never existed. Nothing left to kill, which is the\n // outcome we wanted anyway.\n }\n }\n\n protected exitProcess(code: number): void {\n // Excluded from coverage: calling the real implementation would terminate\n // the process running whichever suite exercises this class, so every\n // test overrides this seam instead of triggering it for real.\n /* v8 ignore next */\n process.exit(code);\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n begin(runId: string, mutateTerminalStatus: (status: TStatus, trigger: TerminalTrigger | undefined) => void): void {\n // A second begin() is a reset: tear down whatever this instance was\n // previously guarding before starting the new one.\n this.dispose();\n this.runId = runId;\n this.mutateTerminalStatus = mutateTerminalStatus;\n this.finalized = false;\n this.trackedChildPids.clear();\n this.writeCrashMarker(runId);\n this.installHandlers();\n }\n\n trackChild(pid: number): void {\n this.trackedChildPids.add(pid);\n }\n\n untrackChild(pid: number): void {\n this.trackedChildPids.delete(pid);\n }\n\n finalize(): void {\n this.triggerFinalize(undefined);\n }\n\n dispose(): void {\n if (!this.handlersInstalled) return;\n this.handlersInstalled = false;\n this.offSignal('SIGTERM', this.handleSigterm);\n this.offSignal('SIGINT', this.handleSigint);\n this.offSignal('SIGHUP', this.handleSighup);\n this.offUncaughtException(this.handleUncaughtException);\n this.offUnhandledRejection(this.handleUnhandledRejection);\n }\n\n // ---------------------------------------------------------------------\n // Internals\n // ---------------------------------------------------------------------\n\n private installHandlers(): void {\n if (this.handlersInstalled) return;\n this.handlersInstalled = true;\n this.onSignal('SIGTERM', this.handleSigterm);\n this.onSignal('SIGINT', this.handleSigint);\n this.onSignal('SIGHUP', this.handleSighup);\n this.onUncaughtException(this.handleUncaughtException);\n this.onUnhandledRejection(this.handleUnhandledRejection);\n }\n\n private handleSignal(signal: NodeJS.Signals): void {\n this.triggerFinalize({ kind: 'signal', signal });\n // Registering a listener for these signals suppresses Node's default\n // terminate-on-signal behaviour, so once we have finalized we must end\n // the process ourselves - otherwise the signal is effectively swallowed\n // and the process hangs instead of honouring the Ctrl-C or `kill` that\n // was sent to it.\n this.exitProcess(signalExitCode(signal));\n }\n\n private handleFatal(trigger: TerminalTrigger): void {\n this.triggerFinalize(trigger);\n this.exitProcess(FATAL_EXCEPTION_EXIT_CODE);\n }\n\n /**\n * The idempotency gate every trigger funnels through.\n *\n * FIX: checked and set before anything else runs, so a second trigger -\n * whether that is `finalize()` called twice, a signal that arrives while\n * the first finalize is still on the stack, or a signal delivered after a\n * normal finalize already ran - observes `finalized === true` and returns\n * immediately. Because every step from here down is synchronous, there is\n * no `await` between the check and the set for a second trigger to race.\n */\n private triggerFinalize(trigger: TerminalTrigger | undefined): void {\n if (this.finalized) return;\n this.finalized = true;\n // Removed before persisting, not after: if a fatal signal arrives while\n // `mutateTerminalStatus` itself is running, it must not re-enter this\n // method through a reinstalled handler while the first pass is already\n // committed to exiting.\n this.dispose();\n try {\n const mutate = this.mutateTerminalStatus;\n // Excluded from coverage: unreachable by construction, for the same\n // reason as the runId guard below - mutateTerminalStatus is always set\n // once begin() has run, which finalized guards.\n /* v8 ignore next */\n if (mutate) this.statusWriter.updateSync((status) => mutate(status, trigger));\n // Only cleared once a result has actually been persisted. If the\n // mutator throws, the marker is left in place - that is the accurate\n // state: the run started and never produced a result, which is exactly\n // what the marker exists to record.\n this.clearCrashMarker();\n } finally {\n // Killing tracked children is unconditional: an orphaned child must not\n // survive a failed persist any more than it survives a successful one.\n this.killTrackedChildren();\n }\n }\n\n private killTrackedChildren(): void {\n for (const pid of this.trackedChildPids) this.killProcess(pid, 'SIGKILL');\n this.trackedChildPids.clear();\n }\n\n private writeCrashMarker(runId: string): void {\n const marker: CrashMarkerRecord = {\n version: CRASH_MARKER_VERSION,\n runId,\n pid: process.pid,\n startedAt: this.now(),\n };\n writeAtomicJson(crashMarkerPath(runId), marker);\n }\n\n private clearCrashMarker(): void {\n // Excluded from coverage: unreachable by construction. triggerFinalize\n // only reaches here once `finalized` has been observed false, which only\n // happens after begin() has already set runId - kept as a defensive\n // check instead of a non-null assertion, not as a reachable branch.\n /* v8 ignore next */\n if (!this.runId) return;\n try {\n fs.rmSync(crashMarkerPath(this.runId), { force: true });\n } catch {\n // Best effort: a marker that resists removal still correctly reads as\n // \"this run may have crashed\" to a later reader, which is the safe\n // direction for this cleanup to fail in.\n }\n }\n}\n"],"mappings":"uyBAwFA,SAAS,EAAgB,EAAuB,CAC9C,OAAO,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAuB,CAInE,SAAS,EAAe,EAAgC,CACtD,MAAO,KAAM,EAAG,UAAU,QAAQ,4BArCuB,IACE,IACN,aAU1C,EAAyB,2BAChC,EAAuB,EAGvB,EAA4B,IAkE3B,KAE2C,CAChD,YACE,EACA,CADgE,KAAA,aAAA,EAGlE,MACA,qBAMA,UAAoB,GACpB,kBAA4B,GAC5B,iBAAoC,IAAI,IAIxC,kBAA6C,KAAK,aAAa,UAAU,CACzE,iBAA4C,KAAK,aAAa,SAAS,CACvE,iBAA4C,KAAK,aAAa,SAAS,CACvE,wBAA4C,GAC1C,KAAK,YAAY,CAAE,KAAM,oBAAqB,QAAO,CAAC,CACxD,yBAA6C,GAC3C,KAAK,YAAY,CAAE,KAAM,qBAAsB,SAAQ,CAAC,CAE1D,KAAwB,CACtB,OAAO,KAAK,KAAK,CASnB,SAAmB,EAAuB,EAAuC,CAC/E,QAAQ,GAAG,EAAO,EAAQ,CAG5B,UAAoB,EAAuB,EAAuC,CAChF,QAAQ,IAAI,EAAO,EAAQ,CAG7B,oBAA8B,EAA+E,CAC3G,QAAQ,GAAG,oBAAqB,EAAQ,CAG1C,qBAA+B,EAA+E,CAC5G,QAAQ,IAAI,oBAAqB,EAAQ,CAG3C,qBAA+B,EAAqE,CAClG,QAAQ,GAAG,qBAAsB,EAAQ,CAG3C,sBAAgC,EAAqE,CACnG,QAAQ,IAAI,qBAAsB,EAAQ,CAG5C,YAAsB,EAAa,EAA8B,CAC/D,GAAI,CACF,QAAQ,KAAK,EAAK,EAAO,MACnB,GAMV,YAAsB,EAAoB,CAKxC,QAAQ,KAAK,EAAK,CAOpB,MAAM,EAAe,EAA6F,CAGhH,KAAK,SAAS,CACd,KAAK,MAAQ,EACb,KAAK,qBAAuB,EAC5B,KAAK,UAAY,GACjB,KAAK,iBAAiB,OAAO,CAC7B,KAAK,iBAAiB,EAAM,CAC5B,KAAK,iBAAiB,CAGxB,WAAW,EAAmB,CAC5B,KAAK,iBAAiB,IAAI,EAAI,CAGhC,aAAa,EAAmB,CAC9B,KAAK,iBAAiB,OAAO,EAAI,CAGnC,UAAiB,CACf,KAAK,gBAAgB,IAAA,GAAU,CAGjC,SAAgB,CACT,KAAK,oBACV,KAAK,kBAAoB,GACzB,KAAK,UAAU,UAAW,KAAK,cAAc,CAC7C,KAAK,UAAU,SAAU,KAAK,aAAa,CAC3C,KAAK,UAAU,SAAU,KAAK,aAAa,CAC3C,KAAK,qBAAqB,KAAK,wBAAwB,CACvD,KAAK,sBAAsB,KAAK,yBAAyB,EAO3D,iBAAgC,CAC1B,KAAK,oBACT,KAAK,kBAAoB,GACzB,KAAK,SAAS,UAAW,KAAK,cAAc,CAC5C,KAAK,SAAS,SAAU,KAAK,aAAa,CAC1C,KAAK,SAAS,SAAU,KAAK,aAAa,CAC1C,KAAK,oBAAoB,KAAK,wBAAwB,CACtD,KAAK,qBAAqB,KAAK,yBAAyB,EAG1D,aAAqB,EAA8B,CACjD,KAAK,gBAAgB,CAAE,KAAM,SAAU,SAAQ,CAAC,CAMhD,KAAK,YAAY,EAAe,EAAO,CAAC,CAG1C,YAAoB,EAAgC,CAClD,KAAK,gBAAgB,EAAQ,CAC7B,KAAK,YAAY,EAA0B,CAa7C,gBAAwB,EAA4C,CAC9D,SAAK,UAMT,CALA,KAAK,UAAY,GAKjB,KAAK,SAAS,CACd,GAAI,CACF,IAAM,EAAS,KAAK,qBAKhB,GAAQ,KAAK,aAAa,WAAY,GAAW,EAAO,EAAQ,EAAQ,CAAC,CAK7E,KAAK,kBAAkB,QACf,CAGR,KAAK,qBAAqB,GAI9B,qBAAoC,CAClC,IAAK,IAAM,KAAO,KAAK,iBAAkB,KAAK,YAAY,EAAK,UAAU,CACzE,KAAK,iBAAiB,OAAO,CAG/B,iBAAyB,EAAqB,CAC5C,IAAM,EAA4B,CAChC,QAAS,EACT,QACA,IAAK,QAAQ,IACb,UAAW,KAAK,KAAK,CACtB,CACD,EAAgB,EAAgB,EAAM,CAAE,EAAO,CAGjD,kBAAiC,CAM1B,QAAK,MACV,GAAI,CACF,EAAG,OAAO,EAAgB,KAAK,MAAM,CAAE,CAAE,MAAO,GAAM,CAAC,MACjD,UAhNX,GAAY,KAKR,EAAO,EAAgB,sBAAsB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"processGroup.cjs","names":[],"sources":["../../../src/runs/registry/processGroup.ts"],"sourcesContent":["/**\n * Stopping a run's whole process tree.\n *\n * Every child is spawned `detached: true`, which puts it in its own process\n * group, which is what makes `kill(-pid)` reach the agent AND whatever it\n * spawned. Signalling the pid alone would leave a CLI's own subprocesses\n * running after their parent died.\n *\n * WHY SIGTERM THEN SIGKILL, RATHER THAN EITHER ALONE:\n * SIGTERM lets a runner finalize - persist its terminal status, release its\n * team membership, kill its own children. SIGKILL cannot be caught, so a runner\n * that is wedged still goes away. Doing only the first leaves a hung child\n * running; doing only the second guarantees a stale `running` status on disk.\n *\n * `doom-runner` does the same thing at `services/Launcher/Launcher.ts`, and Pi\n * itself at `utils/shell.ts`'s `killProcessTree`. Neither is importable from\n * here - Pi exports only `getShellConfig` from that module - so this is a third\n * copy of a nine-line idea rather than a shared dependency.\n *\n * AVOID:\n * - Signalling `pid` instead of `-pid`; that misses the group\n */\n\nconst DEFAULT_GRACE_MS = 3_000;\n\nexport interface ProcessGroupSignals {\n /** Send `signal` to the group led by `pid`. Returns false when it is already gone. */\n signalGroup(pid: number, signal: NodeJS.Signals): boolean;\n isAlive(pid: number): boolean;\n wait(ms: number): Promise<void>;\n}\n\nexport const nodeProcessGroupSignals: ProcessGroupSignals = {\n signalGroup(pid, signal) {\n try {\n process.kill(-pid, signal);\n return true;\n } catch {\n // ESRCH: already gone, which is the outcome we wanted. EPERM: not ours to\n // signal, and retrying with SIGKILL will not change that.\n return false;\n }\n },\n isAlive(pid) {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n },\n wait(ms) {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n },\n};\n\n/**\n * Stop one run's process group, escalating if it outlasts the grace period.\n *\n * Resolves `true` once nothing in the group is alive, `false` if something\n * survived even SIGKILL - which should be impossible on POSIX and is reported\n * rather than asserted, because a caller sweeping many runs needs to know which\n * one it could not clear.\n */\nexport async function stopProcessGroup(\n pid: number,\n graceMs: number = DEFAULT_GRACE_MS,\n signals: ProcessGroupSignals = nodeProcessGroupSignals,\n): Promise<boolean> {\n if (!signals.isAlive(pid)) return true;\n signals.signalGroup(pid, 'SIGTERM');\n await signals.wait(graceMs);\n if (!signals.isAlive(pid)) return true;\n signals.signalGroup(pid, 'SIGKILL');\n await signals.wait(graceMs);\n return !signals.isAlive(pid);\n}\n"],"mappings":"AAuBA,MASa,EAA+C,CAC1D,YAAY,EAAK,EAAQ,CACvB,GAAI,CAEF,OADA,QAAQ,KAAK,CAAC,EAAK,EAAO,CACnB,QACD,CAGN,MAAO,KAGX,QAAQ,EAAK,CACX,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CACd,OAAQ,EAAgC,OAAS,UAGrD,KAAK,EAAI,CACP,OAAO,IAAI,QAAS,GAAY,CAChB,WAAW,EAAS,EAC7B,CAAC,SAAS,EACf,EAEL,CAUD,eAAsB,EACpB,EACA,EAAkB,IAClB,EAA+B,EACb,CAOlB,MANI,CAAC,EAAQ,QAAQ,EAAI,GACzB,EAAQ,YAAY,EAAK,UAAU,CACnC,MAAM,EAAQ,KAAK,EAAQ,CACvB,CAAC,EAAQ,QAAQ,EAAI,EAAS,IAClC,EAAQ,YAAY,EAAK,UAAU,CACnC,MAAM,EAAQ,KAAK,EAAQ,CACpB,CAAC,EAAQ,QAAQ,EAAI"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"processGroup.mjs","names":[],"sources":["../../../src/runs/registry/processGroup.ts"],"sourcesContent":["/**\n * Stopping a run's whole process tree.\n *\n * Every child is spawned `detached: true`, which puts it in its own process\n * group, which is what makes `kill(-pid)` reach the agent AND whatever it\n * spawned. Signalling the pid alone would leave a CLI's own subprocesses\n * running after their parent died.\n *\n * WHY SIGTERM THEN SIGKILL, RATHER THAN EITHER ALONE:\n * SIGTERM lets a runner finalize - persist its terminal status, release its\n * team membership, kill its own children. SIGKILL cannot be caught, so a runner\n * that is wedged still goes away. Doing only the first leaves a hung child\n * running; doing only the second guarantees a stale `running` status on disk.\n *\n * `doom-runner` does the same thing at `services/Launcher/Launcher.ts`, and Pi\n * itself at `utils/shell.ts`'s `killProcessTree`. Neither is importable from\n * here - Pi exports only `getShellConfig` from that module - so this is a third\n * copy of a nine-line idea rather than a shared dependency.\n *\n * AVOID:\n * - Signalling `pid` instead of `-pid`; that misses the group\n */\n\nconst DEFAULT_GRACE_MS = 3_000;\n\nexport interface ProcessGroupSignals {\n /** Send `signal` to the group led by `pid`. Returns false when it is already gone. */\n signalGroup(pid: number, signal: NodeJS.Signals): boolean;\n isAlive(pid: number): boolean;\n wait(ms: number): Promise<void>;\n}\n\nexport const nodeProcessGroupSignals: ProcessGroupSignals = {\n signalGroup(pid, signal) {\n try {\n process.kill(-pid, signal);\n return true;\n } catch {\n // ESRCH: already gone, which is the outcome we wanted. EPERM: not ours to\n // signal, and retrying with SIGKILL will not change that.\n return false;\n }\n },\n isAlive(pid) {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n },\n wait(ms) {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n },\n};\n\n/**\n * Stop one run's process group, escalating if it outlasts the grace period.\n *\n * Resolves `true` once nothing in the group is alive, `false` if something\n * survived even SIGKILL - which should be impossible on POSIX and is reported\n * rather than asserted, because a caller sweeping many runs needs to know which\n * one it could not clear.\n */\nexport async function stopProcessGroup(\n pid: number,\n graceMs: number = DEFAULT_GRACE_MS,\n signals: ProcessGroupSignals = nodeProcessGroupSignals,\n): Promise<boolean> {\n if (!signals.isAlive(pid)) return true;\n signals.signalGroup(pid, 'SIGTERM');\n await signals.wait(graceMs);\n if (!signals.isAlive(pid)) return true;\n signals.signalGroup(pid, 'SIGKILL');\n await signals.wait(graceMs);\n return !signals.isAlive(pid);\n}\n"],"mappings":"AAuBA,MASa,EAA+C,CAC1D,YAAY,EAAK,EAAQ,CACvB,GAAI,CAEF,OADA,QAAQ,KAAK,CAAC,EAAK,EAAO,CACnB,QACD,CAGN,MAAO,KAGX,QAAQ,EAAK,CACX,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CACd,OAAQ,EAAgC,OAAS,UAGrD,KAAK,EAAI,CACP,OAAO,IAAI,QAAS,GAAY,CAChB,WAAW,EAAS,EAC7B,CAAC,SAAS,EACf,EAEL,CAUD,eAAsB,EACpB,EACA,EAAkB,IAClB,EAA+B,EACb,CAOlB,MANI,CAAC,EAAQ,QAAQ,EAAI,GACzB,EAAQ,YAAY,EAAK,UAAU,CACnC,MAAM,EAAQ,KAAK,EAAQ,CACvB,CAAC,EAAQ,QAAQ,EAAI,EAAS,IAClC,EAAQ,YAAY,EAAK,UAAU,CACnC,MAAM,EAAQ,KAAK,EAAQ,CACpB,CAAC,EAAQ,QAAQ,EAAI"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/atomicJson.cjs`),n=require(`../../shared/paths.cjs`),r=require(`../../shared/versioned.cjs`),i=require(`../../shared/scopeOwner.cjs`);let a=require(`node:fs`);a=e.__toESM(a,1);let o=require(`node:path`);o=e.__toESM(o,1);function s(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function c(e){return o.join(n.sessionScopeDir(e),`registry.json`)}function l(e){let t;try{t=a.readFileSync(c(e),`utf-8`)}catch{return}let n;try{n=JSON.parse(t)}catch{return}let i=r.parseVersioned(n,[1]);if(!(!i.ok||!Array.isArray(i.value.runs)))return i.value}function u(e,n){t.writeAtomicJson(c(e),{version:1,runs:n})}function d(e,t){let n=(l(e)?.runs??[]).filter(e=>e.runId!==t.runId);n.push({...t,hostPid:process.pid}),u(e,n)}function f(e,t){let n=l(e)?.runs;n&&u(e,n.filter(e=>e.runId!==t))}function p(e){return l(e)?.runs??[]}function m(e,t=s){let n=l(e)?.runs;if(!n)return[];let r=n.filter(e=>t(e.pid));return r.length===n.length?[]:(u(e,r),n.filter(e=>!t(e.pid)).map(e=>e.runId))}function h(e,t=s){let r;try{r=a.readdirSync(n.SESSIONS_ROOT_DIR)}catch{return[]}let o=[];for(let n of r){if(n===e.scopeKey)continue;let r=i.readScopeOwner({rootSessionId:``,scopeKey:n});if(!r)continue;let a={rootSessionId:r.rootSessionId,scopeKey:n};p(a).some(e=>t(e.pid))||o.push(a)}return o}function g(e){try{a.rmSync(n.sessionScopeDir(e),{recursive:!0,force:!0})}catch{n.scopeOwnerPath(e)}}exports.defaultPidLiveness=s,exports.findReapableScopes=h,exports.listRuns=p,exports.pruneDeadRuns=m,exports.reapScope=g,exports.registerRun=d,exports.releaseRun=f;
|
|
2
|
-
//# sourceMappingURL=runRegistry.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runRegistry.cjs","names":["path","sessionScopeDir","fs","parseVersioned","SESSIONS_ROOT_DIR","readScopeOwner","scopeOwnerPath"],"sources":["../../../src/runs/registry/runRegistry.ts"],"sourcesContent":["/**\n * Which runs this session started, and which process owns them.\n *\n * WHY A FILE AND NOT THE IN-MEMORY TRACKER:\n * `AsyncJobTracker` already knows this session's runs, but only in memory, and\n * its own header refuses to scan the runs directory to work out ownership. That\n * is the right call for delivery, and useless for the one question that matters\n * at shutdown and after a crash: which pids does this scope still own? A\n * process that died cannot answer, so the answer has to be on disk.\n *\n * REAPING KEYS ON RUN LIVENESS, NOT ON `hostPid` ALONE:\n * The obvious rule - \"reap a scope whose owning process is dead\" - is wrong.\n * After `/new` the abandoned scope's `hostPid` is the still-live current\n * process, so a hostPid-only test would never reap it. A scope is reapable when\n * none of its recorded pids is alive; a dead `hostPid` only widens that to\n * scopes whose owner is gone entirely.\n *\n * MODELLED ON `doom-runner`'s `RunnerRegistry`, whose `hostPid`-for-orphan-\n * detection and `listBySession` shape this reuses. The containment boundary\n * differs: there it is the worktree, here it is the session scope, which is\n * already a directory.\n *\n * AVOID:\n * - Treating an unreadable registry as empty. Unknown is not the same as none,\n * and acting on the difference is how a live run gets reaped\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { SESSIONS_ROOT_DIR, scopeOwnerPath, sessionScopeDir, type SessionScope } from '../../shared/paths.ts';\nimport { readScopeOwner } from '../../shared/scopeOwner.ts';\nimport { parseVersioned } from '../../shared/versioned.ts';\n\nconst RUN_REGISTRY_VERSION = 1;\nconst REGISTRY_FILE_NAME = 'registry.json';\n\nexport interface RunRecord {\n runId: string;\n /** Process-group leader. Signals are sent to `-pid`. */\n pid: number;\n agent: string;\n runtime: string;\n startedAt: number;\n /** The process that spawned it, for orphan detection after a crash. */\n hostPid: number;\n}\n\ninterface RunRegistryFile {\n version: typeof RUN_REGISTRY_VERSION;\n runs: RunRecord[];\n}\n\n/** Liveness as a port, so a test never probes a real pid. */\nexport type PidLivenessProbe = (pid: number) => boolean;\n\nexport function defaultPidLiveness(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which is still alive.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nfunction registryPath(scope: SessionScope): string {\n return path.join(sessionScopeDir(scope), REGISTRY_FILE_NAME);\n}\n\nfunction readRegistry(scope: SessionScope): RunRegistryFile | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(registryPath(scope), 'utf-8');\n } catch {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n const result = parseVersioned<RunRegistryFile>(parsed, [RUN_REGISTRY_VERSION]);\n if (!result.ok || !Array.isArray(result.value.runs)) return undefined;\n return result.value;\n}\n\nfunction writeRegistry(scope: SessionScope, runs: RunRecord[]): void {\n writeAtomicJson(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\n/** Record a started run against its scope. Replaces any earlier record for the same id. */\nexport function registerRun(scope: SessionScope, record: Omit<RunRecord, 'hostPid'>): void {\n const existing = readRegistry(scope)?.runs ?? [];\n const runs = existing.filter((run) => run.runId !== record.runId);\n runs.push({ ...record, hostPid: process.pid });\n writeRegistry(scope, runs);\n}\n\n/** Drop one run. Does not signal it; the caller decides that. */\nexport function releaseRun(scope: SessionScope, runId: string): void {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return;\n writeRegistry(\n scope,\n existing.filter((run) => run.runId !== runId),\n );\n}\n\nexport function listRuns(scope: SessionScope): RunRecord[] {\n return readRegistry(scope)?.runs ?? [];\n}\n\n/** Forget records whose process is gone. Returns the run ids dropped. */\nexport function pruneDeadRuns(scope: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): string[] {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return [];\n const live = existing.filter((run) => isAlive(run.pid));\n if (live.length === existing.length) return [];\n writeRegistry(scope, live);\n return existing.filter((run) => !isAlive(run.pid)).map((run) => run.runId);\n}\n\n/**\n * Scopes other than `keep` that hold nothing alive.\n *\n * A scope with no readable owner is skipped rather than reaped: unreadable\n * means unknown, and deleting on unknown is how a live session loses its tree.\n */\nexport function findReapableScopes(keep: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): SessionScope[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = readScopeOwner(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if (listRuns(scope).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\n/** Remove a scope's whole tree. Only ever called on a scope `findReapableScopes` returned. */\nexport function reapScope(scope: SessionScope): void {\n try {\n fs.rmSync(sessionScopeDir(scope), { recursive: true, force: true });\n } catch {\n // Best effort: a tree that resists removal is reconsidered on the next\n // sweep, and its owner file still reads as reapable, which is the safe\n // direction for this to fail in.\n void scopeOwnerPath(scope);\n }\n}\n"],"mappings":"4SAyDA,SAAgB,EAAmB,EAAsB,CACvD,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CAEd,OAAQ,EAAgC,OAAS,SAIrD,SAAS,EAAa,EAA6B,CACjD,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,EAAM,CAAE,gBAAmB,CAG9D,SAAS,EAAa,EAAkD,CACtE,IAAI,EACJ,GAAI,CACF,EAAMC,EAAG,aAAa,EAAa,EAAM,CAAE,QAAQ,MAC7C,CACN,OAEF,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CACN,OAEF,IAAM,EAASC,EAAAA,eAAgC,EAAQ,CAAC,EAAqB,CAAC,CAC1E,MAAC,EAAO,IAAM,CAAC,MAAM,QAAQ,EAAO,MAAM,KAAK,EACnD,OAAO,EAAO,MAGhB,SAAS,EAAc,EAAqB,EAAyB,CACnE,EAAA,gBAAgB,EAAa,EAAM,CAAE,CAAE,QAAS,EAAsB,OAAM,CAAC,CAI/E,SAAgB,EAAY,EAAqB,EAA0C,CAEzF,IAAM,GADW,EAAa,EAAM,EAAE,MAAQ,EAAE,EAC1B,OAAQ,GAAQ,EAAI,QAAU,EAAO,MAAM,CACjE,EAAK,KAAK,CAAE,GAAG,EAAQ,QAAS,QAAQ,IAAK,CAAC,CAC9C,EAAc,EAAO,EAAK,CAI5B,SAAgB,EAAW,EAAqB,EAAqB,CACnE,IAAM,EAAW,EAAa,EAAM,EAAE,KACjC,GACL,EACE,EACA,EAAS,OAAQ,GAAQ,EAAI,QAAU,EAAM,CAC9C,CAGH,SAAgB,EAAS,EAAkC,CACzD,OAAO,EAAa,EAAM,EAAE,MAAQ,EAAE,CAIxC,SAAgB,EAAc,EAAqB,EAA4B,EAA8B,CAC3G,IAAM,EAAW,EAAa,EAAM,EAAE,KACtC,GAAI,CAAC,EAAU,MAAO,EAAE,CACxB,IAAM,EAAO,EAAS,OAAQ,GAAQ,EAAQ,EAAI,IAAI,CAAC,CAGvD,OAFI,EAAK,SAAW,EAAS,OAAe,EAAE,EAC9C,EAAc,EAAO,EAAK,CACnB,EAAS,OAAQ,GAAQ,CAAC,EAAQ,EAAI,IAAI,CAAC,CAAC,IAAK,GAAQ,EAAI,MAAM,EAS5E,SAAgB,EAAmB,EAAoB,EAA4B,EAAoC,CACrH,IAAI,EACJ,GAAI,CACF,EAAUD,EAAG,YAAYE,EAAAA,kBAAkB,MACrC,CACN,MAAO,EAAE,CAGX,IAAM,EAA2B,EAAE,CACnC,IAAK,IAAM,KAAY,EAAS,CAC9B,GAAI,IAAa,EAAK,SAAU,SAEhC,IAAM,EAAQC,EAAAA,eAAe,CADK,cAAe,GAAI,WACf,CAAC,CACvC,GAAI,CAAC,EAAO,SACZ,IAAM,EAAsB,CAAE,cAAe,EAAM,cAAe,WAAU,CACxE,EAAS,EAAM,CAAC,KAAM,GAAQ,EAAQ,EAAI,IAAI,CAAC,EACnD,EAAS,KAAK,EAAM,CAEtB,OAAO,EAIT,SAAgB,EAAU,EAA2B,CACnD,GAAI,CACF,EAAG,OAAOJ,EAAAA,gBAAgB,EAAM,CAAE,CAAE,UAAW,GAAM,MAAO,GAAM,CAAC,MAC7D,CAIDK,EAAAA,eAAe,EAAM"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{init_atomicJson as t,writeAtomicJson as n}from"../../shared/atomicJson.mjs";import{SESSIONS_ROOT_DIR as r,init_paths as i,scopeOwnerPath as a,sessionScopeDir as o}from"../../shared/paths.mjs";import{init_versioned as s,parseVersioned as c}from"../../shared/versioned.mjs";import{init_scopeOwner as l,readScopeOwner as u}from"../../shared/scopeOwner.mjs";import*as d from"node:fs";import*as f from"node:path";function p(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function m(e){return f.join(o(e),w)}function h(e){let t;try{t=d.readFileSync(m(e),`utf-8`)}catch{return}let n;try{n=JSON.parse(t)}catch{return}let r=c(n,[C]);if(!(!r.ok||!Array.isArray(r.value.runs)))return r.value}function g(e,t){n(m(e),{version:C,runs:t})}function _(e,t){let n=(h(e)?.runs??[]).filter(e=>e.runId!==t.runId);n.push({...t,hostPid:process.pid}),g(e,n)}function v(e,t){let n=h(e)?.runs;n&&g(e,n.filter(e=>e.runId!==t))}function y(e){return h(e)?.runs??[]}function b(e,t=p){let n=h(e)?.runs;if(!n)return[];let r=n.filter(e=>t(e.pid));return r.length===n.length?[]:(g(e,r),n.filter(e=>!t(e.pid)).map(e=>e.runId))}function x(e,t=p){let n;try{n=d.readdirSync(r)}catch{return[]}let i=[];for(let r of n){if(r===e.scopeKey)continue;let n=u({rootSessionId:``,scopeKey:r});if(!n)continue;let a={rootSessionId:n.rootSessionId,scopeKey:r};y(a).some(e=>t(e.pid))||i.push(a)}return i}function S(e){try{d.rmSync(o(e),{recursive:!0,force:!0})}catch{a(e)}}var C,w,T=e((()=>{t(),i(),l(),s(),C=1,w=`registry.json`}));T();export{p as defaultPidLiveness,x as findReapableScopes,T as init_runRegistry,y as listRuns,b as pruneDeadRuns,S as reapScope,_ as registerRun,v as releaseRun};
|
|
2
|
-
//# sourceMappingURL=runRegistry.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runRegistry.mjs","names":[],"sources":["../../../src/runs/registry/runRegistry.ts"],"sourcesContent":["/**\n * Which runs this session started, and which process owns them.\n *\n * WHY A FILE AND NOT THE IN-MEMORY TRACKER:\n * `AsyncJobTracker` already knows this session's runs, but only in memory, and\n * its own header refuses to scan the runs directory to work out ownership. That\n * is the right call for delivery, and useless for the one question that matters\n * at shutdown and after a crash: which pids does this scope still own? A\n * process that died cannot answer, so the answer has to be on disk.\n *\n * REAPING KEYS ON RUN LIVENESS, NOT ON `hostPid` ALONE:\n * The obvious rule - \"reap a scope whose owning process is dead\" - is wrong.\n * After `/new` the abandoned scope's `hostPid` is the still-live current\n * process, so a hostPid-only test would never reap it. A scope is reapable when\n * none of its recorded pids is alive; a dead `hostPid` only widens that to\n * scopes whose owner is gone entirely.\n *\n * MODELLED ON `doom-runner`'s `RunnerRegistry`, whose `hostPid`-for-orphan-\n * detection and `listBySession` shape this reuses. The containment boundary\n * differs: there it is the worktree, here it is the session scope, which is\n * already a directory.\n *\n * AVOID:\n * - Treating an unreadable registry as empty. Unknown is not the same as none,\n * and acting on the difference is how a live run gets reaped\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { SESSIONS_ROOT_DIR, scopeOwnerPath, sessionScopeDir, type SessionScope } from '../../shared/paths.ts';\nimport { readScopeOwner } from '../../shared/scopeOwner.ts';\nimport { parseVersioned } from '../../shared/versioned.ts';\n\nconst RUN_REGISTRY_VERSION = 1;\nconst REGISTRY_FILE_NAME = 'registry.json';\n\nexport interface RunRecord {\n runId: string;\n /** Process-group leader. Signals are sent to `-pid`. */\n pid: number;\n agent: string;\n runtime: string;\n startedAt: number;\n /** The process that spawned it, for orphan detection after a crash. */\n hostPid: number;\n}\n\ninterface RunRegistryFile {\n version: typeof RUN_REGISTRY_VERSION;\n runs: RunRecord[];\n}\n\n/** Liveness as a port, so a test never probes a real pid. */\nexport type PidLivenessProbe = (pid: number) => boolean;\n\nexport function defaultPidLiveness(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which is still alive.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nfunction registryPath(scope: SessionScope): string {\n return path.join(sessionScopeDir(scope), REGISTRY_FILE_NAME);\n}\n\nfunction readRegistry(scope: SessionScope): RunRegistryFile | undefined {\n let raw: string;\n try {\n raw = fs.readFileSync(registryPath(scope), 'utf-8');\n } catch {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n const result = parseVersioned<RunRegistryFile>(parsed, [RUN_REGISTRY_VERSION]);\n if (!result.ok || !Array.isArray(result.value.runs)) return undefined;\n return result.value;\n}\n\nfunction writeRegistry(scope: SessionScope, runs: RunRecord[]): void {\n writeAtomicJson(registryPath(scope), { version: RUN_REGISTRY_VERSION, runs });\n}\n\n/** Record a started run against its scope. Replaces any earlier record for the same id. */\nexport function registerRun(scope: SessionScope, record: Omit<RunRecord, 'hostPid'>): void {\n const existing = readRegistry(scope)?.runs ?? [];\n const runs = existing.filter((run) => run.runId !== record.runId);\n runs.push({ ...record, hostPid: process.pid });\n writeRegistry(scope, runs);\n}\n\n/** Drop one run. Does not signal it; the caller decides that. */\nexport function releaseRun(scope: SessionScope, runId: string): void {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return;\n writeRegistry(\n scope,\n existing.filter((run) => run.runId !== runId),\n );\n}\n\nexport function listRuns(scope: SessionScope): RunRecord[] {\n return readRegistry(scope)?.runs ?? [];\n}\n\n/** Forget records whose process is gone. Returns the run ids dropped. */\nexport function pruneDeadRuns(scope: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): string[] {\n const existing = readRegistry(scope)?.runs;\n if (!existing) return [];\n const live = existing.filter((run) => isAlive(run.pid));\n if (live.length === existing.length) return [];\n writeRegistry(scope, live);\n return existing.filter((run) => !isAlive(run.pid)).map((run) => run.runId);\n}\n\n/**\n * Scopes other than `keep` that hold nothing alive.\n *\n * A scope with no readable owner is skipped rather than reaped: unreadable\n * means unknown, and deleting on unknown is how a live session loses its tree.\n */\nexport function findReapableScopes(keep: SessionScope, isAlive: PidLivenessProbe = defaultPidLiveness): SessionScope[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(SESSIONS_ROOT_DIR);\n } catch {\n return [];\n }\n\n const reapable: SessionScope[] = [];\n for (const scopeKey of entries) {\n if (scopeKey === keep.scopeKey) continue;\n const candidate: SessionScope = { rootSessionId: '', scopeKey };\n const owner = readScopeOwner(candidate);\n if (!owner) continue;\n const scope: SessionScope = { rootSessionId: owner.rootSessionId, scopeKey };\n if (listRuns(scope).some((run) => isAlive(run.pid))) continue;\n reapable.push(scope);\n }\n return reapable;\n}\n\n/** Remove a scope's whole tree. Only ever called on a scope `findReapableScopes` returned. */\nexport function reapScope(scope: SessionScope): void {\n try {\n fs.rmSync(sessionScopeDir(scope), { recursive: true, force: true });\n } catch {\n // Best effort: a tree that resists removal is reconsidered on the next\n // sweep, and its owner file still reads as reapable, which is the safe\n // direction for this to fail in.\n void scopeOwnerPath(scope);\n }\n}\n"],"mappings":"+dAyDA,SAAgB,EAAmB,EAAsB,CACvD,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CAEd,OAAQ,EAAgC,OAAS,SAIrD,SAAS,EAAa,EAA6B,CACjD,OAAO,EAAK,KAAK,EAAgB,EAAM,CAAE,EAAmB,CAG9D,SAAS,EAAa,EAAkD,CACtE,IAAI,EACJ,GAAI,CACF,EAAM,EAAG,aAAa,EAAa,EAAM,CAAE,QAAQ,MAC7C,CACN,OAEF,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CACN,OAEF,IAAM,EAAS,EAAgC,EAAQ,CAAC,EAAqB,CAAC,CAC1E,MAAC,EAAO,IAAM,CAAC,MAAM,QAAQ,EAAO,MAAM,KAAK,EACnD,OAAO,EAAO,MAGhB,SAAS,EAAc,EAAqB,EAAyB,CACnE,EAAgB,EAAa,EAAM,CAAE,CAAE,QAAS,EAAsB,OAAM,CAAC,CAI/E,SAAgB,EAAY,EAAqB,EAA0C,CAEzF,IAAM,GADW,EAAa,EAAM,EAAE,MAAQ,EAAE,EAC1B,OAAQ,GAAQ,EAAI,QAAU,EAAO,MAAM,CACjE,EAAK,KAAK,CAAE,GAAG,EAAQ,QAAS,QAAQ,IAAK,CAAC,CAC9C,EAAc,EAAO,EAAK,CAI5B,SAAgB,EAAW,EAAqB,EAAqB,CACnE,IAAM,EAAW,EAAa,EAAM,EAAE,KACjC,GACL,EACE,EACA,EAAS,OAAQ,GAAQ,EAAI,QAAU,EAAM,CAC9C,CAGH,SAAgB,EAAS,EAAkC,CACzD,OAAO,EAAa,EAAM,EAAE,MAAQ,EAAE,CAIxC,SAAgB,EAAc,EAAqB,EAA4B,EAA8B,CAC3G,IAAM,EAAW,EAAa,EAAM,EAAE,KACtC,GAAI,CAAC,EAAU,MAAO,EAAE,CACxB,IAAM,EAAO,EAAS,OAAQ,GAAQ,EAAQ,EAAI,IAAI,CAAC,CAGvD,OAFI,EAAK,SAAW,EAAS,OAAe,EAAE,EAC9C,EAAc,EAAO,EAAK,CACnB,EAAS,OAAQ,GAAQ,CAAC,EAAQ,EAAI,IAAI,CAAC,CAAC,IAAK,GAAQ,EAAI,MAAM,EAS5E,SAAgB,EAAmB,EAAoB,EAA4B,EAAoC,CACrH,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,EAAkB,MACrC,CACN,MAAO,EAAE,CAGX,IAAM,EAA2B,EAAE,CACnC,IAAK,IAAM,KAAY,EAAS,CAC9B,GAAI,IAAa,EAAK,SAAU,SAEhC,IAAM,EAAQ,EAAe,CADK,cAAe,GAAI,WACf,CAAC,CACvC,GAAI,CAAC,EAAO,SACZ,IAAM,EAAsB,CAAE,cAAe,EAAM,cAAe,WAAU,CACxE,EAAS,EAAM,CAAC,KAAM,GAAQ,EAAQ,EAAI,IAAI,CAAC,EACnD,EAAS,KAAK,EAAM,CAEtB,OAAO,EAIT,SAAgB,EAAU,EAA2B,CACnD,GAAI,CACF,EAAG,OAAO,EAAgB,EAAM,CAAE,CAAE,UAAW,GAAM,MAAO,GAAM,CAAC,MAC7D,CAID,EAAe,EAAM,uBAlI+B,IACiD,IAClD,IACD,CAErD,EAAuB,EACvB,EAAqB"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require(`./suspendedRuns.cjs`),t=require(`./runRegistry.cjs`),n=require(`./processGroup.cjs`);function r(e,t,n,r){return{runId:e.runId,agent:e.agent,runtime:e.runtime,task:t?.task??``,cwd:t?.cwd??process.cwd(),...t?.model?{model:t.model}:{},...t?.inlineAgent?{inlineAgent:t.inlineAgent}:{},...t?.summary||t?.state?{lastStatus:t.state}:{},...t?.sessionFile?{sessionFile:t.sessionFile}:{},suspendedAt:r,reason:n}}async function i(i){let a=i.now?.()??Date.now(),o=t.listRuns(i.scope),s=[];for(let t of o)e.suspendRun(i.scope,r(t,i.readStatus(t.runId),i.reason,a)),s.push(t.runId);return{suspended:s,unstoppable:(await Promise.all(o.map(async e=>{let r=await n.stopProcessGroup(e.pid,i.graceMs,i.signals);return t.releaseRun(i.scope,e.runId),{runId:e.runId,stopped:r}}))).filter(e=>!e.stopped).map(e=>e.runId)}}function a(n,i={}){let a=i.isAlive??t.defaultPidLiveness,o=new Set(e.listSuspendedRuns(n).map(e=>e.runId)),s=i.now?.()??Date.now();for(let c of t.listRuns(n))a(c.pid)||o.has(c.runId)||e.suspendRun(n,r(c,i.readStatus?.(c.runId),`parent_lost`,s));let c=t.pruneDeadRuns(n,a),l=t.findReapableScopes(n,a);for(let e of l)t.reapScope(e);return{pruned:c,reaped:l.length,suspended:e.listSuspendedRuns(n)}}exports.openScope=a,exports.suspendScopeRuns=i;
|
|
2
|
-
//# sourceMappingURL=sessionLifecycle.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessionLifecycle.cjs","names":["listRuns","stopProcessGroup","defaultPidLiveness","listSuspendedRuns","pruneDeadRuns","findReapableScopes"],"sources":["../../../src/runs/registry/sessionLifecycle.ts"],"sourcesContent":["/**\n * What happens to this scope's runs when a session starts or ends.\n *\n * Two operations, deliberately kept out of the extension's activation function\n * so both are testable without a Pi host:\n *\n * - `suspendScopeRuns` on every shutdown: stop each recorded run's process\n * group and write down enough to start it again.\n * - `openScope` on every start: prune records whose process is gone, reap\n * sibling scopes that hold nothing alive, and report what is suspended\n * WITHOUT restoring any of it.\n *\n * WHY SHUTDOWN DOES NOT BRANCH ON `reason`:\n * See `suspendedRuns.ts`. Uniform beats one remembered exception, and restore\n * is cheap because a Pi child continues its own transcript.\n *\n * AVOID:\n * - Restoring from here. `openScope` reports; something explicit restores\n */\n\nimport type { AsyncRunStatus } from '../background/asyncExecution.ts';\nimport { type SessionScope } from '../../shared/paths.ts';\nimport { stopProcessGroup, type ProcessGroupSignals } from './processGroup.ts';\nimport {\n defaultPidLiveness,\n findReapableScopes,\n listRuns,\n pruneDeadRuns,\n reapScope,\n releaseRun,\n type PidLivenessProbe,\n type RunRecord,\n} from './runRegistry.ts';\nimport { listSuspendedRuns, suspendRun, type SuspendedRun } from './suspendedRuns.ts';\n\nconst PARENT_LOST_REASON = 'parent_lost';\n\n/** Reads a run's own status, so a suspended record can carry its last known state. */\nexport type RunStatusReader = (runId: string) => AsyncRunStatus | undefined;\n\nexport interface SuspendScopeOptions {\n scope: SessionScope;\n /** Pi's `session_shutdown` reason, recorded so a later report can say why. */\n reason: string;\n readStatus: RunStatusReader;\n now?: () => number;\n signals?: ProcessGroupSignals;\n graceMs?: number;\n}\n\nexport interface SuspendScopeResult {\n suspended: string[];\n /** Runs whose group survived even SIGKILL. Should be empty; reported rather than assumed. */\n unstoppable: string[];\n}\n\nfunction toSuspendedRecord(run: RunRecord, status: AsyncRunStatus | undefined, reason: string, at: number) {\n return {\n runId: run.runId,\n agent: run.agent,\n runtime: run.runtime,\n task: status?.task ?? '',\n cwd: status?.cwd ?? process.cwd(),\n ...(status?.model ? { model: status.model } : {}),\n ...(status?.inlineAgent ? { inlineAgent: status.inlineAgent } : {}),\n ...(status?.summary ? { lastStatus: status.state } : status?.state ? { lastStatus: status.state } : {}),\n ...(status?.sessionFile ? { sessionFile: status.sessionFile } : {}),\n suspendedAt: at,\n reason,\n };\n}\n\n/**\n * Stop every run this scope owns and record how to bring it back.\n *\n * The record is written BEFORE the signal: a process that dies between the two\n * leaves a restorable record, whereas the other order can lose the run\n * entirely if the parent is killed mid-sweep.\n */\nexport async function suspendScopeRuns(options: SuspendScopeOptions): Promise<SuspendScopeResult> {\n const at = options.now?.() ?? Date.now();\n const runs = listRuns(options.scope);\n const suspended: string[] = [];\n\n // Pi does not await session_shutdown handlers. Persist every run before the\n // first async wait so process exit cannot truncate this sweep after one run.\n for (const run of runs) {\n suspendRun(options.scope, toSuspendedRecord(run, options.readStatus(run.runId), options.reason, at));\n suspended.push(run.runId);\n }\n\n // Starting all async stop operations in one map dispatches every initial\n // SIGTERM synchronously before any grace-period promise is awaited.\n const stopResults = await Promise.all(\n runs.map(async (run) => {\n const stopped = await stopProcessGroup(run.pid, options.graceMs, options.signals);\n releaseRun(options.scope, run.runId);\n return { runId: run.runId, stopped };\n }),\n );\n const unstoppable = stopResults.filter((result) => !result.stopped).map((result) => result.runId);\n\n return { suspended, unstoppable };\n}\n\nexport interface OpenScopeResult {\n /** Run ids whose process was already gone; their records were dropped. */\n pruned: string[];\n /** Sibling scopes removed because nothing in them was alive. */\n reaped: number;\n /** Suspended runs awaiting an explicit restore. Reported, never started here. */\n suspended: SuspendedRun[];\n}\n\nexport interface OpenScopeOptions {\n isAlive?: PidLivenessProbe;\n readStatus?: RunStatusReader;\n now?: () => number;\n}\n\nexport function openScope(scope: SessionScope, options: OpenScopeOptions = {}): OpenScopeResult {\n const isAlive = options.isAlive ?? defaultPidLiveness;\n const alreadySuspended = new Set(listSuspendedRuns(scope).map((run) => run.runId));\n const at = options.now?.() ?? Date.now();\n for (const run of listRuns(scope)) {\n if (isAlive(run.pid) || alreadySuspended.has(run.runId)) continue;\n suspendRun(scope, toSuspendedRecord(run, options.readStatus?.(run.runId), PARENT_LOST_REASON, at));\n }\n\n const pruned = pruneDeadRuns(scope, isAlive);\n const reapable = findReapableScopes(scope, isAlive);\n for (const stale of reapable) reapScope(stale);\n return { pruned, reaped: reapable.length, suspended: listSuspendedRuns(scope) };\n}\n"],"mappings":"sGAwDA,SAAS,EAAkB,EAAgB,EAAoC,EAAgB,EAAY,CACzG,MAAO,CACL,MAAO,EAAI,MACX,MAAO,EAAI,MACX,QAAS,EAAI,QACb,KAAM,GAAQ,MAAQ,GACtB,IAAK,GAAQ,KAAO,QAAQ,KAAK,CACjC,GAAI,GAAQ,MAAQ,CAAE,MAAO,EAAO,MAAO,CAAG,EAAE,CAChD,GAAI,GAAQ,YAAc,CAAE,YAAa,EAAO,YAAa,CAAG,EAAE,CAClE,GAAI,GAAQ,SAAyC,GAAQ,MAAvC,CAAE,WAAY,EAAO,MAAO,CAAkD,EAAE,CACtG,GAAI,GAAQ,YAAc,CAAE,YAAa,EAAO,YAAa,CAAG,EAAE,CAClE,YAAa,EACb,SACD,CAUH,eAAsB,EAAiB,EAA2D,CAChG,IAAM,EAAK,EAAQ,OAAO,EAAI,KAAK,KAAK,CAClC,EAAOA,EAAAA,SAAS,EAAQ,MAAM,CAC9B,EAAsB,EAAE,CAI9B,IAAK,IAAM,KAAO,EAChB,EAAA,WAAW,EAAQ,MAAO,EAAkB,EAAK,EAAQ,WAAW,EAAI,MAAM,CAAE,EAAQ,OAAQ,EAAG,CAAC,CACpG,EAAU,KAAK,EAAI,MAAM,CAc3B,MAAO,CAAE,YAAW,aAFA,MAPM,QAAQ,IAChC,EAAK,IAAI,KAAO,IAAQ,CACtB,IAAM,EAAU,MAAMC,EAAAA,iBAAiB,EAAI,IAAK,EAAQ,QAAS,EAAQ,QAAQ,CAEjF,OADA,EAAA,WAAW,EAAQ,MAAO,EAAI,MAAM,CAC7B,CAAE,MAAO,EAAI,MAAO,UAAS,EACpC,CACH,EAC+B,OAAQ,GAAW,CAAC,EAAO,QAAQ,CAAC,IAAK,GAAW,EAAO,MAE5D,CAAE,CAkBnC,SAAgB,EAAU,EAAqB,EAA4B,EAAE,CAAmB,CAC9F,IAAM,EAAU,EAAQ,SAAWC,EAAAA,mBAC7B,EAAmB,IAAI,IAAIC,EAAAA,kBAAkB,EAAM,CAAC,IAAK,GAAQ,EAAI,MAAM,CAAC,CAC5E,EAAK,EAAQ,OAAO,EAAI,KAAK,KAAK,CACxC,IAAK,IAAM,KAAOH,EAAAA,SAAS,EAAM,CAC3B,EAAQ,EAAI,IAAI,EAAI,EAAiB,IAAI,EAAI,MAAM,EACvD,EAAA,WAAW,EAAO,EAAkB,EAAK,EAAQ,aAAa,EAAI,MAAM,CAAE,cAAoB,EAAG,CAAC,CAGpG,IAAM,EAASI,EAAAA,cAAc,EAAO,EAAQ,CACtC,EAAWC,EAAAA,mBAAmB,EAAO,EAAQ,CACnD,IAAK,IAAM,KAAS,EAAU,EAAA,UAAU,EAAM,CAC9C,MAAO,CAAE,SAAQ,OAAQ,EAAS,OAAQ,UAAWF,EAAAA,kBAAkB,EAAM,CAAE"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{init_suspendedRuns as e,listSuspendedRuns as t,suspendRun as n}from"./suspendedRuns.mjs";import{defaultPidLiveness as r,findReapableScopes as i,init_runRegistry as a,listRuns as o,pruneDeadRuns as s,reapScope as c,releaseRun as l}from"./runRegistry.mjs";import{stopProcessGroup as u}from"./processGroup.mjs";a(),e();function d(e,t,n,r){return{runId:e.runId,agent:e.agent,runtime:e.runtime,task:t?.task??``,cwd:t?.cwd??process.cwd(),...t?.model?{model:t.model}:{},...t?.inlineAgent?{inlineAgent:t.inlineAgent}:{},...t?.summary||t?.state?{lastStatus:t.state}:{},...t?.sessionFile?{sessionFile:t.sessionFile}:{},suspendedAt:r,reason:n}}async function f(e){let t=e.now?.()??Date.now(),r=o(e.scope),i=[];for(let a of r)n(e.scope,d(a,e.readStatus(a.runId),e.reason,t)),i.push(a.runId);return{suspended:i,unstoppable:(await Promise.all(r.map(async t=>{let n=await u(t.pid,e.graceMs,e.signals);return l(e.scope,t.runId),{runId:t.runId,stopped:n}}))).filter(e=>!e.stopped).map(e=>e.runId)}}function p(e,a={}){let l=a.isAlive??r,u=new Set(t(e).map(e=>e.runId)),f=a.now?.()??Date.now();for(let t of o(e))l(t.pid)||u.has(t.runId)||n(e,d(t,a.readStatus?.(t.runId),`parent_lost`,f));let p=s(e,l),m=i(e,l);for(let e of m)c(e);return{pruned:p,reaped:m.length,suspended:t(e)}}export{p as openScope,f as suspendScopeRuns};
|
|
2
|
-
//# sourceMappingURL=sessionLifecycle.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessionLifecycle.mjs","names":[],"sources":["../../../src/runs/registry/sessionLifecycle.ts"],"sourcesContent":["/**\n * What happens to this scope's runs when a session starts or ends.\n *\n * Two operations, deliberately kept out of the extension's activation function\n * so both are testable without a Pi host:\n *\n * - `suspendScopeRuns` on every shutdown: stop each recorded run's process\n * group and write down enough to start it again.\n * - `openScope` on every start: prune records whose process is gone, reap\n * sibling scopes that hold nothing alive, and report what is suspended\n * WITHOUT restoring any of it.\n *\n * WHY SHUTDOWN DOES NOT BRANCH ON `reason`:\n * See `suspendedRuns.ts`. Uniform beats one remembered exception, and restore\n * is cheap because a Pi child continues its own transcript.\n *\n * AVOID:\n * - Restoring from here. `openScope` reports; something explicit restores\n */\n\nimport type { AsyncRunStatus } from '../background/asyncExecution.ts';\nimport { type SessionScope } from '../../shared/paths.ts';\nimport { stopProcessGroup, type ProcessGroupSignals } from './processGroup.ts';\nimport {\n defaultPidLiveness,\n findReapableScopes,\n listRuns,\n pruneDeadRuns,\n reapScope,\n releaseRun,\n type PidLivenessProbe,\n type RunRecord,\n} from './runRegistry.ts';\nimport { listSuspendedRuns, suspendRun, type SuspendedRun } from './suspendedRuns.ts';\n\nconst PARENT_LOST_REASON = 'parent_lost';\n\n/** Reads a run's own status, so a suspended record can carry its last known state. */\nexport type RunStatusReader = (runId: string) => AsyncRunStatus | undefined;\n\nexport interface SuspendScopeOptions {\n scope: SessionScope;\n /** Pi's `session_shutdown` reason, recorded so a later report can say why. */\n reason: string;\n readStatus: RunStatusReader;\n now?: () => number;\n signals?: ProcessGroupSignals;\n graceMs?: number;\n}\n\nexport interface SuspendScopeResult {\n suspended: string[];\n /** Runs whose group survived even SIGKILL. Should be empty; reported rather than assumed. */\n unstoppable: string[];\n}\n\nfunction toSuspendedRecord(run: RunRecord, status: AsyncRunStatus | undefined, reason: string, at: number) {\n return {\n runId: run.runId,\n agent: run.agent,\n runtime: run.runtime,\n task: status?.task ?? '',\n cwd: status?.cwd ?? process.cwd(),\n ...(status?.model ? { model: status.model } : {}),\n ...(status?.inlineAgent ? { inlineAgent: status.inlineAgent } : {}),\n ...(status?.summary ? { lastStatus: status.state } : status?.state ? { lastStatus: status.state } : {}),\n ...(status?.sessionFile ? { sessionFile: status.sessionFile } : {}),\n suspendedAt: at,\n reason,\n };\n}\n\n/**\n * Stop every run this scope owns and record how to bring it back.\n *\n * The record is written BEFORE the signal: a process that dies between the two\n * leaves a restorable record, whereas the other order can lose the run\n * entirely if the parent is killed mid-sweep.\n */\nexport async function suspendScopeRuns(options: SuspendScopeOptions): Promise<SuspendScopeResult> {\n const at = options.now?.() ?? Date.now();\n const runs = listRuns(options.scope);\n const suspended: string[] = [];\n\n // Pi does not await session_shutdown handlers. Persist every run before the\n // first async wait so process exit cannot truncate this sweep after one run.\n for (const run of runs) {\n suspendRun(options.scope, toSuspendedRecord(run, options.readStatus(run.runId), options.reason, at));\n suspended.push(run.runId);\n }\n\n // Starting all async stop operations in one map dispatches every initial\n // SIGTERM synchronously before any grace-period promise is awaited.\n const stopResults = await Promise.all(\n runs.map(async (run) => {\n const stopped = await stopProcessGroup(run.pid, options.graceMs, options.signals);\n releaseRun(options.scope, run.runId);\n return { runId: run.runId, stopped };\n }),\n );\n const unstoppable = stopResults.filter((result) => !result.stopped).map((result) => result.runId);\n\n return { suspended, unstoppable };\n}\n\nexport interface OpenScopeResult {\n /** Run ids whose process was already gone; their records were dropped. */\n pruned: string[];\n /** Sibling scopes removed because nothing in them was alive. */\n reaped: number;\n /** Suspended runs awaiting an explicit restore. Reported, never started here. */\n suspended: SuspendedRun[];\n}\n\nexport interface OpenScopeOptions {\n isAlive?: PidLivenessProbe;\n readStatus?: RunStatusReader;\n now?: () => number;\n}\n\nexport function openScope(scope: SessionScope, options: OpenScopeOptions = {}): OpenScopeResult {\n const isAlive = options.isAlive ?? defaultPidLiveness;\n const alreadySuspended = new Set(listSuspendedRuns(scope).map((run) => run.runId));\n const at = options.now?.() ?? Date.now();\n for (const run of listRuns(scope)) {\n if (isAlive(run.pid) || alreadySuspended.has(run.runId)) continue;\n suspendRun(scope, toSuspendedRecord(run, options.readStatus?.(run.runId), PARENT_LOST_REASON, at));\n }\n\n const pruned = pruneDeadRuns(scope, isAlive);\n const reapable = findReapableScopes(scope, isAlive);\n for (const stale of reapable) reapScope(stale);\n return { pruned, reaped: reapable.length, suspended: listSuspendedRuns(scope) };\n}\n"],"mappings":"8TAgC0B,IAC4D,CAuBtF,SAAS,EAAkB,EAAgB,EAAoC,EAAgB,EAAY,CACzG,MAAO,CACL,MAAO,EAAI,MACX,MAAO,EAAI,MACX,QAAS,EAAI,QACb,KAAM,GAAQ,MAAQ,GACtB,IAAK,GAAQ,KAAO,QAAQ,KAAK,CACjC,GAAI,GAAQ,MAAQ,CAAE,MAAO,EAAO,MAAO,CAAG,EAAE,CAChD,GAAI,GAAQ,YAAc,CAAE,YAAa,EAAO,YAAa,CAAG,EAAE,CAClE,GAAI,GAAQ,SAAyC,GAAQ,MAAvC,CAAE,WAAY,EAAO,MAAO,CAAkD,EAAE,CACtG,GAAI,GAAQ,YAAc,CAAE,YAAa,EAAO,YAAa,CAAG,EAAE,CAClE,YAAa,EACb,SACD,CAUH,eAAsB,EAAiB,EAA2D,CAChG,IAAM,EAAK,EAAQ,OAAO,EAAI,KAAK,KAAK,CAClC,EAAO,EAAS,EAAQ,MAAM,CAC9B,EAAsB,EAAE,CAI9B,IAAK,IAAM,KAAO,EAChB,EAAW,EAAQ,MAAO,EAAkB,EAAK,EAAQ,WAAW,EAAI,MAAM,CAAE,EAAQ,OAAQ,EAAG,CAAC,CACpG,EAAU,KAAK,EAAI,MAAM,CAc3B,MAAO,CAAE,YAAW,aAFA,MAPM,QAAQ,IAChC,EAAK,IAAI,KAAO,IAAQ,CACtB,IAAM,EAAU,MAAM,EAAiB,EAAI,IAAK,EAAQ,QAAS,EAAQ,QAAQ,CAEjF,OADA,EAAW,EAAQ,MAAO,EAAI,MAAM,CAC7B,CAAE,MAAO,EAAI,MAAO,UAAS,EACpC,CACH,EAC+B,OAAQ,GAAW,CAAC,EAAO,QAAQ,CAAC,IAAK,GAAW,EAAO,MAE5D,CAAE,CAkBnC,SAAgB,EAAU,EAAqB,EAA4B,EAAE,CAAmB,CAC9F,IAAM,EAAU,EAAQ,SAAW,EAC7B,EAAmB,IAAI,IAAI,EAAkB,EAAM,CAAC,IAAK,GAAQ,EAAI,MAAM,CAAC,CAC5E,EAAK,EAAQ,OAAO,EAAI,KAAK,KAAK,CACxC,IAAK,IAAM,KAAO,EAAS,EAAM,CAC3B,EAAQ,EAAI,IAAI,EAAI,EAAiB,IAAI,EAAI,MAAM,EACvD,EAAW,EAAO,EAAkB,EAAK,EAAQ,aAAa,EAAI,MAAM,CAAE,cAAoB,EAAG,CAAC,CAGpG,IAAM,EAAS,EAAc,EAAO,EAAQ,CACtC,EAAW,EAAmB,EAAO,EAAQ,CACnD,IAAK,IAAM,KAAS,EAAU,EAAU,EAAM,CAC9C,MAAO,CAAE,SAAQ,OAAQ,EAAS,OAAQ,UAAW,EAAkB,EAAM,CAAE"}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/atomicJson.cjs`),n=require(`../../shared/paths.cjs`),r=require(`../../shared/versioned.cjs`);let i=require(`node:fs`);i=e.__toESM(i,1);let a=require(`node:path`);a=e.__toESM(a,1);const o=`.json`;function s(e,t){return a.join(n.scopeSuspendedDir(e),`${t}${o}`)}function c(e,n){t.writeAtomicJson(s(e,n.runId),{version:1,...n})}function l(e){let t;try{t=i.readdirSync(n.scopeSuspendedDir(e))}catch{return[]}let s=[];for(let c of t){if(!c.endsWith(o))continue;let t;try{t=JSON.parse(i.readFileSync(a.join(n.scopeSuspendedDir(e),c),`utf-8`))}catch{continue}let l=r.parseVersioned(t,[1]);l.ok&&typeof l.value.runId==`string`&&s.push(l.value)}return s.sort((e,t)=>e.suspendedAt-t.suspendedAt)}function u(e,t){try{i.rmSync(s(e,t),{force:!0})}catch{}}function d(e){if(e.runtime!==`pi`||!e.agent.trim()||!e.task.trim()||!e.cwd.trim()||!e.sessionFile)return!1;try{return i.accessSync(e.sessionFile,i.constants.R_OK),!0}catch{return!1}}function f(e){if(e.length===0)return``;let t=e.map(e=>{let t=d(e)?`resumable`:`not resumable`;return`- ${e.runId} (${e.agent}, ${e.runtime}, ${t}): ${e.task.split(`
|
|
2
|
-
`)[0]}`}),n=e.length===1?`subagent`:`subagents`;return[`${e.length} suspended ${n} from this session:`,...t,``,`They are stopped. Restore one with subagent {"action":"restore","id":"<run-id>"}, or leave them.`].join(`
|
|
3
|
-
`)}exports.clearSuspendedRun=u,exports.formatSuspendedRuns=f,exports.isSuspendedRunResumable=d,exports.listSuspendedRuns=l,exports.suspendRun=c;
|
|
4
|
-
//# sourceMappingURL=suspendedRuns.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"suspendedRuns.cjs","names":["path","scopeSuspendedDir","fs","parseVersioned"],"sources":["../../../src/runs/registry/suspendedRuns.ts"],"sourcesContent":["/**\n * Runs stopped by a session shutdown, and enough about each to start it again.\n *\n * WHY EVERY SHUTDOWN SUSPENDS, WITH NO SPECIAL CASE FOR `reload`:\n * Pi emits `session_shutdown` for `quit`, `reload`, `new`, `resume` and `fork`.\n * An earlier design kept children alive across `reload` alone, on the grounds\n * that `/domains`, `/major-mode` and `/profile` reload the extension rather than\n * change the session. That is true, and it still makes `reload` the one case\n * behaving differently from the other four - which is a rule someone has to\n * remember, and a branch that silently does the wrong thing if Pi ever adds a\n * sixth reason. Suspending uniformly costs a respawn on reload and removes the\n * special case; restore is cheap because the child continues its own transcript\n * rather than starting over.\n *\n * WHY RESTORE IS OFFERED AND NEVER AUTOMATIC:\n * Reopening a session would otherwise spawn N processes and start spending\n * tokens on tasks that may be days stale. `session_start` reports what is\n * suspended; something explicit has to ask for it back.\n *\n * AVOID:\n * - Auto-restoring from any lifecycle event\n * - Recording a suspended run without its `sessionFile`; without it a restore\n * restarts the work instead of continuing it\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { InlineAgent } from '@agimon-ai/doompi-extension-contracts/subagent-tool';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { scopeSuspendedDir, type SessionScope } from '../../shared/paths.ts';\nimport { parseVersioned } from '../../shared/versioned.ts';\n\nconst SUSPENDED_RUN_VERSION = 1;\nconst JSON_SUFFIX = '.json';\n\nexport interface SuspendedRun {\n version: typeof SUSPENDED_RUN_VERSION;\n runId: string;\n agent: string;\n runtime: string;\n task: string;\n cwd: string;\n model?: string;\n /** One-shot agent definition required to reconstruct an inline agent. */\n inlineAgent?: InlineAgent;\n context?: 'fresh' | 'fork';\n /** Last state the run reported before it was stopped. */\n lastStatus?: string;\n /**\n * The child's own transcript.\n *\n * This is what makes a restore a continuation: `sdkRunnerEntry.ts` opens it\n * through `SessionManager.open` when `sdk.sessionFile` is set. Absent for a\n * run that never persisted one (an external CLI, or a headless child), in\n * which case the record remains visible but cannot be restored.\n */\n sessionFile?: string;\n suspendedAt: number;\n /** Why the session went away, for the report a later `session_start` shows. */\n reason: string;\n}\n\nfunction suspendedPath(scope: SessionScope, runId: string): string {\n return path.join(scopeSuspendedDir(scope), `${runId}${JSON_SUFFIX}`);\n}\n\nexport function suspendRun(scope: SessionScope, record: Omit<SuspendedRun, 'version'>): void {\n writeAtomicJson(suspendedPath(scope, record.runId), { version: SUSPENDED_RUN_VERSION, ...record });\n}\n\nexport function listSuspendedRuns(scope: SessionScope): SuspendedRun[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(scopeSuspendedDir(scope));\n } catch {\n return [];\n }\n\n const runs: SuspendedRun[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(JSON_SUFFIX)) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(path.join(scopeSuspendedDir(scope), entry), 'utf-8'));\n } catch {\n // A record this build cannot read is skipped, not deleted: it may belong\n // to a newer build that will read it back correctly.\n continue;\n }\n const result = parseVersioned<SuspendedRun>(parsed, [SUSPENDED_RUN_VERSION]);\n if (result.ok && typeof result.value.runId === 'string') runs.push(result.value);\n }\n return runs.sort((left, right) => left.suspendedAt - right.suspendedAt);\n}\n\n/** Drop a record once its run has been restored, or explicitly discarded. */\nexport function clearSuspendedRun(scope: SessionScope, runId: string): void {\n try {\n fs.rmSync(suspendedPath(scope, runId), { force: true });\n } catch {\n // Best effort: a leftover record shows up again in the next report, which\n // is a visible annoyance rather than a lost run.\n }\n}\n\n/** Whether an explicit restore can safely continue this exact run. */\nexport function isSuspendedRunResumable(run: SuspendedRun): boolean {\n if (run.runtime !== 'pi' || !run.agent.trim() || !run.task.trim() || !run.cwd.trim() || !run.sessionFile) {\n return false;\n }\n try {\n fs.accessSync(run.sessionFile, fs.constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/** One line per suspended run, for the report `session_start` prints. */\nexport function formatSuspendedRuns(runs: SuspendedRun[]): string {\n if (runs.length === 0) return '';\n const lines = runs.map((run) => {\n const continues = isSuspendedRunResumable(run) ? 'resumable' : 'not resumable';\n return `- ${run.runId} (${run.agent}, ${run.runtime}, ${continues}): ${run.task.split('\\n')[0]}`;\n });\n const noun = runs.length === 1 ? 'subagent' : 'subagents';\n return [\n `${runs.length} suspended ${noun} from this session:`,\n ...lines,\n '',\n 'They are stopped. Restore one with subagent {\"action\":\"restore\",\"id\":\"<run-id>\"}, or leave them.',\n ].join('\\n');\n}\n"],"mappings":"mQAiCA,MACM,EAAc,QA6BpB,SAAS,EAAc,EAAqB,EAAuB,CACjE,OAAOA,EAAK,KAAKC,EAAAA,kBAAkB,EAAM,CAAE,GAAG,IAAQ,IAAc,CAGtE,SAAgB,EAAW,EAAqB,EAA6C,CAC3F,EAAA,gBAAgB,EAAc,EAAO,EAAO,MAAM,CAAE,CAAE,QAAS,EAAuB,GAAG,EAAQ,CAAC,CAGpG,SAAgB,EAAkB,EAAqC,CACrE,IAAI,EACJ,GAAI,CACF,EAAUC,EAAG,YAAYD,EAAAA,kBAAkB,EAAM,CAAC,MAC5C,CACN,MAAO,EAAE,CAGX,IAAM,EAAuB,EAAE,CAC/B,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAM,SAAS,EAAY,CAAE,SAClC,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAMC,EAAG,aAAaF,EAAK,KAAKC,EAAAA,kBAAkB,EAAM,CAAE,EAAM,CAAE,QAAQ,CAAC,MACnF,CAGN,SAEF,IAAM,EAASE,EAAAA,eAA6B,EAAQ,CAAC,EAAsB,CAAC,CACxE,EAAO,IAAM,OAAO,EAAO,MAAM,OAAU,UAAU,EAAK,KAAK,EAAO,MAAM,CAElF,OAAO,EAAK,MAAM,EAAM,IAAU,EAAK,YAAc,EAAM,YAAY,CAIzE,SAAgB,EAAkB,EAAqB,EAAqB,CAC1E,GAAI,CACF,EAAG,OAAO,EAAc,EAAO,EAAM,CAAE,CAAE,MAAO,GAAM,CAAC,MACjD,GAOV,SAAgB,EAAwB,EAA4B,CAClE,GAAI,EAAI,UAAY,MAAQ,CAAC,EAAI,MAAM,MAAM,EAAI,CAAC,EAAI,KAAK,MAAM,EAAI,CAAC,EAAI,IAAI,MAAM,EAAI,CAAC,EAAI,YAC3F,MAAO,GAET,GAAI,CAEF,OADA,EAAG,WAAW,EAAI,YAAaD,EAAG,UAAU,KAAK,CAC1C,QACD,CACN,MAAO,IAKX,SAAgB,EAAoB,EAA8B,CAChE,GAAI,EAAK,SAAW,EAAG,MAAO,GAC9B,IAAM,EAAQ,EAAK,IAAK,GAAQ,CAC9B,IAAM,EAAY,EAAwB,EAAI,CAAG,YAAc,gBAC/D,MAAO,KAAK,EAAI,MAAM,IAAI,EAAI,MAAM,IAAI,EAAI,QAAQ,IAAI,EAAU,KAAK,EAAI,KAAK,MAAM;EAAK,CAAC,MAC5F,CACI,EAAO,EAAK,SAAW,EAAI,WAAa,YAC9C,MAAO,CACL,GAAG,EAAK,OAAO,aAAa,EAAK,qBACjC,GAAG,EACH,GACA,mGACD,CAAC,KAAK;EAAK"}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{init_atomicJson as t,writeAtomicJson as n}from"../../shared/atomicJson.mjs";import{init_paths as r,scopeSuspendedDir as i}from"../../shared/paths.mjs";import{init_versioned as a,parseVersioned as o}from"../../shared/versioned.mjs";import*as s from"node:fs";import*as c from"node:path";function l(e,t){return c.join(i(e),`${t}${g}`)}function u(e,t){n(l(e,t.runId),{version:h,...t})}function d(e){let t;try{t=s.readdirSync(i(e))}catch{return[]}let n=[];for(let r of t){if(!r.endsWith(g))continue;let t;try{t=JSON.parse(s.readFileSync(c.join(i(e),r),`utf-8`))}catch{continue}let a=o(t,[h]);a.ok&&typeof a.value.runId==`string`&&n.push(a.value)}return n.sort((e,t)=>e.suspendedAt-t.suspendedAt)}function f(e,t){try{s.rmSync(l(e,t),{force:!0})}catch{}}function p(e){if(e.runtime!==`pi`||!e.agent.trim()||!e.task.trim()||!e.cwd.trim()||!e.sessionFile)return!1;try{return s.accessSync(e.sessionFile,s.constants.R_OK),!0}catch{return!1}}function m(e){if(e.length===0)return``;let t=e.map(e=>{let t=p(e)?`resumable`:`not resumable`;return`- ${e.runId} (${e.agent}, ${e.runtime}, ${t}): ${e.task.split(`
|
|
2
|
-
`)[0]}`}),n=e.length===1?`subagent`:`subagents`;return[`${e.length} suspended ${n} from this session:`,...t,``,`They are stopped. Restore one with subagent {"action":"restore","id":"<run-id>"}, or leave them.`].join(`
|
|
3
|
-
`)}var h,g,_=e((()=>{t(),r(),a(),h=1,g=`.json`}));_();export{f as clearSuspendedRun,m as formatSuspendedRuns,_ as init_suspendedRuns,p as isSuspendedRunResumable,d as listSuspendedRuns,u as suspendRun};
|
|
4
|
-
//# sourceMappingURL=suspendedRuns.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"suspendedRuns.mjs","names":[],"sources":["../../../src/runs/registry/suspendedRuns.ts"],"sourcesContent":["/**\n * Runs stopped by a session shutdown, and enough about each to start it again.\n *\n * WHY EVERY SHUTDOWN SUSPENDS, WITH NO SPECIAL CASE FOR `reload`:\n * Pi emits `session_shutdown` for `quit`, `reload`, `new`, `resume` and `fork`.\n * An earlier design kept children alive across `reload` alone, on the grounds\n * that `/domains`, `/major-mode` and `/profile` reload the extension rather than\n * change the session. That is true, and it still makes `reload` the one case\n * behaving differently from the other four - which is a rule someone has to\n * remember, and a branch that silently does the wrong thing if Pi ever adds a\n * sixth reason. Suspending uniformly costs a respawn on reload and removes the\n * special case; restore is cheap because the child continues its own transcript\n * rather than starting over.\n *\n * WHY RESTORE IS OFFERED AND NEVER AUTOMATIC:\n * Reopening a session would otherwise spawn N processes and start spending\n * tokens on tasks that may be days stale. `session_start` reports what is\n * suspended; something explicit has to ask for it back.\n *\n * AVOID:\n * - Auto-restoring from any lifecycle event\n * - Recording a suspended run without its `sessionFile`; without it a restore\n * restarts the work instead of continuing it\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { InlineAgent } from '@agimon-ai/doompi-extension-contracts/subagent-tool';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { scopeSuspendedDir, type SessionScope } from '../../shared/paths.ts';\nimport { parseVersioned } from '../../shared/versioned.ts';\n\nconst SUSPENDED_RUN_VERSION = 1;\nconst JSON_SUFFIX = '.json';\n\nexport interface SuspendedRun {\n version: typeof SUSPENDED_RUN_VERSION;\n runId: string;\n agent: string;\n runtime: string;\n task: string;\n cwd: string;\n model?: string;\n /** One-shot agent definition required to reconstruct an inline agent. */\n inlineAgent?: InlineAgent;\n context?: 'fresh' | 'fork';\n /** Last state the run reported before it was stopped. */\n lastStatus?: string;\n /**\n * The child's own transcript.\n *\n * This is what makes a restore a continuation: `sdkRunnerEntry.ts` opens it\n * through `SessionManager.open` when `sdk.sessionFile` is set. Absent for a\n * run that never persisted one (an external CLI, or a headless child), in\n * which case the record remains visible but cannot be restored.\n */\n sessionFile?: string;\n suspendedAt: number;\n /** Why the session went away, for the report a later `session_start` shows. */\n reason: string;\n}\n\nfunction suspendedPath(scope: SessionScope, runId: string): string {\n return path.join(scopeSuspendedDir(scope), `${runId}${JSON_SUFFIX}`);\n}\n\nexport function suspendRun(scope: SessionScope, record: Omit<SuspendedRun, 'version'>): void {\n writeAtomicJson(suspendedPath(scope, record.runId), { version: SUSPENDED_RUN_VERSION, ...record });\n}\n\nexport function listSuspendedRuns(scope: SessionScope): SuspendedRun[] {\n let entries: string[];\n try {\n entries = fs.readdirSync(scopeSuspendedDir(scope));\n } catch {\n return [];\n }\n\n const runs: SuspendedRun[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(JSON_SUFFIX)) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(path.join(scopeSuspendedDir(scope), entry), 'utf-8'));\n } catch {\n // A record this build cannot read is skipped, not deleted: it may belong\n // to a newer build that will read it back correctly.\n continue;\n }\n const result = parseVersioned<SuspendedRun>(parsed, [SUSPENDED_RUN_VERSION]);\n if (result.ok && typeof result.value.runId === 'string') runs.push(result.value);\n }\n return runs.sort((left, right) => left.suspendedAt - right.suspendedAt);\n}\n\n/** Drop a record once its run has been restored, or explicitly discarded. */\nexport function clearSuspendedRun(scope: SessionScope, runId: string): void {\n try {\n fs.rmSync(suspendedPath(scope, runId), { force: true });\n } catch {\n // Best effort: a leftover record shows up again in the next report, which\n // is a visible annoyance rather than a lost run.\n }\n}\n\n/** Whether an explicit restore can safely continue this exact run. */\nexport function isSuspendedRunResumable(run: SuspendedRun): boolean {\n if (run.runtime !== 'pi' || !run.agent.trim() || !run.task.trim() || !run.cwd.trim() || !run.sessionFile) {\n return false;\n }\n try {\n fs.accessSync(run.sessionFile, fs.constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/** One line per suspended run, for the report `session_start` prints. */\nexport function formatSuspendedRuns(runs: SuspendedRun[]): string {\n if (runs.length === 0) return '';\n const lines = runs.map((run) => {\n const continues = isSuspendedRunResumable(run) ? 'resumable' : 'not resumable';\n return `- ${run.runId} (${run.agent}, ${run.runtime}, ${continues}): ${run.task.split('\\n')[0]}`;\n });\n const noun = runs.length === 1 ? 'subagent' : 'subagents';\n return [\n `${runs.length} suspended ${noun} from this session:`,\n ...lines,\n '',\n 'They are stopped. Restore one with subagent {\"action\":\"restore\",\"id\":\"<run-id>\"}, or leave them.',\n ].join('\\n');\n}\n"],"mappings":"oWA+DA,SAAS,EAAc,EAAqB,EAAuB,CACjE,OAAO,EAAK,KAAK,EAAkB,EAAM,CAAE,GAAG,IAAQ,IAAc,CAGtE,SAAgB,EAAW,EAAqB,EAA6C,CAC3F,EAAgB,EAAc,EAAO,EAAO,MAAM,CAAE,CAAE,QAAS,EAAuB,GAAG,EAAQ,CAAC,CAGpG,SAAgB,EAAkB,EAAqC,CACrE,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,EAAkB,EAAM,CAAC,MAC5C,CACN,MAAO,EAAE,CAGX,IAAM,EAAuB,EAAE,CAC/B,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,EAAM,SAAS,EAAY,CAAE,SAClC,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAG,aAAa,EAAK,KAAK,EAAkB,EAAM,CAAE,EAAM,CAAE,QAAQ,CAAC,MACnF,CAGN,SAEF,IAAM,EAAS,EAA6B,EAAQ,CAAC,EAAsB,CAAC,CACxE,EAAO,IAAM,OAAO,EAAO,MAAM,OAAU,UAAU,EAAK,KAAK,EAAO,MAAM,CAElF,OAAO,EAAK,MAAM,EAAM,IAAU,EAAK,YAAc,EAAM,YAAY,CAIzE,SAAgB,EAAkB,EAAqB,EAAqB,CAC1E,GAAI,CACF,EAAG,OAAO,EAAc,EAAO,EAAM,CAAE,CAAE,MAAO,GAAM,CAAC,MACjD,GAOV,SAAgB,EAAwB,EAA4B,CAClE,GAAI,EAAI,UAAY,MAAQ,CAAC,EAAI,MAAM,MAAM,EAAI,CAAC,EAAI,KAAK,MAAM,EAAI,CAAC,EAAI,IAAI,MAAM,EAAI,CAAC,EAAI,YAC3F,MAAO,GAET,GAAI,CAEF,OADA,EAAG,WAAW,EAAI,YAAa,EAAG,UAAU,KAAK,CAC1C,QACD,CACN,MAAO,IAKX,SAAgB,EAAoB,EAA8B,CAChE,GAAI,EAAK,SAAW,EAAG,MAAO,GAC9B,IAAM,EAAQ,EAAK,IAAK,GAAQ,CAC9B,IAAM,EAAY,EAAwB,EAAI,CAAG,YAAc,gBAC/D,MAAO,KAAK,EAAI,MAAM,IAAI,EAAI,MAAM,IAAI,EAAI,QAAQ,IAAI,EAAU,KAAK,EAAI,KAAK,MAAM;EAAK,CAAC,MAC5F,CACI,EAAO,EAAK,SAAW,EAAI,WAAa,YAC9C,MAAO,CACL,GAAG,EAAK,OAAO,aAAa,EAAK,qBACjC,GAAG,EACH,GACA,mGACD,CAAC,KAAK;EAAK,sBAvG+C,IACgB,IAClB,CAErD,EAAwB,EACxB,EAAc"}
|