@agimon-ai/doompi-team 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +46 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs +1 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/_virtual/_rolldown/runtime.mjs +1 -0
- package/dist/agents/discovery.cjs +2 -0
- package/dist/agents/discovery.cjs.map +1 -0
- package/dist/agents/discovery.mjs +2 -0
- package/dist/agents/discovery.mjs.map +1 -0
- package/dist/agents/frontmatter.cjs +10 -0
- package/dist/agents/frontmatter.cjs.map +1 -0
- package/dist/agents/frontmatter.mjs +10 -0
- package/dist/agents/frontmatter.mjs.map +1 -0
- package/dist/agents/loader.cjs +2 -0
- package/dist/agents/loader.cjs.map +1 -0
- package/dist/agents/loader.mjs +2 -0
- package/dist/agents/loader.mjs.map +1 -0
- package/dist/agents/memory.cjs +3 -0
- package/dist/agents/memory.cjs.map +1 -0
- package/dist/agents/memory.mjs +3 -0
- package/dist/agents/memory.mjs.map +1 -0
- package/dist/agents/projectRoot.cjs +2 -0
- package/dist/agents/projectRoot.cjs.map +1 -0
- package/dist/agents/projectRoot.mjs +2 -0
- package/dist/agents/projectRoot.mjs.map +1 -0
- package/dist/agents/settings.cjs +2 -0
- package/dist/agents/settings.cjs.map +1 -0
- package/dist/agents/settings.mjs +2 -0
- package/dist/agents/settings.mjs.map +1 -0
- package/dist/agents/skills.cjs +0 -0
- package/dist/agents/skills.cjs.map +1 -0
- package/dist/agents/skills.mjs +5 -0
- package/dist/agents/skills.mjs.map +1 -0
- package/dist/agents/types.d.cts +121 -0
- package/dist/agents/types.d.cts.map +1 -0
- package/dist/agents/types.d.mts +121 -0
- package/dist/agents/types.d.mts.map +1 -0
- package/dist/api/backgroundWork.cjs +1 -0
- package/dist/api/backgroundWork.d.cts +2 -0
- package/dist/api/backgroundWork.d.mts +2 -0
- package/dist/api/backgroundWork.mjs +1 -0
- package/dist/api/capabilityCeiling.cjs +1 -0
- package/dist/api/capabilityCeiling.d.cts +3 -0
- package/dist/api/capabilityCeiling.d.mts +3 -0
- package/dist/api/capabilityCeiling.mjs +2 -0
- package/dist/api/capabilityCeiling.mjs.map +1 -0
- package/dist/api/delegation.cjs +1 -0
- package/dist/api/delegation.d.cts +2 -0
- package/dist/api/delegation.d.mts +2 -0
- package/dist/api/delegation.mjs +1 -0
- package/dist/api/teamSnapshot.cjs +1 -0
- package/dist/api/teamSnapshot.d.cts +2 -0
- package/dist/api/teamSnapshot.d.mts +2 -0
- package/dist/api/teamSnapshot.mjs +2 -0
- package/dist/api/teamSnapshot.mjs.map +1 -0
- package/dist/container/bindOrRebind.cjs +2 -0
- package/dist/container/bindOrRebind.cjs.map +1 -0
- package/dist/container/bindOrRebind.mjs +2 -0
- package/dist/container/bindOrRebind.mjs.map +1 -0
- package/dist/container/globalBridge.cjs +2 -0
- package/dist/container/globalBridge.cjs.map +1 -0
- package/dist/container/globalBridge.d.cts +19 -0
- package/dist/container/globalBridge.d.cts.map +1 -0
- package/dist/container/globalBridge.d.mts +19 -0
- package/dist/container/globalBridge.d.mts.map +1 -0
- package/dist/container/globalBridge.mjs +2 -0
- package/dist/container/globalBridge.mjs.map +1 -0
- package/dist/container/index.cjs +2 -0
- package/dist/container/index.cjs.map +1 -0
- package/dist/container/index.d.cts +38 -0
- package/dist/container/index.d.cts.map +1 -0
- package/dist/container/index.d.mts +38 -0
- package/dist/container/index.d.mts.map +1 -0
- package/dist/container/index.mjs +2 -0
- package/dist/container/index.mjs.map +1 -0
- package/dist/container/modules/agents.cjs +2 -0
- package/dist/container/modules/agents.cjs.map +1 -0
- package/dist/container/modules/agents.d.cts +1 -0
- package/dist/container/modules/agents.d.mts +1 -0
- package/dist/container/modules/agents.mjs +2 -0
- package/dist/container/modules/agents.mjs.map +1 -0
- package/dist/container/modules/core.cjs +2 -0
- package/dist/container/modules/core.cjs.map +1 -0
- package/dist/container/modules/core.d.cts +1 -0
- package/dist/container/modules/core.d.mts +1 -0
- package/dist/container/modules/core.mjs +2 -0
- package/dist/container/modules/core.mjs.map +1 -0
- package/dist/container/modules/extensions.cjs +2 -0
- package/dist/container/modules/extensions.cjs.map +1 -0
- package/dist/container/modules/extensions.mjs +2 -0
- package/dist/container/modules/extensions.mjs.map +1 -0
- package/dist/container/modules/intercom.cjs +2 -0
- package/dist/container/modules/intercom.cjs.map +1 -0
- package/dist/container/modules/intercom.mjs +2 -0
- package/dist/container/modules/intercom.mjs.map +1 -0
- package/dist/container/modules/runs.cjs +2 -0
- package/dist/container/modules/runs.cjs.map +1 -0
- package/dist/container/modules/runs.mjs +2 -0
- package/dist/container/modules/runs.mjs.map +1 -0
- package/dist/container/types.cjs +2 -0
- package/dist/container/types.cjs.map +1 -0
- package/dist/container/types.d.cts +45 -0
- package/dist/container/types.d.cts.map +1 -0
- package/dist/container/types.d.mts +45 -0
- package/dist/container/types.d.mts.map +1 -0
- package/dist/container/types.mjs +2 -0
- package/dist/container/types.mjs.map +1 -0
- package/dist/env.cjs +2 -0
- package/dist/env.cjs.map +1 -0
- package/dist/env.d.cts +65 -0
- package/dist/env.d.cts.map +1 -0
- package/dist/env.d.mts +65 -0
- package/dist/env.d.mts.map +1 -0
- package/dist/env.mjs +2 -0
- package/dist/env.mjs.map +1 -0
- package/dist/extension.cjs +2 -0
- package/dist/extension.cjs.map +1 -0
- package/dist/extension.mjs +2 -0
- package/dist/extension.mjs.map +1 -0
- package/dist/extensions/config.cjs +2 -0
- package/dist/extensions/config.cjs.map +1 -0
- package/dist/extensions/config.mjs +2 -0
- package/dist/extensions/config.mjs.map +1 -0
- package/dist/extensions/delegationBridge.cjs +2 -0
- package/dist/extensions/delegationBridge.cjs.map +1 -0
- package/dist/extensions/delegationBridge.mjs +2 -0
- package/dist/extensions/delegationBridge.mjs.map +1 -0
- package/dist/extensions/doom.cjs +2 -0
- package/dist/extensions/doom.cjs.map +1 -0
- package/dist/extensions/doom.d.cts +8 -0
- package/dist/extensions/doom.d.cts.map +1 -0
- package/dist/extensions/doom.d.mts +8 -0
- package/dist/extensions/doom.d.mts.map +1 -0
- package/dist/extensions/doom.mjs +2 -0
- package/dist/extensions/doom.mjs.map +1 -0
- package/dist/extensions/fablePlanBridge.cjs +2 -0
- package/dist/extensions/fablePlanBridge.cjs.map +1 -0
- package/dist/extensions/fablePlanBridge.mjs +2 -0
- package/dist/extensions/fablePlanBridge.mjs.map +1 -0
- package/dist/extensions/managementActions.cjs +2 -0
- package/dist/extensions/managementActions.cjs.map +1 -0
- package/dist/extensions/managementActions.mjs +2 -0
- package/dist/extensions/managementActions.mjs.map +1 -0
- package/dist/extensions/operationJournal.cjs +2 -0
- package/dist/extensions/operationJournal.cjs.map +1 -0
- package/dist/extensions/operationJournal.mjs +2 -0
- package/dist/extensions/operationJournal.mjs.map +1 -0
- package/dist/extensions/orchestratorPrompt.cjs +35 -0
- package/dist/extensions/orchestratorPrompt.cjs.map +1 -0
- package/dist/extensions/orchestratorPrompt.mjs +35 -0
- package/dist/extensions/orchestratorPrompt.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +2 -0
- package/dist/extensions/pi.d.mts +2 -0
- package/dist/extensions/pi.mjs +2 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/extensions/schemas.cjs +1 -0
- package/dist/extensions/schemas.mjs +2 -0
- package/dist/extensions/schemas.mjs.map +1 -0
- package/dist/extensions/spawnPlan.cjs +3 -0
- package/dist/extensions/spawnPlan.cjs.map +1 -0
- package/dist/extensions/spawnPlan.mjs +3 -0
- package/dist/extensions/spawnPlan.mjs.map +1 -0
- package/dist/extensions/standard.cjs +2 -0
- package/dist/extensions/standard.cjs.map +1 -0
- package/dist/extensions/standard.d.cts +8 -0
- package/dist/extensions/standard.d.cts.map +1 -0
- package/dist/extensions/standard.d.mts +8 -0
- package/dist/extensions/standard.d.mts.map +1 -0
- package/dist/extensions/standard.mjs +2 -0
- package/dist/extensions/standard.mjs.map +1 -0
- package/dist/extensions/statusViews.cjs +7 -0
- package/dist/extensions/statusViews.cjs.map +1 -0
- package/dist/extensions/statusViews.mjs +7 -0
- package/dist/extensions/statusViews.mjs.map +1 -0
- package/dist/extensions/subagentPromptRuntime.cjs +18 -0
- package/dist/extensions/subagentPromptRuntime.cjs.map +1 -0
- package/dist/extensions/subagentPromptRuntime.d.cts +58 -0
- package/dist/extensions/subagentPromptRuntime.d.cts.map +1 -0
- package/dist/extensions/subagentPromptRuntime.d.mts +58 -0
- package/dist/extensions/subagentPromptRuntime.d.mts.map +1 -0
- package/dist/extensions/subagentPromptRuntime.mjs +18 -0
- package/dist/extensions/subagentPromptRuntime.mjs.map +1 -0
- package/dist/extensions/subagentPromptRuntimeEntry.cjs +2 -0
- package/dist/extensions/subagentPromptRuntimeEntry.cjs.map +1 -0
- package/dist/extensions/subagentPromptRuntimeEntry.d.cts +2 -0
- package/dist/extensions/subagentPromptRuntimeEntry.d.mts +2 -0
- package/dist/extensions/subagentPromptRuntimeEntry.mjs +2 -0
- package/dist/extensions/subagentPromptRuntimeEntry.mjs.map +1 -0
- package/dist/extensions/subagentTool.cjs +7 -0
- package/dist/extensions/subagentTool.cjs.map +1 -0
- package/dist/extensions/subagentTool.mjs +7 -0
- package/dist/extensions/subagentTool.mjs.map +1 -0
- package/dist/extensions/toolDescription.cjs +2 -0
- package/dist/extensions/toolDescription.cjs.map +1 -0
- package/dist/extensions/toolDescription.mjs +2 -0
- package/dist/extensions/toolDescription.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/intercom/intercomBridge.cjs +2 -0
- package/dist/intercom/intercomBridge.cjs.map +1 -0
- package/dist/intercom/intercomBridge.mjs +2 -0
- package/dist/intercom/intercomBridge.mjs.map +1 -0
- package/dist/intercom/nativeTeamChannel.cjs +5 -0
- package/dist/intercom/nativeTeamChannel.cjs.map +1 -0
- package/dist/intercom/nativeTeamChannel.d.cts +22 -0
- package/dist/intercom/nativeTeamChannel.d.cts.map +1 -0
- package/dist/intercom/nativeTeamChannel.d.mts +22 -0
- package/dist/intercom/nativeTeamChannel.d.mts.map +1 -0
- package/dist/intercom/nativeTeamChannel.mjs +5 -0
- package/dist/intercom/nativeTeamChannel.mjs.map +1 -0
- package/dist/intercom/supervisorControlChannel.cjs +2 -0
- package/dist/intercom/supervisorControlChannel.cjs.map +1 -0
- package/dist/intercom/supervisorControlChannel.mjs +2 -0
- package/dist/intercom/supervisorControlChannel.mjs.map +1 -0
- package/dist/protocol/backgroundWork.cjs +2 -0
- package/dist/protocol/backgroundWork.cjs.map +1 -0
- package/dist/protocol/backgroundWork.d.cts +52 -0
- package/dist/protocol/backgroundWork.d.cts.map +1 -0
- package/dist/protocol/backgroundWork.d.mts +52 -0
- package/dist/protocol/backgroundWork.d.mts.map +1 -0
- package/dist/protocol/backgroundWork.mjs +2 -0
- package/dist/protocol/backgroundWork.mjs.map +1 -0
- package/dist/protocol/capabilityCeiling.cjs +2 -0
- package/dist/protocol/capabilityCeiling.cjs.map +1 -0
- package/dist/protocol/capabilityCeiling.d.cts +51 -0
- package/dist/protocol/capabilityCeiling.d.cts.map +1 -0
- package/dist/protocol/capabilityCeiling.d.mts +51 -0
- package/dist/protocol/capabilityCeiling.d.mts.map +1 -0
- package/dist/protocol/capabilityCeiling.mjs +2 -0
- package/dist/protocol/capabilityCeiling.mjs.map +1 -0
- package/dist/protocol/delegation.cjs +2 -0
- package/dist/protocol/delegation.cjs.map +1 -0
- package/dist/protocol/delegation.d.cts +269 -0
- package/dist/protocol/delegation.d.cts.map +1 -0
- package/dist/protocol/delegation.d.mts +269 -0
- package/dist/protocol/delegation.d.mts.map +1 -0
- package/dist/protocol/delegation.mjs +2 -0
- package/dist/protocol/delegation.mjs.map +1 -0
- package/dist/runs/background/asyncExecution.cjs +2 -0
- package/dist/runs/background/asyncExecution.cjs.map +1 -0
- package/dist/runs/background/asyncExecution.mjs +2 -0
- package/dist/runs/background/asyncExecution.mjs.map +1 -0
- package/dist/runs/background/asyncJobTracker.cjs +2 -0
- package/dist/runs/background/asyncJobTracker.cjs.map +1 -0
- package/dist/runs/background/asyncJobTracker.mjs +2 -0
- package/dist/runs/background/asyncJobTracker.mjs.map +1 -0
- package/dist/runs/background/cliRunnerEntry.cjs +2 -0
- package/dist/runs/background/cliRunnerEntry.cjs.map +1 -0
- package/dist/runs/background/cliRunnerEntry.d.cts +1 -0
- package/dist/runs/background/cliRunnerEntry.d.mts +1 -0
- package/dist/runs/background/cliRunnerEntry.mjs +2 -0
- package/dist/runs/background/cliRunnerEntry.mjs.map +1 -0
- package/dist/runs/background/completionBatcher.cjs +2 -0
- package/dist/runs/background/completionBatcher.cjs.map +1 -0
- package/dist/runs/background/completionBatcher.mjs +2 -0
- package/dist/runs/background/completionBatcher.mjs.map +1 -0
- package/dist/runs/background/notify.cjs +4 -0
- package/dist/runs/background/notify.cjs.map +1 -0
- package/dist/runs/background/notify.mjs +4 -0
- package/dist/runs/background/notify.mjs.map +1 -0
- package/dist/runs/background/parentWatchdog.cjs +2 -0
- package/dist/runs/background/parentWatchdog.cjs.map +1 -0
- package/dist/runs/background/parentWatchdog.mjs +2 -0
- package/dist/runs/background/parentWatchdog.mjs.map +1 -0
- package/dist/runs/background/processTerminal.cjs +2 -0
- package/dist/runs/background/processTerminal.cjs.map +1 -0
- package/dist/runs/background/processTerminal.mjs +2 -0
- package/dist/runs/background/processTerminal.mjs.map +1 -0
- package/dist/runs/background/resultWatcher.cjs +2 -0
- package/dist/runs/background/resultWatcher.cjs.map +1 -0
- package/dist/runs/background/resultWatcher.mjs +2 -0
- package/dist/runs/background/resultWatcher.mjs.map +1 -0
- package/dist/runs/background/runIdResolver.cjs +2 -0
- package/dist/runs/background/runIdResolver.cjs.map +1 -0
- package/dist/runs/background/runIdResolver.mjs +2 -0
- package/dist/runs/background/runIdResolver.mjs.map +1 -0
- package/dist/runs/background/runnerBootstrap.cjs +2 -0
- package/dist/runs/background/runnerBootstrap.cjs.map +1 -0
- package/dist/runs/background/runnerBootstrap.mjs +2 -0
- package/dist/runs/background/runnerBootstrap.mjs.map +1 -0
- package/dist/runs/background/runnerExecution.cjs +2 -0
- package/dist/runs/background/runnerExecution.cjs.map +1 -0
- package/dist/runs/background/runnerExecution.mjs +2 -0
- package/dist/runs/background/runnerExecution.mjs.map +1 -0
- package/dist/runs/background/runnerReporting.cjs +3 -0
- package/dist/runs/background/runnerReporting.cjs.map +1 -0
- package/dist/runs/background/runnerReporting.mjs +3 -0
- package/dist/runs/background/runnerReporting.mjs.map +1 -0
- package/dist/runs/background/runnerTeamMembership.cjs +2 -0
- package/dist/runs/background/runnerTeamMembership.cjs.map +1 -0
- package/dist/runs/background/runnerTeamMembership.mjs +2 -0
- package/dist/runs/background/runnerTeamMembership.mjs.map +1 -0
- package/dist/runs/background/spawnHandshake.cjs +2 -0
- package/dist/runs/background/spawnHandshake.cjs.map +1 -0
- package/dist/runs/background/spawnHandshake.mjs +2 -0
- package/dist/runs/background/spawnHandshake.mjs.map +1 -0
- package/dist/runs/background/staleRunReconciler.cjs +2 -0
- package/dist/runs/background/staleRunReconciler.cjs.map +1 -0
- package/dist/runs/background/staleRunReconciler.mjs +2 -0
- package/dist/runs/background/staleRunReconciler.mjs.map +1 -0
- package/dist/runs/background/statusReader.cjs +2 -0
- package/dist/runs/background/statusReader.cjs.map +1 -0
- package/dist/runs/background/statusReader.mjs +2 -0
- package/dist/runs/background/statusReader.mjs.map +1 -0
- package/dist/runs/background/statusWriter.cjs +2 -0
- package/dist/runs/background/statusWriter.cjs.map +1 -0
- package/dist/runs/background/statusWriter.mjs +2 -0
- package/dist/runs/background/statusWriter.mjs.map +1 -0
- package/dist/runs/background/subagentWait.cjs +2 -0
- package/dist/runs/background/subagentWait.cjs.map +1 -0
- package/dist/runs/background/subagentWait.mjs +2 -0
- package/dist/runs/background/subagentWait.mjs.map +1 -0
- package/dist/runs/background/terminalPersistence.cjs +2 -0
- package/dist/runs/background/terminalPersistence.cjs.map +1 -0
- package/dist/runs/background/terminalPersistence.mjs +2 -0
- package/dist/runs/background/terminalPersistence.mjs.map +1 -0
- package/dist/runs/registry/processGroup.cjs +2 -0
- package/dist/runs/registry/processGroup.cjs.map +1 -0
- package/dist/runs/registry/processGroup.mjs +2 -0
- package/dist/runs/registry/processGroup.mjs.map +1 -0
- package/dist/runs/registry/runRegistry.cjs +2 -0
- package/dist/runs/registry/runRegistry.cjs.map +1 -0
- package/dist/runs/registry/runRegistry.mjs +2 -0
- package/dist/runs/registry/runRegistry.mjs.map +1 -0
- package/dist/runs/registry/sessionLifecycle.cjs +2 -0
- package/dist/runs/registry/sessionLifecycle.cjs.map +1 -0
- package/dist/runs/registry/sessionLifecycle.mjs +2 -0
- package/dist/runs/registry/sessionLifecycle.mjs.map +1 -0
- package/dist/runs/registry/suspendedRuns.cjs +4 -0
- package/dist/runs/registry/suspendedRuns.cjs.map +1 -0
- package/dist/runs/registry/suspendedRuns.mjs +4 -0
- package/dist/runs/registry/suspendedRuns.mjs.map +1 -0
- package/dist/runs/sdkRunnerEntry.cjs +2 -0
- package/dist/runs/sdkRunnerEntry.cjs.map +1 -0
- package/dist/runs/sdkRunnerEntry.d.cts +1 -0
- package/dist/runs/sdkRunnerEntry.d.mts +1 -0
- package/dist/runs/sdkRunnerEntry.mjs +2 -0
- package/dist/runs/sdkRunnerEntry.mjs.map +1 -0
- package/dist/runs/shared/claudeFableProfile.cjs +4 -0
- package/dist/runs/shared/claudeFableProfile.cjs.map +1 -0
- package/dist/runs/shared/claudeFableProfile.mjs +4 -0
- package/dist/runs/shared/claudeFableProfile.mjs.map +1 -0
- package/dist/runs/shared/depthGuard.cjs +2 -0
- package/dist/runs/shared/depthGuard.cjs.map +1 -0
- package/dist/runs/shared/depthGuard.mjs +2 -0
- package/dist/runs/shared/depthGuard.mjs.map +1 -0
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs +2 -0
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs.map +1 -0
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs +2 -0
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs.map +1 -0
- package/dist/runs/shared/modelFallback.cjs +2 -0
- package/dist/runs/shared/modelFallback.cjs.map +1 -0
- package/dist/runs/shared/modelFallback.mjs +2 -0
- package/dist/runs/shared/modelFallback.mjs.map +1 -0
- package/dist/runs/shared/modelScope.cjs +2 -0
- package/dist/runs/shared/modelScope.cjs.map +1 -0
- package/dist/runs/shared/modelScope.mjs +2 -0
- package/dist/runs/shared/modelScope.mjs.map +1 -0
- package/dist/runs/shared/nestedPath.cjs +2 -0
- package/dist/runs/shared/nestedPath.cjs.map +1 -0
- package/dist/runs/shared/nestedPath.mjs +2 -0
- package/dist/runs/shared/nestedPath.mjs.map +1 -0
- package/dist/runs/shared/piArgs.cjs +2 -0
- package/dist/runs/shared/piArgs.cjs.map +1 -0
- package/dist/runs/shared/piArgs.mjs +2 -0
- package/dist/runs/shared/piArgs.mjs.map +1 -0
- package/dist/runs/shared/piSpawn.cjs +2 -0
- package/dist/runs/shared/piSpawn.cjs.map +1 -0
- package/dist/runs/shared/piSpawn.mjs +2 -0
- package/dist/runs/shared/piSpawn.mjs.map +1 -0
- package/dist/runs/shared/runWithConcurrency.cjs +2 -0
- package/dist/runs/shared/runWithConcurrency.cjs.map +1 -0
- package/dist/runs/shared/runWithConcurrency.mjs +2 -0
- package/dist/runs/shared/runWithConcurrency.mjs.map +1 -0
- package/dist/runs/shared/runtimeRegistry.cjs +2 -0
- package/dist/runs/shared/runtimeRegistry.cjs.map +1 -0
- package/dist/runs/shared/runtimeRegistry.mjs +2 -0
- package/dist/runs/shared/runtimeRegistry.mjs.map +1 -0
- package/dist/runs/shared/structuredOutput.cjs +2 -0
- package/dist/runs/shared/structuredOutput.cjs.map +1 -0
- package/dist/runs/shared/structuredOutput.mjs +2 -0
- package/dist/runs/shared/structuredOutput.mjs.map +1 -0
- package/dist/runs/shared/toolAvailability.cjs +2 -0
- package/dist/runs/shared/toolAvailability.cjs.map +1 -0
- package/dist/runs/shared/toolAvailability.mjs +2 -0
- package/dist/runs/shared/toolAvailability.mjs.map +1 -0
- package/dist/runs/shared/toolBudget.cjs +2 -0
- package/dist/runs/shared/toolBudget.cjs.map +1 -0
- package/dist/runs/shared/toolBudget.mjs +2 -0
- package/dist/runs/shared/toolBudget.mjs.map +1 -0
- package/dist/shared/artifacts.cjs +2 -0
- package/dist/shared/artifacts.cjs.map +1 -0
- package/dist/shared/artifacts.mjs +2 -0
- package/dist/shared/artifacts.mjs.map +1 -0
- package/dist/shared/atomicJson.cjs +2 -0
- package/dist/shared/atomicJson.cjs.map +1 -0
- package/dist/shared/atomicJson.mjs +2 -0
- package/dist/shared/atomicJson.mjs.map +1 -0
- package/dist/shared/boundedKeySet.cjs +2 -0
- package/dist/shared/boundedKeySet.cjs.map +1 -0
- package/dist/shared/boundedKeySet.mjs +2 -0
- package/dist/shared/boundedKeySet.mjs.map +1 -0
- package/dist/shared/childTranscript.cjs +5 -0
- package/dist/shared/childTranscript.cjs.map +1 -0
- package/dist/shared/childTranscript.mjs +5 -0
- package/dist/shared/childTranscript.mjs.map +1 -0
- package/dist/shared/configDir.cjs +2 -0
- package/dist/shared/configDir.cjs.map +1 -0
- package/dist/shared/configDir.mjs +2 -0
- package/dist/shared/configDir.mjs.map +1 -0
- package/dist/shared/errors.cjs +2 -0
- package/dist/shared/errors.cjs.map +1 -0
- package/dist/shared/errors.mjs +2 -0
- package/dist/shared/errors.mjs.map +1 -0
- package/dist/shared/fileSystemRetry.cjs +2 -0
- package/dist/shared/fileSystemRetry.cjs.map +1 -0
- package/dist/shared/fileSystemRetry.mjs +2 -0
- package/dist/shared/fileSystemRetry.mjs.map +1 -0
- package/dist/shared/formatters.cjs +2 -0
- package/dist/shared/formatters.cjs.map +1 -0
- package/dist/shared/formatters.mjs +2 -0
- package/dist/shared/formatters.mjs.map +1 -0
- package/dist/shared/lruCache.cjs +2 -0
- package/dist/shared/lruCache.cjs.map +1 -0
- package/dist/shared/lruCache.mjs +2 -0
- package/dist/shared/lruCache.mjs.map +1 -0
- package/dist/shared/modelInfo.cjs +2 -0
- package/dist/shared/modelInfo.cjs.map +1 -0
- package/dist/shared/modelInfo.mjs +2 -0
- package/dist/shared/modelInfo.mjs.map +1 -0
- package/dist/shared/modelResolution.cjs +2 -0
- package/dist/shared/modelResolution.cjs.map +1 -0
- package/dist/shared/modelResolution.mjs +2 -0
- package/dist/shared/modelResolution.mjs.map +1 -0
- package/dist/shared/paths.cjs +2 -0
- package/dist/shared/paths.cjs.map +1 -0
- package/dist/shared/paths.mjs +2 -0
- package/dist/shared/paths.mjs.map +1 -0
- package/dist/shared/pollScheduler.cjs +2 -0
- package/dist/shared/pollScheduler.cjs.map +1 -0
- package/dist/shared/pollScheduler.mjs +2 -0
- package/dist/shared/pollScheduler.mjs.map +1 -0
- package/dist/shared/scopeOwner.cjs +2 -0
- package/dist/shared/scopeOwner.cjs.map +1 -0
- package/dist/shared/scopeOwner.mjs +2 -0
- package/dist/shared/scopeOwner.mjs.map +1 -0
- package/dist/shared/versioned.cjs +2 -0
- package/dist/shared/versioned.cjs.map +1 -0
- package/dist/shared/versioned.mjs +2 -0
- package/dist/shared/versioned.mjs.map +1 -0
- package/dist/slash/chainExpression.cjs +2 -0
- package/dist/slash/chainExpression.cjs.map +1 -0
- package/dist/slash/chainExpression.mjs +2 -0
- package/dist/slash/chainExpression.mjs.map +1 -0
- package/dist/slash/doctor.cjs +5 -0
- package/dist/slash/doctor.cjs.map +1 -0
- package/dist/slash/doctor.mjs +5 -0
- package/dist/slash/doctor.mjs.map +1 -0
- package/dist/slash/slashCommands.cjs +4 -0
- package/dist/slash/slashCommands.cjs.map +1 -0
- package/dist/slash/slashCommands.mjs +4 -0
- package/dist/slash/slashCommands.mjs.map +1 -0
- package/dist/slash/spawnRequestMapping.cjs +2 -0
- package/dist/slash/spawnRequestMapping.cjs.map +1 -0
- package/dist/slash/spawnRequestMapping.mjs +2 -0
- package/dist/slash/spawnRequestMapping.mjs.map +1 -0
- package/dist/slash/subagentLaunch.cjs +2 -0
- package/dist/slash/subagentLaunch.cjs.map +1 -0
- package/dist/slash/subagentLaunch.mjs +2 -0
- package/dist/slash/subagentLaunch.mjs.map +1 -0
- package/dist/tui/agentCatalog.cjs +2 -0
- package/dist/tui/agentCatalog.cjs.map +1 -0
- package/dist/tui/agentCatalog.mjs +2 -0
- package/dist/tui/agentCatalog.mjs.map +1 -0
- package/dist/tui/agentResourceProjection.cjs +2 -0
- package/dist/tui/agentResourceProjection.cjs.map +1 -0
- package/dist/tui/agentResourceProjection.mjs +2 -0
- package/dist/tui/agentResourceProjection.mjs.map +1 -0
- package/dist/tui/completionNotice.cjs +5 -0
- package/dist/tui/completionNotice.cjs.map +1 -0
- package/dist/tui/completionNotice.mjs +5 -0
- package/dist/tui/completionNotice.mjs.map +1 -0
- package/dist/tui/fleet.cjs +3 -0
- package/dist/tui/fleet.cjs.map +1 -0
- package/dist/tui/fleet.mjs +3 -0
- package/dist/tui/fleet.mjs.map +1 -0
- package/dist/tui/fleetAgentView.cjs +2 -0
- package/dist/tui/fleetAgentView.cjs.map +1 -0
- package/dist/tui/fleetAgentView.mjs +2 -0
- package/dist/tui/fleetAgentView.mjs.map +1 -0
- package/dist/tui/fleetStatus.cjs +2 -0
- package/dist/tui/fleetStatus.cjs.map +1 -0
- package/dist/tui/fleetStatus.mjs +2 -0
- package/dist/tui/fleetStatus.mjs.map +1 -0
- package/dist/tui/fleetTranscript.cjs +5 -0
- package/dist/tui/fleetTranscript.cjs.map +1 -0
- package/dist/tui/fleetTranscript.mjs +5 -0
- package/dist/tui/fleetTranscript.mjs.map +1 -0
- package/dist/tui/register.cjs +2 -0
- package/dist/tui/register.cjs.map +1 -0
- package/dist/tui/register.mjs +2 -0
- package/dist/tui/register.mjs.map +1 -0
- package/dist/types.d.cts +59 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +59 -0
- package/dist/types.d.mts.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerExecution.mjs","names":[],"sources":["../../../src/runs/background/runnerExecution.ts"],"sourcesContent":["/**\n * The \"while running\" glue: consumes steer/interrupt/timeout/stop requests\n * via `ControlChannelWatcher`, and is the ONE place every terminal trigger\n * (interrupt/stop/timeout, and normal completion) funnels through\n * `TerminalPersistenceService.finalize()`.\n *\n * WHY EVERY TRIGGER CALLS THE SAME `finalize()`, INSTEAD OF EACH DOING ITS\n * OWN THING:\n * The predecessor's `interruptRunner`/`stopRunner`/`timeoutRunner` and its\n * post-loop completion tail each independently computed terminal state and\n * independently wrote it - four triggers, four write sites, each guarded\n * only by a LOCAL boolean that stopped re-entry of THAT SAME handler, none\n * of them aware of the others (see `asyncExecution.ts`'s header for the\n * full diagnosis, including why that made a stray control event able to\n * race the completion tail). Here, `interrupt()`/`stop()`/`timeout()`/\n * `complete()` each do exactly two things: record the outcome via\n * `reporting.prepareResult()`, then call `terminalPersistence.finalize()`.\n * `TerminalPersistenceService`'s own `finalized` boolean, checked and set\n * before anything else runs, is the ONLY thing that decides whether a given\n * call actually does anything. No new idempotency logic is added here -\n * every trigger is made to go through the one that already exists, rather\n * than rolling its own.\n *\n * WHY INTERRUPT/STOP/TIMEOUT EACH STILL CALL `prepareResult()`:\n * `RunnerReporting.mutateTerminalStatus`'s fallback derives a correct\n * outcome from `trigger` for real OS signals/exceptions - but interrupt/\n * stop/timeout are NOT `TerminalTrigger` values (those are this run's own\n * request-driven terminal reasons, not the signals/exceptions\n * `TerminalPersistenceService` itself watches for), and `finalize()` always\n * calls `mutateTerminalStatus` with `trigger === undefined` regardless of\n * WHY this class decided to call it. So each of `interrupt()`/`stop()`/\n * `timeout()` prepares its own result before calling `finalize()`, the same\n * as `complete()` does - `mutateTerminalStatus` cannot otherwise tell\n * interrupt apart from stop apart from normal completion.\n *\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { ActivityState } from '../../types.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { runDirFor } from './asyncExecution.ts';\nimport type {\n ControlChannelWatchHandlers,\n IControlChannelWatcher,\n SteerRequest,\n} from '../../intercom/supervisorControlChannel.ts';\nimport type { IRunnerReporting } from './runnerReporting.ts';\nimport type { ICoalescedStatusWriter } from './statusWriter.ts';\nimport type { ITerminalPersistenceService } from './terminalPersistence.ts';\n\nexport interface RunnerExecutionHandlers {\n /** Forwarded steer requests, for whatever delivers them to the running step (not this class's job). */\n onSteer?: (request: SteerRequest) => void;\n}\n\nexport interface RunnerProgress {\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport interface IRunnerExecution {\n /**\n * Start watching this run's control inbox and consuming any chain-append\n * requests already waiting. Returns a disposer that stops watching\n * without finalizing (used on a clean, already-finalized exit).\n */\n start(runId: string, handlers?: RunnerExecutionHandlers): () => void;\n /** Record a material, event-driven activity transition. */\n setActivity(activity: ActivityState): void;\n /** Record live child metrics in one coalesced status update. */\n setProgress(progress: RunnerProgress): void;\n /** This run finished normally. Idempotent via `TerminalPersistenceService`. */\n complete(success: boolean, summary: string): void;\n}\n\n@injectable()\nexport class RunnerExecution implements IRunnerExecution {\n constructor(\n @inject(DOOM_TEAM_TYPES.ControlChannelWatcher) private readonly controlChannel: IControlChannelWatcher,\n @inject(DOOM_TEAM_TYPES.TerminalPersistenceService)\n private readonly terminalPersistence: ITerminalPersistenceService<AsyncRunStatus>,\n @inject(DOOM_TEAM_TYPES.RunnerReporting) private readonly reporting: IRunnerReporting,\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter)\n private readonly statusWriter: ICoalescedStatusWriter<AsyncRunStatus>,\n ) {}\n\n /** Dedicated runner processes own exactly one session, so a stop may exit after synchronous persistence. */\n protected exitProcess(code: number): void {\n process.exit(code);\n }\n\n start(runId: string, handlers: RunnerExecutionHandlers = {}): () => void {\n const controlHandlers: ControlChannelWatchHandlers = {\n onInterrupt: () => this.interrupt(),\n onStop: () => this.stop(),\n onTimeout: () => this.timeout(),\n ...(handlers.onSteer ? { onSteer: handlers.onSteer } : {}),\n };\n return this.controlChannel.watch(runDirFor(runId), controlHandlers);\n }\n\n setActivity(activity: ActivityState): void {\n this.statusWriter.update((status) => {\n status.activityState = activity;\n status.lastUpdate = Date.now();\n });\n }\n\n setProgress(progress: RunnerProgress): void {\n if (progress.tokens === undefined && progress.currentTool === undefined && progress.toolCount === undefined) {\n return;\n }\n this.statusWriter.update((status) => {\n if (progress.tokens !== undefined) status.tokens = progress.tokens;\n if (progress.currentTool !== undefined) status.currentTool = progress.currentTool;\n if (progress.toolCount !== undefined) status.toolCount = progress.toolCount;\n status.lastUpdate = Date.now();\n });\n }\n\n complete(success: boolean, summary: string): void {\n this.reporting.prepareResult({ success, summary });\n this.terminalPersistence.finalize();\n }\n\n interrupt(): void {\n this.reporting.prepareResult({ success: false, summary: 'Interrupted before completion.' });\n this.terminalPersistence.finalize();\n }\n\n stop(): void {\n this.reporting.prepareResult({ success: false, state: 'stopped', summary: 'Stopped before completion.' });\n try {\n this.terminalPersistence.finalize();\n } finally {\n this.exitProcess(0);\n }\n }\n\n timeout(): void {\n this.reporting.prepareResult({ success: false, summary: 'Timed out before completion.' });\n this.terminalPersistence.finalize();\n }\n}\n"],"mappings":"ypBAuC2D,IAGX,eAqCzC,KAAkD,CACvD,YACE,EACA,EAEA,EACA,EAEA,CANgE,KAAA,eAAA,EAE/C,KAAA,oBAAA,EACyC,KAAA,UAAA,EAEzC,KAAA,aAAA,EAInB,YAAsB,EAAoB,CACxC,QAAQ,KAAK,EAAK,CAGpB,MAAM,EAAe,EAAoC,EAAE,CAAc,CACvE,IAAM,EAA+C,CACnD,gBAAmB,KAAK,WAAW,CACnC,WAAc,KAAK,MAAM,CACzB,cAAiB,KAAK,SAAS,CAC/B,GAAI,EAAS,QAAU,CAAE,QAAS,EAAS,QAAS,CAAG,EAAE,CAC1D,CACD,OAAO,KAAK,eAAe,MAAM,EAAU,EAAM,CAAE,EAAgB,CAGrE,YAAY,EAA+B,CACzC,KAAK,aAAa,OAAQ,GAAW,CACnC,EAAO,cAAgB,EACvB,EAAO,WAAa,KAAK,KAAK,EAC9B,CAGJ,YAAY,EAAgC,CACtC,EAAS,SAAW,IAAA,IAAa,EAAS,cAAgB,IAAA,IAAa,EAAS,YAAc,IAAA,IAGlG,KAAK,aAAa,OAAQ,GAAW,CAC/B,EAAS,SAAW,IAAA,KAAW,EAAO,OAAS,EAAS,QACxD,EAAS,cAAgB,IAAA,KAAW,EAAO,YAAc,EAAS,aAClE,EAAS,YAAc,IAAA,KAAW,EAAO,UAAY,EAAS,WAClE,EAAO,WAAa,KAAK,KAAK,EAC9B,CAGJ,SAAS,EAAkB,EAAuB,CAChD,KAAK,UAAU,cAAc,CAAE,UAAS,UAAS,CAAC,CAClD,KAAK,oBAAoB,UAAU,CAGrC,WAAkB,CAChB,KAAK,UAAU,cAAc,CAAE,QAAS,GAAO,QAAS,iCAAkC,CAAC,CAC3F,KAAK,oBAAoB,UAAU,CAGrC,MAAa,CACX,KAAK,UAAU,cAAc,CAAE,QAAS,GAAO,MAAO,UAAW,QAAS,6BAA8B,CAAC,CACzG,GAAI,CACF,KAAK,oBAAoB,UAAU,QAC3B,CACR,KAAK,YAAY,EAAE,EAIvB,SAAgB,CACd,KAAK,UAAU,cAAc,CAAE,QAAS,GAAO,QAAS,+BAAgC,CAAC,CACzF,KAAK,oBAAoB,UAAU,QAlEtC,GAAY,KAGR,EAAO,EAAgB,sBAAsB,CAAA,KAC7C,EAAO,EAAgB,2BAA2B,CAAA,KAElD,EAAO,EAAgB,gBAAgB,CAAA,KACvC,EAAO,EAAgB,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,3 @@
|
|
|
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`),s=require(`./resultWatcher.cjs`);let c=require(`node:path`);c=e.__toESM(c,1);let l=require(`inversify`);function u(e){return c.join(o.currentResultsDir(),`${e}${s.RESULT_FILE_SUFFIX}`)}const d=4e3;function f(e){return e.length<=d?e:e.slice(0,d-71)+`
|
|
2
|
+
... [truncated for status.json; see the result file for the full text]`}function p(e){return e instanceof Error?e.message:typeof e==`string`?e:`Unhandled rejection with no error message.`}let m=class{constructor(e){this.statusWriter=e}now(){return Date.now()}writeResultFile(e,n){t.writeAtomicJson(u(e),n)}prepared;recordedSessionFile;prepareResult(e){this.prepared=e}recordSessionFile(e){return this.recordedSessionFile=e.length<=4096?e:void 0,this.recordedSessionFile}resolveOutcome(e){return e?.kind===`signal`?{state:`stopped`,success:!1,error:`Terminated by signal ${e.signal}.`}:e?.kind===`uncaughtException`?{state:`failed`,success:!1,error:e.error.message}:e?.kind===`unhandledRejection`?{state:`failed`,success:!1,error:p(e.reason)}:this.prepared?{state:this.prepared.state??(this.prepared.success?`completed`:`failed`),success:this.prepared.success}:{state:`failed`,success:!1,error:`Run finalized without ever calling prepareResult().`}}mutateTerminalStatus(e,t){let n=this.now(),r=this.resolveOutcome(t),i=this.prepared?.summary??r.error??`(no summary)`;e.state=r.state,e.endedAt=n,e.lastUpdate=n,e.summary=f(i),r.error&&(e.error=r.error),this.recordedSessionFile&&(e.sessionFile=this.recordedSessionFile),this.statusWriter.updateSync(()=>{});let a={runId:e.runId,agent:e.agent,success:r.success,state:r.state,summary:i,startedAt:e.startedAt,endedAt:n,...r.error?{error:r.error}:{},...this.prepared?.acceptance?{acceptance:this.prepared.acceptance}:{}};this.writeResultFile(e.runId,a)}};m=n.__decorate([(0,l.injectable)(),a.__decorateParam(0,(0,l.inject)(r.DOOM_TEAM_TYPES.CoalescedStatusWriter)),i.__decorateMetadata(`design:paramtypes`,[Object])],m),Object.defineProperty(exports,`RunnerReporting`,{enumerable:!0,get:function(){return m}});
|
|
3
|
+
//# sourceMappingURL=runnerReporting.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerReporting.cjs","names":["path","currentResultsDir","RESULT_FILE_SUFFIX","DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/runnerReporting.ts"],"sourcesContent":["/**\n * Owns the single, synchronous callback wired into\n * `TerminalPersistenceService.begin()`: it both finalizes the run's status\n * and writes the terminal result file `ResultWatcher` claims by rename.\n *\n * WHY BOTH WRITES LIVE IN ONE CALLBACK:\n * `TerminalPersistenceService`'s idempotency guarantee only covers what\n * happens inside that one callback - a second, independent write site for\n * the result file would be exactly the \"fourth, unguarded terminal write\"\n * shape that made the predecessor's finalization the worst bug in its\n * review (see `asyncExecution.ts`'s header for the full diagnosis). Putting\n * the result-file write here means it inherits the same at-most-once\n * guarantee the status write already has, from the same source, rather than\n * needing a second one invented for it.\n *\n * WRITE ORDER, DECIDED DELIBERATELY (status, then result):\n * - status terminal / result missing: `ResultWatcher` delivers nothing, and\n * `StaleRunReconciler` sees no result plus a dead process, so it repairs.\n * RECOVERABLE\n * - result present / status not terminal: `ResultWatcher` delivers, but\n * `StaleRunReconciler` sees a result exists and correctly leaves the run\n * alone (see that class's header), so the stale status persists forever.\n * NOT RECOVERABLE\n * So status must land on disk first. The subtlety: `mutateTerminalStatus` is\n * the MUTATOR `TerminalPersistenceService` passes to\n * `CoalescedStatusWriter.updateSync(mutator)`, and that writer's own flush\n * only happens AFTER the mutator returns - so mutating `status` in place and\n * then writing the result file, in that order, inside this one function,\n * would put the result file on disk BEFORE the status flush, exactly\n * backwards. The fix: call `this.statusWriter.updateSync(() => {})` - a\n * second, no-op-mutator call to the SAME writer, re-entered from inside the\n * first one - immediately after mutating `status`. `updateSync` increments\n * its dirty counter and flushes unconditionally on every call regardless of\n * what its own mutator did, so this forces the just-mutated `status` onto\n * disk right there, before this function does anything else. The one\n * (harmless) cost is that `TerminalPersistenceService`'s own flush, once\n * this callback returns, then flushes again - a redundant but correct\n * second write of the same, already-correct status.\n *\n * WHY A THROW FROM THE RESULT WRITE CANNOT UNDO THE STATUS FLUSH:\n * The forced flush above happens, completes, and returns before\n * `writeResultFile` is even called - not \"before it succeeds\", before it is\n * INVOKED at all. A throw from that call propagates up through\n * `TerminalPersistenceService.triggerFinalize`'s `try` (skipping\n * `clearCrashMarker()`, per that class's own documented \"leave the marker in\n * place\" behavior on a mutator throw) into its `finally` (which still kills\n * tracked children) - but the status write already happened and cannot be\n * un-happened by anything that runs after it.\n *\n * WHY THE RESULT SHAPE IS MINIMAL, WITH ONE DELIBERATE EXCEPTION:\n * Most of the predecessor's ~40 result fields (structured output, artifacts,\n * workflow graph, capability audit) depend on subsystems not fully composed\n * here yet; shipping a correct minimal contract now beats an unverifiable\n * rich one. The exception: `acceptance` (an `AcceptanceLedger`) IS included\n * when `prepareResult()` was given one, because `acceptance.ts` is fully\n * ported and tested in this package today - an agent that declared\n * acceptance criteria and had them evaluated would otherwise produce a\n * result that silently omits the verdict, which is the same failure shape\n * as `DeliverableGuard`'s policy being unobservable (see\n * `asyncExecution.ts`'s header on that).\n *\n * WHY `summary` IS ALSO WRITTEN TO `status.json`, TRUNCATED:\n * Chain mode (`extensions/spawnPlan.ts`) needs a completed step's summary\n * to resolve the next step's `{previous}`/`{outputs.name}` template, but the\n * only thing that ever reads a delivered `RunResultFile` is whoever is\n * registered as `ResultWatcher`'s ONE consumer slot (`CompletionNotifier`,\n * by design). A second reader competing for that single claim-by-rename\n * would either displace the registered consumer or race it - both wrong.\n * `status.json` has no such problem: it has exactly one writer (this\n * callback, via `CoalescedStatusWriter`), is never claimed or unlinked, and\n * `AsyncJobTracker`/`SubagentWaiter` already read it to learn a run\n * finished - chain mode reads a step's summary through the path it already\n * uses. The copy in `status.json` is TRUNCATED (`MAX_STATUS_SUMMARY_CHARS`):\n * status is a coalesced, rewritten-in-full state file, not a log, and an\n * unbounded field there is a size problem waiting to happen. The full,\n * untruncated text still goes to the result file, for anything that reads\n * that instead. `acceptance` is deliberately NOT duplicated into status:\n * nothing this package builds yet gates chain advancement on an acceptance\n * verdict (only on execution success - see `spawnPlan.ts`'s header), so\n * there is no reader for a second copy of it yet. Add it if and when one\n * exists, the same reasoning as everything else flagged \"not wired yet\" in\n * this package.\n *\n * WHY `sessionFile` IS RECORDED HERE TOO, AND BOUNDED BY OMISSION, NOT\n * TRUNCATION:\n * `action='resume'` needs to know which Pi session transcript a completed\n * run's own child was writing to, and the CHILD is the only party that ever\n * knows that - `childTranscript.ts`'s header says the PARENT does not learn\n * a child's session id until the run ends, which already rules out the\n * parent recording it. `recordSessionFile()` is `prepareResult()`'s sibling:\n * a value set from outside, read back inside this same terminal-write\n * callback, on the SAME `CoalescedStatusWriter`, so it lands in the SAME\n * flush as everything else `mutateTerminalStatus` computes - no second\n * writer, no second flush. Where it is called from is\n * `subagentPromptRuntime.ts`'s `session_start` handler, the one place in\n * the child process that receives `ExtensionContext.sessionManager` at all.\n * `summary`'s bound is a truncation with a marker, because a shortened\n * summary is still a readable, honest summary. A shortened PATH is not a\n * smaller path to the same file - it is a WRONG path to some other file, or\n * no file at all, and `resume` acting on it would silently target the wrong\n * transcript. So this bound is \"drop entirely past `MAX_SESSION_FILE_CHARS`,\n * never truncate\" - the field goes missing rather than becoming a plausible-\n * looking lie an eventual `resume` implementation would trust.\n */\n\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentResultsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { RESULT_FILE_SUFFIX, type RunResultFile } from './resultWatcher.ts';\nimport { type ICoalescedStatusWriter } from './statusWriter.ts';\nimport type { TerminalTrigger } from './terminalPersistence.ts';\nimport type { AcceptanceLedger } from '../types.ts';\n\nfunction resultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\n/** Generous but bounded - see the module header's summary-in-status section. */\nconst MAX_STATUS_SUMMARY_CHARS = 4_000;\nconst TRUNCATION_MARKER = '\\n... [truncated for status.json; see the result file for the full text]';\n\nfunction truncateForStatus(summary: string): string {\n if (summary.length <= MAX_STATUS_SUMMARY_CHARS) return summary;\n return summary.slice(0, MAX_STATUS_SUMMARY_CHARS - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;\n}\n\n/** Generous for any real filesystem path; see the module header's `sessionFile` section for why exceeding this drops the field instead of truncating it. */\nconst MAX_SESSION_FILE_CHARS = 4_096;\n\n/** What the run itself knows about its own outcome, recorded before a trigger fires. */\nexport interface RunnerResultInput {\n success: boolean;\n summary: string;\n /** Explicit terminal state for controlled outcomes such as an operator stop. */\n state?: 'completed' | 'failed' | 'stopped';\n acceptance?: AcceptanceLedger;\n}\n\nexport interface IRunnerReporting {\n /**\n * Record what to report if/when this run reaches a terminal state. Call\n * this before triggering finalize (normal completion, or right before an\n * interrupt/stop/timeout handler calls `terminalPersistence.finalize()`).\n * A trigger that fires with nothing prepared (a signal or exception mid-run)\n * still produces a result - see `mutateTerminalStatus`'s fallback.\n */\n prepareResult(input: RunnerResultInput): void;\n /**\n * Record this run's own Pi session transcript path, if/when the child\n * learns it. Call this as soon as it is known (`subagentPromptRuntime.ts`'s\n * `session_start` handler, today). The accepted value is returned for the\n * bootstrap status write, then retained here for the terminal status write.\n * See the module header's `sessionFile` section for why a too-long value is\n * dropped, not truncated.\n */\n recordSessionFile(sessionFile: string): string | undefined;\n /** The callback to wire into `TerminalPersistenceService.begin()`. See the module header for what it does and why, in this order. */\n mutateTerminalStatus(status: AsyncRunStatus, trigger: TerminalTrigger | undefined): void;\n}\n\nfunction describeReason(reason: unknown): string {\n if (reason instanceof Error) return reason.message;\n return typeof reason === 'string' ? reason : 'Unhandled rejection with no error message.';\n}\n\ninterface TerminalOutcome {\n state: AsyncRunStatus['state'];\n success: boolean;\n error?: string;\n}\n\n@injectable()\nexport class RunnerReporting implements IRunnerReporting {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter)\n private readonly statusWriter: ICoalescedStatusWriter<AsyncRunStatus>,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n protected writeResultFile(runId: string, result: RunResultFile): void {\n writeAtomicJson(resultPathFor(runId), result);\n }\n\n private prepared: RunnerResultInput | undefined;\n private recordedSessionFile: string | undefined;\n\n prepareResult(input: RunnerResultInput): void {\n this.prepared = input;\n }\n\n recordSessionFile(sessionFile: string): string | undefined {\n // Drop, don't truncate, past the bound - see the module header's\n // `sessionFile` section for why a shortened path is worse than no path.\n this.recordedSessionFile = sessionFile.length <= MAX_SESSION_FILE_CHARS ? sessionFile : undefined;\n return this.recordedSessionFile;\n }\n\n private resolveOutcome(trigger: TerminalTrigger | undefined): TerminalOutcome {\n if (trigger?.kind === 'signal') {\n return { state: 'stopped', success: false, error: `Terminated by signal ${trigger.signal}.` };\n }\n if (trigger?.kind === 'uncaughtException') {\n return { state: 'failed', success: false, error: trigger.error.message };\n }\n if (trigger?.kind === 'unhandledRejection') {\n return { state: 'failed', success: false, error: describeReason(trigger.reason) };\n }\n // trigger === undefined: the run's own explicit finalize() call.\n if (this.prepared) {\n return {\n state: this.prepared.state ?? (this.prepared.success ? 'completed' : 'failed'),\n success: this.prepared.success,\n };\n }\n return { state: 'failed', success: false, error: 'Run finalized without ever calling prepareResult().' };\n }\n\n mutateTerminalStatus(status: AsyncRunStatus, trigger: TerminalTrigger | undefined): void {\n const now = this.now();\n const outcome = this.resolveOutcome(trigger);\n\n const summary = this.prepared?.summary ?? outcome.error ?? '(no summary)';\n\n status.state = outcome.state;\n status.endedAt = now;\n status.lastUpdate = now;\n status.summary = truncateForStatus(summary);\n if (outcome.error) status.error = outcome.error;\n if (this.recordedSessionFile) status.sessionFile = this.recordedSessionFile;\n\n // Forces the mutation above onto disk before anything else in this\n // function runs. See the module header's WRITE ORDER section for why\n // this specific call, at this specific point, is what achieves that.\n this.statusWriter.updateSync(() => {});\n\n const result: RunResultFile = {\n runId: status.runId,\n agent: status.agent,\n success: outcome.success,\n state: outcome.state,\n summary,\n startedAt: status.startedAt,\n endedAt: now,\n ...(outcome.error ? { error: outcome.error } : {}),\n ...(this.prepared?.acceptance ? { acceptance: this.prepared.acceptance } : {}),\n };\n this.writeResultFile(status.runId, result);\n }\n}\n"],"mappings":"8gBAqHA,SAAS,EAAc,EAAuB,CAC5C,OAAOA,EAAK,KAAKC,EAAAA,mBAAmB,CAAE,GAAG,IAAQC,EAAAA,qBAAqB,CAIxE,MAAM,EAA2B,IAGjC,SAAS,EAAkB,EAAyB,CAElD,OADI,EAAQ,QAAU,EAAiC,EAChD,EAAQ,MAAM,EAAG,EAA2B,GAAyB,CAAG;wEAqCjF,SAAS,EAAe,EAAyB,CAE/C,OADI,aAAkB,MAAc,EAAO,QACpC,OAAO,GAAW,SAAW,EAAS,6CAUxC,IAAA,EAAA,KAAkD,CACvD,YACE,EAEA,CADiB,KAAA,aAAA,EAGnB,KAAwB,CACtB,OAAO,KAAK,KAAK,CAGnB,gBAA0B,EAAe,EAA6B,CACpE,EAAA,gBAAgB,EAAc,EAAM,CAAE,EAAO,CAG/C,SACA,oBAEA,cAAc,EAAgC,CAC5C,KAAK,SAAW,EAGlB,kBAAkB,EAAyC,CAIzD,MADA,MAAK,oBAAsB,EAAY,QAAU,KAAyB,EAAc,IAAA,GACjF,KAAK,oBAGd,eAAuB,EAAuD,CAiB5E,OAhBI,GAAS,OAAS,SACb,CAAE,MAAO,UAAW,QAAS,GAAO,MAAO,wBAAwB,EAAQ,OAAO,GAAI,CAE3F,GAAS,OAAS,oBACb,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,EAAQ,MAAM,QAAS,CAEtE,GAAS,OAAS,qBACb,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,EAAe,EAAQ,OAAO,CAAE,CAG/E,KAAK,SACA,CACL,MAAO,KAAK,SAAS,QAAU,KAAK,SAAS,QAAU,YAAc,UACrE,QAAS,KAAK,SAAS,QACxB,CAEI,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,sDAAuD,CAG1G,qBAAqB,EAAwB,EAA4C,CACvF,IAAM,EAAM,KAAK,KAAK,CAChB,EAAU,KAAK,eAAe,EAAQ,CAEtC,EAAU,KAAK,UAAU,SAAW,EAAQ,OAAS,eAE3D,EAAO,MAAQ,EAAQ,MACvB,EAAO,QAAU,EACjB,EAAO,WAAa,EACpB,EAAO,QAAU,EAAkB,EAAQ,CACvC,EAAQ,QAAO,EAAO,MAAQ,EAAQ,OACtC,KAAK,sBAAqB,EAAO,YAAc,KAAK,qBAKxD,KAAK,aAAa,eAAiB,GAAG,CAEtC,IAAM,EAAwB,CAC5B,MAAO,EAAO,MACd,MAAO,EAAO,MACd,QAAS,EAAQ,QACjB,MAAO,EAAQ,MACf,UACA,UAAW,EAAO,UAClB,QAAS,EACT,GAAI,EAAQ,MAAQ,CAAE,MAAO,EAAQ,MAAO,CAAG,EAAE,CACjD,GAAI,KAAK,UAAU,WAAa,CAAE,WAAY,KAAK,SAAS,WAAY,CAAG,EAAE,CAC9E,CACD,KAAK,gBAAgB,EAAO,MAAO,EAAO,qCA9EjC,kCAGDC,EAAAA,gBAAgB,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,3 @@
|
|
|
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{DOOM_TEAM_TYPES as a,init_types as o}from"../../container/types.mjs";import{__decorateMetadata as s,init_decorateMetadata as c}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as l,init_decorateParam as u}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentResultsDir as d,init_paths as f}from"../../shared/paths.mjs";import{RESULT_FILE_SUFFIX as p,init_resultWatcher as m}from"./resultWatcher.mjs";import*as h from"node:path";import{inject as g,injectable as _}from"inversify";function v(e){return h.join(d(),`${e}${p}`)}function y(e){return e.length<=x?e:e.slice(0,x-71)+S}function b(e){return e instanceof Error?e.message:typeof e==`string`?e:`Unhandled rejection with no error message.`}var x,S,C,w,T=e((()=>{t(),f(),o(),m(),c(),u(),i(),x=4e3,S=`
|
|
2
|
+
... [truncated for status.json; see the result file for the full text]`,C=4096,w=class{constructor(e){this.statusWriter=e}now(){return Date.now()}writeResultFile(e,t){n(v(e),t)}prepared;recordedSessionFile;prepareResult(e){this.prepared=e}recordSessionFile(e){return this.recordedSessionFile=e.length<=C?e:void 0,this.recordedSessionFile}resolveOutcome(e){return e?.kind===`signal`?{state:`stopped`,success:!1,error:`Terminated by signal ${e.signal}.`}:e?.kind===`uncaughtException`?{state:`failed`,success:!1,error:e.error.message}:e?.kind===`unhandledRejection`?{state:`failed`,success:!1,error:b(e.reason)}:this.prepared?{state:this.prepared.state??(this.prepared.success?`completed`:`failed`),success:this.prepared.success}:{state:`failed`,success:!1,error:`Run finalized without ever calling prepareResult().`}}mutateTerminalStatus(e,t){let n=this.now(),r=this.resolveOutcome(t),i=this.prepared?.summary??r.error??`(no summary)`;e.state=r.state,e.endedAt=n,e.lastUpdate=n,e.summary=y(i),r.error&&(e.error=r.error),this.recordedSessionFile&&(e.sessionFile=this.recordedSessionFile),this.statusWriter.updateSync(()=>{});let a={runId:e.runId,agent:e.agent,success:r.success,state:r.state,summary:i,startedAt:e.startedAt,endedAt:n,...r.error?{error:r.error}:{},...this.prepared?.acceptance?{acceptance:this.prepared.acceptance}:{}};this.writeResultFile(e.runId,a)}},w=r([_(),l(0,g(a.CoalescedStatusWriter)),s(`design:paramtypes`,[Object])],w)}));T();export{w as RunnerReporting,T as init_runnerReporting};
|
|
3
|
+
//# sourceMappingURL=runnerReporting.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerReporting.mjs","names":[],"sources":["../../../src/runs/background/runnerReporting.ts"],"sourcesContent":["/**\n * Owns the single, synchronous callback wired into\n * `TerminalPersistenceService.begin()`: it both finalizes the run's status\n * and writes the terminal result file `ResultWatcher` claims by rename.\n *\n * WHY BOTH WRITES LIVE IN ONE CALLBACK:\n * `TerminalPersistenceService`'s idempotency guarantee only covers what\n * happens inside that one callback - a second, independent write site for\n * the result file would be exactly the \"fourth, unguarded terminal write\"\n * shape that made the predecessor's finalization the worst bug in its\n * review (see `asyncExecution.ts`'s header for the full diagnosis). Putting\n * the result-file write here means it inherits the same at-most-once\n * guarantee the status write already has, from the same source, rather than\n * needing a second one invented for it.\n *\n * WRITE ORDER, DECIDED DELIBERATELY (status, then result):\n * - status terminal / result missing: `ResultWatcher` delivers nothing, and\n * `StaleRunReconciler` sees no result plus a dead process, so it repairs.\n * RECOVERABLE\n * - result present / status not terminal: `ResultWatcher` delivers, but\n * `StaleRunReconciler` sees a result exists and correctly leaves the run\n * alone (see that class's header), so the stale status persists forever.\n * NOT RECOVERABLE\n * So status must land on disk first. The subtlety: `mutateTerminalStatus` is\n * the MUTATOR `TerminalPersistenceService` passes to\n * `CoalescedStatusWriter.updateSync(mutator)`, and that writer's own flush\n * only happens AFTER the mutator returns - so mutating `status` in place and\n * then writing the result file, in that order, inside this one function,\n * would put the result file on disk BEFORE the status flush, exactly\n * backwards. The fix: call `this.statusWriter.updateSync(() => {})` - a\n * second, no-op-mutator call to the SAME writer, re-entered from inside the\n * first one - immediately after mutating `status`. `updateSync` increments\n * its dirty counter and flushes unconditionally on every call regardless of\n * what its own mutator did, so this forces the just-mutated `status` onto\n * disk right there, before this function does anything else. The one\n * (harmless) cost is that `TerminalPersistenceService`'s own flush, once\n * this callback returns, then flushes again - a redundant but correct\n * second write of the same, already-correct status.\n *\n * WHY A THROW FROM THE RESULT WRITE CANNOT UNDO THE STATUS FLUSH:\n * The forced flush above happens, completes, and returns before\n * `writeResultFile` is even called - not \"before it succeeds\", before it is\n * INVOKED at all. A throw from that call propagates up through\n * `TerminalPersistenceService.triggerFinalize`'s `try` (skipping\n * `clearCrashMarker()`, per that class's own documented \"leave the marker in\n * place\" behavior on a mutator throw) into its `finally` (which still kills\n * tracked children) - but the status write already happened and cannot be\n * un-happened by anything that runs after it.\n *\n * WHY THE RESULT SHAPE IS MINIMAL, WITH ONE DELIBERATE EXCEPTION:\n * Most of the predecessor's ~40 result fields (structured output, artifacts,\n * workflow graph, capability audit) depend on subsystems not fully composed\n * here yet; shipping a correct minimal contract now beats an unverifiable\n * rich one. The exception: `acceptance` (an `AcceptanceLedger`) IS included\n * when `prepareResult()` was given one, because `acceptance.ts` is fully\n * ported and tested in this package today - an agent that declared\n * acceptance criteria and had them evaluated would otherwise produce a\n * result that silently omits the verdict, which is the same failure shape\n * as `DeliverableGuard`'s policy being unobservable (see\n * `asyncExecution.ts`'s header on that).\n *\n * WHY `summary` IS ALSO WRITTEN TO `status.json`, TRUNCATED:\n * Chain mode (`extensions/spawnPlan.ts`) needs a completed step's summary\n * to resolve the next step's `{previous}`/`{outputs.name}` template, but the\n * only thing that ever reads a delivered `RunResultFile` is whoever is\n * registered as `ResultWatcher`'s ONE consumer slot (`CompletionNotifier`,\n * by design). A second reader competing for that single claim-by-rename\n * would either displace the registered consumer or race it - both wrong.\n * `status.json` has no such problem: it has exactly one writer (this\n * callback, via `CoalescedStatusWriter`), is never claimed or unlinked, and\n * `AsyncJobTracker`/`SubagentWaiter` already read it to learn a run\n * finished - chain mode reads a step's summary through the path it already\n * uses. The copy in `status.json` is TRUNCATED (`MAX_STATUS_SUMMARY_CHARS`):\n * status is a coalesced, rewritten-in-full state file, not a log, and an\n * unbounded field there is a size problem waiting to happen. The full,\n * untruncated text still goes to the result file, for anything that reads\n * that instead. `acceptance` is deliberately NOT duplicated into status:\n * nothing this package builds yet gates chain advancement on an acceptance\n * verdict (only on execution success - see `spawnPlan.ts`'s header), so\n * there is no reader for a second copy of it yet. Add it if and when one\n * exists, the same reasoning as everything else flagged \"not wired yet\" in\n * this package.\n *\n * WHY `sessionFile` IS RECORDED HERE TOO, AND BOUNDED BY OMISSION, NOT\n * TRUNCATION:\n * `action='resume'` needs to know which Pi session transcript a completed\n * run's own child was writing to, and the CHILD is the only party that ever\n * knows that - `childTranscript.ts`'s header says the PARENT does not learn\n * a child's session id until the run ends, which already rules out the\n * parent recording it. `recordSessionFile()` is `prepareResult()`'s sibling:\n * a value set from outside, read back inside this same terminal-write\n * callback, on the SAME `CoalescedStatusWriter`, so it lands in the SAME\n * flush as everything else `mutateTerminalStatus` computes - no second\n * writer, no second flush. Where it is called from is\n * `subagentPromptRuntime.ts`'s `session_start` handler, the one place in\n * the child process that receives `ExtensionContext.sessionManager` at all.\n * `summary`'s bound is a truncation with a marker, because a shortened\n * summary is still a readable, honest summary. A shortened PATH is not a\n * smaller path to the same file - it is a WRONG path to some other file, or\n * no file at all, and `resume` acting on it would silently target the wrong\n * transcript. So this bound is \"drop entirely past `MAX_SESSION_FILE_CHARS`,\n * never truncate\" - the field goes missing rather than becoming a plausible-\n * looking lie an eventual `resume` implementation would trust.\n */\n\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { currentResultsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { RESULT_FILE_SUFFIX, type RunResultFile } from './resultWatcher.ts';\nimport { type ICoalescedStatusWriter } from './statusWriter.ts';\nimport type { TerminalTrigger } from './terminalPersistence.ts';\nimport type { AcceptanceLedger } from '../types.ts';\n\nfunction resultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\n/** Generous but bounded - see the module header's summary-in-status section. */\nconst MAX_STATUS_SUMMARY_CHARS = 4_000;\nconst TRUNCATION_MARKER = '\\n... [truncated for status.json; see the result file for the full text]';\n\nfunction truncateForStatus(summary: string): string {\n if (summary.length <= MAX_STATUS_SUMMARY_CHARS) return summary;\n return summary.slice(0, MAX_STATUS_SUMMARY_CHARS - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;\n}\n\n/** Generous for any real filesystem path; see the module header's `sessionFile` section for why exceeding this drops the field instead of truncating it. */\nconst MAX_SESSION_FILE_CHARS = 4_096;\n\n/** What the run itself knows about its own outcome, recorded before a trigger fires. */\nexport interface RunnerResultInput {\n success: boolean;\n summary: string;\n /** Explicit terminal state for controlled outcomes such as an operator stop. */\n state?: 'completed' | 'failed' | 'stopped';\n acceptance?: AcceptanceLedger;\n}\n\nexport interface IRunnerReporting {\n /**\n * Record what to report if/when this run reaches a terminal state. Call\n * this before triggering finalize (normal completion, or right before an\n * interrupt/stop/timeout handler calls `terminalPersistence.finalize()`).\n * A trigger that fires with nothing prepared (a signal or exception mid-run)\n * still produces a result - see `mutateTerminalStatus`'s fallback.\n */\n prepareResult(input: RunnerResultInput): void;\n /**\n * Record this run's own Pi session transcript path, if/when the child\n * learns it. Call this as soon as it is known (`subagentPromptRuntime.ts`'s\n * `session_start` handler, today). The accepted value is returned for the\n * bootstrap status write, then retained here for the terminal status write.\n * See the module header's `sessionFile` section for why a too-long value is\n * dropped, not truncated.\n */\n recordSessionFile(sessionFile: string): string | undefined;\n /** The callback to wire into `TerminalPersistenceService.begin()`. See the module header for what it does and why, in this order. */\n mutateTerminalStatus(status: AsyncRunStatus, trigger: TerminalTrigger | undefined): void;\n}\n\nfunction describeReason(reason: unknown): string {\n if (reason instanceof Error) return reason.message;\n return typeof reason === 'string' ? reason : 'Unhandled rejection with no error message.';\n}\n\ninterface TerminalOutcome {\n state: AsyncRunStatus['state'];\n success: boolean;\n error?: string;\n}\n\n@injectable()\nexport class RunnerReporting implements IRunnerReporting {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter)\n private readonly statusWriter: ICoalescedStatusWriter<AsyncRunStatus>,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n protected writeResultFile(runId: string, result: RunResultFile): void {\n writeAtomicJson(resultPathFor(runId), result);\n }\n\n private prepared: RunnerResultInput | undefined;\n private recordedSessionFile: string | undefined;\n\n prepareResult(input: RunnerResultInput): void {\n this.prepared = input;\n }\n\n recordSessionFile(sessionFile: string): string | undefined {\n // Drop, don't truncate, past the bound - see the module header's\n // `sessionFile` section for why a shortened path is worse than no path.\n this.recordedSessionFile = sessionFile.length <= MAX_SESSION_FILE_CHARS ? sessionFile : undefined;\n return this.recordedSessionFile;\n }\n\n private resolveOutcome(trigger: TerminalTrigger | undefined): TerminalOutcome {\n if (trigger?.kind === 'signal') {\n return { state: 'stopped', success: false, error: `Terminated by signal ${trigger.signal}.` };\n }\n if (trigger?.kind === 'uncaughtException') {\n return { state: 'failed', success: false, error: trigger.error.message };\n }\n if (trigger?.kind === 'unhandledRejection') {\n return { state: 'failed', success: false, error: describeReason(trigger.reason) };\n }\n // trigger === undefined: the run's own explicit finalize() call.\n if (this.prepared) {\n return {\n state: this.prepared.state ?? (this.prepared.success ? 'completed' : 'failed'),\n success: this.prepared.success,\n };\n }\n return { state: 'failed', success: false, error: 'Run finalized without ever calling prepareResult().' };\n }\n\n mutateTerminalStatus(status: AsyncRunStatus, trigger: TerminalTrigger | undefined): void {\n const now = this.now();\n const outcome = this.resolveOutcome(trigger);\n\n const summary = this.prepared?.summary ?? outcome.error ?? '(no summary)';\n\n status.state = outcome.state;\n status.endedAt = now;\n status.lastUpdate = now;\n status.summary = truncateForStatus(summary);\n if (outcome.error) status.error = outcome.error;\n if (this.recordedSessionFile) status.sessionFile = this.recordedSessionFile;\n\n // Forces the mutation above onto disk before anything else in this\n // function runs. See the module header's WRITE ORDER section for why\n // this specific call, at this specific point, is what achieves that.\n this.statusWriter.updateSync(() => {});\n\n const result: RunResultFile = {\n runId: status.runId,\n agent: status.agent,\n success: outcome.success,\n state: outcome.state,\n summary,\n startedAt: status.startedAt,\n endedAt: now,\n ...(outcome.error ? { error: outcome.error } : {}),\n ...(this.prepared?.acceptance ? { acceptance: this.prepared.acceptance } : {}),\n };\n this.writeResultFile(status.runId, result);\n }\n}\n"],"mappings":"u0BAqHA,SAAS,EAAc,EAAuB,CAC5C,OAAO,EAAK,KAAK,GAAmB,CAAE,GAAG,IAAQ,IAAqB,CAOxE,SAAS,EAAkB,EAAyB,CAElD,OADI,EAAQ,QAAU,EAAiC,EAChD,EAAQ,MAAM,EAAG,EAA2B,GAAyB,CAAG,EAqCjF,SAAS,EAAe,EAAyB,CAE/C,OADI,aAAkB,MAAc,EAAO,QACpC,OAAO,GAAW,SAAW,EAAS,sEA1Dc,IACH,IACC,IAEiB,aAUtE,EAA2B,IAC3B,EAAoB;wEAQpB,EAAyB,OA6CxB,KAAkD,CACvD,YACE,EAEA,CADiB,KAAA,aAAA,EAGnB,KAAwB,CACtB,OAAO,KAAK,KAAK,CAGnB,gBAA0B,EAAe,EAA6B,CACpE,EAAgB,EAAc,EAAM,CAAE,EAAO,CAG/C,SACA,oBAEA,cAAc,EAAgC,CAC5C,KAAK,SAAW,EAGlB,kBAAkB,EAAyC,CAIzD,MADA,MAAK,oBAAsB,EAAY,QAAU,EAAyB,EAAc,IAAA,GACjF,KAAK,oBAGd,eAAuB,EAAuD,CAiB5E,OAhBI,GAAS,OAAS,SACb,CAAE,MAAO,UAAW,QAAS,GAAO,MAAO,wBAAwB,EAAQ,OAAO,GAAI,CAE3F,GAAS,OAAS,oBACb,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,EAAQ,MAAM,QAAS,CAEtE,GAAS,OAAS,qBACb,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,EAAe,EAAQ,OAAO,CAAE,CAG/E,KAAK,SACA,CACL,MAAO,KAAK,SAAS,QAAU,KAAK,SAAS,QAAU,YAAc,UACrE,QAAS,KAAK,SAAS,QACxB,CAEI,CAAE,MAAO,SAAU,QAAS,GAAO,MAAO,sDAAuD,CAG1G,qBAAqB,EAAwB,EAA4C,CACvF,IAAM,EAAM,KAAK,KAAK,CAChB,EAAU,KAAK,eAAe,EAAQ,CAEtC,EAAU,KAAK,UAAU,SAAW,EAAQ,OAAS,eAE3D,EAAO,MAAQ,EAAQ,MACvB,EAAO,QAAU,EACjB,EAAO,WAAa,EACpB,EAAO,QAAU,EAAkB,EAAQ,CACvC,EAAQ,QAAO,EAAO,MAAQ,EAAQ,OACtC,KAAK,sBAAqB,EAAO,YAAc,KAAK,qBAKxD,KAAK,aAAa,eAAiB,GAAG,CAEtC,IAAM,EAAwB,CAC5B,MAAO,EAAO,MACd,MAAO,EAAO,MACd,QAAS,EAAQ,QACjB,MAAO,EAAQ,MACf,UACA,UAAW,EAAO,UAClB,QAAS,EACT,GAAI,EAAQ,MAAQ,CAAE,MAAO,EAAQ,MAAO,CAAG,EAAE,CACjD,GAAI,KAAK,UAAU,WAAa,CAAE,WAAY,KAAK,SAAS,WAAY,CAAG,EAAE,CAC9E,CACD,KAAK,gBAAgB,EAAO,MAAO,EAAO,QA9E7C,GAAY,KAGR,EAAO,EAAgB,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
require(`../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),t=require(`../../intercom/nativeTeamChannel.cjs`);let n=require(`inversify`),r=class{readRoot(){return t.readNativeTeamRootFromEnvironment()}register(e){return t.registerNativeTeamMember(e)}};r=e.__decorate([(0,n.injectable)()],r),Object.defineProperty(exports,`RunnerTeamMembership`,{enumerable:!0,get:function(){return r}});
|
|
2
|
+
//# sourceMappingURL=runnerTeamMembership.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerTeamMembership.cjs","names":["readNativeTeamRootFromEnvironment","registerNativeTeamMember"],"sources":["../../../src/runs/background/runnerTeamMembership.ts"],"sourcesContent":["/**\n * Thin composition seam over `nativeTeamChannel.ts`'s registration\n * functions, the same shape as `RunnerRevivalLease` over `session-lease.ts`:\n * no logic of its own, just an injectable wrapper so `RunnerBootstrap` can be\n * tested without writing real team files to disk.\n *\n * ERROR POLICY LIVES IN THE CALLER, NOT HERE:\n * `register()` throws exactly what `registerNativeTeamMember()` throws;\n * whether that is fatal to the run is `RunnerBootstrap`'s decision (it is\n * not - see that class's header for why team membership is additive), not\n * this wrapper's.\n */\n\nimport { injectable } from 'inversify';\nimport {\n readNativeTeamRootFromEnvironment,\n registerNativeTeamMember,\n type RegisterNativeTeamMemberInput,\n type TeamMemberContext,\n type TeamRootContext,\n} from '../../intercom/nativeTeamChannel.ts';\n\nexport interface RunnerTeamRegistration {\n context: TeamMemberContext;\n dispose: () => void;\n}\n\nexport interface IRunnerTeamMembership {\n /** This process's own team root context, if one was forwarded by whatever spawned it. `undefined` when this run is not part of a team. */\n readRoot(): TeamRootContext | undefined;\n /** Join the team as a new member. Throws on failure - see `RunnerBootstrap` for what it does with that. */\n register(input: RegisterNativeTeamMemberInput): RunnerTeamRegistration;\n}\n\n@injectable()\nexport class RunnerTeamMembership implements IRunnerTeamMembership {\n readRoot(): TeamRootContext | undefined {\n return readNativeTeamRootFromEnvironment();\n }\n\n register(input: RegisterNativeTeamMemberInput): RunnerTeamRegistration {\n return registerNativeTeamMember(input);\n }\n}\n"],"mappings":"kNAmCO,EAAA,KAA4D,CACjE,UAAwC,CACtC,OAAOA,EAAAA,mCAAmC,CAG5C,SAAS,EAA8D,CACrE,OAAOC,EAAAA,yBAAyB,EAAM,qCAP7B,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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{init_nativeTeamChannel as r,readNativeTeamRootFromEnvironment as i,registerNativeTeamMember as a}from"../../intercom/nativeTeamChannel.mjs";import{injectable as o}from"inversify";var s,c=e((()=>{r(),n(),s=class{readRoot(){return i()}register(e){return a(e)}},s=t([o()],s)}));c();export{s as RunnerTeamMembership,c as init_runnerTeamMembership};
|
|
2
|
+
//# sourceMappingURL=runnerTeamMembership.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerTeamMembership.mjs","names":[],"sources":["../../../src/runs/background/runnerTeamMembership.ts"],"sourcesContent":["/**\n * Thin composition seam over `nativeTeamChannel.ts`'s registration\n * functions, the same shape as `RunnerRevivalLease` over `session-lease.ts`:\n * no logic of its own, just an injectable wrapper so `RunnerBootstrap` can be\n * tested without writing real team files to disk.\n *\n * ERROR POLICY LIVES IN THE CALLER, NOT HERE:\n * `register()` throws exactly what `registerNativeTeamMember()` throws;\n * whether that is fatal to the run is `RunnerBootstrap`'s decision (it is\n * not - see that class's header for why team membership is additive), not\n * this wrapper's.\n */\n\nimport { injectable } from 'inversify';\nimport {\n readNativeTeamRootFromEnvironment,\n registerNativeTeamMember,\n type RegisterNativeTeamMemberInput,\n type TeamMemberContext,\n type TeamRootContext,\n} from '../../intercom/nativeTeamChannel.ts';\n\nexport interface RunnerTeamRegistration {\n context: TeamMemberContext;\n dispose: () => void;\n}\n\nexport interface IRunnerTeamMembership {\n /** This process's own team root context, if one was forwarded by whatever spawned it. `undefined` when this run is not part of a team. */\n readRoot(): TeamRootContext | undefined;\n /** Join the team as a new member. Throws on failure - see `RunnerBootstrap` for what it does with that. */\n register(input: RegisterNativeTeamMemberInput): RunnerTeamRegistration;\n}\n\n@injectable()\nexport class RunnerTeamMembership implements IRunnerTeamMembership {\n readRoot(): TeamRootContext | undefined {\n return readNativeTeamRootFromEnvironment();\n }\n\n register(input: RegisterNativeTeamMemberInput): RunnerTeamRegistration {\n return registerNativeTeamMember(input);\n }\n}\n"],"mappings":"+XAoB6C,OAetC,KAA4D,CACjE,UAAwC,CACtC,OAAO,GAAmC,CAG5C,SAAS,EAA8D,CACrE,OAAO,EAAyB,EAAM,QAPzC,GAAY,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`);let n=require(`node:fs`);n=e.__toESM(n,1);let r=require(`node:path`);r=e.__toESM(r,1);let i=require(`inversify`),a=class{pollIntervalMs=20;defaultTimeoutMs=1e4;readFile(e){try{return n.readFileSync(e,`utf-8`)}catch{return}}watchDirectory(e,t){try{let r=n.watch(e,()=>t());return r.on(`error`,()=>t()),r}catch{return}}readOutcome(e){let t=this.readFile(e);if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}if(typeof n!=`object`||!n)return;let r=n.state;if(r===`ready`)return{status:`signalled`};if(r===`error`){let e=n.error;return{status:`failed`,error:typeof e==`string`?e:`Async runner reported an error.`}}}waitForHandshake(e){let t=e.timeoutMs??this.defaultTimeoutMs,n=!1,i,a,o,s,c=()=>{i&&clearInterval(i),a&&clearTimeout(a),o?.close(),i=void 0,a=void 0,o=void 0};return{promise:new Promise((l,u)=>{s=u;let d=e=>{n||(n=!0,c(),l(e))},f=()=>{if(n)return;let t=this.readOutcome(e.path);t&&d(t)};a=setTimeout(()=>d({status:`timed-out`}),t),a.unref?.(),o=this.watchDirectory(r.dirname(e.path),f),i=setInterval(f,this.pollIntervalMs),i.unref?.(),f()}),cancel:e=>{n||(n=!0,c(),s?.(Error(e)))}}}};a=t.__decorate([(0,i.injectable)()],a),Object.defineProperty(exports,`SpawnHandshake`,{enumerable:!0,get:function(){return a}});
|
|
2
|
+
//# sourceMappingURL=spawnHandshake.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawnHandshake.cjs","names":["fs","path"],"sources":["../../../src/runs/background/spawnHandshake.ts"],"sourcesContent":["/**\n * Promise-based wait for a spawned child to signal that it has started.\n *\n * WHY THIS EXISTS:\n * The predecessor package waits for this same signal with `Atomics.wait`\n * (`doom-pi-subagents/src/runs/background/asyncExecution.ts:376-380`), which\n * blocks the parent's event loop for up to 10 seconds. That froze the whole TUI.\n * It was survivable when background spawns were opt-in; this package is\n * async-only, so every spawn would pay it. This module reads the same\n * readiness signal (a JSON file the child writes, `{ state: 'ready' }` or\n * `{ state: 'error', error }`) but never blocks the event loop to wait for it.\n *\n * WATCH-FIRST, POLL AS A SAFETY NET:\n * Same pattern as `PollScheduler`'s header, for the same reason: `fs.watch` is\n * the fast path, a ~20ms poll is the safety net for the platforms and mount\n * types where watches are unreliable or do not fire, and neither path is\n * trusted alone.\n *\n * WHY THIS DOES NOT REGISTER WITH `PollScheduler`:\n * `PollScheduler` exists to let background housekeeping (a handful of\n * long-lived subscribers: channel polling, status writing) back off toward\n * multi-second intervals while idle. A handshake wait is the opposite shape: a\n * per-spawn, short-lived (bounded by `timeoutMs`, default 10s), low-latency\n * wait that specifically must NOT inherit the scheduler's idle backoff. Sharing\n * the master tick would force a choice between two bad options: either every\n * concurrent handshake wait keeps the whole scheduler pinned near its floor\n * (so unrelated housekeeping never gets to back off while any spawn is\n * starting), or the handshake is checked only as often as the scheduler's\n * current backed-off interval allows (which defeats the ~20ms safety net this\n * requirement exists for). A dedicated, self-terminating timer avoids coupling\n * spawn concurrency to the cadence of unrelated background work.\n *\n * DESIGN PATTERNS:\n * - `waitForHandshake` returns `{ promise, cancel }` rather than only a promise.\n * `cancel` is a real exit path, not a convenience: an orchestrator that sees\n * the child process itself exit before signalling should be able to reject\n * immediately rather than waiting out the remaining timeout doing nothing\n * - The three terminal outcomes a caller actually wants to distinguish\n * (`signalled`, `failed`, `timed-out`) are all resolutions, not rejections,\n * so a plain timeout does not force every caller into a try/catch. `cancel`\n * is the one path that rejects, because it is the caller abandoning the wait\n * for a reason of its own, not an outcome of the handshake itself\n * - One `cleanup` function is the only place that clears the timers and closes\n * the watcher, and every terminal path (signal, failure, timeout, cancel)\n * goes through it, so there is exactly one place a leak could be introduced\n * - Reading the handshake file behind protected seams rather than importing\n * `node:fs` calls directly into tests: ESM module namespaces are frozen, so\n * `vi.spyOn(fs, ...)` throws, and a seam is the documented way around that\n *\n * AVOID:\n * - Treating a JSON parse failure on the handshake file as a definite outcome;\n * the caller writes it with a rename-based atomic write, so a failure here\n * means this check landed mid-write, not that the child failed. It must be\n * treated as \"still pending\" and retried on the next check\n * - Letting `watchDirectory` throwing propagate; a platform or mount that\n * cannot support `fs.watch` is exactly the case the poll safety net exists\n * for, so watch setup failing must fall through to poll-only, not abort\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\n/** Poll safety net cadence. Watches are the fast path; this bounds the worst case. */\nconst DEFAULT_POLL_INTERVAL_MS = 20;\n/** Matches the predecessor's `RUNNER_STARTUP_TIMEOUT_MS`: this is the wire protocol's known-good bound. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\nexport interface SpawnHandshakeOptions {\n /** Absolute path of the file the child writes when it starts (or fails to). */\n path: string;\n /** Upper bound on how long to wait before resolving `timed-out`. Defaults to 10s. */\n timeoutMs?: number;\n}\n\nexport type SpawnHandshakeOutcome =\n | { status: 'signalled' }\n | { status: 'failed'; error: string }\n | { status: 'timed-out' };\n\nexport interface SpawnHandshakeWait {\n promise: Promise<SpawnHandshakeOutcome>;\n /** Reject early and tear down. For a caller that already knows waiting further is pointless. */\n cancel: (reason: string) => void;\n}\n\nexport interface ISpawnHandshake {\n waitForHandshake(options: SpawnHandshakeOptions): SpawnHandshakeWait;\n}\n\n/** Minimal surface this module needs from a `fs.FSWatcher`, so a test seam does not need the real type. */\ninterface DirectoryWatch {\n close: () => void;\n}\n\n@injectable()\nexport class SpawnHandshake implements ISpawnHandshake {\n /**\n * Seams for tests, as protected members rather than constructor arguments.\n * The container resolves this service by type, and inversify cannot supply a\n * primitive like a poll interval, so a test subclass overrides these instead.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly defaultTimeoutMs: number = DEFAULT_TIMEOUT_MS;\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch {\n // Missing (not yet written) or transiently unreadable: keep waiting.\n return undefined;\n }\n }\n\n /**\n * Watches the handshake file's directory, not the file itself: the file does\n * not exist yet when the wait starts, and `fs.watch` on a path that does not\n * exist throws on most platforms. Returns `undefined` when watching could not\n * be set up at all, which the caller treats as \"poll-only for this wait\".\n */\n protected watchDirectory(dirPath: string, onEvent: () => void): DirectoryWatch | undefined {\n try {\n const watcher = fs.watch(dirPath, () => onEvent());\n // An unhandled 'error' event on an FSWatcher crashes the process; this\n // wait has a poll safety net regardless, so a watch error just means\n // falling back to it rather than losing the wait entirely.\n watcher.on('error', () => onEvent());\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n /** Interpret the handshake file's contents, or `undefined` when the outcome is not decided yet. */\n private readOutcome(filePath: string): SpawnHandshakeOutcome | undefined {\n const raw = this.readFile(filePath);\n if (raw === undefined) return undefined;\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n // A rename-based atomic write should never be readable mid-write, but a\n // torn read is cheaper to tolerate than to rule out; the next check retries.\n return undefined;\n }\n if (typeof payload !== 'object' || payload === null) return undefined;\n const state = (payload as { state?: unknown }).state;\n if (state === 'ready') return { status: 'signalled' };\n if (state === 'error') {\n const error = (payload as { error?: unknown }).error;\n return { status: 'failed', error: typeof error === 'string' ? error : 'Async runner reported an error.' };\n }\n return undefined;\n }\n\n waitForHandshake(options: SpawnHandshakeOptions): SpawnHandshakeWait {\n const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;\n let settled = false;\n let pollTimer: ReturnType<typeof setInterval> | undefined;\n let timeoutTimer: ReturnType<typeof setTimeout> | undefined;\n let watcher: DirectoryWatch | undefined;\n let rejectWait: ((reason: unknown) => void) | undefined;\n\n const cleanup = (): void => {\n if (pollTimer) clearInterval(pollTimer);\n if (timeoutTimer) clearTimeout(timeoutTimer);\n watcher?.close();\n pollTimer = undefined;\n timeoutTimer = undefined;\n watcher = undefined;\n };\n\n const promise = new Promise<SpawnHandshakeOutcome>((resolve, reject) => {\n rejectWait = reject;\n\n const settle = (outcome: SpawnHandshakeOutcome): void => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(outcome);\n };\n\n const checkNow = (): void => {\n if (settled) return;\n const outcome = this.readOutcome(options.path);\n if (outcome) settle(outcome);\n };\n\n timeoutTimer = setTimeout(() => settle({ status: 'timed-out' }), timeoutMs);\n timeoutTimer.unref?.();\n\n watcher = this.watchDirectory(path.dirname(options.path), checkNow);\n\n pollTimer = setInterval(checkNow, this.pollIntervalMs);\n pollTimer.unref?.();\n\n // The file may already exist (a very fast child, or a wait that started\n // late), so the first check happens immediately rather than waiting out\n // either the poll interval or the first watch event.\n checkNow();\n });\n\n const cancel = (reason: string): void => {\n if (settled) return;\n settled = true;\n cleanup();\n rejectWait?.(new Error(reason));\n };\n\n return { promise, cancel };\n }\n}\n"],"mappings":"wPAgGO,EAAA,KAAgD,CAMrD,eAA4C,GAC5C,iBAA8C,IAE9C,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAOA,EAAG,aAAa,EAAU,QAAQ,MACnC,CAEN,QAUJ,eAAyB,EAAiB,EAAiD,CACzF,GAAI,CACF,IAAM,EAAUA,EAAG,MAAM,MAAe,GAAS,CAAC,CAKlD,OADA,EAAQ,GAAG,YAAe,GAAS,CAAC,CAC7B,OACD,CACN,QAKJ,YAAoB,EAAqD,CACvE,IAAM,EAAM,KAAK,SAAS,EAAS,CACnC,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAU,KAAK,MAAM,EAAI,MACnB,CAGN,OAEF,GAAI,OAAO,GAAY,WAAY,EAAkB,OACrD,IAAM,EAAS,EAAgC,MAC/C,GAAI,IAAU,QAAS,MAAO,CAAE,OAAQ,YAAa,CACrD,GAAI,IAAU,QAAS,CACrB,IAAM,EAAS,EAAgC,MAC/C,MAAO,CAAE,OAAQ,SAAU,MAAO,OAAO,GAAU,SAAW,EAAQ,kCAAmC,EAK7G,iBAAiB,EAAoD,CACnE,IAAM,EAAY,EAAQ,WAAa,KAAK,iBACxC,EAAU,GACV,EACA,EACA,EACA,EAEE,MAAsB,CACtB,GAAW,cAAc,EAAU,CACnC,GAAc,aAAa,EAAa,CAC5C,GAAS,OAAO,CAChB,EAAY,IAAA,GACZ,EAAe,IAAA,GACf,EAAU,IAAA,IAwCZ,MAAO,CAAE,YArCW,SAAgC,EAAS,IAAW,CACtE,EAAa,EAEb,IAAM,EAAU,GAAyC,CACnD,IACJ,EAAU,GACV,GAAS,CACT,EAAQ,EAAQ,GAGZ,MAAuB,CAC3B,GAAI,EAAS,OACb,IAAM,EAAU,KAAK,YAAY,EAAQ,KAAK,CAC1C,GAAS,EAAO,EAAQ,EAG9B,EAAe,eAAiB,EAAO,CAAE,OAAQ,YAAa,CAAC,CAAE,EAAU,CAC3E,EAAa,SAAS,CAEtB,EAAU,KAAK,eAAeC,EAAK,QAAQ,EAAQ,KAAK,CAAE,EAAS,CAEnE,EAAY,YAAY,EAAU,KAAK,eAAe,CACtD,EAAU,SAAS,CAKnB,GAAU,EAUH,CAAS,OAPF,GAAyB,CACnC,IACJ,EAAU,GACV,GAAS,CACT,IAAiB,MAAM,EAAO,CAAC,GAGP,qCAlHjB,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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*as r from"node:fs";import*as i from"node:path";import{injectable as a}from"inversify";var o,s,c,l=e((()=>{n(),o=20,s=1e4,c=class{pollIntervalMs=o;defaultTimeoutMs=s;readFile(e){try{return r.readFileSync(e,`utf-8`)}catch{return}}watchDirectory(e,t){try{let n=r.watch(e,()=>t());return n.on(`error`,()=>t()),n}catch{return}}readOutcome(e){let t=this.readFile(e);if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}if(typeof n!=`object`||!n)return;let r=n.state;if(r===`ready`)return{status:`signalled`};if(r===`error`){let e=n.error;return{status:`failed`,error:typeof e==`string`?e:`Async runner reported an error.`}}}waitForHandshake(e){let t=e.timeoutMs??this.defaultTimeoutMs,n=!1,r,a,o,s,c=()=>{r&&clearInterval(r),a&&clearTimeout(a),o?.close(),r=void 0,a=void 0,o=void 0};return{promise:new Promise((l,u)=>{s=u;let d=e=>{n||(n=!0,c(),l(e))},f=()=>{if(n)return;let t=this.readOutcome(e.path);t&&d(t)};a=setTimeout(()=>d({status:`timed-out`}),t),a.unref?.(),o=this.watchDirectory(i.dirname(e.path),f),r=setInterval(f,this.pollIntervalMs),r.unref?.(),f()}),cancel:e=>{n||(n=!0,c(),s?.(Error(e)))}}}},c=t([a()],c)}));l();export{c as SpawnHandshake,l as init_spawnHandshake};
|
|
2
|
+
//# sourceMappingURL=spawnHandshake.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawnHandshake.mjs","names":[],"sources":["../../../src/runs/background/spawnHandshake.ts"],"sourcesContent":["/**\n * Promise-based wait for a spawned child to signal that it has started.\n *\n * WHY THIS EXISTS:\n * The predecessor package waits for this same signal with `Atomics.wait`\n * (`doom-pi-subagents/src/runs/background/asyncExecution.ts:376-380`), which\n * blocks the parent's event loop for up to 10 seconds. That froze the whole TUI.\n * It was survivable when background spawns were opt-in; this package is\n * async-only, so every spawn would pay it. This module reads the same\n * readiness signal (a JSON file the child writes, `{ state: 'ready' }` or\n * `{ state: 'error', error }`) but never blocks the event loop to wait for it.\n *\n * WATCH-FIRST, POLL AS A SAFETY NET:\n * Same pattern as `PollScheduler`'s header, for the same reason: `fs.watch` is\n * the fast path, a ~20ms poll is the safety net for the platforms and mount\n * types where watches are unreliable or do not fire, and neither path is\n * trusted alone.\n *\n * WHY THIS DOES NOT REGISTER WITH `PollScheduler`:\n * `PollScheduler` exists to let background housekeeping (a handful of\n * long-lived subscribers: channel polling, status writing) back off toward\n * multi-second intervals while idle. A handshake wait is the opposite shape: a\n * per-spawn, short-lived (bounded by `timeoutMs`, default 10s), low-latency\n * wait that specifically must NOT inherit the scheduler's idle backoff. Sharing\n * the master tick would force a choice between two bad options: either every\n * concurrent handshake wait keeps the whole scheduler pinned near its floor\n * (so unrelated housekeeping never gets to back off while any spawn is\n * starting), or the handshake is checked only as often as the scheduler's\n * current backed-off interval allows (which defeats the ~20ms safety net this\n * requirement exists for). A dedicated, self-terminating timer avoids coupling\n * spawn concurrency to the cadence of unrelated background work.\n *\n * DESIGN PATTERNS:\n * - `waitForHandshake` returns `{ promise, cancel }` rather than only a promise.\n * `cancel` is a real exit path, not a convenience: an orchestrator that sees\n * the child process itself exit before signalling should be able to reject\n * immediately rather than waiting out the remaining timeout doing nothing\n * - The three terminal outcomes a caller actually wants to distinguish\n * (`signalled`, `failed`, `timed-out`) are all resolutions, not rejections,\n * so a plain timeout does not force every caller into a try/catch. `cancel`\n * is the one path that rejects, because it is the caller abandoning the wait\n * for a reason of its own, not an outcome of the handshake itself\n * - One `cleanup` function is the only place that clears the timers and closes\n * the watcher, and every terminal path (signal, failure, timeout, cancel)\n * goes through it, so there is exactly one place a leak could be introduced\n * - Reading the handshake file behind protected seams rather than importing\n * `node:fs` calls directly into tests: ESM module namespaces are frozen, so\n * `vi.spyOn(fs, ...)` throws, and a seam is the documented way around that\n *\n * AVOID:\n * - Treating a JSON parse failure on the handshake file as a definite outcome;\n * the caller writes it with a rename-based atomic write, so a failure here\n * means this check landed mid-write, not that the child failed. It must be\n * treated as \"still pending\" and retried on the next check\n * - Letting `watchDirectory` throwing propagate; a platform or mount that\n * cannot support `fs.watch` is exactly the case the poll safety net exists\n * for, so watch setup failing must fall through to poll-only, not abort\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\n/** Poll safety net cadence. Watches are the fast path; this bounds the worst case. */\nconst DEFAULT_POLL_INTERVAL_MS = 20;\n/** Matches the predecessor's `RUNNER_STARTUP_TIMEOUT_MS`: this is the wire protocol's known-good bound. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\nexport interface SpawnHandshakeOptions {\n /** Absolute path of the file the child writes when it starts (or fails to). */\n path: string;\n /** Upper bound on how long to wait before resolving `timed-out`. Defaults to 10s. */\n timeoutMs?: number;\n}\n\nexport type SpawnHandshakeOutcome =\n | { status: 'signalled' }\n | { status: 'failed'; error: string }\n | { status: 'timed-out' };\n\nexport interface SpawnHandshakeWait {\n promise: Promise<SpawnHandshakeOutcome>;\n /** Reject early and tear down. For a caller that already knows waiting further is pointless. */\n cancel: (reason: string) => void;\n}\n\nexport interface ISpawnHandshake {\n waitForHandshake(options: SpawnHandshakeOptions): SpawnHandshakeWait;\n}\n\n/** Minimal surface this module needs from a `fs.FSWatcher`, so a test seam does not need the real type. */\ninterface DirectoryWatch {\n close: () => void;\n}\n\n@injectable()\nexport class SpawnHandshake implements ISpawnHandshake {\n /**\n * Seams for tests, as protected members rather than constructor arguments.\n * The container resolves this service by type, and inversify cannot supply a\n * primitive like a poll interval, so a test subclass overrides these instead.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly defaultTimeoutMs: number = DEFAULT_TIMEOUT_MS;\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch {\n // Missing (not yet written) or transiently unreadable: keep waiting.\n return undefined;\n }\n }\n\n /**\n * Watches the handshake file's directory, not the file itself: the file does\n * not exist yet when the wait starts, and `fs.watch` on a path that does not\n * exist throws on most platforms. Returns `undefined` when watching could not\n * be set up at all, which the caller treats as \"poll-only for this wait\".\n */\n protected watchDirectory(dirPath: string, onEvent: () => void): DirectoryWatch | undefined {\n try {\n const watcher = fs.watch(dirPath, () => onEvent());\n // An unhandled 'error' event on an FSWatcher crashes the process; this\n // wait has a poll safety net regardless, so a watch error just means\n // falling back to it rather than losing the wait entirely.\n watcher.on('error', () => onEvent());\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n /** Interpret the handshake file's contents, or `undefined` when the outcome is not decided yet. */\n private readOutcome(filePath: string): SpawnHandshakeOutcome | undefined {\n const raw = this.readFile(filePath);\n if (raw === undefined) return undefined;\n let payload: unknown;\n try {\n payload = JSON.parse(raw);\n } catch {\n // A rename-based atomic write should never be readable mid-write, but a\n // torn read is cheaper to tolerate than to rule out; the next check retries.\n return undefined;\n }\n if (typeof payload !== 'object' || payload === null) return undefined;\n const state = (payload as { state?: unknown }).state;\n if (state === 'ready') return { status: 'signalled' };\n if (state === 'error') {\n const error = (payload as { error?: unknown }).error;\n return { status: 'failed', error: typeof error === 'string' ? error : 'Async runner reported an error.' };\n }\n return undefined;\n }\n\n waitForHandshake(options: SpawnHandshakeOptions): SpawnHandshakeWait {\n const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;\n let settled = false;\n let pollTimer: ReturnType<typeof setInterval> | undefined;\n let timeoutTimer: ReturnType<typeof setTimeout> | undefined;\n let watcher: DirectoryWatch | undefined;\n let rejectWait: ((reason: unknown) => void) | undefined;\n\n const cleanup = (): void => {\n if (pollTimer) clearInterval(pollTimer);\n if (timeoutTimer) clearTimeout(timeoutTimer);\n watcher?.close();\n pollTimer = undefined;\n timeoutTimer = undefined;\n watcher = undefined;\n };\n\n const promise = new Promise<SpawnHandshakeOutcome>((resolve, reject) => {\n rejectWait = reject;\n\n const settle = (outcome: SpawnHandshakeOutcome): void => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(outcome);\n };\n\n const checkNow = (): void => {\n if (settled) return;\n const outcome = this.readOutcome(options.path);\n if (outcome) settle(outcome);\n };\n\n timeoutTimer = setTimeout(() => settle({ status: 'timed-out' }), timeoutMs);\n timeoutTimer.unref?.();\n\n watcher = this.watchDirectory(path.dirname(options.path), checkNow);\n\n pollTimer = setInterval(checkNow, this.pollIntervalMs);\n pollTimer.unref?.();\n\n // The file may already exist (a very fast child, or a wait that started\n // late), so the first check happens immediately rather than waiting out\n // either the poll interval or the first watch event.\n checkNow();\n });\n\n const cancel = (reason: string): void => {\n if (settled) return;\n settled = true;\n cleanup();\n rejectWait?.(new Error(reason));\n };\n\n return { promise, cancel };\n }\n}\n"],"mappings":"uSAgEM,EAA2B,GAE3B,EAAqB,MA8BpB,KAAgD,CAMrD,eAA4C,EAC5C,iBAA8C,EAE9C,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAO,EAAG,aAAa,EAAU,QAAQ,MACnC,CAEN,QAUJ,eAAyB,EAAiB,EAAiD,CACzF,GAAI,CACF,IAAM,EAAU,EAAG,MAAM,MAAe,GAAS,CAAC,CAKlD,OADA,EAAQ,GAAG,YAAe,GAAS,CAAC,CAC7B,OACD,CACN,QAKJ,YAAoB,EAAqD,CACvE,IAAM,EAAM,KAAK,SAAS,EAAS,CACnC,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAU,KAAK,MAAM,EAAI,MACnB,CAGN,OAEF,GAAI,OAAO,GAAY,WAAY,EAAkB,OACrD,IAAM,EAAS,EAAgC,MAC/C,GAAI,IAAU,QAAS,MAAO,CAAE,OAAQ,YAAa,CACrD,GAAI,IAAU,QAAS,CACrB,IAAM,EAAS,EAAgC,MAC/C,MAAO,CAAE,OAAQ,SAAU,MAAO,OAAO,GAAU,SAAW,EAAQ,kCAAmC,EAK7G,iBAAiB,EAAoD,CACnE,IAAM,EAAY,EAAQ,WAAa,KAAK,iBACxC,EAAU,GACV,EACA,EACA,EACA,EAEE,MAAsB,CACtB,GAAW,cAAc,EAAU,CACnC,GAAc,aAAa,EAAa,CAC5C,GAAS,OAAO,CAChB,EAAY,IAAA,GACZ,EAAe,IAAA,GACf,EAAU,IAAA,IAwCZ,MAAO,CAAE,YArCW,SAAgC,EAAS,IAAW,CACtE,EAAa,EAEb,IAAM,EAAU,GAAyC,CACnD,IACJ,EAAU,GACV,GAAS,CACT,EAAQ,EAAQ,GAGZ,MAAuB,CAC3B,GAAI,EAAS,OACb,IAAM,EAAU,KAAK,YAAY,EAAQ,KAAK,CAC1C,GAAS,EAAO,EAAQ,EAG9B,EAAe,eAAiB,EAAO,CAAE,OAAQ,YAAa,CAAC,CAAE,EAAU,CAC3E,EAAa,SAAS,CAEtB,EAAU,KAAK,eAAe,EAAK,QAAQ,EAAQ,KAAK,CAAE,EAAS,CAEnE,EAAY,YAAY,EAAU,KAAK,eAAe,CACtD,EAAU,SAAS,CAKnB,GAAU,EAUH,CAAS,OAPF,GAAyB,CACnC,IACJ,EAAU,GACV,GAAS,CACT,IAAiB,MAAM,EAAO,CAAC,GAGP,QAlH7B,GAAY,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),n=require(`../../container/types.cjs`),r=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),i=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`),a=require(`../../shared/paths.cjs`),o=require(`./statusWriter.cjs`),s=require(`./asyncJobTracker.cjs`),c=require(`../../shared/artifacts.cjs`);let l=require(`node:fs`);l=e.__toESM(l,1);let u=require(`node:path`);u=e.__toESM(u,1);let d=require(`inversify`);const f=`.claim-`;function p(e){return u.join(a.currentRunsDir(),e,o.STATUS_FILE_NAME)}function m(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}let g=class{constructor(e,t){this.processTerminal=e,this.runIdResolver=t}now(){return Date.now()}createStatusWriter(){return new o.CoalescedStatusWriter}readFile(e){try{return l.readFileSync(e,`utf-8`)}catch(e){if(m(e)===`ENOENT`)return;throw e}}readStatus(e){let t=this.readFile(p(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}return h(n)?n:void 0}appendEvent(e,t){try{c.appendJsonl(u.join(a.currentRunsDir(),e,`events.jsonl`),JSON.stringify(t))}catch{}}walkForClaims(e,t=0){let n;try{n=l.readdirSync(e,{withFileTypes:!0})}catch(e){if(m(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(f)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<8&&r.push(...this.walkForClaims(u.join(e,i.name),t+1));return r}recoverClaim(e,t){try{return l.renameSync(e,t),!0}catch{try{l.unlinkSync(e)}catch{}return!1}}inspectOnce(e){let t;return()=>t??=this.processTerminal.inspect(e)}reconcile(e){return this.reconcileWith(e,this.inspectOnce(e))}sweepOrphanedClaims(e){return this.sweepWith(e,this.inspectOnce(e))}reconcileAndSweep(e){let t=this.inspectOnce(e);return{reconcile:this.reconcileWith(e,t),sweep:this.sweepWith(e,t)}}reconcileWith(e,t){let n=this.readStatus(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&s.TERMINAL_ASYNC_JOB_STATES.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if(this.runIdResolver.resolve(e)?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=t();return i.state===`crashed`?this.repair(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}sweepWith(e,t){let n=t();if(n.state!==`crashed`)return{recovered:[]};let r=u.join(a.currentRunsDir(),e),i=[];for(let{dir:t,name:a}of this.walkForClaims(r)){let o=a.lastIndexOf(f);if(o===-1)continue;let s=a.slice(0,o);if(this.recoverClaim(u.join(t,a),u.join(t,s))){let a=u.relative(r,t).split(u.sep).join(`/`);i.push({queue:a,fileName:s}),this.appendEvent(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}repair(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),this.appendEvent(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}};g=t.__decorate([(0,d.injectable)(),i.__decorateParam(0,(0,d.inject)(n.DOOM_TEAM_TYPES.ProcessTerminalInspector)),i.__decorateParam(1,(0,d.inject)(n.DOOM_TEAM_TYPES.RunIdResolver)),r.__decorateMetadata(`design:paramtypes`,[Object,Object])],g),Object.defineProperty(exports,`StaleRunReconciler`,{enumerable:!0,get:function(){return g}});
|
|
2
|
+
//# sourceMappingURL=staleRunReconciler.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staleRunReconciler.cjs","names":["path","currentRunsDir","STATUS_FILE_NAME","CoalescedStatusWriter","fs","TERMINAL_ASYNC_JOB_STATES","DOOM_TEAM_TYPES"],"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":"ylBAoJA,MAQM,EAAsB,UAwF5B,SAAS,EAAc,EAAuB,CAC5C,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAOC,EAAAA,iBAAiB,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,CAItE,IAAA,EAAA,KAAwD,CAC7D,YACE,EACA,EACA,CAFmE,KAAA,gBAAA,EACX,KAAA,cAAA,EAG1D,KAAwB,CACtB,OAAO,KAAK,KAAK,CAQnB,oBAA2E,CACzE,OAAO,IAAIC,EAAAA,sBAGb,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAOC,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,EAAA,YAAYJ,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAO,eAAiB,CAAE,KAAK,UAAU,EAAQ,CAAC,MACpF,GAcV,cAAwB,EAAa,EAAQ,EAAyC,CACpF,IAAI,EACJ,GAAI,CACF,EAAUG,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,cAAcJ,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,IAAaK,EAAAA,0BAA0B,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,EAASL,EAAK,KAAKC,EAAAA,gBAAgB,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,aAAaD,EAAK,KAAK,EAAK,EAAM,CAAEA,EAAK,KAAK,EAAK,EAAS,CACtE,CAAE,CAChB,IAAM,EAAQA,EAAK,SAAS,EAAQ,EAAI,CAAC,MAAMA,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,qCAxMjC,kCAGDM,EAAAA,gBAAgB,yBAAyB,CAAA,kCACzCA,EAAAA,gBAAgB,cAAc,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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{currentRunsDir as l,init_paths as u}from"../../shared/paths.mjs";import{CoalescedStatusWriter as d,STATUS_FILE_NAME as f,init_statusWriter as p}from"./statusWriter.mjs";import{TERMINAL_ASYNC_JOB_STATES as m,init_asyncJobTracker as h}from"./asyncJobTracker.mjs";import{appendJsonl as g,init_artifacts as _}from"../../shared/artifacts.mjs";import*as v from"node:fs";import*as y from"node:path";import{inject as b,injectable as x}from"inversify";function S(e){return y.join(l(),e,f)}function C(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function w(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}var T,E,D,O,k=e((()=>{u(),_(),i(),h(),p(),o(),c(),n(),T=`events.jsonl`,E=`.claim-`,D=8,O=class{constructor(e,t){this.processTerminal=e,this.runIdResolver=t}now(){return Date.now()}createStatusWriter(){return new d}readFile(e){try{return v.readFileSync(e,`utf-8`)}catch(e){if(C(e)===`ENOENT`)return;throw e}}readStatus(e){let t=this.readFile(S(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}return w(n)?n:void 0}appendEvent(e,t){try{g(y.join(l(),e,T),JSON.stringify(t))}catch{}}walkForClaims(e,t=0){let n;try{n=v.readdirSync(e,{withFileTypes:!0})}catch(e){if(C(e)===`ENOENT`)return[];throw e}let r=[];for(let i of n)i.name.includes(E)?r.push({dir:e,name:i.name}):i.isDirectory()&&t<D&&r.push(...this.walkForClaims(y.join(e,i.name),t+1));return r}recoverClaim(e,t){try{return v.renameSync(e,t),!0}catch{try{v.unlinkSync(e)}catch{}return!1}}inspectOnce(e){let t;return()=>t??=this.processTerminal.inspect(e)}reconcile(e){return this.reconcileWith(e,this.inspectOnce(e))}sweepOrphanedClaims(e){return this.sweepWith(e,this.inspectOnce(e))}reconcileAndSweep(e){let t=this.inspectOnce(e);return{reconcile:this.reconcileWith(e,t),sweep:this.sweepWith(e,t)}}reconcileWith(e,t){let n=this.readStatus(e);if(!n)return{repaired:!1,reason:`No status file for this run.`};let r=n.state;if(r!==void 0&&m.has(r))return{repaired:!1,reason:`Run already terminal (${r}).`};if(this.runIdResolver.resolve(e)?.resultPath)return{repaired:!1,reason:`A result already exists; delivery is ResultWatcher's job, not a repair.`};let i=t();return i.state===`crashed`?this.repair(e,n,i.marker.pid):{repaired:!1,reason:`Owning process liveness is '${i.state}', which does not prove death; leaving the run as is.`}}sweepWith(e,t){let n=t();if(n.state!==`crashed`)return{recovered:[]};let r=y.join(l(),e),i=[];for(let{dir:t,name:a}of this.walkForClaims(r)){let o=a.lastIndexOf(E);if(o===-1)continue;let s=a.slice(0,o);if(this.recoverClaim(y.join(t,a),y.join(t,s))){let a=y.relative(r,t).split(y.sep).join(`/`);i.push({queue:a,fileName:s}),this.appendEvent(e,{type:`subagent.run.claim_recovered`,ts:this.now(),runId:e,pid:n.marker.pid,queue:a,recovered:s})}}return{recovered:i}}repair(e,t,n){let r=this.now(),i=`Runner process ${n} exited without finalizing this run. Marked failed by stale-run reconciliation.`,a=this.createStatusWriter();return a.open(e,t),a.updateSync(e=>{e.state=`failed`,e.error=e.error??i,e.lastUpdate=r,e.endedAt=e.endedAt??r}),this.appendEvent(e,{type:`subagent.run.repaired_stale`,ts:r,runId:e,pid:n,message:i}),{repaired:!0,reason:i}}},O=t([x(),s(0,b(r.ProcessTerminalInspector)),s(1,b(r.RunIdResolver)),a(`design:paramtypes`,[Object,Object])],O)}));k();export{O as StaleRunReconciler,k as init_staleRunReconciler};
|
|
2
|
+
//# sourceMappingURL=staleRunReconciler.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|