@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":"notify.cjs","names":["createCompletionBatcher","resolveCompletionBatchConfig"],"sources":["../../../src/runs/background/notify.ts"],"sourcesContent":["/**\n * Formats a run's terminal result into a completion notification and hands\n * it to `ResultWatcher` as its `ResultConsumer`, batching successful\n * completions and bypassing that batching for anything that needs the\n * operator's attention right away.\n *\n * WHY `completion-dedupe.ts` IS NOT PORTED:\n * The predecessor deduped completion notifications with its own TTL-keyed\n * map (`completion-dedupe.ts`), independently of the identically-shaped TTL\n * dedupe `resultWatcher.ts` already does before it ever calls a consumer.\n * Those two copies could disagree - the exact bug class `resultWatcher.ts`'s\n * header now documents fixing. This package has exactly one producer of\n * completions today: `ResultWatcher`, which only calls its consumer once per\n * `runId` per TTL window. `CompletionNotifier.deliver()` is that consumer, so\n * it trusts that guarantee instead of re-implementing it. The predecessor\n * needed its own copy because it had two independent producers (async result\n * files AND a synchronous foreground-run event); this package is async-only\n * (see `spawnHandshake.ts`'s header) and has not ported foreground runs. IF\n * a second completion producer is added later, `deliver()` can no longer\n * assume `ResultWatcher` already deduped for it, and this module will need\n * its own dedupe again at that point - not before.\n *\n * DESIGN PATTERNS:\n * - `deliver()` matches `ResultConsumer`'s exact shape\n * (`(result: RunResultFile) => boolean | Promise<boolean>`), so this class\n * is meant to be registered directly as `ResultWatcher`'s consumer, not\n * subscribed to a separate event bus the way the predecessor's notifier\n * subscribed to two\n * - Batching (`completionBatcher.ts`) only ever holds a *successful*\n * completion. A failure or a pause is exactly the situation batching's\n * debounce would otherwise delay reporting, so those flush whatever is\n * already held for the same group and emit immediately, unbatched\n * - One batcher per group key (by `sessionId`, falling back to `cwd`, then a\n * shared `'unknown'` bucket), created lazily and kept for the process\n * lifetime: siblings of the same run share a group, unrelated runs never\n * hold each other up\n * - `deliver()`'s returned promise resolves only once the item's group\n * actually emits (immediately for a bypass, after the batcher's debounce\n * for a grouped success). `ResultWatcher` awaits this before deciding\n * whether to release the claim, so a claim legitimately stays in flight for\n * as long as its group is held open - that is intended, not a stall\n *\n * WHY `sendMessage` IS INERT UNTIL A HOST IS ATTACHED:\n * The host channel arrives at runtime, not at construction: this is a\n * container singleton, and the `ExtensionAPI` that owns the chat surface only\n * exists once `extensions/pi.ts` is activated. `attachHost` is what closes\n * that gap, and until it is called - in the runner child, in a test, or in\n * any process with no TUI - returning `false` rather than claiming success is\n * deliberate. It is the honest answer (\"this was not actually shown to\n * anyone\"), and it is what lets `ResultWatcher`'s retry keep the claim around\n * to be flushed later instead of dropping the completion on the floor.\n *\n * AVOID:\n * - Reintroducing a dedupe map here \"just in case\"; see the header note above\n * - Calling `batcher.push()` for anything whose status is not `'completed'`\n */\n\nimport { injectable } from 'inversify';\n\nimport type { RunResultFile } from './resultWatcher.ts';\nimport {\n type CompletionBatchConfig,\n type CompletionBatcher,\n createCompletionBatcher,\n resolveCompletionBatchConfig,\n} from './completionBatcher.ts';\n\nexport type CompletionStatus = 'completed' | 'failed' | 'paused';\n\nconst EMPTY_OUTPUT = '(no output)';\nconst UNKNOWN_VALUE = 'unknown';\n\n/** What a completion actually renders as, independent of the raw result shape. */\nexport interface CompletionNotifyDetails {\n agent: string;\n /**\n * The run this completion belongs to.\n *\n * Load-bearing, not decoration: a completion is the ONLY message the model\n * gets when a background run ends, and every follow-up it might want -\n * `{action:'status', id}`, `steer`, `resume`, tailing a transcript - is\n * keyed by run id. Without it the model has to re-list every run and guess\n * which one just finished, which is exactly the wasted turn the sibling\n * implementation avoids by putting `<task-id>` in its own notification.\n */\n runId: string;\n status: CompletionStatus;\n resultPreview: string;\n taskInfo?: string;\n durationMs?: number;\n sessionLabel?: string;\n sessionValue?: string;\n handoffPath?: string;\n}\n\n/**\n * What the model is expected to DO about this completion, stated in the\n * message rather than left to inference.\n *\n * A bare \"Background task completed\" invites a passive acknowledgement. Naming\n * the required handling - and, for the empty-summary case, naming the exact\n * call that recovers the output - is what turns a notification into an\n * instruction. Mirrors the sibling implementation's `<coordinator-action>`.\n */\nfunction completionAction(details: CompletionNotifyDetails): string {\n const hasPreview = details.resultPreview.trim().length > 0;\n if (details.status === 'completed') {\n return hasPreview\n ? 'Read this result now and incorporate, reject, or mark it irrelevant before replying to the user.'\n : `No summary was produced. Call { action: \"status\", id: \"${details.runId}\", view: \"transcript\" } before replying.`;\n }\n if (details.status === 'paused') {\n return `Run is paused and resumable. Inspect it with { action: \"status\", id: \"${details.runId}\" }, then decide whether to resume or abandon it.`;\n }\n return `Review this failure and tell the user whether it affects the current work. Full output: { action: \"status\", id: \"${details.runId}\", view: \"transcript\" }.`;\n}\n\nfunction formatSessionLine(details: CompletionNotifyDetails): string | undefined {\n if (!details.sessionValue) return undefined;\n return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;\n}\n\nexport function formatSingleCompletion(details: CompletionNotifyDetails): string {\n const sessionLine = formatSessionLine(details);\n return [\n `Background task ${details.status}: **${details.agent}**${details.taskInfo ?? ''}`,\n `run id: ${details.runId}`,\n '',\n details.resultPreview.trim() ? details.resultPreview : EMPTY_OUTPUT,\n details.handoffPath ? '' : undefined,\n details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,\n sessionLine ? '' : undefined,\n sessionLine,\n '',\n completionAction(details),\n ]\n .filter((line) => line !== undefined)\n .join('\\n');\n}\n\nexport function formatGroupedCompletion(details: CompletionNotifyDetails[]): string {\n const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ''}`).join(', ')}`;\n const blocks: string[] = [header, ''];\n for (let index = 0; index < details.length; index++) {\n const detail = details[index];\n if (!detail) continue;\n const sessionLine = formatSessionLine(detail);\n blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ''} - run id: ${detail.runId}`);\n blocks.push(detail.resultPreview.trim() ? detail.resultPreview : EMPTY_OUTPUT);\n if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);\n if (sessionLine) blocks.push(sessionLine);\n blocks.push(completionAction(detail));\n blocks.push('');\n }\n return blocks.join('\\n').trimEnd();\n}\n\n/** Narrow an opaque `RunResultFile` field to a string, the same defensive read used throughout. */\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction completionGroupKey(result: RunResultFile): string {\n const sessionId = asString(result.sessionId)?.trim();\n if (sessionId) return `session:${sessionId}`;\n const cwd = asString(result.cwd)?.trim();\n return cwd ? `cwd:${cwd}` : UNKNOWN_VALUE;\n}\n\n/** Turn a raw, opaque result record into what `formatSingleCompletion`/`formatGroupedCompletion` render. */\nexport function buildCompletionDetails(result: RunResultFile): CompletionNotifyDetails {\n const agent = asString(result.agent) ?? UNKNOWN_VALUE;\n const summary = asString(result.summary) ?? '';\n const success = typeof result.success === 'boolean' ? result.success : undefined;\n const state = asString(result.state);\n const paused = success === false && (state === 'paused' || summary.startsWith('Paused after interrupt.'));\n const status: CompletionStatus = paused ? 'paused' : success === false ? 'failed' : 'completed';\n\n const taskIndex = asNumber(result.taskIndex);\n const totalTasks = asNumber(result.totalTasks);\n const taskInfo =\n taskIndex !== undefined && totalTasks !== undefined ? ` (${taskIndex + 1}/${totalTasks})` : undefined;\n\n const parallelHandoff =\n typeof result.parallelHandoff === 'object' && result.parallelHandoff !== null\n ? (result.parallelHandoff as { path?: unknown })\n : undefined;\n const handoffPath = asString(parallelHandoff?.path);\n\n const shareUrl = asString(result.shareUrl);\n const shareError = asString(result.shareError);\n const sessionFile = asString(result.sessionFile);\n const session = shareUrl\n ? { label: 'Session', value: shareUrl }\n : shareError\n ? { label: 'Session share error', value: shareError }\n : sessionFile\n ? { label: 'Session file', value: sessionFile }\n : undefined;\n\n return {\n agent,\n // `RunResultFile` guarantees `runId` (see `resultWatcher.ts`), so there is\n // no honest fallback to invent here - an empty string would be a run id the\n // model could not use, dressed up as one it could.\n runId: result.runId,\n status,\n resultPreview: summary,\n ...(taskInfo ? { taskInfo } : {}),\n ...(asNumber(result.durationMs) !== undefined ? { durationMs: asNumber(result.durationMs) } : {}),\n ...(handoffPath ? { handoffPath } : {}),\n ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),\n };\n}\n\ninterface PendingCompletion {\n details: CompletionNotifyDetails;\n triggerTurn: boolean;\n resolve(delivered: boolean): void;\n}\n\n/**\n * The host capability this notifier needs, narrowed to the one method it\n * calls. Structural rather than `ExtensionAPI` itself so the runs domain does\n * not take a dependency on the host type for a single call, and so a test can\n * supply a recorder without constructing a whole extension API.\n */\nexport interface CompletionNotifyHost {\n sendMessage(\n message: { customType: string; content: string; display: boolean; details?: CompletionNotifyDetails[] },\n options?: { triggerTurn?: boolean; deliverAs?: 'steer' },\n ): void;\n}\n\nexport interface ICompletionNotifier {\n /** Matches `ResultConsumer` exactly; register this directly with `ResultWatcher.start()`. */\n deliver(result: RunResultFile): Promise<boolean>;\n /**\n * Give the notifier the host channel to render into. Called by the parent\n * composition root (`extensions/pi.ts`); until it is, `sendMessage` stays\n * inert and honestly reports non-delivery - see the module header.\n */\n attachHost(host: CompletionNotifyHost): void;\n /** Flush every held batch immediately (unemitted items resolve `false`) and stop accepting new ones. */\n dispose(): void;\n}\n\n/**\n * The custom-message type the parent renders completions under. Cross-process\n * significant in the same way the nested-event tokens are: the child-side\n * prompt runtime strips messages of this type out of a child's inherited\n * history (`stripParentOnlySubagentMessages`), so the two sides must agree on\n * the literal.\n */\nexport const SUBAGENT_NOTIFY_MESSAGE_TYPE = 'subagent-notify';\n\n@injectable()\nexport class CompletionNotifier implements ICompletionNotifier {\n /**\n * Seam for tests, as a protected member rather than a constructor\n * argument: inversify cannot resolve a primitive-shaped config object. See\n * `pollScheduler.ts` for the same pattern.\n */\n protected readonly batchConfig: CompletionBatchConfig | undefined = undefined;\n\n private readonly batchers = new Map<string, CompletionBatcher<PendingCompletion>>();\n private disposed = false;\n private host: CompletionNotifyHost | undefined;\n\n /**\n * Last host attachment wins. A reload that rebuilds the extension against a\n * fresh `ExtensionAPI` re-attaches rather than stacking channels, matching\n * how every other registration in `extensions/pi.ts` is idempotent.\n */\n attachHost(host: CompletionNotifyHost): void {\n this.host = host;\n }\n\n /**\n * Deliver a formatted completion message. Inert until `attachHost` supplies\n * a host (see the module header for why non-delivery is reported honestly\n * rather than optimistically). Not `async`: `emit()` below does not await it\n * either way - see its doc comment for why.\n *\n * A throw from the host is caught and reported as non-delivery rather than\n * propagated: `emit()` is a batcher callback running on a timer with no\n * caller to catch for it, and a failed render must leave `ResultWatcher`'s\n * claim in place for a retry, which is exactly what `false` does.\n */\n protected sendMessage(\n content: string,\n options: { triggerTurn: boolean; details: CompletionNotifyDetails[] },\n ): boolean {\n if (!this.host) return false;\n try {\n this.host.sendMessage(\n { customType: SUBAGENT_NOTIFY_MESSAGE_TYPE, content, display: true, details: options.details },\n { triggerTurn: options.triggerTurn, deliverAs: 'steer' },\n );\n return true;\n } catch {\n return false;\n }\n }\n\n async deliver(result: RunResultFile): Promise<boolean> {\n if (result.internal === true) return true;\n if (this.disposed) return false;\n const details = buildCompletionDetails(result);\n const triggerTurn = result.triggerTurn !== false;\n\n return new Promise<boolean>((resolve) => {\n const item: PendingCompletion = { details, triggerTurn, resolve };\n const batcher = this.getBatcher(result);\n if (details.status !== 'completed') {\n // A failure or a pause must not sit behind a debounce meant for\n // grouping good news; flush whatever this group was already holding,\n // then emit this one immediately, unbatched.\n batcher.flush();\n this.emit([item]);\n return;\n }\n batcher.push(item);\n });\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n for (const batcher of this.batchers.values()) {\n for (const item of batcher.dispose()) item.resolve(false);\n }\n this.batchers.clear();\n }\n\n private getBatcher(result: RunResultFile): CompletionBatcher<PendingCompletion> {\n const key = completionGroupKey(result);\n let batcher = this.batchers.get(key);\n if (!batcher) {\n batcher = createCompletionBatcher<PendingCompletion>({\n config: resolveCompletionBatchConfig(this.batchConfig),\n emit: (items) => this.emit(items),\n });\n this.batchers.set(key, batcher);\n }\n return batcher;\n }\n\n /**\n * The batcher's `emit` callback: format the group (single vs. grouped\n * rendering, `formatSingleCompletion`/`formatGroupedCompletion`) and hand\n * it to `sendMessage`. Deliberately not `async` and does not await\n * `sendMessage`: `createCompletionBatcher`'s `emit` option is a synchronous\n * callback (see its own contract), and every item's promise still resolves\n * correctly here regardless, since `sendMessage`'s result is available\n * synchronously to whichever implementation is active.\n */\n private emit(items: PendingCompletion[]): void {\n if (items.length === 0) return;\n const details = items.map((item) => item.details);\n const content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);\n const delivered = this.sendMessage(content, {\n triggerTurn: items.some((item) => item.triggerTurn),\n details,\n });\n for (const item of items) item.resolve(delivered);\n }\n}\n"],"mappings":"qMAqEA,MAAM,EAAe,cACf,EAAgB,UAkCtB,SAAS,EAAiB,EAA0C,CAClE,IAAM,EAAa,EAAQ,cAAc,MAAM,CAAC,OAAS,EASzD,OARI,EAAQ,SAAW,YACd,EACH,mGACA,0DAA0D,EAAQ,MAAM,0CAE1E,EAAQ,SAAW,SACd,yEAAyE,EAAQ,MAAM,mDAEzF,oHAAoH,EAAQ,MAAM,0BAG3I,SAAS,EAAkB,EAAsD,CAC1E,KAAQ,aACb,OAAO,EAAQ,aAAe,GAAG,EAAQ,aAAa,IAAI,EAAQ,eAAiB,EAAQ,aAG7F,SAAgB,EAAuB,EAA0C,CAC/E,IAAM,EAAc,EAAkB,EAAQ,CAC9C,MAAO,CACL,mBAAmB,EAAQ,OAAO,MAAM,EAAQ,MAAM,IAAI,EAAQ,UAAY,KAC9E,WAAW,EAAQ,QACnB,GACA,EAAQ,cAAc,MAAM,CAAG,EAAQ,cAAgB,EACvD,EAAQ,YAAc,GAAK,IAAA,GAC3B,EAAQ,YAAc,qBAAqB,EAAQ,cAAgB,IAAA,GACnE,EAAc,GAAK,IAAA,GACnB,EACA,GACA,EAAiB,EAAQ,CAC1B,CACE,OAAQ,GAAS,IAAS,IAAA,GAAU,CACpC,KAAK;EAAK,CAGf,SAAgB,EAAwB,EAA4C,CAElF,IAAM,EAAmB,CAAC,+BADoB,EAAQ,OAAO,KAAK,EAAQ,IAAK,GAAM,KAAK,EAAE,MAAM,IAAI,EAAE,UAAY,KAAK,CAAC,KAAK,KAAK,GAClG,GAAG,CACrC,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,IAAS,CACnD,IAAM,EAAS,EAAQ,GACvB,GAAI,CAAC,EAAQ,SACb,IAAM,EAAc,EAAkB,EAAO,CAC7C,EAAO,KAAK,GAAG,EAAQ,EAAE,IAAI,EAAO,QAAQ,EAAO,UAAY,GAAG,aAAa,EAAO,QAAQ,CAC9F,EAAO,KAAK,EAAO,cAAc,MAAM,CAAG,EAAO,cAAgB,EAAa,CAC1E,EAAO,aAAa,EAAO,KAAK,qBAAqB,EAAO,cAAc,CAC1E,GAAa,EAAO,KAAK,EAAY,CACzC,EAAO,KAAK,EAAiB,EAAO,CAAC,CACrC,EAAO,KAAK,GAAG,CAEjB,OAAO,EAAO,KAAK;EAAK,CAAC,SAAS,CAIpC,SAAS,EAAS,EAAoC,CACpD,OAAO,OAAO,GAAU,SAAW,EAAQ,IAAA,GAG7C,SAAS,EAAS,EAAoC,CACpD,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,EAAM,CAAG,EAAQ,IAAA,GAGvE,SAAS,EAAmB,EAA+B,CACzD,IAAM,EAAY,EAAS,EAAO,UAAU,EAAE,MAAM,CACpD,GAAI,EAAW,MAAO,WAAW,IACjC,IAAM,EAAM,EAAS,EAAO,IAAI,EAAE,MAAM,CACxC,OAAO,EAAM,OAAO,IAAQ,EAI9B,SAAgB,EAAuB,EAAgD,CACrF,IAAM,EAAQ,EAAS,EAAO,MAAM,EAAI,EAClC,EAAU,EAAS,EAAO,QAAQ,EAAI,GACtC,EAAU,OAAO,EAAO,SAAY,UAAY,EAAO,QAAU,IAAA,GACjE,EAAQ,EAAS,EAAO,MAAM,CAE9B,EADS,IAAY,KAAU,IAAU,UAAY,EAAQ,WAAW,0BAA0B,EAC9D,SAAW,IAAY,GAAQ,SAAW,YAE9E,EAAY,EAAS,EAAO,UAAU,CACtC,EAAa,EAAS,EAAO,WAAW,CACxC,EACJ,IAAc,IAAA,IAAa,IAAe,IAAA,GAAY,KAAK,EAAY,EAAE,GAAG,EAAW,GAAK,IAAA,GAMxF,EAAc,GAHlB,OAAO,EAAO,iBAAoB,UAAY,EAAO,kBAAoB,KACpE,EAAO,gBACR,IAAA,KACwC,KAAK,CAE7C,EAAW,EAAS,EAAO,SAAS,CACpC,EAAa,EAAS,EAAO,WAAW,CACxC,EAAc,EAAS,EAAO,YAAY,CAC1C,EAAU,EACZ,CAAE,MAAO,UAAW,MAAO,EAAU,CACrC,EACE,CAAE,MAAO,sBAAuB,MAAO,EAAY,CACnD,EACE,CAAE,MAAO,eAAgB,MAAO,EAAa,CAC7C,IAAA,GAER,MAAO,CACL,QAIA,MAAO,EAAO,MACd,SACA,cAAe,EACf,GAAI,EAAW,CAAE,WAAU,CAAG,EAAE,CAChC,GAAI,EAAS,EAAO,WAAW,GAAK,IAAA,GAA0D,EAAE,CAAhD,CAAE,WAAY,EAAS,EAAO,WAAW,CAAE,CAC3F,GAAI,EAAc,CAAE,cAAa,CAAG,EAAE,CACtC,GAAI,EAAU,CAAE,aAAc,EAAQ,MAAO,aAAc,EAAQ,MAAO,CAAG,EAAE,CAChF,CA0CH,MAAa,EAA+B,kBAGrC,IAAA,EAAA,KAAwD,CAM7D,YAAoE,IAAA,GAEpE,SAA4B,IAAI,IAChC,SAAmB,GACnB,KAOA,WAAW,EAAkC,CAC3C,KAAK,KAAO,EAcd,YACE,EACA,EACS,CACT,GAAI,CAAC,KAAK,KAAM,MAAO,GACvB,GAAI,CAKF,OAJA,KAAK,KAAK,YACR,CAAE,WAAY,EAA8B,UAAS,QAAS,GAAM,QAAS,EAAQ,QAAS,CAC9F,CAAE,YAAa,EAAQ,YAAa,UAAW,QAAS,CACzD,CACM,QACD,CACN,MAAO,IAIX,MAAM,QAAQ,EAAyC,CACrD,GAAI,EAAO,WAAa,GAAM,MAAO,GACrC,GAAI,KAAK,SAAU,MAAO,GAC1B,IAAM,EAAU,EAAuB,EAAO,CACxC,EAAc,EAAO,cAAgB,GAE3C,OAAO,IAAI,QAAkB,GAAY,CACvC,IAAM,EAA0B,CAAE,UAAS,cAAa,UAAS,CAC3D,EAAU,KAAK,WAAW,EAAO,CACvC,GAAI,EAAQ,SAAW,YAAa,CAIlC,EAAQ,OAAO,CACf,KAAK,KAAK,CAAC,EAAK,CAAC,CACjB,OAEF,EAAQ,KAAK,EAAK,EAClB,CAGJ,SAAgB,CACV,SAAK,SACT,MAAK,SAAW,GAChB,IAAK,IAAM,KAAW,KAAK,SAAS,QAAQ,CAC1C,IAAK,IAAM,KAAQ,EAAQ,SAAS,CAAE,EAAK,QAAQ,GAAM,CAE3D,KAAK,SAAS,OAAO,EAGvB,WAAmB,EAA6D,CAC9E,IAAM,EAAM,EAAmB,EAAO,CAClC,EAAU,KAAK,SAAS,IAAI,EAAI,CAQpC,OAPK,IACH,EAAUA,EAAAA,wBAA2C,CACnD,OAAQC,EAAAA,6BAA6B,KAAK,YAAY,CACtD,KAAO,GAAU,KAAK,KAAK,EAAM,CAClC,CAAC,CACF,KAAK,SAAS,IAAI,EAAK,EAAQ,EAE1B,EAYT,KAAa,EAAkC,CAC7C,GAAI,EAAM,SAAW,EAAG,OACxB,IAAM,EAAU,EAAM,IAAK,GAAS,EAAK,QAAQ,CAC3C,EAAU,EAAQ,SAAW,EAAI,EAAuB,EAAQ,GAAI,CAAG,EAAwB,EAAQ,CACvG,EAAY,KAAK,YAAY,EAAS,CAC1C,YAAa,EAAM,KAAM,GAAS,EAAK,YAAY,CACnD,UACD,CAAC,CACF,IAAK,IAAM,KAAQ,EAAO,EAAK,QAAQ,EAAU,qCA7GxC,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
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{createCompletionBatcher as r,init_completionBatcher as i,resolveCompletionBatchConfig as a}from"./completionBatcher.mjs";import{injectable as o}from"inversify";function s(e){let t=e.resultPreview.trim().length>0;return e.status===`completed`?t?`Read this result now and incorporate, reject, or mark it irrelevant before replying to the user.`:`No summary was produced. Call { action: "status", id: "${e.runId}", view: "transcript" } before replying.`:e.status===`paused`?`Run is paused and resumable. Inspect it with { action: "status", id: "${e.runId}" }, then decide whether to resume or abandon it.`:`Review this failure and tell the user whether it affects the current work. Full output: { action: "status", id: "${e.runId}", view: "transcript" }.`}function c(e){if(e.sessionValue)return e.sessionLabel?`${e.sessionLabel}: ${e.sessionValue}`:e.sessionValue}function l(e){let t=c(e);return[`Background task ${e.status}: **${e.agent}**${e.taskInfo??``}`,`run id: ${e.runId}`,``,e.resultPreview.trim()?e.resultPreview:h,e.handoffPath?``:void 0,e.handoffPath?`Parallel handoff: ${e.handoffPath}`:void 0,t?``:void 0,t,``,s(e)].filter(e=>e!==void 0).join(`
|
|
2
|
+
`)}function u(e){let t=[`Background tasks completed (${e.length}): ${e.map(e=>`**${e.agent}**${e.taskInfo??``}`).join(`, `)}`,``];for(let n=0;n<e.length;n++){let r=e[n];if(!r)continue;let i=c(r);t.push(`${n+1}. ${r.agent}${r.taskInfo??``} - run id: ${r.runId}`),t.push(r.resultPreview.trim()?r.resultPreview:h),r.handoffPath&&t.push(`Parallel handoff: ${r.handoffPath}`),i&&t.push(i),t.push(s(r)),t.push(``)}return t.join(`
|
|
3
|
+
`).trimEnd()}function d(e){return typeof e==`string`?e:void 0}function f(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function p(e){let t=d(e.sessionId)?.trim();if(t)return`session:${t}`;let n=d(e.cwd)?.trim();return n?`cwd:${n}`:g}function m(e){let t=d(e.agent)??g,n=d(e.summary)??``,r=typeof e.success==`boolean`?e.success:void 0,i=d(e.state),a=r===!1&&(i===`paused`||n.startsWith(`Paused after interrupt.`))?`paused`:r===!1?`failed`:`completed`,o=f(e.taskIndex),s=f(e.totalTasks),c=o!==void 0&&s!==void 0?` (${o+1}/${s})`:void 0,l=d((typeof e.parallelHandoff==`object`&&e.parallelHandoff!==null?e.parallelHandoff:void 0)?.path),u=d(e.shareUrl),p=d(e.shareError),m=d(e.sessionFile),h=u?{label:`Session`,value:u}:p?{label:`Session share error`,value:p}:m?{label:`Session file`,value:m}:void 0;return{agent:t,runId:e.runId,status:a,resultPreview:n,...c?{taskInfo:c}:{},...f(e.durationMs)===void 0?{}:{durationMs:f(e.durationMs)},...l?{handoffPath:l}:{},...h?{sessionLabel:h.label,sessionValue:h.value}:{}}}var h,g,_,v,y=e((()=>{i(),n(),h=`(no output)`,g=`unknown`,_=`subagent-notify`,v=class{batchConfig=void 0;batchers=new Map;disposed=!1;host;attachHost(e){this.host=e}sendMessage(e,t){if(!this.host)return!1;try{return this.host.sendMessage({customType:_,content:e,display:!0,details:t.details},{triggerTurn:t.triggerTurn,deliverAs:`steer`}),!0}catch{return!1}}async deliver(e){if(e.internal===!0)return!0;if(this.disposed)return!1;let t=m(e),n=e.triggerTurn!==!1;return new Promise(r=>{let i={details:t,triggerTurn:n,resolve:r},a=this.getBatcher(e);if(t.status!==`completed`){a.flush(),this.emit([i]);return}a.push(i)})}dispose(){if(!this.disposed){this.disposed=!0;for(let e of this.batchers.values())for(let t of e.dispose())t.resolve(!1);this.batchers.clear()}}getBatcher(e){let t=p(e),n=this.batchers.get(t);return n||(n=r({config:a(this.batchConfig),emit:e=>this.emit(e)}),this.batchers.set(t,n)),n}emit(e){if(e.length===0)return;let t=e.map(e=>e.details),n=t.length===1?l(t[0]):u(t),r=this.sendMessage(n,{triggerTurn:e.some(e=>e.triggerTurn),details:t});for(let t of e)t.resolve(r)}},v=t([o()],v)}));y();export{v as CompletionNotifier,_ as SUBAGENT_NOTIFY_MESSAGE_TYPE,y as init_notify};
|
|
4
|
+
//# sourceMappingURL=notify.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.mjs","names":[],"sources":["../../../src/runs/background/notify.ts"],"sourcesContent":["/**\n * Formats a run's terminal result into a completion notification and hands\n * it to `ResultWatcher` as its `ResultConsumer`, batching successful\n * completions and bypassing that batching for anything that needs the\n * operator's attention right away.\n *\n * WHY `completion-dedupe.ts` IS NOT PORTED:\n * The predecessor deduped completion notifications with its own TTL-keyed\n * map (`completion-dedupe.ts`), independently of the identically-shaped TTL\n * dedupe `resultWatcher.ts` already does before it ever calls a consumer.\n * Those two copies could disagree - the exact bug class `resultWatcher.ts`'s\n * header now documents fixing. This package has exactly one producer of\n * completions today: `ResultWatcher`, which only calls its consumer once per\n * `runId` per TTL window. `CompletionNotifier.deliver()` is that consumer, so\n * it trusts that guarantee instead of re-implementing it. The predecessor\n * needed its own copy because it had two independent producers (async result\n * files AND a synchronous foreground-run event); this package is async-only\n * (see `spawnHandshake.ts`'s header) and has not ported foreground runs. IF\n * a second completion producer is added later, `deliver()` can no longer\n * assume `ResultWatcher` already deduped for it, and this module will need\n * its own dedupe again at that point - not before.\n *\n * DESIGN PATTERNS:\n * - `deliver()` matches `ResultConsumer`'s exact shape\n * (`(result: RunResultFile) => boolean | Promise<boolean>`), so this class\n * is meant to be registered directly as `ResultWatcher`'s consumer, not\n * subscribed to a separate event bus the way the predecessor's notifier\n * subscribed to two\n * - Batching (`completionBatcher.ts`) only ever holds a *successful*\n * completion. A failure or a pause is exactly the situation batching's\n * debounce would otherwise delay reporting, so those flush whatever is\n * already held for the same group and emit immediately, unbatched\n * - One batcher per group key (by `sessionId`, falling back to `cwd`, then a\n * shared `'unknown'` bucket), created lazily and kept for the process\n * lifetime: siblings of the same run share a group, unrelated runs never\n * hold each other up\n * - `deliver()`'s returned promise resolves only once the item's group\n * actually emits (immediately for a bypass, after the batcher's debounce\n * for a grouped success). `ResultWatcher` awaits this before deciding\n * whether to release the claim, so a claim legitimately stays in flight for\n * as long as its group is held open - that is intended, not a stall\n *\n * WHY `sendMessage` IS INERT UNTIL A HOST IS ATTACHED:\n * The host channel arrives at runtime, not at construction: this is a\n * container singleton, and the `ExtensionAPI` that owns the chat surface only\n * exists once `extensions/pi.ts` is activated. `attachHost` is what closes\n * that gap, and until it is called - in the runner child, in a test, or in\n * any process with no TUI - returning `false` rather than claiming success is\n * deliberate. It is the honest answer (\"this was not actually shown to\n * anyone\"), and it is what lets `ResultWatcher`'s retry keep the claim around\n * to be flushed later instead of dropping the completion on the floor.\n *\n * AVOID:\n * - Reintroducing a dedupe map here \"just in case\"; see the header note above\n * - Calling `batcher.push()` for anything whose status is not `'completed'`\n */\n\nimport { injectable } from 'inversify';\n\nimport type { RunResultFile } from './resultWatcher.ts';\nimport {\n type CompletionBatchConfig,\n type CompletionBatcher,\n createCompletionBatcher,\n resolveCompletionBatchConfig,\n} from './completionBatcher.ts';\n\nexport type CompletionStatus = 'completed' | 'failed' | 'paused';\n\nconst EMPTY_OUTPUT = '(no output)';\nconst UNKNOWN_VALUE = 'unknown';\n\n/** What a completion actually renders as, independent of the raw result shape. */\nexport interface CompletionNotifyDetails {\n agent: string;\n /**\n * The run this completion belongs to.\n *\n * Load-bearing, not decoration: a completion is the ONLY message the model\n * gets when a background run ends, and every follow-up it might want -\n * `{action:'status', id}`, `steer`, `resume`, tailing a transcript - is\n * keyed by run id. Without it the model has to re-list every run and guess\n * which one just finished, which is exactly the wasted turn the sibling\n * implementation avoids by putting `<task-id>` in its own notification.\n */\n runId: string;\n status: CompletionStatus;\n resultPreview: string;\n taskInfo?: string;\n durationMs?: number;\n sessionLabel?: string;\n sessionValue?: string;\n handoffPath?: string;\n}\n\n/**\n * What the model is expected to DO about this completion, stated in the\n * message rather than left to inference.\n *\n * A bare \"Background task completed\" invites a passive acknowledgement. Naming\n * the required handling - and, for the empty-summary case, naming the exact\n * call that recovers the output - is what turns a notification into an\n * instruction. Mirrors the sibling implementation's `<coordinator-action>`.\n */\nfunction completionAction(details: CompletionNotifyDetails): string {\n const hasPreview = details.resultPreview.trim().length > 0;\n if (details.status === 'completed') {\n return hasPreview\n ? 'Read this result now and incorporate, reject, or mark it irrelevant before replying to the user.'\n : `No summary was produced. Call { action: \"status\", id: \"${details.runId}\", view: \"transcript\" } before replying.`;\n }\n if (details.status === 'paused') {\n return `Run is paused and resumable. Inspect it with { action: \"status\", id: \"${details.runId}\" }, then decide whether to resume or abandon it.`;\n }\n return `Review this failure and tell the user whether it affects the current work. Full output: { action: \"status\", id: \"${details.runId}\", view: \"transcript\" }.`;\n}\n\nfunction formatSessionLine(details: CompletionNotifyDetails): string | undefined {\n if (!details.sessionValue) return undefined;\n return details.sessionLabel ? `${details.sessionLabel}: ${details.sessionValue}` : details.sessionValue;\n}\n\nexport function formatSingleCompletion(details: CompletionNotifyDetails): string {\n const sessionLine = formatSessionLine(details);\n return [\n `Background task ${details.status}: **${details.agent}**${details.taskInfo ?? ''}`,\n `run id: ${details.runId}`,\n '',\n details.resultPreview.trim() ? details.resultPreview : EMPTY_OUTPUT,\n details.handoffPath ? '' : undefined,\n details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,\n sessionLine ? '' : undefined,\n sessionLine,\n '',\n completionAction(details),\n ]\n .filter((line) => line !== undefined)\n .join('\\n');\n}\n\nexport function formatGroupedCompletion(details: CompletionNotifyDetails[]): string {\n const header = `Background tasks completed (${details.length}): ${details.map((d) => `**${d.agent}**${d.taskInfo ?? ''}`).join(', ')}`;\n const blocks: string[] = [header, ''];\n for (let index = 0; index < details.length; index++) {\n const detail = details[index];\n if (!detail) continue;\n const sessionLine = formatSessionLine(detail);\n blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ''} - run id: ${detail.runId}`);\n blocks.push(detail.resultPreview.trim() ? detail.resultPreview : EMPTY_OUTPUT);\n if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);\n if (sessionLine) blocks.push(sessionLine);\n blocks.push(completionAction(detail));\n blocks.push('');\n }\n return blocks.join('\\n').trimEnd();\n}\n\n/** Narrow an opaque `RunResultFile` field to a string, the same defensive read used throughout. */\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction completionGroupKey(result: RunResultFile): string {\n const sessionId = asString(result.sessionId)?.trim();\n if (sessionId) return `session:${sessionId}`;\n const cwd = asString(result.cwd)?.trim();\n return cwd ? `cwd:${cwd}` : UNKNOWN_VALUE;\n}\n\n/** Turn a raw, opaque result record into what `formatSingleCompletion`/`formatGroupedCompletion` render. */\nexport function buildCompletionDetails(result: RunResultFile): CompletionNotifyDetails {\n const agent = asString(result.agent) ?? UNKNOWN_VALUE;\n const summary = asString(result.summary) ?? '';\n const success = typeof result.success === 'boolean' ? result.success : undefined;\n const state = asString(result.state);\n const paused = success === false && (state === 'paused' || summary.startsWith('Paused after interrupt.'));\n const status: CompletionStatus = paused ? 'paused' : success === false ? 'failed' : 'completed';\n\n const taskIndex = asNumber(result.taskIndex);\n const totalTasks = asNumber(result.totalTasks);\n const taskInfo =\n taskIndex !== undefined && totalTasks !== undefined ? ` (${taskIndex + 1}/${totalTasks})` : undefined;\n\n const parallelHandoff =\n typeof result.parallelHandoff === 'object' && result.parallelHandoff !== null\n ? (result.parallelHandoff as { path?: unknown })\n : undefined;\n const handoffPath = asString(parallelHandoff?.path);\n\n const shareUrl = asString(result.shareUrl);\n const shareError = asString(result.shareError);\n const sessionFile = asString(result.sessionFile);\n const session = shareUrl\n ? { label: 'Session', value: shareUrl }\n : shareError\n ? { label: 'Session share error', value: shareError }\n : sessionFile\n ? { label: 'Session file', value: sessionFile }\n : undefined;\n\n return {\n agent,\n // `RunResultFile` guarantees `runId` (see `resultWatcher.ts`), so there is\n // no honest fallback to invent here - an empty string would be a run id the\n // model could not use, dressed up as one it could.\n runId: result.runId,\n status,\n resultPreview: summary,\n ...(taskInfo ? { taskInfo } : {}),\n ...(asNumber(result.durationMs) !== undefined ? { durationMs: asNumber(result.durationMs) } : {}),\n ...(handoffPath ? { handoffPath } : {}),\n ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),\n };\n}\n\ninterface PendingCompletion {\n details: CompletionNotifyDetails;\n triggerTurn: boolean;\n resolve(delivered: boolean): void;\n}\n\n/**\n * The host capability this notifier needs, narrowed to the one method it\n * calls. Structural rather than `ExtensionAPI` itself so the runs domain does\n * not take a dependency on the host type for a single call, and so a test can\n * supply a recorder without constructing a whole extension API.\n */\nexport interface CompletionNotifyHost {\n sendMessage(\n message: { customType: string; content: string; display: boolean; details?: CompletionNotifyDetails[] },\n options?: { triggerTurn?: boolean; deliverAs?: 'steer' },\n ): void;\n}\n\nexport interface ICompletionNotifier {\n /** Matches `ResultConsumer` exactly; register this directly with `ResultWatcher.start()`. */\n deliver(result: RunResultFile): Promise<boolean>;\n /**\n * Give the notifier the host channel to render into. Called by the parent\n * composition root (`extensions/pi.ts`); until it is, `sendMessage` stays\n * inert and honestly reports non-delivery - see the module header.\n */\n attachHost(host: CompletionNotifyHost): void;\n /** Flush every held batch immediately (unemitted items resolve `false`) and stop accepting new ones. */\n dispose(): void;\n}\n\n/**\n * The custom-message type the parent renders completions under. Cross-process\n * significant in the same way the nested-event tokens are: the child-side\n * prompt runtime strips messages of this type out of a child's inherited\n * history (`stripParentOnlySubagentMessages`), so the two sides must agree on\n * the literal.\n */\nexport const SUBAGENT_NOTIFY_MESSAGE_TYPE = 'subagent-notify';\n\n@injectable()\nexport class CompletionNotifier implements ICompletionNotifier {\n /**\n * Seam for tests, as a protected member rather than a constructor\n * argument: inversify cannot resolve a primitive-shaped config object. See\n * `pollScheduler.ts` for the same pattern.\n */\n protected readonly batchConfig: CompletionBatchConfig | undefined = undefined;\n\n private readonly batchers = new Map<string, CompletionBatcher<PendingCompletion>>();\n private disposed = false;\n private host: CompletionNotifyHost | undefined;\n\n /**\n * Last host attachment wins. A reload that rebuilds the extension against a\n * fresh `ExtensionAPI` re-attaches rather than stacking channels, matching\n * how every other registration in `extensions/pi.ts` is idempotent.\n */\n attachHost(host: CompletionNotifyHost): void {\n this.host = host;\n }\n\n /**\n * Deliver a formatted completion message. Inert until `attachHost` supplies\n * a host (see the module header for why non-delivery is reported honestly\n * rather than optimistically). Not `async`: `emit()` below does not await it\n * either way - see its doc comment for why.\n *\n * A throw from the host is caught and reported as non-delivery rather than\n * propagated: `emit()` is a batcher callback running on a timer with no\n * caller to catch for it, and a failed render must leave `ResultWatcher`'s\n * claim in place for a retry, which is exactly what `false` does.\n */\n protected sendMessage(\n content: string,\n options: { triggerTurn: boolean; details: CompletionNotifyDetails[] },\n ): boolean {\n if (!this.host) return false;\n try {\n this.host.sendMessage(\n { customType: SUBAGENT_NOTIFY_MESSAGE_TYPE, content, display: true, details: options.details },\n { triggerTurn: options.triggerTurn, deliverAs: 'steer' },\n );\n return true;\n } catch {\n return false;\n }\n }\n\n async deliver(result: RunResultFile): Promise<boolean> {\n if (result.internal === true) return true;\n if (this.disposed) return false;\n const details = buildCompletionDetails(result);\n const triggerTurn = result.triggerTurn !== false;\n\n return new Promise<boolean>((resolve) => {\n const item: PendingCompletion = { details, triggerTurn, resolve };\n const batcher = this.getBatcher(result);\n if (details.status !== 'completed') {\n // A failure or a pause must not sit behind a debounce meant for\n // grouping good news; flush whatever this group was already holding,\n // then emit this one immediately, unbatched.\n batcher.flush();\n this.emit([item]);\n return;\n }\n batcher.push(item);\n });\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n for (const batcher of this.batchers.values()) {\n for (const item of batcher.dispose()) item.resolve(false);\n }\n this.batchers.clear();\n }\n\n private getBatcher(result: RunResultFile): CompletionBatcher<PendingCompletion> {\n const key = completionGroupKey(result);\n let batcher = this.batchers.get(key);\n if (!batcher) {\n batcher = createCompletionBatcher<PendingCompletion>({\n config: resolveCompletionBatchConfig(this.batchConfig),\n emit: (items) => this.emit(items),\n });\n this.batchers.set(key, batcher);\n }\n return batcher;\n }\n\n /**\n * The batcher's `emit` callback: format the group (single vs. grouped\n * rendering, `formatSingleCompletion`/`formatGroupedCompletion`) and hand\n * it to `sendMessage`. Deliberately not `async` and does not await\n * `sendMessage`: `createCompletionBatcher`'s `emit` option is a synchronous\n * callback (see its own contract), and every item's promise still resolves\n * correctly here regardless, since `sendMessage`'s result is available\n * synchronously to whichever implementation is active.\n */\n private emit(items: PendingCompletion[]): void {\n if (items.length === 0) return;\n const details = items.map((item) => item.details);\n const content = details.length === 1 ? formatSingleCompletion(details[0]!) : formatGroupedCompletion(details);\n const delivered = this.sendMessage(content, {\n triggerTurn: items.some((item) => item.triggerTurn),\n details,\n });\n for (const item of items) item.resolve(delivered);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function t(t){let n=t.isAlive??e,r=t.parentPid??process.ppid,i=!1,a=()=>{i||r>1&&n(r)||(i=!0,t.onParentLost())};if(r<=1)return a(),()=>{};let o=setInterval(a,t.intervalMs??5e3);return o.unref?.(),()=>clearInterval(o)}exports.startParentWatchdog=t;
|
|
2
|
+
//# sourceMappingURL=parentWatchdog.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parentWatchdog.cjs","names":[],"sources":["../../../src/runs/background/parentWatchdog.ts"],"sourcesContent":["/**\n * A child's own check that its parent is still there.\n *\n * WHY THIS EXISTS WHEN THE PARENT ALREADY SWEEPS AT SHUTDOWN:\n * The parent's `session_shutdown` sweep covers every case where the parent gets\n * to run code: quit, reload, `/new`, `/resume`, `/fork`. It cannot cover\n * `SIGKILL`, an OOM kill, or a host crash - no handler runs, and because every\n * child is spawned `detached: true` it is in its own process group, so the\n * terminal's Ctrl-C never reaches it either. Without this, exactly those cases\n * leave an agent running and spending tokens with nothing left to manage it.\n *\n * WHY POLLING AND NOT AN IPC CHANNEL:\n * A detached child has no inherited channel to watch, and adding one would\n * couple the child's liveness to a socket that can fail on its own. `kill(pid, 0)`\n * is the cheapest possible question and has no failure mode of its own.\n *\n * WHY IT DOES NOT JUST `process.exit`:\n * Exiting would leave the run reporting `running` forever and orphan the\n * agent's own subprocesses. The whole point is to reach the same terminal path\n * a clean stop takes, so `onParentLost` is expected to be\n * `TerminalPersistenceService.finalize()`, which persists a terminal status and\n * kills tracked children before the process goes.\n *\n * AVOID:\n * - A short interval. This is a safety net for a rare event, not a heartbeat;\n * polling hard buys nothing and costs a syscall per child per tick\n */\n\nconst DEFAULT_INTERVAL_MS = 5_000;\n\nexport interface ParentWatchdogOptions {\n /** Pid to watch. Defaults to this process's parent. */\n parentPid?: number;\n intervalMs?: number;\n /** Liveness as a port, so a test never probes a real pid. */\n isAlive?: (pid: number) => boolean;\n /** What to do once the parent is gone. Expected to finalize and exit. */\n onParentLost: () => void;\n}\n\nfunction defaultIsAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Start watching. Returns a disposer.\n *\n * A `parentPid` of 1 means this process has already been reparented to init -\n * the parent is gone and nothing is coming back - so the callback fires\n * immediately rather than after one interval.\n */\nexport function startParentWatchdog(options: ParentWatchdogOptions): () => void {\n const isAlive = options.isAlive ?? defaultIsAlive;\n const parentPid = options.parentPid ?? process.ppid;\n let fired = false;\n\n const check = (): void => {\n if (fired) return;\n if (parentPid > 1 && isAlive(parentPid)) return;\n fired = true;\n options.onParentLost();\n };\n\n if (parentPid <= 1) {\n check();\n return () => {};\n }\n\n const timer = setInterval(check, options.intervalMs ?? DEFAULT_INTERVAL_MS);\n // Unref'd so this never by itself keeps a finished child alive.\n timer.unref?.();\n return () => clearInterval(timer);\n}\n"],"mappings":"AAwCA,SAAS,EAAe,EAAsB,CAC5C,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CACd,OAAQ,EAAgC,OAAS,SAWrD,SAAgB,EAAoB,EAA4C,CAC9E,IAAM,EAAU,EAAQ,SAAW,EAC7B,EAAY,EAAQ,WAAa,QAAQ,KAC3C,EAAQ,GAEN,MAAoB,CACpB,GACA,EAAY,GAAK,EAAQ,EAAU,GACvC,EAAQ,GACR,EAAQ,cAAc,GAGxB,GAAI,GAAa,EAEf,OADA,GAAO,KACM,GAGf,IAAM,EAAQ,YAAY,EAAO,EAAQ,YAAc,IAAoB,CAG3E,OADA,EAAM,SAAS,KACF,cAAc,EAAM"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function t(t){let n=t.isAlive??e,r=t.parentPid??process.ppid,i=!1,a=()=>{i||r>1&&n(r)||(i=!0,t.onParentLost())};if(r<=1)return a(),()=>{};let o=setInterval(a,t.intervalMs??5e3);return o.unref?.(),()=>clearInterval(o)}export{t as startParentWatchdog};
|
|
2
|
+
//# sourceMappingURL=parentWatchdog.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parentWatchdog.mjs","names":[],"sources":["../../../src/runs/background/parentWatchdog.ts"],"sourcesContent":["/**\n * A child's own check that its parent is still there.\n *\n * WHY THIS EXISTS WHEN THE PARENT ALREADY SWEEPS AT SHUTDOWN:\n * The parent's `session_shutdown` sweep covers every case where the parent gets\n * to run code: quit, reload, `/new`, `/resume`, `/fork`. It cannot cover\n * `SIGKILL`, an OOM kill, or a host crash - no handler runs, and because every\n * child is spawned `detached: true` it is in its own process group, so the\n * terminal's Ctrl-C never reaches it either. Without this, exactly those cases\n * leave an agent running and spending tokens with nothing left to manage it.\n *\n * WHY POLLING AND NOT AN IPC CHANNEL:\n * A detached child has no inherited channel to watch, and adding one would\n * couple the child's liveness to a socket that can fail on its own. `kill(pid, 0)`\n * is the cheapest possible question and has no failure mode of its own.\n *\n * WHY IT DOES NOT JUST `process.exit`:\n * Exiting would leave the run reporting `running` forever and orphan the\n * agent's own subprocesses. The whole point is to reach the same terminal path\n * a clean stop takes, so `onParentLost` is expected to be\n * `TerminalPersistenceService.finalize()`, which persists a terminal status and\n * kills tracked children before the process goes.\n *\n * AVOID:\n * - A short interval. This is a safety net for a rare event, not a heartbeat;\n * polling hard buys nothing and costs a syscall per child per tick\n */\n\nconst DEFAULT_INTERVAL_MS = 5_000;\n\nexport interface ParentWatchdogOptions {\n /** Pid to watch. Defaults to this process's parent. */\n parentPid?: number;\n intervalMs?: number;\n /** Liveness as a port, so a test never probes a real pid. */\n isAlive?: (pid: number) => boolean;\n /** What to do once the parent is gone. Expected to finalize and exit. */\n onParentLost: () => void;\n}\n\nfunction defaultIsAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Start watching. Returns a disposer.\n *\n * A `parentPid` of 1 means this process has already been reparented to init -\n * the parent is gone and nothing is coming back - so the callback fires\n * immediately rather than after one interval.\n */\nexport function startParentWatchdog(options: ParentWatchdogOptions): () => void {\n const isAlive = options.isAlive ?? defaultIsAlive;\n const parentPid = options.parentPid ?? process.ppid;\n let fired = false;\n\n const check = (): void => {\n if (fired) return;\n if (parentPid > 1 && isAlive(parentPid)) return;\n fired = true;\n options.onParentLost();\n };\n\n if (parentPid <= 1) {\n check();\n return () => {};\n }\n\n const timer = setInterval(check, options.intervalMs ?? DEFAULT_INTERVAL_MS);\n // Unref'd so this never by itself keeps a finished child alive.\n timer.unref?.();\n return () => clearInterval(timer);\n}\n"],"mappings":"AAwCA,SAAS,EAAe,EAAsB,CAC5C,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,EAAE,CACb,SACA,EAAO,CACd,OAAQ,EAAgC,OAAS,SAWrD,SAAgB,EAAoB,EAA4C,CAC9E,IAAM,EAAU,EAAQ,SAAW,EAC7B,EAAY,EAAQ,WAAa,QAAQ,KAC3C,EAAQ,GAEN,MAAoB,CACpB,GACA,EAAY,GAAK,EAAQ,EAAU,GACvC,EAAQ,GACR,EAAQ,cAAc,GAGxB,GAAI,GAAa,EAEf,OADA,GAAO,KACM,GAGf,IAAM,EAAQ,YAAY,EAAO,EAAQ,YAAc,IAAoB,CAG3E,OADA,EAAM,SAAS,KACF,cAAc,EAAM"}
|
|
@@ -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(`../../shared/paths.cjs`),r=require(`./terminalPersistence.cjs`);let i=require(`node:fs`);i=e.__toESM(i,1);let a=require(`node:path`);a=e.__toESM(a,1);let o=require(`inversify`);function s(e){return a.join(n.currentRunsDir(),e,r.CRASH_MARKER_FILE_NAME)}function c(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function l(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function u(e,t=process.kill.bind(process)){try{return t(e,0),`alive`}catch(e){return l(e)===`ESRCH`?`dead`:`unknown`}}let d=class{probePid(e){return u(e)}readFile(e){try{return i.readFileSync(e,`utf-8`)}catch(e){if(l(e)===`ENOENT`)return;throw e}}readCrashMarker(e){let t=this.readFile(s(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}if(!(!c(n)||n.version!==1||typeof n.runId!=`string`||typeof n.pid!=`number`||typeof n.startedAt!=`number`))return{version:1,runId:n.runId,pid:n.pid,startedAt:n.startedAt}}inspect(e){let t=this.readCrashMarker(e);if(!t)return{state:`unknown`,marker:void 0};let n=this.probePid(t.pid);return n===`dead`?{state:`crashed`,marker:t}:n===`alive`?{state:`alive`,marker:t}:{state:`unknown`,marker:t}}};d=t.__decorate([(0,o.injectable)()],d),Object.defineProperty(exports,`ProcessTerminalInspector`,{enumerable:!0,get:function(){return d}});
|
|
2
|
+
//# sourceMappingURL=processTerminal.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processTerminal.cjs","names":["path","currentRunsDir","CRASH_MARKER_FILE_NAME","fs"],"sources":["../../../src/runs/background/processTerminal.ts"],"sourcesContent":["/**\n * Provable-death check for one run's owning process.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/processTerminal.ts` proves process\n * termination by cross-referencing every spawned `pi` writer's own close\n * observation against an expected-writer-count candidate file, plus a\n * canonical-session-lease projection for resumability. None of that\n * infrastructure exists in this package yet (writer-close observation and\n * session-lease-aware resumability are later work). What this package already\n * has is `TerminalPersistenceService`'s crash marker: one file, written before\n * a run does anything else and cleared only once `finalize()` has actually\n * persisted a terminal result. That is already a complete answer to the one\n * question `StaleRunReconciler` needs answered - \"did this run's process die\n * without ever finishing?\" - so this module reads that marker instead of\n * reintroducing the predecessor's writer-candidate apparatus.\n *\n * WHY A SEPARATE MODULE FROM `TerminalPersistenceService`:\n * `TerminalPersistenceService` writes and clears the crash marker for the ONE\n * run its own process is guarding. Reading someone else's marker to judge\n * whether THEIR process died is a different responsibility, usually exercised\n * from a different process entirely (the one asking \"is this stale?\", not the\n * one that was running). Splitting it out is what lets `TerminalPersistenceService`'s\n * own header say \"reading it to make that determination is a different\n * module's job\" and mean it.\n *\n * THE VERDICT IS DELIBERATELY THREE-VALUED, NOT TWO:\n * `'crashed'` is the only value that authorizes a repair. `'alive'` and\n * `'unknown'` both mean \"do not touch this run\" - a live pid means it is\n * genuinely still running, and an unreadable pid liveness (`EPERM`, or no\n * marker at all for a run whose status claims to be in flight) means this\n * module cannot prove anything either way. Collapsing `'alive'` and\n * `'unknown'` into one \"don't repair\" value would hide, from every caller,\n * that ambiguity is a real observable outcome, not just the absence of one.\n *\n * AVOID:\n * - Ever returning `'crashed'` from anything other than an OS-confirmed ESRCH\n * on the marker's pid. `EPERM`, a missing marker, or \"no update in a long\n * time\" are all real conditions a caller may still want to know about, but\n * none of them prove death, and treating them as though they did is exactly\n * the guess `StaleRunReconciler` exists to refuse\n * - Writing anything here. This module only reads\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport { CRASH_MARKER_FILE_NAME } from './terminalPersistence.ts';\n\nconst CRASH_MARKER_VERSION = 1;\n\nexport type ProcessLiveness = 'alive' | 'dead' | 'unknown';\n\n/** A signal-0 probe, injectable so a test never sends a real signal to a real pid. */\nexport type PidProbe = (pid: number, signal?: NodeJS.Signals | 0) => boolean;\n\nexport interface CrashMarkerRecord {\n version: typeof CRASH_MARKER_VERSION;\n runId: string;\n pid: number;\n startedAt: number;\n}\n\nexport type ProcessTerminalVerdict =\n | { state: 'alive'; marker: CrashMarkerRecord }\n | { state: 'crashed'; marker: CrashMarkerRecord }\n | { state: 'unknown'; marker: CrashMarkerRecord | undefined };\n\nexport interface IProcessTerminalInspector {\n /** Whether `runId`'s owning process is provably dead, based on its crash marker. */\n inspect(runId: string): ProcessTerminalVerdict;\n}\n\nfunction crashMarkerPath(runId: string): string {\n return path.join(currentRunsDir(), runId, CRASH_MARKER_FILE_NAME);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\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\n/**\n * `kill(pid, 0)` sends no signal; it only asks the OS whether the pid could be\n * signalled. `ESRCH` is the OS actively confirming no such process exists -\n * the one condition this module trusts as proof. `EPERM` means a process is\n * there but this process may not signal it (commonly a different user), which\n * proves the opposite of death and is reported as `'unknown'` rather than\n * `'alive'` only because ownership, not existence, is what is uncertain there;\n * treating it as `'alive'` would also be a safe, defensible reading, but\n * `'unknown'` keeps the caller from building any certainty either way on it.\n */\nexport function checkPidLiveness(pid: number, probe: PidProbe = process.kill.bind(process)): ProcessLiveness {\n try {\n probe(pid, 0);\n return 'alive';\n } catch (error) {\n const code = errorCode(error);\n if (code === 'ESRCH') return 'dead';\n return 'unknown';\n }\n}\n\n@injectable()\nexport class ProcessTerminalInspector implements IProcessTerminalInspector {\n /**\n * Seam for tests, as a protected member rather than a constructor argument:\n * inversify cannot resolve a function value as a dependency, and a unit test\n * must never deliver a real signal to a real pid.\n */\n protected probePid(pid: number): ProcessLiveness {\n return checkPidLiveness(pid);\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 readCrashMarker(runId: string): CrashMarkerRecord | undefined {\n const raw = this.readFile(crashMarkerPath(runId));\n if (raw === undefined) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A marker mid-write (rename not yet visible, or a torn read) is not\n // evidence of anything; treated the same as no marker at all.\n return undefined;\n }\n if (\n !isRecord(parsed) ||\n parsed.version !== CRASH_MARKER_VERSION ||\n typeof parsed.runId !== 'string' ||\n typeof parsed.pid !== 'number' ||\n typeof parsed.startedAt !== 'number'\n ) {\n return undefined;\n }\n return { version: CRASH_MARKER_VERSION, runId: parsed.runId, pid: parsed.pid, startedAt: parsed.startedAt };\n }\n\n inspect(runId: string): ProcessTerminalVerdict {\n const marker = this.readCrashMarker(runId);\n if (!marker) return { state: 'unknown', marker: undefined };\n\n const liveness = this.probePid(marker.pid);\n if (liveness === 'dead') return { state: 'crashed', marker };\n if (liveness === 'alive') return { state: 'alive', marker };\n return { state: 'unknown', marker };\n }\n}\n"],"mappings":"mUA2EA,SAAS,EAAgB,EAAuB,CAC9C,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAOC,EAAAA,uBAAuB,CAGnE,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAG7E,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAaN,SAAgB,EAAiB,EAAa,EAAkB,QAAQ,KAAK,KAAK,QAAQ,CAAmB,CAC3G,GAAI,CAEF,OADA,EAAM,EAAK,EAAE,CACN,cACA,EAAO,CAGd,OAFa,EAAU,EACf,GAAK,QAAgB,OACtB,WAKJ,IAAA,EAAA,KAAoE,CAMzE,SAAmB,EAA8B,CAC/C,OAAO,EAAiB,EAAI,CAG9B,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAOC,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,GAIV,gBAA0B,EAA8C,CACtE,IAAM,EAAM,KAAK,SAAS,EAAgB,EAAM,CAAC,CACjD,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CAGN,OAGA,MAAC,EAAS,EAAO,EACjB,EAAO,UAAY,GACnB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,KAAQ,UACtB,OAAO,EAAO,WAAc,UAI9B,MAAO,CAAE,QAAS,EAAsB,MAAO,EAAO,MAAO,IAAK,EAAO,IAAK,UAAW,EAAO,UAAW,CAG7G,QAAQ,EAAuC,CAC7C,IAAM,EAAS,KAAK,gBAAgB,EAAM,CAC1C,GAAI,CAAC,EAAQ,MAAO,CAAE,MAAO,UAAW,OAAQ,IAAA,GAAW,CAE3D,IAAM,EAAW,KAAK,SAAS,EAAO,IAAI,CAG1C,OAFI,IAAa,OAAe,CAAE,MAAO,UAAW,SAAQ,CACxD,IAAa,QAAgB,CAAE,MAAO,QAAS,SAAQ,CACpD,CAAE,MAAO,UAAW,SAAQ,qCAlD1B,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{currentRunsDir as r,init_paths as i}from"../../shared/paths.mjs";import{CRASH_MARKER_FILE_NAME as a,init_terminalPersistence as o}from"./terminalPersistence.mjs";import*as s from"node:fs";import*as c from"node:path";import{injectable as l}from"inversify";function u(e){return c.join(r(),e,a)}function d(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function f(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function p(e,t=process.kill.bind(process)){try{return t(e,0),`alive`}catch(e){return f(e)===`ESRCH`?`dead`:`unknown`}}var m,h,g=e((()=>{i(),o(),n(),m=1,h=class{probePid(e){return p(e)}readFile(e){try{return s.readFileSync(e,`utf-8`)}catch(e){if(f(e)===`ENOENT`)return;throw e}}readCrashMarker(e){let t=this.readFile(u(e));if(t===void 0)return;let n;try{n=JSON.parse(t)}catch{return}if(!(!d(n)||n.version!==m||typeof n.runId!=`string`||typeof n.pid!=`number`||typeof n.startedAt!=`number`))return{version:m,runId:n.runId,pid:n.pid,startedAt:n.startedAt}}inspect(e){let t=this.readCrashMarker(e);if(!t)return{state:`unknown`,marker:void 0};let n=this.probePid(t.pid);return n===`dead`?{state:`crashed`,marker:t}:n===`alive`?{state:`alive`,marker:t}:{state:`unknown`,marker:t}}},h=t([l()],h)}));g();export{h as ProcessTerminalInspector,g as init_processTerminal};
|
|
2
|
+
//# sourceMappingURL=processTerminal.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processTerminal.mjs","names":[],"sources":["../../../src/runs/background/processTerminal.ts"],"sourcesContent":["/**\n * Provable-death check for one run's owning process.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/processTerminal.ts` proves process\n * termination by cross-referencing every spawned `pi` writer's own close\n * observation against an expected-writer-count candidate file, plus a\n * canonical-session-lease projection for resumability. None of that\n * infrastructure exists in this package yet (writer-close observation and\n * session-lease-aware resumability are later work). What this package already\n * has is `TerminalPersistenceService`'s crash marker: one file, written before\n * a run does anything else and cleared only once `finalize()` has actually\n * persisted a terminal result. That is already a complete answer to the one\n * question `StaleRunReconciler` needs answered - \"did this run's process die\n * without ever finishing?\" - so this module reads that marker instead of\n * reintroducing the predecessor's writer-candidate apparatus.\n *\n * WHY A SEPARATE MODULE FROM `TerminalPersistenceService`:\n * `TerminalPersistenceService` writes and clears the crash marker for the ONE\n * run its own process is guarding. Reading someone else's marker to judge\n * whether THEIR process died is a different responsibility, usually exercised\n * from a different process entirely (the one asking \"is this stale?\", not the\n * one that was running). Splitting it out is what lets `TerminalPersistenceService`'s\n * own header say \"reading it to make that determination is a different\n * module's job\" and mean it.\n *\n * THE VERDICT IS DELIBERATELY THREE-VALUED, NOT TWO:\n * `'crashed'` is the only value that authorizes a repair. `'alive'` and\n * `'unknown'` both mean \"do not touch this run\" - a live pid means it is\n * genuinely still running, and an unreadable pid liveness (`EPERM`, or no\n * marker at all for a run whose status claims to be in flight) means this\n * module cannot prove anything either way. Collapsing `'alive'` and\n * `'unknown'` into one \"don't repair\" value would hide, from every caller,\n * that ambiguity is a real observable outcome, not just the absence of one.\n *\n * AVOID:\n * - Ever returning `'crashed'` from anything other than an OS-confirmed ESRCH\n * on the marker's pid. `EPERM`, a missing marker, or \"no update in a long\n * time\" are all real conditions a caller may still want to know about, but\n * none of them prove death, and treating them as though they did is exactly\n * the guess `StaleRunReconciler` exists to refuse\n * - Writing anything here. This module only reads\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport { CRASH_MARKER_FILE_NAME } from './terminalPersistence.ts';\n\nconst CRASH_MARKER_VERSION = 1;\n\nexport type ProcessLiveness = 'alive' | 'dead' | 'unknown';\n\n/** A signal-0 probe, injectable so a test never sends a real signal to a real pid. */\nexport type PidProbe = (pid: number, signal?: NodeJS.Signals | 0) => boolean;\n\nexport interface CrashMarkerRecord {\n version: typeof CRASH_MARKER_VERSION;\n runId: string;\n pid: number;\n startedAt: number;\n}\n\nexport type ProcessTerminalVerdict =\n | { state: 'alive'; marker: CrashMarkerRecord }\n | { state: 'crashed'; marker: CrashMarkerRecord }\n | { state: 'unknown'; marker: CrashMarkerRecord | undefined };\n\nexport interface IProcessTerminalInspector {\n /** Whether `runId`'s owning process is provably dead, based on its crash marker. */\n inspect(runId: string): ProcessTerminalVerdict;\n}\n\nfunction crashMarkerPath(runId: string): string {\n return path.join(currentRunsDir(), runId, CRASH_MARKER_FILE_NAME);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\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\n/**\n * `kill(pid, 0)` sends no signal; it only asks the OS whether the pid could be\n * signalled. `ESRCH` is the OS actively confirming no such process exists -\n * the one condition this module trusts as proof. `EPERM` means a process is\n * there but this process may not signal it (commonly a different user), which\n * proves the opposite of death and is reported as `'unknown'` rather than\n * `'alive'` only because ownership, not existence, is what is uncertain there;\n * treating it as `'alive'` would also be a safe, defensible reading, but\n * `'unknown'` keeps the caller from building any certainty either way on it.\n */\nexport function checkPidLiveness(pid: number, probe: PidProbe = process.kill.bind(process)): ProcessLiveness {\n try {\n probe(pid, 0);\n return 'alive';\n } catch (error) {\n const code = errorCode(error);\n if (code === 'ESRCH') return 'dead';\n return 'unknown';\n }\n}\n\n@injectable()\nexport class ProcessTerminalInspector implements IProcessTerminalInspector {\n /**\n * Seam for tests, as a protected member rather than a constructor argument:\n * inversify cannot resolve a function value as a dependency, and a unit test\n * must never deliver a real signal to a real pid.\n */\n protected probePid(pid: number): ProcessLiveness {\n return checkPidLiveness(pid);\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 readCrashMarker(runId: string): CrashMarkerRecord | undefined {\n const raw = this.readFile(crashMarkerPath(runId));\n if (raw === undefined) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A marker mid-write (rename not yet visible, or a torn read) is not\n // evidence of anything; treated the same as no marker at all.\n return undefined;\n }\n if (\n !isRecord(parsed) ||\n parsed.version !== CRASH_MARKER_VERSION ||\n typeof parsed.runId !== 'string' ||\n typeof parsed.pid !== 'number' ||\n typeof parsed.startedAt !== 'number'\n ) {\n return undefined;\n }\n return { version: CRASH_MARKER_VERSION, runId: parsed.runId, pid: parsed.pid, startedAt: parsed.startedAt };\n }\n\n inspect(runId: string): ProcessTerminalVerdict {\n const marker = this.readCrashMarker(runId);\n if (!marker) return { state: 'unknown', marker: undefined };\n\n const liveness = this.probePid(marker.pid);\n if (liveness === 'dead') return { state: 'crashed', marker };\n if (liveness === 'alive') return { state: 'alive', marker };\n return { state: 'unknown', marker };\n }\n}\n"],"mappings":"wbA2EA,SAAS,EAAgB,EAAuB,CAC9C,OAAO,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAuB,CAGnE,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAG7E,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAaN,SAAgB,EAAiB,EAAa,EAAkB,QAAQ,KAAK,KAAK,QAAQ,CAAmB,CAC3G,GAAI,CAEF,OADA,EAAM,EAAK,EAAE,CACN,cACA,EAAO,CAGd,OAFa,EAAU,EACf,GAAK,QAAgB,OACtB,gCA1D4C,IACW,KAE5D,EAAuB,IA4DtB,KAAoE,CAMzE,SAAmB,EAA8B,CAC/C,OAAO,EAAiB,EAAI,CAG9B,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAO,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,GAIV,gBAA0B,EAA8C,CACtE,IAAM,EAAM,KAAK,SAAS,EAAgB,EAAM,CAAC,CACjD,GAAI,IAAQ,IAAA,GAAW,OACvB,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,MAClB,CAGN,OAGA,MAAC,EAAS,EAAO,EACjB,EAAO,UAAY,GACnB,OAAO,EAAO,OAAU,UACxB,OAAO,EAAO,KAAQ,UACtB,OAAO,EAAO,WAAc,UAI9B,MAAO,CAAE,QAAS,EAAsB,MAAO,EAAO,MAAO,IAAK,EAAO,IAAK,UAAW,EAAO,UAAW,CAG7G,QAAQ,EAAuC,CAC7C,IAAM,EAAS,KAAK,gBAAgB,EAAM,CAC1C,GAAI,CAAC,EAAQ,MAAO,CAAE,MAAO,UAAW,OAAQ,IAAA,GAAW,CAE3D,IAAM,EAAW,KAAK,SAAS,EAAO,IAAI,CAG1C,OAFI,IAAa,OAAe,CAAE,MAAO,UAAW,SAAQ,CACxD,IAAa,QAAgB,CAAE,MAAO,QAAS,SAAQ,CACpD,CAAE,MAAO,UAAW,SAAQ,QAlDtC,GAAY,CAAA,CAAA,EAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`../../_virtual/_rolldown/runtime.cjs`),t=require(`../../shared/configDir.cjs`),n=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.cjs`),r=require(`../../env.cjs`),i=require(`../../container/types.cjs`),a=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),o=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`),s=require(`../../shared/paths.cjs`);let c=require(`node:fs`);c=e.__toESM(c,1);let l=require(`node:path`);l=e.__toESM(l,1);let u=require(`inversify`);const d=`.json`,f=`claimed-result.json`;function p(e){return l.join(s.currentResultsDir(),e)}function m(e){return l.join(s.currentRunsDir(),e)}function h(e){return l.join(m(e),f)}function g(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function _(e){return g(e)===`ENOENT`}let v=class{constructor(e){this.scheduler=e}pollIntervalMs=4e3;dedupeTtlMs=6e4;now(){return Date.now()}onProcessingError(e,t){this.lastProcessingError={id:e,error:t,at:this.now()},this.processingErrorCount+=1}lastProcessingError;processingErrorCount=0;consumer;watcher;unregisterPoll;claims=new Map;deliveredAt=new Map;listResultFiles(){try{return c.readdirSync(s.currentResultsDir()).filter(e=>e.endsWith(d))}catch(e){if(_(e))return[];throw e}}listClaimedRunIds(){try{return c.readdirSync(s.currentRunsDir()).filter(e=>this.fileExists(l.join(m(e),f)))}catch(e){if(_(e))return[];throw e}}fileExists(e){return c.existsSync(e)}readFile(e){try{return c.readFileSync(e,`utf-8`)}catch(e){if(_(e))return;throw e}}ensureDir(e){c.mkdirSync(e,{recursive:!0})}renameFile(e,t){try{return c.renameSync(e,t),!0}catch(e){if(_(e))return!1;throw e}}unlinkFile(e){try{c.unlinkSync(e)}catch(t){_(t)||this.onProcessingError(e,t)}}handleWatchEvent(e,t){t&&t.toString().endsWith(`.json`)&&this.scheduler.wake()}handleWatchError(){this.watcher=void 0}getWatcher(){return this.watcher}watchResultsDir(){try{this.ensureDir(s.currentResultsDir());let e=t.resolveWatchPath(s.currentResultsDir(),c.realpathSync.native),n=c.watch(e,(e,t)=>this.handleWatchEvent(e,t));return n.on(`error`,()=>this.handleWatchError()),n.unref?.(),n}catch{return}}pruneDeliveredAt(e){for(let[t,n]of this.deliveredAt)e-n>this.dedupeTtlMs&&this.deliveredAt.delete(t)}hasRecentlyDelivered(e){let t=this.now();return this.pruneDeliveredAt(t),this.deliveredAt.has(e)}markDelivered(e){let t=this.now();if(this.pruneDeliveredAt(t),this.deliveredAt.set(e,t),this.deliveredAt.size>256){let e=this.deliveredAt.keys().next();e.done||this.deliveredAt.delete(e.value)}}claimResult(e,t){return this.ensureDir(m(e)),this.renameFile(p(t),h(e))}discoverAndClaim(){let e=0,t=process.env[r.SUBAGENT_RUN_ID_ENV]?.trim();for(let n of this.listResultFiles()){let r=n.slice(0,-5);!r||r===t||this.claims.has(r)||this.claimResult(r,n)&&(this.claims.set(r,{inFlight:!1}),e+=1)}return e}discardClaim(e){this.unlinkFile(h(e)),this.claims.delete(e)}async deliverClaim(e,t){let n=h(e);try{let r=this.readFile(n);if(r===void 0){this.claims.delete(e);return}let i;try{i=JSON.parse(r)}catch(t){this.onProcessingError(e,t),this.discardClaim(e);return}if(typeof i!=`object`||!i){this.discardClaim(e);return}let a=i,o=typeof a.runId==`string`&&a.runId?a.runId:e;if(this.hasRecentlyDelivered(o)){this.discardClaim(e);return}let s=this.consumer;if(!s){t.inFlight=!1;return}await s({...a,runId:o})?(this.markDelivered(o),this.discardClaim(e)):t.inFlight=!1}catch(n){this.onProcessingError(e,n),t.inFlight=!1}}run(){this.watcher||=this.watchResultsDir();let e=this.discoverAndClaim();for(let[e,t]of this.claims)t.inFlight||(t.inFlight=!0,this.deliverClaim(e,t));return e>0}start(e){this.stop(),this.consumer=e;for(let e of this.listClaimedRunIds())this.claims.set(e,{inFlight:!1});this.watcher=this.watchResultsDir(),this.unregisterPoll=this.scheduler.register({id:`result-watcher`,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.consumer=void 0,this.watcher?.close(),this.watcher=void 0,this.unregisterPoll?.(),this.unregisterPoll=void 0,this.claims.clear(),this.deliveredAt.clear()}};v=n.__decorate([(0,u.injectable)(),o.__decorateParam(0,(0,u.inject)(i.DOOM_TEAM_TYPES.PollScheduler)),a.__decorateMetadata(`design:paramtypes`,[Object])],v),exports.CLAIMED_RESULT_FILE_NAME=f,exports.RESULT_FILE_SUFFIX=d,Object.defineProperty(exports,`ResultWatcher`,{enumerable:!0,get:function(){return v}});
|
|
2
|
+
//# sourceMappingURL=resultWatcher.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resultWatcher.cjs","names":["path","currentResultsDir","currentRunsDir","fs","resolveWatchPath","SUBAGENT_RUN_ID_ENV","DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/resultWatcher.ts"],"sourcesContent":["/**\n * Watches `currentResultsDir()` for a run's terminal result and delivers each one to\n * a consumer exactly once.\n *\n * WHY THIS EXISTS:\n * The predecessor's watcher\n * (`doom-pi-subagents/src/runs/background/resultWatcher.ts`) had four\n * separate, verified bugs. This is a redesign around the fixes, not a line\n * port - the fixes below are structural, not patches on the old control flow.\n *\n * FIX 1 - WATCH BOTH 'rename' AND 'change':\n * The predecessor only reacted to `event === 'rename'` (`resultWatcher.ts:398-399`),\n * so a result file rewritten in place - same path, new contents, no\n * create/delete/rename - was never noticed until the poll safety net happened\n * to run. `watchResultsDir()` below reacts to any `fs.watch` callback whose\n * filename matches, regardless of `eventType`, the same way\n * `SpawnHandshake.watchDirectory` already does in this package.\n *\n * FIX 2 - TTL DEDUPE:\n * The predecessor re-implemented \"is this a duplicate\" inline at its call\n * site (`resultWatcher.ts:180-195`) *and* separately inside\n * `completion-dedupe.ts`'s `markSeenWithTtl`, so the check and the record used\n * two independently maintained copies of the same expiry logic. Every check\n * and every record here goes through `hasRecentlyDelivered()` and\n * `markDelivered()` and nothing else - one implementation of the TTL policy,\n * used the same way on both sides.\n *\n * FIX 3 - TIMER TYPES:\n * The predecessor stored `setTimeout`'s return value in fields typed (or\n * used) as if they were numbers (`resultWatcher.ts:361,371,425`), which only\n * happens to work under Node's `setTimeout` and breaks the moment the return\n * value is compared, serialized, or ported somewhere `setTimeout` returns a\n * number (browsers, some polyfills). This module does not own a timer at all:\n * `fs.watch` is the fast path and `PollScheduler` is the bounded safety net\n * (see fix 5), so there is no `Timeout` handle here for the compiler to get\n * wrong in the first place.\n *\n * FIX 4 - ATOMIC CONSUME (TOCTOU):\n * The predecessor checked a result file's existence, then unlinked it, as two\n * separate steps (`resultWatcher.ts:184-194,324-332`), racing any other\n * reader of the same file between the two. Here, the *first* thing that\n * happens to a candidate result is an attempted rename into\n * `currentRunsDir()/<runId>/claimed-result.json` (`claimResult()`). A successful\n * rename is exclusive ownership: nothing else can also be holding that\n * result, because there is only ever one file at the source path and only one\n * caller's `renameSync` can move it. A failed rename (source already gone)\n * means another claimer got there first, which is `claimResult()`'s normal,\n * expected return of `false` - not a caught error.\n *\n * FIX 5 - WATCH-FIRST, POLL AS A BOUNDED SAFETY NET:\n * `start()` registers with `PollScheduler` instead of owning a timer, the\n * same pattern as every other long-lived background subscriber in this\n * directory. `fs.watch` gives low latency; the scheduler's tick is the bound\n * on how late a check can ever be on a platform or mount where watches are\n * unreliable. Neither path is trusted alone.\n *\n * DESIGN PATTERNS:\n * - A claimed result lives at `currentRunsDir()/<runId>/claimed-result.json`, inside\n * the run's own working directory rather than a subdirectory of\n * `currentResultsDir()`. Putting it under `currentResultsDir()` would make our own claims\n * visible to the very `fs.watch` this module registers, and would risk\n * colliding with `currentResultsDir()/<runId>.json` naming. `currentRunsDir()/<runId>/` is\n * already this run's own directory (see `statusWriter.ts`,\n * `terminalPersistence.ts`), so this reuses existing per-run space instead\n * of inventing a second one\n * - `run()` is synchronous, matching `PollScheduler`'s contract, but delivery\n * is not: `deliverClaim()` is fired without being awaited, and a claim's\n * `inFlight` flag stops it from being re-fired on the next tick while the\n * previous attempt is still outstanding\n * - `run()` reports \"work happened\" only when it claimed a new file, not when\n * it merely retried a still-pending delivery. A consumer that keeps\n * rejecting the same result forever would otherwise pin the scheduler's\n * backoff at the floor indefinitely\n * - A crash between claiming a result and delivering it leaves\n * `claimed-result.json` on disk with nothing tracking it in memory.\n * `start()` scans for these once, so a fresh process picks up an\n * interrupted delivery instead of leaving it stranded forever\n *\n * AVOID:\n * - Reading or unlinking anything at `currentResultsDir()/<runId>.json` once\n * `claimResult()` has returned true for it; ownership has moved to the\n * claimed copy, and the source name may already belong to an unrelated,\n * later result\n * - Treating a rename failure from `claimResult()` as noteworthy; the\n * `onProcessingError` path is for delivery failures and parse failures, not\n * for the ordinary case of losing a claim race\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { resolveWatchPath } from '../../shared/configDir.ts';\nimport { currentResultsDir, currentRunsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport type { IPollScheduler } from '../../shared/pollScheduler.ts';\n\n/** Suffix a candidate result file must have in `currentResultsDir()`. */\nexport const RESULT_FILE_SUFFIX = '.json';\n/** Leaf name of a claimed result inside `currentRunsDir()/<runId>/`. */\nexport const CLAIMED_RESULT_FILE_NAME = 'claimed-result.json';\n\n/** Poll safety-net cadence, generously looser than `fs.watch`'s latency (see `PollScheduler`'s header). */\nconst DEFAULT_POLL_INTERVAL_MS = 4_000;\n/** How long a delivered run id is remembered, so a re-appearing result for it is dropped instead of redelivered. */\nconst DEFAULT_DEDUPE_TTL_MS = 60_000;\n/** Cap on tracked delivered ids, so a very long session cannot grow this without bound. */\nconst MAX_TRACKED_DELIVERIES = 256;\n\nconst POLL_SUBSCRIBER_ID = 'result-watcher';\n\n/** The parsed contents of a result file, with `runId` guaranteed present. */\nexport interface RunResultFile {\n runId: string;\n [key: string]: unknown;\n}\n\n/**\n * Handles one delivered result. Returns (or resolves to) `true` once it has\n * been durably accepted, which is what lets the claimed copy be removed;\n * `false` means \"not yet\", and the same claim is retried on a later tick.\n */\nexport type ResultConsumer = (result: RunResultFile) => boolean | Promise<boolean>;\n\nexport interface IResultWatcher {\n /** Begin watching. Calling this again resets whatever was previously tracked, same as `CoalescedStatusWriter.open`. */\n start(consumer: ResultConsumer): void;\n /** Stop watching, unregister from the scheduler, and drop all in-memory state. */\n stop(): void;\n}\n\ninterface ClaimEntry {\n /** True while `deliverClaim()` is awaiting this claim's consumer call, so `run()` does not fire it twice. */\n inFlight: boolean;\n}\n\nfunction resultPathFor(file: string): string {\n return path.join(currentResultsDir(), file);\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction claimPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_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 isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class ResultWatcher implements IResultWatcher {\n constructor(@inject(DOOM_TEAM_TYPES.PollScheduler) private readonly scheduler: IPollScheduler) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive, so a test subclass overrides these.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly dedupeTtlMs: number = DEFAULT_DEDUPE_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Records a claim or delivery failure. Overridable; never a stdio write (see package rules). */\n protected onProcessingError(id: string, error: unknown): void {\n this.lastProcessingError = { id, error, at: this.now() };\n this.processingErrorCount += 1;\n }\n\n /** The most recent processing failure, for diagnostics. */\n lastProcessingError: { id: string; error: unknown; at: number } | undefined;\n /** How many processing failures this watcher has absorbed. */\n processingErrorCount = 0;\n\n private consumer: ResultConsumer | undefined;\n private watcher: fs.FSWatcher | undefined;\n private unregisterPoll: (() => void) | undefined;\n private readonly claims = new Map<string, ClaimEntry>();\n private readonly deliveredAt = new Map<string, number>();\n\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected listResultFiles(): string[] {\n try {\n return fs.readdirSync(currentResultsDir()).filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n /** Run ids with a lingering claimed copy, found by scanning `currentRunsDir()` once at `start()`. */\n protected listClaimedRunIds(): string[] {\n try {\n return fs\n .readdirSync(currentRunsDir())\n .filter((entry) => this.fileExists(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME)));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected fileExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected ensureDir(dirPath: string): void {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n\n /**\n * The atomic claim (fix 4). Returns `false`, without throwing, when the\n * source is already gone - another claimer won the race, which is the\n * expected outcome of two watchers noticing the same file.\n */\n protected renameFile(fromPath: string, toPath: string): boolean {\n try {\n fs.renameSync(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected unlinkFile(filePath: string): void {\n try {\n fs.unlinkSync(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n /**\n * The `fs.watch` callback's logic, factored out so it can be exercised\n * deterministically (fix 1's actual test coverage): calling this directly\n * with synthetic `eventType`/`filename` values proves the filter reacts to\n * 'change' exactly like 'rename', without depending on whether - or when -\n * the OS actually delivers a real filesystem event. `fs.watch` gives no\n * delivery guarantee at all, so a test that waits on a real one to arrive\n * is testing the OS's scheduler, not this code; see `watchResultsDir()`'s\n * only caller of this, and the poll safety net (fix 5) that exists\n * precisely because that guarantee does not hold.\n */\n protected handleWatchEvent(_eventType: fs.WatchEventType, filename: string | Buffer | null): void {\n if (!filename) return;\n if (!filename.toString().endsWith(RESULT_FILE_SUFFIX)) return;\n this.scheduler.wake();\n }\n\n /**\n * A watch that has emitted 'error' is treated as gone: `run()` checks\n * `getWatcher()` on its next tick and re-establishes it, falling back to\n * the poll safety net (fix 5) in the meantime. Factored out, like\n * `handleWatchEvent`, so a test can trigger it directly rather than by\n * provoking a real watch failure.\n */\n protected handleWatchError(): void {\n this.watcher = undefined;\n }\n\n /** The currently-held watch, if any. Exposed for tests; `run()` reads the field directly. */\n protected getWatcher(): fs.FSWatcher | undefined {\n return this.watcher;\n }\n\n /**\n * Watch `currentResultsDir()` for candidate result files. Reacts to every `fs.watch`\n * callback regardless of `eventType` (fix 1, see `handleWatchEvent`).\n * Returns `undefined`, without throwing, when the watch could not be\n * established at all - `run()` retries this on its next tick, and the poll\n * safety net (fix 5) covers the gap in the meantime.\n */\n protected watchResultsDir(): fs.FSWatcher | undefined {\n try {\n this.ensureDir(currentResultsDir());\n const watchPath = resolveWatchPath(currentResultsDir(), fs.realpathSync.native);\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n // ---------------------------------------------------------------------\n // TTL dedupe (fix 2). One implementation, used by both the check and the\n // record, so they cannot drift the way the predecessor's two copies did.\n // ---------------------------------------------------------------------\n\n private pruneDeliveredAt(now: number): void {\n for (const [key, at] of this.deliveredAt) {\n if (now - at > this.dedupeTtlMs) this.deliveredAt.delete(key);\n }\n }\n\n private hasRecentlyDelivered(runId: string): boolean {\n const now = this.now();\n this.pruneDeliveredAt(now);\n return this.deliveredAt.has(runId);\n }\n\n private markDelivered(runId: string): void {\n const now = this.now();\n this.pruneDeliveredAt(now);\n this.deliveredAt.set(runId, now);\n if (this.deliveredAt.size > MAX_TRACKED_DELIVERIES) {\n const oldest = this.deliveredAt.keys().next();\n if (!oldest.done) this.deliveredAt.delete(oldest.value);\n }\n }\n\n // ---------------------------------------------------------------------\n // Claim + deliver\n // ---------------------------------------------------------------------\n\n /** Attempt the atomic claim for one candidate file. `true` only when this call won the race. */\n protected claimResult(runId: string, file: string): boolean {\n this.ensureDir(runDirFor(runId));\n return this.renameFile(resultPathFor(file), claimPathFor(runId));\n }\n\n /** Claim every new candidate in `currentResultsDir()`. Returns how many were newly claimed. */\n private discoverAndClaim(): number {\n let claimedCount = 0;\n const ownRunId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n for (const file of this.listResultFiles()) {\n const runId = file.slice(0, -RESULT_FILE_SUFFIX.length);\n // A nested-capable child loads the same Doom Team extension as its\n // parent. It must not claim the result for its own one-shot run.\n if (!runId || runId === ownRunId || this.claims.has(runId)) continue;\n if (this.claimResult(runId, file)) {\n this.claims.set(runId, { inFlight: false });\n claimedCount += 1;\n }\n // A `false` result means another claimer already took this file; that\n // is the normal outcome of a race, not an error (fix 4).\n }\n return claimedCount;\n }\n\n private discardClaim(runId: string): void {\n this.unlinkFile(claimPathFor(runId));\n this.claims.delete(runId);\n }\n\n private async deliverClaim(runId: string, entry: ClaimEntry): Promise<void> {\n const claimPath = claimPathFor(runId);\n try {\n const raw = this.readFile(claimPath);\n if (raw === undefined) {\n // Claimed file vanished from under us (manual cleanup, disk issue);\n // nothing left to deliver.\n this.claims.delete(runId);\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n this.onProcessingError(runId, error);\n this.discardClaim(runId);\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n this.discardClaim(runId);\n return;\n }\n\n const parsedRecord = parsed as Record<string, unknown>;\n const resolvedRunId = typeof parsedRecord.runId === 'string' && parsedRecord.runId ? parsedRecord.runId : runId;\n if (this.hasRecentlyDelivered(resolvedRunId)) {\n // A duplicate write for a run we already delivered inside the TTL\n // window: dropped, not redelivered (fix 2).\n this.discardClaim(runId);\n return;\n }\n\n const consumer = this.consumer;\n if (!consumer) {\n // No subscriber yet (start() raced ahead of the caller wiring one\n // in); retry once one is registered.\n entry.inFlight = false;\n return;\n }\n\n const record: RunResultFile = { ...parsedRecord, runId: resolvedRunId };\n const accepted = await consumer(record);\n if (accepted) {\n this.markDelivered(resolvedRunId);\n this.discardClaim(runId);\n } else {\n entry.inFlight = false;\n }\n } catch (error) {\n this.onProcessingError(runId, error);\n entry.inFlight = false;\n }\n }\n\n /**\n * One scheduler tick. Synchronous, per `PollScheduler`'s contract: it kicks\n * off delivery attempts without awaiting them (`deliverClaim` runs to\n * completion on its own). Returns `true` only when a new result was\n * claimed this tick, so a consumer that keeps saying \"not yet\" cannot pin\n * the scheduler's backoff at the floor forever.\n */\n protected run(): boolean {\n if (!this.watcher) this.watcher = this.watchResultsDir();\n\n const claimedCount = this.discoverAndClaim();\n\n for (const [runId, entry] of this.claims) {\n if (entry.inFlight) continue;\n entry.inFlight = true;\n void this.deliverClaim(runId, entry);\n }\n\n return claimedCount > 0;\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n start(consumer: ResultConsumer): void {\n // A second start() is a reset, the same idiom as CoalescedStatusWriter.open().\n this.stop();\n this.consumer = consumer;\n for (const runId of this.listClaimedRunIds()) this.claims.set(runId, { inFlight: false });\n this.watcher = this.watchResultsDir();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.consumer = undefined;\n this.watcher?.close();\n this.watcher = undefined;\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.claims.clear();\n this.deliveredAt.clear();\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{init_configDir as t,resolveWatchPath as n}from"../../shared/configDir.mjs";import{__decorate as r,init_decorate as i}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{SUBAGENT_RUN_ID_ENV as a,init_env as o}from"../../env.mjs";import{DOOM_TEAM_TYPES as s,init_types as c}from"../../container/types.mjs";import{__decorateMetadata as l,init_decorateMetadata as u}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as d,init_decorateParam as f}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentResultsDir as p,currentRunsDir as m,init_paths as h}from"../../shared/paths.mjs";import*as g from"node:fs";import*as _ from"node:path";import{inject as v,injectable as y}from"inversify";function b(e){return _.join(p(),e)}function x(e){return _.join(m(),e)}function S(e){return _.join(x(e),E)}function C(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function w(e){return C(e)===`ENOENT`}var T,E,D,O,k,A,j,M=e((()=>{t(),h(),c(),o(),u(),f(),i(),T=`.json`,E=`claimed-result.json`,D=4e3,O=6e4,k=256,A=`result-watcher`,j=class{constructor(e){this.scheduler=e}pollIntervalMs=D;dedupeTtlMs=O;now(){return Date.now()}onProcessingError(e,t){this.lastProcessingError={id:e,error:t,at:this.now()},this.processingErrorCount+=1}lastProcessingError;processingErrorCount=0;consumer;watcher;unregisterPoll;claims=new Map;deliveredAt=new Map;listResultFiles(){try{return g.readdirSync(p()).filter(e=>e.endsWith(T))}catch(e){if(w(e))return[];throw e}}listClaimedRunIds(){try{return g.readdirSync(m()).filter(e=>this.fileExists(_.join(x(e),E)))}catch(e){if(w(e))return[];throw e}}fileExists(e){return g.existsSync(e)}readFile(e){try{return g.readFileSync(e,`utf-8`)}catch(e){if(w(e))return;throw e}}ensureDir(e){g.mkdirSync(e,{recursive:!0})}renameFile(e,t){try{return g.renameSync(e,t),!0}catch(e){if(w(e))return!1;throw e}}unlinkFile(e){try{g.unlinkSync(e)}catch(t){w(t)||this.onProcessingError(e,t)}}handleWatchEvent(e,t){t&&t.toString().endsWith(`.json`)&&this.scheduler.wake()}handleWatchError(){this.watcher=void 0}getWatcher(){return this.watcher}watchResultsDir(){try{this.ensureDir(p());let e=n(p(),g.realpathSync.native),t=g.watch(e,(e,t)=>this.handleWatchEvent(e,t));return t.on(`error`,()=>this.handleWatchError()),t.unref?.(),t}catch{return}}pruneDeliveredAt(e){for(let[t,n]of this.deliveredAt)e-n>this.dedupeTtlMs&&this.deliveredAt.delete(t)}hasRecentlyDelivered(e){let t=this.now();return this.pruneDeliveredAt(t),this.deliveredAt.has(e)}markDelivered(e){let t=this.now();if(this.pruneDeliveredAt(t),this.deliveredAt.set(e,t),this.deliveredAt.size>k){let e=this.deliveredAt.keys().next();e.done||this.deliveredAt.delete(e.value)}}claimResult(e,t){return this.ensureDir(x(e)),this.renameFile(b(t),S(e))}discoverAndClaim(){let e=0,t=process.env[a]?.trim();for(let n of this.listResultFiles()){let r=n.slice(0,-5);!r||r===t||this.claims.has(r)||this.claimResult(r,n)&&(this.claims.set(r,{inFlight:!1}),e+=1)}return e}discardClaim(e){this.unlinkFile(S(e)),this.claims.delete(e)}async deliverClaim(e,t){let n=S(e);try{let r=this.readFile(n);if(r===void 0){this.claims.delete(e);return}let i;try{i=JSON.parse(r)}catch(t){this.onProcessingError(e,t),this.discardClaim(e);return}if(typeof i!=`object`||!i){this.discardClaim(e);return}let a=i,o=typeof a.runId==`string`&&a.runId?a.runId:e;if(this.hasRecentlyDelivered(o)){this.discardClaim(e);return}let s=this.consumer;if(!s){t.inFlight=!1;return}await s({...a,runId:o})?(this.markDelivered(o),this.discardClaim(e)):t.inFlight=!1}catch(n){this.onProcessingError(e,n),t.inFlight=!1}}run(){this.watcher||=this.watchResultsDir();let e=this.discoverAndClaim();for(let[e,t]of this.claims)t.inFlight||(t.inFlight=!0,this.deliverClaim(e,t));return e>0}start(e){this.stop(),this.consumer=e;for(let e of this.listClaimedRunIds())this.claims.set(e,{inFlight:!1});this.watcher=this.watchResultsDir(),this.unregisterPoll=this.scheduler.register({id:A,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.consumer=void 0,this.watcher?.close(),this.watcher=void 0,this.unregisterPoll?.(),this.unregisterPoll=void 0,this.claims.clear(),this.deliveredAt.clear()}},j=r([y(),d(0,v(s.PollScheduler)),l(`design:paramtypes`,[Object])],j)}));M();export{E as CLAIMED_RESULT_FILE_NAME,T as RESULT_FILE_SUFFIX,j as ResultWatcher,M as init_resultWatcher};
|
|
2
|
+
//# sourceMappingURL=resultWatcher.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resultWatcher.mjs","names":[],"sources":["../../../src/runs/background/resultWatcher.ts"],"sourcesContent":["/**\n * Watches `currentResultsDir()` for a run's terminal result and delivers each one to\n * a consumer exactly once.\n *\n * WHY THIS EXISTS:\n * The predecessor's watcher\n * (`doom-pi-subagents/src/runs/background/resultWatcher.ts`) had four\n * separate, verified bugs. This is a redesign around the fixes, not a line\n * port - the fixes below are structural, not patches on the old control flow.\n *\n * FIX 1 - WATCH BOTH 'rename' AND 'change':\n * The predecessor only reacted to `event === 'rename'` (`resultWatcher.ts:398-399`),\n * so a result file rewritten in place - same path, new contents, no\n * create/delete/rename - was never noticed until the poll safety net happened\n * to run. `watchResultsDir()` below reacts to any `fs.watch` callback whose\n * filename matches, regardless of `eventType`, the same way\n * `SpawnHandshake.watchDirectory` already does in this package.\n *\n * FIX 2 - TTL DEDUPE:\n * The predecessor re-implemented \"is this a duplicate\" inline at its call\n * site (`resultWatcher.ts:180-195`) *and* separately inside\n * `completion-dedupe.ts`'s `markSeenWithTtl`, so the check and the record used\n * two independently maintained copies of the same expiry logic. Every check\n * and every record here goes through `hasRecentlyDelivered()` and\n * `markDelivered()` and nothing else - one implementation of the TTL policy,\n * used the same way on both sides.\n *\n * FIX 3 - TIMER TYPES:\n * The predecessor stored `setTimeout`'s return value in fields typed (or\n * used) as if they were numbers (`resultWatcher.ts:361,371,425`), which only\n * happens to work under Node's `setTimeout` and breaks the moment the return\n * value is compared, serialized, or ported somewhere `setTimeout` returns a\n * number (browsers, some polyfills). This module does not own a timer at all:\n * `fs.watch` is the fast path and `PollScheduler` is the bounded safety net\n * (see fix 5), so there is no `Timeout` handle here for the compiler to get\n * wrong in the first place.\n *\n * FIX 4 - ATOMIC CONSUME (TOCTOU):\n * The predecessor checked a result file's existence, then unlinked it, as two\n * separate steps (`resultWatcher.ts:184-194,324-332`), racing any other\n * reader of the same file between the two. Here, the *first* thing that\n * happens to a candidate result is an attempted rename into\n * `currentRunsDir()/<runId>/claimed-result.json` (`claimResult()`). A successful\n * rename is exclusive ownership: nothing else can also be holding that\n * result, because there is only ever one file at the source path and only one\n * caller's `renameSync` can move it. A failed rename (source already gone)\n * means another claimer got there first, which is `claimResult()`'s normal,\n * expected return of `false` - not a caught error.\n *\n * FIX 5 - WATCH-FIRST, POLL AS A BOUNDED SAFETY NET:\n * `start()` registers with `PollScheduler` instead of owning a timer, the\n * same pattern as every other long-lived background subscriber in this\n * directory. `fs.watch` gives low latency; the scheduler's tick is the bound\n * on how late a check can ever be on a platform or mount where watches are\n * unreliable. Neither path is trusted alone.\n *\n * DESIGN PATTERNS:\n * - A claimed result lives at `currentRunsDir()/<runId>/claimed-result.json`, inside\n * the run's own working directory rather than a subdirectory of\n * `currentResultsDir()`. Putting it under `currentResultsDir()` would make our own claims\n * visible to the very `fs.watch` this module registers, and would risk\n * colliding with `currentResultsDir()/<runId>.json` naming. `currentRunsDir()/<runId>/` is\n * already this run's own directory (see `statusWriter.ts`,\n * `terminalPersistence.ts`), so this reuses existing per-run space instead\n * of inventing a second one\n * - `run()` is synchronous, matching `PollScheduler`'s contract, but delivery\n * is not: `deliverClaim()` is fired without being awaited, and a claim's\n * `inFlight` flag stops it from being re-fired on the next tick while the\n * previous attempt is still outstanding\n * - `run()` reports \"work happened\" only when it claimed a new file, not when\n * it merely retried a still-pending delivery. A consumer that keeps\n * rejecting the same result forever would otherwise pin the scheduler's\n * backoff at the floor indefinitely\n * - A crash between claiming a result and delivering it leaves\n * `claimed-result.json` on disk with nothing tracking it in memory.\n * `start()` scans for these once, so a fresh process picks up an\n * interrupted delivery instead of leaving it stranded forever\n *\n * AVOID:\n * - Reading or unlinking anything at `currentResultsDir()/<runId>.json` once\n * `claimResult()` has returned true for it; ownership has moved to the\n * claimed copy, and the source name may already belong to an unrelated,\n * later result\n * - Treating a rename failure from `claimResult()` as noteworthy; the\n * `onProcessingError` path is for delivery failures and parse failures, not\n * for the ordinary case of losing a claim race\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { resolveWatchPath } from '../../shared/configDir.ts';\nimport { currentResultsDir, currentRunsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport type { IPollScheduler } from '../../shared/pollScheduler.ts';\n\n/** Suffix a candidate result file must have in `currentResultsDir()`. */\nexport const RESULT_FILE_SUFFIX = '.json';\n/** Leaf name of a claimed result inside `currentRunsDir()/<runId>/`. */\nexport const CLAIMED_RESULT_FILE_NAME = 'claimed-result.json';\n\n/** Poll safety-net cadence, generously looser than `fs.watch`'s latency (see `PollScheduler`'s header). */\nconst DEFAULT_POLL_INTERVAL_MS = 4_000;\n/** How long a delivered run id is remembered, so a re-appearing result for it is dropped instead of redelivered. */\nconst DEFAULT_DEDUPE_TTL_MS = 60_000;\n/** Cap on tracked delivered ids, so a very long session cannot grow this without bound. */\nconst MAX_TRACKED_DELIVERIES = 256;\n\nconst POLL_SUBSCRIBER_ID = 'result-watcher';\n\n/** The parsed contents of a result file, with `runId` guaranteed present. */\nexport interface RunResultFile {\n runId: string;\n [key: string]: unknown;\n}\n\n/**\n * Handles one delivered result. Returns (or resolves to) `true` once it has\n * been durably accepted, which is what lets the claimed copy be removed;\n * `false` means \"not yet\", and the same claim is retried on a later tick.\n */\nexport type ResultConsumer = (result: RunResultFile) => boolean | Promise<boolean>;\n\nexport interface IResultWatcher {\n /** Begin watching. Calling this again resets whatever was previously tracked, same as `CoalescedStatusWriter.open`. */\n start(consumer: ResultConsumer): void;\n /** Stop watching, unregister from the scheduler, and drop all in-memory state. */\n stop(): void;\n}\n\ninterface ClaimEntry {\n /** True while `deliverClaim()` is awaiting this claim's consumer call, so `run()` does not fire it twice. */\n inFlight: boolean;\n}\n\nfunction resultPathFor(file: string): string {\n return path.join(currentResultsDir(), file);\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction claimPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_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 isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class ResultWatcher implements IResultWatcher {\n constructor(@inject(DOOM_TEAM_TYPES.PollScheduler) private readonly scheduler: IPollScheduler) {}\n\n /**\n * Seams for tests, as protected members rather than constructor arguments:\n * inversify cannot resolve a primitive, so a test subclass overrides these.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly dedupeTtlMs: number = DEFAULT_DEDUPE_TTL_MS;\n\n protected now(): number {\n return Date.now();\n }\n\n /** Records a claim or delivery failure. Overridable; never a stdio write (see package rules). */\n protected onProcessingError(id: string, error: unknown): void {\n this.lastProcessingError = { id, error, at: this.now() };\n this.processingErrorCount += 1;\n }\n\n /** The most recent processing failure, for diagnostics. */\n lastProcessingError: { id: string; error: unknown; at: number } | undefined;\n /** How many processing failures this watcher has absorbed. */\n processingErrorCount = 0;\n\n private consumer: ResultConsumer | undefined;\n private watcher: fs.FSWatcher | undefined;\n private unregisterPoll: (() => void) | undefined;\n private readonly claims = new Map<string, ClaimEntry>();\n private readonly deliveredAt = new Map<string, number>();\n\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected listResultFiles(): string[] {\n try {\n return fs.readdirSync(currentResultsDir()).filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n /** Run ids with a lingering claimed copy, found by scanning `currentRunsDir()` once at `start()`. */\n protected listClaimedRunIds(): string[] {\n try {\n return fs\n .readdirSync(currentRunsDir())\n .filter((entry) => this.fileExists(path.join(runDirFor(entry), CLAIMED_RESULT_FILE_NAME)));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected fileExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n protected readFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, 'utf-8');\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw error;\n }\n }\n\n protected ensureDir(dirPath: string): void {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n\n /**\n * The atomic claim (fix 4). Returns `false`, without throwing, when the\n * source is already gone - another claimer won the race, which is the\n * expected outcome of two watchers noticing the same file.\n */\n protected renameFile(fromPath: string, toPath: string): boolean {\n try {\n fs.renameSync(fromPath, toPath);\n return true;\n } catch (error) {\n if (isNotFound(error)) return false;\n throw error;\n }\n }\n\n protected unlinkFile(filePath: string): void {\n try {\n fs.unlinkSync(filePath);\n } catch (error) {\n if (!isNotFound(error)) this.onProcessingError(filePath, error);\n }\n }\n\n /**\n * The `fs.watch` callback's logic, factored out so it can be exercised\n * deterministically (fix 1's actual test coverage): calling this directly\n * with synthetic `eventType`/`filename` values proves the filter reacts to\n * 'change' exactly like 'rename', without depending on whether - or when -\n * the OS actually delivers a real filesystem event. `fs.watch` gives no\n * delivery guarantee at all, so a test that waits on a real one to arrive\n * is testing the OS's scheduler, not this code; see `watchResultsDir()`'s\n * only caller of this, and the poll safety net (fix 5) that exists\n * precisely because that guarantee does not hold.\n */\n protected handleWatchEvent(_eventType: fs.WatchEventType, filename: string | Buffer | null): void {\n if (!filename) return;\n if (!filename.toString().endsWith(RESULT_FILE_SUFFIX)) return;\n this.scheduler.wake();\n }\n\n /**\n * A watch that has emitted 'error' is treated as gone: `run()` checks\n * `getWatcher()` on its next tick and re-establishes it, falling back to\n * the poll safety net (fix 5) in the meantime. Factored out, like\n * `handleWatchEvent`, so a test can trigger it directly rather than by\n * provoking a real watch failure.\n */\n protected handleWatchError(): void {\n this.watcher = undefined;\n }\n\n /** The currently-held watch, if any. Exposed for tests; `run()` reads the field directly. */\n protected getWatcher(): fs.FSWatcher | undefined {\n return this.watcher;\n }\n\n /**\n * Watch `currentResultsDir()` for candidate result files. Reacts to every `fs.watch`\n * callback regardless of `eventType` (fix 1, see `handleWatchEvent`).\n * Returns `undefined`, without throwing, when the watch could not be\n * established at all - `run()` retries this on its next tick, and the poll\n * safety net (fix 5) covers the gap in the meantime.\n */\n protected watchResultsDir(): fs.FSWatcher | undefined {\n try {\n this.ensureDir(currentResultsDir());\n const watchPath = resolveWatchPath(currentResultsDir(), fs.realpathSync.native);\n const watcher = fs.watch(watchPath, (eventType, filename) => this.handleWatchEvent(eventType, filename));\n watcher.on('error', () => this.handleWatchError());\n watcher.unref?.();\n return watcher;\n } catch {\n return undefined;\n }\n }\n\n // ---------------------------------------------------------------------\n // TTL dedupe (fix 2). One implementation, used by both the check and the\n // record, so they cannot drift the way the predecessor's two copies did.\n // ---------------------------------------------------------------------\n\n private pruneDeliveredAt(now: number): void {\n for (const [key, at] of this.deliveredAt) {\n if (now - at > this.dedupeTtlMs) this.deliveredAt.delete(key);\n }\n }\n\n private hasRecentlyDelivered(runId: string): boolean {\n const now = this.now();\n this.pruneDeliveredAt(now);\n return this.deliveredAt.has(runId);\n }\n\n private markDelivered(runId: string): void {\n const now = this.now();\n this.pruneDeliveredAt(now);\n this.deliveredAt.set(runId, now);\n if (this.deliveredAt.size > MAX_TRACKED_DELIVERIES) {\n const oldest = this.deliveredAt.keys().next();\n if (!oldest.done) this.deliveredAt.delete(oldest.value);\n }\n }\n\n // ---------------------------------------------------------------------\n // Claim + deliver\n // ---------------------------------------------------------------------\n\n /** Attempt the atomic claim for one candidate file. `true` only when this call won the race. */\n protected claimResult(runId: string, file: string): boolean {\n this.ensureDir(runDirFor(runId));\n return this.renameFile(resultPathFor(file), claimPathFor(runId));\n }\n\n /** Claim every new candidate in `currentResultsDir()`. Returns how many were newly claimed. */\n private discoverAndClaim(): number {\n let claimedCount = 0;\n const ownRunId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n for (const file of this.listResultFiles()) {\n const runId = file.slice(0, -RESULT_FILE_SUFFIX.length);\n // A nested-capable child loads the same Doom Team extension as its\n // parent. It must not claim the result for its own one-shot run.\n if (!runId || runId === ownRunId || this.claims.has(runId)) continue;\n if (this.claimResult(runId, file)) {\n this.claims.set(runId, { inFlight: false });\n claimedCount += 1;\n }\n // A `false` result means another claimer already took this file; that\n // is the normal outcome of a race, not an error (fix 4).\n }\n return claimedCount;\n }\n\n private discardClaim(runId: string): void {\n this.unlinkFile(claimPathFor(runId));\n this.claims.delete(runId);\n }\n\n private async deliverClaim(runId: string, entry: ClaimEntry): Promise<void> {\n const claimPath = claimPathFor(runId);\n try {\n const raw = this.readFile(claimPath);\n if (raw === undefined) {\n // Claimed file vanished from under us (manual cleanup, disk issue);\n // nothing left to deliver.\n this.claims.delete(runId);\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n this.onProcessingError(runId, error);\n this.discardClaim(runId);\n return;\n }\n if (typeof parsed !== 'object' || parsed === null) {\n this.discardClaim(runId);\n return;\n }\n\n const parsedRecord = parsed as Record<string, unknown>;\n const resolvedRunId = typeof parsedRecord.runId === 'string' && parsedRecord.runId ? parsedRecord.runId : runId;\n if (this.hasRecentlyDelivered(resolvedRunId)) {\n // A duplicate write for a run we already delivered inside the TTL\n // window: dropped, not redelivered (fix 2).\n this.discardClaim(runId);\n return;\n }\n\n const consumer = this.consumer;\n if (!consumer) {\n // No subscriber yet (start() raced ahead of the caller wiring one\n // in); retry once one is registered.\n entry.inFlight = false;\n return;\n }\n\n const record: RunResultFile = { ...parsedRecord, runId: resolvedRunId };\n const accepted = await consumer(record);\n if (accepted) {\n this.markDelivered(resolvedRunId);\n this.discardClaim(runId);\n } else {\n entry.inFlight = false;\n }\n } catch (error) {\n this.onProcessingError(runId, error);\n entry.inFlight = false;\n }\n }\n\n /**\n * One scheduler tick. Synchronous, per `PollScheduler`'s contract: it kicks\n * off delivery attempts without awaiting them (`deliverClaim` runs to\n * completion on its own). Returns `true` only when a new result was\n * claimed this tick, so a consumer that keeps saying \"not yet\" cannot pin\n * the scheduler's backoff at the floor forever.\n */\n protected run(): boolean {\n if (!this.watcher) this.watcher = this.watchResultsDir();\n\n const claimedCount = this.discoverAndClaim();\n\n for (const [runId, entry] of this.claims) {\n if (entry.inFlight) continue;\n entry.inFlight = true;\n void this.deliverClaim(runId, entry);\n }\n\n return claimedCount > 0;\n }\n\n // ---------------------------------------------------------------------\n // Public API\n // ---------------------------------------------------------------------\n\n start(consumer: ResultConsumer): void {\n // A second start() is a reset, the same idiom as CoalescedStatusWriter.open().\n this.stop();\n this.consumer = consumer;\n for (const runId of this.listClaimedRunIds()) this.claims.set(runId, { inFlight: false });\n this.watcher = this.watchResultsDir();\n this.unregisterPoll = this.scheduler.register({\n id: POLL_SUBSCRIBER_ID,\n intervalMs: this.pollIntervalMs,\n run: () => this.run(),\n });\n }\n\n stop(): void {\n this.consumer = undefined;\n this.watcher?.close();\n this.watcher = undefined;\n this.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.claims.clear();\n this.deliveredAt.clear();\n }\n}\n"],"mappings":"q2BAyIA,SAAS,EAAc,EAAsB,CAC3C,OAAO,EAAK,KAAK,GAAmB,CAAE,EAAK,CAG7C,SAAS,EAAU,EAAuB,CACxC,OAAO,EAAK,KAAK,GAAgB,CAAE,EAAM,CAG3C,SAAS,EAAa,EAAuB,CAC3C,OAAO,EAAK,KAAK,EAAU,EAAM,CAAE,EAAyB,CAG9D,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAGN,SAAS,EAAW,EAAyB,CAC3C,OAAO,EAAU,EAAM,GAAK,wCAhE+B,IACa,IACf,IACR,aAItC,EAAqB,QAErB,EAA2B,sBAGlC,EAA2B,IAE3B,EAAwB,IAExB,EAAyB,IAEzB,EAAqB,mBAkDpB,KAA8C,CACnD,YAAY,EAAmF,CAA3B,KAAA,UAAA,EAMpE,eAA4C,EAC5C,YAAyC,EAEzC,KAAwB,CACtB,OAAO,KAAK,KAAK,CAInB,kBAA4B,EAAY,EAAsB,CAC5D,KAAK,oBAAsB,CAAE,KAAI,QAAO,GAAI,KAAK,KAAK,CAAE,CACxD,KAAK,sBAAwB,EAI/B,oBAEA,qBAAuB,EAEvB,SACA,QACA,eACA,OAA0B,IAAI,IAC9B,YAA+B,IAAI,IAOnC,iBAAsC,CACpC,GAAI,CACF,OAAO,EAAG,YAAY,GAAmB,CAAC,CAAC,OAAQ,GAAU,EAAM,SAAS,EAAmB,CAAC,OACzF,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAKV,mBAAwC,CACtC,GAAI,CACF,OAAO,EACJ,YAAY,GAAgB,CAAC,CAC7B,OAAQ,GAAU,KAAK,WAAW,EAAK,KAAK,EAAU,EAAM,CAAE,EAAyB,CAAC,CAAC,OACrF,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAIV,WAAqB,EAA2B,CAC9C,OAAO,EAAG,WAAW,EAAS,CAGhC,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAO,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,OACvB,MAAM,GAIV,UAAoB,EAAuB,CACzC,EAAG,UAAU,EAAS,CAAE,UAAW,GAAM,CAAC,CAQ5C,WAAqB,EAAkB,EAAyB,CAC9D,GAAI,CAEF,OADA,EAAG,WAAW,EAAU,EAAO,CACxB,SACA,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,GAC9B,MAAM,GAIV,WAAqB,EAAwB,CAC3C,GAAI,CACF,EAAG,WAAW,EAAS,OAChB,EAAO,CACT,EAAW,EAAM,EAAE,KAAK,kBAAkB,EAAU,EAAM,EAenE,iBAA2B,EAA+B,EAAwC,CAC3F,GACA,EAAS,UAAU,CAAC,SAAA,QAA4B,EACrD,KAAK,UAAU,MAAM,CAUvB,kBAAmC,CACjC,KAAK,QAAU,IAAA,GAIjB,YAAiD,CAC/C,OAAO,KAAK,QAUd,iBAAsD,CACpD,GAAI,CACF,KAAK,UAAU,GAAmB,CAAC,CACnC,IAAM,EAAY,EAAiB,GAAmB,CAAE,EAAG,aAAa,OAAO,CACzE,EAAU,EAAG,MAAM,GAAY,EAAW,IAAa,KAAK,iBAAiB,EAAW,EAAS,CAAC,CAGxG,OAFA,EAAQ,GAAG,YAAe,KAAK,kBAAkB,CAAC,CAClD,EAAQ,SAAS,CACV,OACD,CACN,QASJ,iBAAyB,EAAmB,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAO,KAAK,YACvB,EAAM,EAAK,KAAK,aAAa,KAAK,YAAY,OAAO,EAAI,CAIjE,qBAA6B,EAAwB,CACnD,IAAM,EAAM,KAAK,KAAK,CAEtB,OADA,KAAK,iBAAiB,EAAI,CACnB,KAAK,YAAY,IAAI,EAAM,CAGpC,cAAsB,EAAqB,CACzC,IAAM,EAAM,KAAK,KAAK,CAGtB,GAFA,KAAK,iBAAiB,EAAI,CAC1B,KAAK,YAAY,IAAI,EAAO,EAAI,CAC5B,KAAK,YAAY,KAAO,EAAwB,CAClD,IAAM,EAAS,KAAK,YAAY,MAAM,CAAC,MAAM,CACxC,EAAO,MAAM,KAAK,YAAY,OAAO,EAAO,MAAM,EAS3D,YAAsB,EAAe,EAAuB,CAE1D,OADA,KAAK,UAAU,EAAU,EAAM,CAAC,CACzB,KAAK,WAAW,EAAc,EAAK,CAAE,EAAa,EAAM,CAAC,CAIlE,kBAAmC,CACjC,IAAI,EAAe,EACb,EAAW,QAAQ,IAAI,IAAsB,MAAM,CACzD,IAAK,IAAM,KAAQ,KAAK,iBAAiB,CAAE,CACzC,IAAM,EAAQ,EAAK,MAAM,EAAG,GAA2B,CAGnD,CAAC,GAAS,IAAU,GAAY,KAAK,OAAO,IAAI,EAAM,EACtD,KAAK,YAAY,EAAO,EAAK,GAC/B,KAAK,OAAO,IAAI,EAAO,CAAE,SAAU,GAAO,CAAC,CAC3C,GAAgB,GAKpB,OAAO,EAGT,aAAqB,EAAqB,CACxC,KAAK,WAAW,EAAa,EAAM,CAAC,CACpC,KAAK,OAAO,OAAO,EAAM,CAG3B,MAAc,aAAa,EAAe,EAAkC,CAC1E,IAAM,EAAY,EAAa,EAAM,CACrC,GAAI,CACF,IAAM,EAAM,KAAK,SAAS,EAAU,CACpC,GAAI,IAAQ,IAAA,GAAW,CAGrB,KAAK,OAAO,OAAO,EAAM,CACzB,OAGF,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,OACjB,EAAO,CACd,KAAK,kBAAkB,EAAO,EAAM,CACpC,KAAK,aAAa,EAAM,CACxB,OAEF,GAAI,OAAO,GAAW,WAAY,EAAiB,CACjD,KAAK,aAAa,EAAM,CACxB,OAGF,IAAM,EAAe,EACf,EAAgB,OAAO,EAAa,OAAU,UAAY,EAAa,MAAQ,EAAa,MAAQ,EAC1G,GAAI,KAAK,qBAAqB,EAAc,CAAE,CAG5C,KAAK,aAAa,EAAM,CACxB,OAGF,IAAM,EAAW,KAAK,SACtB,GAAI,CAAC,EAAU,CAGb,EAAM,SAAW,GACjB,OAKE,MADmB,EAAS,CADA,GAAG,EAAc,MAAO,EAClB,CAAC,EAErC,KAAK,cAAc,EAAc,CACjC,KAAK,aAAa,EAAM,EAExB,EAAM,SAAW,SAEZ,EAAO,CACd,KAAK,kBAAkB,EAAO,EAAM,CACpC,EAAM,SAAW,IAWrB,KAAyB,CACvB,AAAmB,KAAK,UAAU,KAAK,iBAAiB,CAExD,IAAM,EAAe,KAAK,kBAAkB,CAE5C,IAAK,GAAM,CAAC,EAAO,KAAU,KAAK,OAC5B,EAAM,WACV,EAAM,SAAW,GACZ,KAAK,aAAa,EAAO,EAAM,EAGtC,OAAO,EAAe,EAOxB,MAAM,EAAgC,CAEpC,KAAK,MAAM,CACX,KAAK,SAAW,EAChB,IAAK,IAAM,KAAS,KAAK,mBAAmB,CAAE,KAAK,OAAO,IAAI,EAAO,CAAE,SAAU,GAAO,CAAC,CACzF,KAAK,QAAU,KAAK,iBAAiB,CACrC,KAAK,eAAiB,KAAK,UAAU,SAAS,CAC5C,GAAI,EACJ,WAAY,KAAK,eACjB,QAAW,KAAK,KAAK,CACtB,CAAC,CAGJ,MAAa,CACX,KAAK,SAAW,IAAA,GAChB,KAAK,SAAS,OAAO,CACrB,KAAK,QAAU,IAAA,GACf,KAAK,kBAAkB,CACvB,KAAK,eAAiB,IAAA,GACtB,KAAK,OAAO,OAAO,CACnB,KAAK,YAAY,OAAO,QAxT3B,GAAY,KAEE,EAAO,EAAgB,cAAc,CAAA"}
|
|
@@ -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(`../../shared/paths.cjs`),r=require(`./resultWatcher.cjs`);let i=require(`node:fs`);i=e.__toESM(i,1);let a=require(`node:path`);a=e.__toESM(a,1);let o=require(`inversify`);function s(e){return a.join(n.currentRunsDir(),e)}function c(e){return a.join(n.currentResultsDir(),`${e}${r.RESULT_FILE_SUFFIX}`)}function l(e){return a.join(s(e),r.CLAIMED_RESULT_FILE_NAME)}function u(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function d(e){return u(e)===`ENOENT`}let f=class{pathExists(e){return i.existsSync(e)}runsDir(){return n.currentRunsDir()}resultsDir(){return n.currentResultsDir()}listRunDirNames(){try{return i.readdirSync(this.runsDir(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(d(e))return[];throw e}}listPendingResultIds(){try{return i.readdirSync(this.resultsDir()).filter(e=>e.endsWith(r.RESULT_FILE_SUFFIX)).map(e=>e.slice(0,-r.RESULT_FILE_SUFFIX.length))}catch(e){if(d(e))return[];throw e}}resolve(e){let t=this.locate(e);if(t)return t;let n=this.candidateRunIds().filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locate(r):void 0}candidateRunIds(){return[...new Set([...this.listRunDirNames(),...this.listPendingResultIds()])]}locate(e){let t=this.pathExists(s(e))?s(e):void 0,n=l(e);if(this.pathExists(n))return{runId:e,runDir:t,resultPath:n,claimed:!0};let r=c(e);if(this.pathExists(r))return{runId:e,runDir:t,resultPath:r,claimed:!1};if(t)return{runId:e,runDir:t,resultPath:void 0,claimed:!1}}};f=t.__decorate([(0,o.injectable)()],f),Object.defineProperty(exports,`RunIdResolver`,{enumerable:!0,get:function(){return f}});
|
|
2
|
+
//# sourceMappingURL=runIdResolver.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runIdResolver.cjs","names":["path","currentRunsDir","currentResultsDir","RESULT_FILE_SUFFIX","CLAIMED_RESULT_FILE_NAME","fs"],"sources":["../../../src/runs/background/runIdResolver.ts"],"sourcesContent":["/**\n * Resolve a possibly-abbreviated subagent run id to where its files live.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/runIdResolver.ts` disambiguates\n * three kinds of run - foreground, async, nested - because that package\n * tracks all three in `SubagentState`. This package is async-only (see\n * `spawnHandshake.ts`'s header) and has not yet ported foreground or nested\n * run tracking (that is later work; see `SubagentState` in the predecessor).\n * This resolver covers the one kind of run this package currently has: a run\n * whose state lives under `currentRunsDir()/<runId>/` and whose terminal result lives\n * in `currentResultsDir()/<runId>.json` until `ResultWatcher` claims it. It is\n * designed to grow, not to be replaced, once the other kinds land.\n *\n * DESIGN PATTERNS:\n * - Aware of `ResultWatcher`'s claim convention\n * (`currentRunsDir()/<runId>/claimed-result.json`): a run whose result has already\n * been claimed for delivery must still resolve, and to the claimed copy,\n * not to a `currentResultsDir()` entry that no longer exists\n * - Prefix matching mirrors the predecessor's ambiguity handling: an\n * unambiguous prefix resolves like an exact id, an ambiguous one throws\n * naming every match, and a prefix matching nothing returns `undefined`\n * rather than throwing - only ambiguity is exceptional, not \"not found\"\n *\n * AVOID:\n * - Reading `currentResultsDir()/<runId>.json` for a run that `pathExists` shows has\n * a claimed copy; the source file may already belong to an unrelated, later\n * run by the time this runs\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentResultsDir, currentRunsDir } from '../../shared/paths.ts';\nimport { CLAIMED_RESULT_FILE_NAME, RESULT_FILE_SUFFIX } from './resultWatcher.ts';\n\nexport interface ResolvedRunLocation {\n runId: string;\n /** `currentRunsDir()/<runId>`, when it exists. */\n runDir: string | undefined;\n /** Wherever the run's result currently lives - pending or already claimed. */\n resultPath: string | undefined;\n /** True when `resultPath` is a claimed copy awaiting delivery, not the original. */\n claimed: boolean;\n}\n\nexport interface IRunIdResolver {\n /**\n * Resolve an exact id or an unambiguous prefix. `undefined` when nothing\n * matches. Throws when a prefix matches more than one run id.\n */\n resolve(id: string): ResolvedRunLocation | undefined;\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction pendingResultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\nfunction claimedResultPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_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 isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class RunIdResolver implements IRunIdResolver {\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected pathExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n /**\n * Seams for the two directory-listing methods below, as protected methods\n * rather than constructor arguments (inversify cannot resolve a primitive\n * like a path, and a directory is no different from `AgentDiscoveryService`'s\n * `now()`/`load()` seams in that respect). A test subclass points these at\n * a directory that does not exist, which exercises the `isNotFound` branch\n * for real without touching the actual shared `currentRunsDir()`/`currentResultsDir()` -\n * closing the only reason this class's tests would otherwise need to.\n */\n protected runsDir(): string {\n return currentRunsDir();\n }\n\n protected resultsDir(): string {\n return currentResultsDir();\n }\n\n protected listRunDirNames(): string[] {\n try {\n return fs\n .readdirSync(this.runsDir(), { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected listPendingResultIds(): string[] {\n try {\n return fs\n .readdirSync(this.resultsDir())\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n resolve(id: string): ResolvedRunLocation | undefined {\n const exact = this.locate(id);\n if (exact) return exact;\n\n const candidates = this.candidateRunIds().filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locate(onlyMatch) : undefined;\n }\n\n private candidateRunIds(): string[] {\n return [...new Set([...this.listRunDirNames(), ...this.listPendingResultIds()])];\n }\n\n private locate(runId: string): ResolvedRunLocation | undefined {\n const runDir = this.pathExists(runDirFor(runId)) ? runDirFor(runId) : undefined;\n const claimedPath = claimedResultPathFor(runId);\n if (this.pathExists(claimedPath)) {\n return { runId, runDir, resultPath: claimedPath, claimed: true };\n }\n const pendingPath = pendingResultPathFor(runId);\n if (this.pathExists(pendingPath)) {\n return { runId, runDir, resultPath: pendingPath, claimed: false };\n }\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n}\n"],"mappings":"6TAuDA,SAAS,EAAU,EAAuB,CACxC,OAAOA,EAAK,KAAKC,EAAAA,gBAAgB,CAAE,EAAM,CAG3C,SAAS,EAAqB,EAAuB,CACnD,OAAOD,EAAK,KAAKE,EAAAA,mBAAmB,CAAE,GAAG,IAAQC,EAAAA,qBAAqB,CAGxE,SAAS,EAAqB,EAAuB,CACnD,OAAOH,EAAK,KAAK,EAAU,EAAM,CAAEI,EAAAA,yBAAyB,CAG9D,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAGN,SAAS,EAAW,EAAyB,CAC3C,OAAO,EAAU,EAAM,GAAK,SAIvB,IAAA,EAAA,KAA8C,CAMnD,WAAqB,EAA2B,CAC9C,OAAOC,EAAG,WAAW,EAAS,CAYhC,SAA4B,CAC1B,OAAOJ,EAAAA,gBAAgB,CAGzB,YAA+B,CAC7B,OAAOC,EAAAA,mBAAmB,CAG5B,iBAAsC,CACpC,GAAI,CACF,OAAOG,EACJ,YAAY,KAAK,SAAS,CAAE,CAAE,cAAe,GAAM,CAAC,CACpD,OAAQ,GAAU,EAAM,aAAa,CAAC,CACtC,IAAK,GAAU,EAAM,KAAK,OACtB,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAIV,sBAA2C,CACzC,GAAI,CACF,OAAOA,EACJ,YAAY,KAAK,YAAY,CAAC,CAC9B,OAAQ,GAAU,EAAM,SAASF,EAAAA,mBAAmB,CAAC,CACrD,IAAK,GAAU,EAAM,MAAM,EAAG,CAACA,EAAAA,mBAAmB,OAAO,CAAC,OACtD,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAIV,QAAQ,EAA6C,CACnD,IAAM,EAAQ,KAAK,OAAO,EAAG,CAC7B,GAAI,EAAO,OAAO,EAElB,IAAM,EAAa,KAAK,iBAAiB,CAAC,OAAQ,GAAc,EAAU,WAAW,EAAG,CAAC,CACzF,GAAI,EAAW,OAAS,EACtB,MAAU,MAAM,4BAA4B,EAAG,aAAa,EAAW,KAAK,KAAK,CAAC,wBAAwB,CAE5G,GAAM,CAAC,GAAa,EACpB,OAAO,EAAY,KAAK,OAAO,EAAU,CAAG,IAAA,GAG9C,iBAAoC,CAClC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,iBAAiB,CAAE,GAAG,KAAK,sBAAsB,CAAC,CAAC,CAAC,CAGlF,OAAe,EAAgD,CAC7D,IAAM,EAAS,KAAK,WAAW,EAAU,EAAM,CAAC,CAAG,EAAU,EAAM,CAAG,IAAA,GAChE,EAAc,EAAqB,EAAM,CAC/C,GAAI,KAAK,WAAW,EAAY,CAC9B,MAAO,CAAE,QAAO,SAAQ,WAAY,EAAa,QAAS,GAAM,CAElE,IAAM,EAAc,EAAqB,EAAM,CAC/C,GAAI,KAAK,WAAW,EAAY,CAC9B,MAAO,CAAE,QAAO,SAAQ,WAAY,EAAa,QAAS,GAAO,CAEnE,GAAI,EAAQ,MAAO,CAAE,QAAO,SAAQ,WAAY,IAAA,GAAW,QAAS,GAAO,qCA9ElE,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{currentResultsDir as r,currentRunsDir as i,init_paths as a}from"../../shared/paths.mjs";import{CLAIMED_RESULT_FILE_NAME as o,RESULT_FILE_SUFFIX as s,init_resultWatcher as c}from"./resultWatcher.mjs";import*as l from"node:fs";import*as u from"node:path";import{injectable as d}from"inversify";function f(e){return u.join(i(),e)}function p(e){return u.join(r(),`${e}${s}`)}function m(e){return u.join(f(e),o)}function h(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function g(e){return h(e)===`ENOENT`}var _,v=e((()=>{a(),c(),n(),_=class{pathExists(e){return l.existsSync(e)}runsDir(){return i()}resultsDir(){return r()}listRunDirNames(){try{return l.readdirSync(this.runsDir(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name)}catch(e){if(g(e))return[];throw e}}listPendingResultIds(){try{return l.readdirSync(this.resultsDir()).filter(e=>e.endsWith(s)).map(e=>e.slice(0,-s.length))}catch(e){if(g(e))return[];throw e}}resolve(e){let t=this.locate(e);if(t)return t;let n=this.candidateRunIds().filter(t=>t.startsWith(e));if(n.length>1)throw Error(`Ambiguous run id prefix '${e}' matched: ${n.join(`, `)}. Provide a longer id.`);let[r]=n;return r?this.locate(r):void 0}candidateRunIds(){return[...new Set([...this.listRunDirNames(),...this.listPendingResultIds()])]}locate(e){let t=this.pathExists(f(e))?f(e):void 0,n=m(e);if(this.pathExists(n))return{runId:e,runDir:t,resultPath:n,claimed:!0};let r=p(e);if(this.pathExists(r))return{runId:e,runDir:t,resultPath:r,claimed:!1};if(t)return{runId:e,runDir:t,resultPath:void 0,claimed:!1}}},_=t([d()],_)}));v();export{_ as RunIdResolver,v as init_runIdResolver};
|
|
2
|
+
//# sourceMappingURL=runIdResolver.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runIdResolver.mjs","names":[],"sources":["../../../src/runs/background/runIdResolver.ts"],"sourcesContent":["/**\n * Resolve a possibly-abbreviated subagent run id to where its files live.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/runIdResolver.ts` disambiguates\n * three kinds of run - foreground, async, nested - because that package\n * tracks all three in `SubagentState`. This package is async-only (see\n * `spawnHandshake.ts`'s header) and has not yet ported foreground or nested\n * run tracking (that is later work; see `SubagentState` in the predecessor).\n * This resolver covers the one kind of run this package currently has: a run\n * whose state lives under `currentRunsDir()/<runId>/` and whose terminal result lives\n * in `currentResultsDir()/<runId>.json` until `ResultWatcher` claims it. It is\n * designed to grow, not to be replaced, once the other kinds land.\n *\n * DESIGN PATTERNS:\n * - Aware of `ResultWatcher`'s claim convention\n * (`currentRunsDir()/<runId>/claimed-result.json`): a run whose result has already\n * been claimed for delivery must still resolve, and to the claimed copy,\n * not to a `currentResultsDir()` entry that no longer exists\n * - Prefix matching mirrors the predecessor's ambiguity handling: an\n * unambiguous prefix resolves like an exact id, an ambiguous one throws\n * naming every match, and a prefix matching nothing returns `undefined`\n * rather than throwing - only ambiguity is exceptional, not \"not found\"\n *\n * AVOID:\n * - Reading `currentResultsDir()/<runId>.json` for a run that `pathExists` shows has\n * a claimed copy; the source file may already belong to an unrelated, later\n * run by the time this runs\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { injectable } from 'inversify';\n\nimport { currentResultsDir, currentRunsDir } from '../../shared/paths.ts';\nimport { CLAIMED_RESULT_FILE_NAME, RESULT_FILE_SUFFIX } from './resultWatcher.ts';\n\nexport interface ResolvedRunLocation {\n runId: string;\n /** `currentRunsDir()/<runId>`, when it exists. */\n runDir: string | undefined;\n /** Wherever the run's result currently lives - pending or already claimed. */\n resultPath: string | undefined;\n /** True when `resultPath` is a claimed copy awaiting delivery, not the original. */\n claimed: boolean;\n}\n\nexport interface IRunIdResolver {\n /**\n * Resolve an exact id or an unambiguous prefix. `undefined` when nothing\n * matches. Throws when a prefix matches more than one run id.\n */\n resolve(id: string): ResolvedRunLocation | undefined;\n}\n\nfunction runDirFor(runId: string): string {\n return path.join(currentRunsDir(), runId);\n}\n\nfunction pendingResultPathFor(runId: string): string {\n return path.join(currentResultsDir(), `${runId}${RESULT_FILE_SUFFIX}`);\n}\n\nfunction claimedResultPathFor(runId: string): string {\n return path.join(runDirFor(runId), CLAIMED_RESULT_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 isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\n@injectable()\nexport class RunIdResolver implements IRunIdResolver {\n // ---------------------------------------------------------------------\n // Filesystem seams. ESM module namespaces are frozen (`vi.spyOn(fs, ...)`\n // throws), so every `node:fs` call a test needs to redirect is wrapped here.\n // ---------------------------------------------------------------------\n\n protected pathExists(filePath: string): boolean {\n return fs.existsSync(filePath);\n }\n\n /**\n * Seams for the two directory-listing methods below, as protected methods\n * rather than constructor arguments (inversify cannot resolve a primitive\n * like a path, and a directory is no different from `AgentDiscoveryService`'s\n * `now()`/`load()` seams in that respect). A test subclass points these at\n * a directory that does not exist, which exercises the `isNotFound` branch\n * for real without touching the actual shared `currentRunsDir()`/`currentResultsDir()` -\n * closing the only reason this class's tests would otherwise need to.\n */\n protected runsDir(): string {\n return currentRunsDir();\n }\n\n protected resultsDir(): string {\n return currentResultsDir();\n }\n\n protected listRunDirNames(): string[] {\n try {\n return fs\n .readdirSync(this.runsDir(), { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n protected listPendingResultIds(): string[] {\n try {\n return fs\n .readdirSync(this.resultsDir())\n .filter((entry) => entry.endsWith(RESULT_FILE_SUFFIX))\n .map((entry) => entry.slice(0, -RESULT_FILE_SUFFIX.length));\n } catch (error) {\n if (isNotFound(error)) return [];\n throw error;\n }\n }\n\n resolve(id: string): ResolvedRunLocation | undefined {\n const exact = this.locate(id);\n if (exact) return exact;\n\n const candidates = this.candidateRunIds().filter((candidate) => candidate.startsWith(id));\n if (candidates.length > 1) {\n throw new Error(`Ambiguous run id prefix '${id}' matched: ${candidates.join(', ')}. Provide a longer id.`);\n }\n const [onlyMatch] = candidates;\n return onlyMatch ? this.locate(onlyMatch) : undefined;\n }\n\n private candidateRunIds(): string[] {\n return [...new Set([...this.listRunDirNames(), ...this.listPendingResultIds()])];\n }\n\n private locate(runId: string): ResolvedRunLocation | undefined {\n const runDir = this.pathExists(runDirFor(runId)) ? runDirFor(runId) : undefined;\n const claimedPath = claimedResultPathFor(runId);\n if (this.pathExists(claimedPath)) {\n return { runId, runDir, resultPath: claimedPath, claimed: true };\n }\n const pendingPath = pendingResultPathFor(runId);\n if (this.pathExists(pendingPath)) {\n return { runId, runDir, resultPath: pendingPath, claimed: false };\n }\n if (runDir) return { runId, runDir, resultPath: undefined, claimed: false };\n return undefined;\n }\n}\n"],"mappings":"6dAuDA,SAAS,EAAU,EAAuB,CACxC,OAAO,EAAK,KAAK,GAAgB,CAAE,EAAM,CAG3C,SAAS,EAAqB,EAAuB,CACnD,OAAO,EAAK,KAAK,GAAmB,CAAE,GAAG,IAAQ,IAAqB,CAGxE,SAAS,EAAqB,EAAuB,CACnD,OAAO,EAAK,KAAK,EAAU,EAAM,CAAE,EAAyB,CAG9D,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,GAGN,SAAS,EAAW,EAAyB,CAC3C,OAAO,EAAU,EAAM,GAAK,4BAxC4C,IACQ,OA2C3E,KAA8C,CAMnD,WAAqB,EAA2B,CAC9C,OAAO,EAAG,WAAW,EAAS,CAYhC,SAA4B,CAC1B,OAAO,GAAgB,CAGzB,YAA+B,CAC7B,OAAO,GAAmB,CAG5B,iBAAsC,CACpC,GAAI,CACF,OAAO,EACJ,YAAY,KAAK,SAAS,CAAE,CAAE,cAAe,GAAM,CAAC,CACpD,OAAQ,GAAU,EAAM,aAAa,CAAC,CACtC,IAAK,GAAU,EAAM,KAAK,OACtB,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAIV,sBAA2C,CACzC,GAAI,CACF,OAAO,EACJ,YAAY,KAAK,YAAY,CAAC,CAC9B,OAAQ,GAAU,EAAM,SAAS,EAAmB,CAAC,CACrD,IAAK,GAAU,EAAM,MAAM,EAAG,CAAC,EAAmB,OAAO,CAAC,OACtD,EAAO,CACd,GAAI,EAAW,EAAM,CAAE,MAAO,EAAE,CAChC,MAAM,GAIV,QAAQ,EAA6C,CACnD,IAAM,EAAQ,KAAK,OAAO,EAAG,CAC7B,GAAI,EAAO,OAAO,EAElB,IAAM,EAAa,KAAK,iBAAiB,CAAC,OAAQ,GAAc,EAAU,WAAW,EAAG,CAAC,CACzF,GAAI,EAAW,OAAS,EACtB,MAAU,MAAM,4BAA4B,EAAG,aAAa,EAAW,KAAK,KAAK,CAAC,wBAAwB,CAE5G,GAAM,CAAC,GAAa,EACpB,OAAO,EAAY,KAAK,OAAO,EAAU,CAAG,IAAA,GAG9C,iBAAoC,CAClC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,iBAAiB,CAAE,GAAG,KAAK,sBAAsB,CAAC,CAAC,CAAC,CAGlF,OAAe,EAAgD,CAC7D,IAAM,EAAS,KAAK,WAAW,EAAU,EAAM,CAAC,CAAG,EAAU,EAAM,CAAG,IAAA,GAChE,EAAc,EAAqB,EAAM,CAC/C,GAAI,KAAK,WAAW,EAAY,CAC9B,MAAO,CAAE,QAAO,SAAQ,WAAY,EAAa,QAAS,GAAM,CAElE,IAAM,EAAc,EAAqB,EAAM,CAC/C,GAAI,KAAK,WAAW,EAAY,CAC9B,MAAO,CAAE,QAAO,SAAQ,WAAY,EAAa,QAAS,GAAO,CAEnE,GAAI,EAAQ,MAAO,CAAE,QAAO,SAAQ,WAAY,IAAA,GAAW,QAAS,GAAO,QA9E9E,GAAY,CAAA,CAAA,EAAA"}
|
|
@@ -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(`../../env.cjs`),i=require(`../../container/types.cjs`),a=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.cjs`),o=require(`../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.cjs`),s=require(`../../shared/paths.cjs`),c=require(`./asyncExecution.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`);function f(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function p(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}let m=class{constructor(e,t,n,r){this.statusWriter=e,this.terminalPersistence=t,this.reporting=n,this.teamMembership=r}now(){return Date.now()}resolveRunIdFromEnv(){let e=process.env[r.SUBAGENT_RUN_ID_ENV];return e?.trim()?e:void 0}readFile(e){try{return l.readFileSync(e,`utf-8`)}catch(e){if(p(e)===`ENOENT`)return;throw e}}writeHandshake(e,n){t.writeAtomicJson(e,n)}lastTeamRegistrationError;onTeamRegistrationFailed(e,t){this.lastTeamRegistrationError={runId:e,error:t,at:this.now()}}registerTeamMembership(e,t){let n=this.teamMembership.readRoot();if(n)return this.teamMembership.register({root:n,role:`subagent`,agent:e.agent,runId:e.runId,childIndex:e.childIndex,fanoutIndex:t,task:{id:e.runId,subject:e.task},pid:process.pid})}readExistingStatus(e){let t=this.readFile(u.join(s.currentRunsDir(),e,`status.json`));if(t!==void 0)try{let e=JSON.parse(t);return f(e)?e:void 0}catch{return}}readLaunchConfig(e){let t=s.currentRunConfigPath(e),n=this.readFile(t);if(n===void 0)throw Error(`No launch config found for run '${e}' at '${t}'.`);let r;try{r=JSON.parse(n)}catch(t){throw Error(`Launch config for run '${e}' is not valid JSON: ${t.message}`)}if(!f(r)||typeof r.runId!=`string`||r.runId!==e)throw Error(`Launch config for run '${e}' is missing or has a mismatched 'runId'.`);return r}bootstrap(e,t){let n=e??this.resolveRunIdFromEnv();if(!n)throw Error(`No run id available: '${r.SUBAGENT_RUN_ID_ENV}' is not set and none was supplied.`);let i;try{i=this.readLaunchConfig(n)}catch(e){throw this.writeHandshake(c.handshakePathFor(n),{state:`error`,error:e instanceof Error?e.message:String(e)}),e}try{let e=this.now(),r=this.readExistingStatus(n),a=r?.fanoutIndex;this.statusWriter.open(n,{...r,version:1,runId:n,agent:i.agent,state:`running`,startedAt:r?.startedAt??e,lastUpdate:e,...t?{sessionFile:t}:{},...a===void 0?{}:{fanoutIndex:a}});let o;try{o=this.registerTeamMembership(i,a)}catch(e){this.onTeamRegistrationFailed(n,e)}return this.terminalPersistence.begin(n,(e,t)=>{this.reporting.mutateTerminalStatus(e,t),o?.dispose()}),this.writeHandshake(i.handshakePath,{state:`ready`}),{runId:n,launchConfig:i,teamContext:o?.context}}catch(e){throw this.writeHandshake(i.handshakePath,{state:`error`,error:e instanceof Error?e.message:String(e)}),e}}};m=n.__decorate([(0,d.injectable)(),o.__decorateParam(0,(0,d.inject)(i.DOOM_TEAM_TYPES.CoalescedStatusWriter)),o.__decorateParam(1,(0,d.inject)(i.DOOM_TEAM_TYPES.TerminalPersistenceService)),o.__decorateParam(2,(0,d.inject)(i.DOOM_TEAM_TYPES.RunnerReporting)),o.__decorateParam(3,(0,d.inject)(i.DOOM_TEAM_TYPES.RunnerTeamMembership)),a.__decorateMetadata(`design:paramtypes`,[Object,Object,Object,Object])],m),Object.defineProperty(exports,`RunnerBootstrap`,{enumerable:!0,get:function(){return m}});
|
|
2
|
+
//# sourceMappingURL=runnerBootstrap.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerBootstrap.cjs","names":["SUBAGENT_RUN_ID_ENV","fs","path","currentRunsDir","currentRunConfigPath","handshakePathFor","DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/runnerBootstrap.ts"],"sourcesContent":["/**\n * The child-side half of the spawn handshake: reads the launch config the\n * parent (`AsyncSubagentSpawner`) wrote, opens this run's status, wires\n * `RunnerReporting`'s finalize callback into `TerminalPersistenceService`,\n * and signals the parent that this process is ready to proceed.\n *\n * \"Nothing works until this exists\" (per the task brief): `SpawnHandshake`\n * on the parent side blocks on exactly the file this class writes at the\n * end of `bootstrap()`.\n *\n * WHY THE RUN ID COMES FROM THE ENVIRONMENT, NOT AN ARGUMENT:\n * `SUBAGENT_RUN_ID_ENV` is already the wire contract `buildPiArgs` encodes a\n * run's id into (see `env.ts`); this process discovers its own identity the\n * same way every other piece of this package's child-side machinery would\n * (`SUBAGENT_CHILD_INDEX_ENV`, etc.), rather than requiring whatever calls\n * `bootstrap()` to already know it and pass it in redundantly. An explicit\n * override is still accepted for tests and for a caller that has a genuine\n * reason to know better.\n *\n * WHY A BOOTSTRAP FAILURE REPORTS THROUGH THE HANDSHAKE'S `error` FIELD\n * ONLY:\n * The predecessor invented a second file (`runner-startup.json`, `state:\n * 'error'`) for exactly this case, which is part of why a reader had to\n * check three separate files to know whether a run was actually finished\n * (see `asyncExecution.ts`'s finalization diagnosis). `SpawnHandshake`\n * already has a vocabulary for this - `{state:'error', error}` at the SAME\n * path it watches for `{state:'ready'}` - so a bootstrap failure uses that,\n * not a parallel channel.\n *\n * WHAT REMAINS UNPROVEN UNTIL `subagentPromptRuntime.ts` LANDS:\n * This class is complete and independently testable, but nothing in this\n * package yet calls it for a real spawned child - that entry point is a\n * separate, concurrent port (see `asyncExecution.ts`'s header). Until then,\n * `bootstrap()`'s only proof of correctness is its own unit tests against\n * the documented contract, not an end-to-end run.\n *\n * TEAM MEMBERSHIP IS ADDITIVE, NEVER FATAL:\n * `bootstrap()` also joins this run's `runId` to the native team as a new\n * member (`RunnerTeamMembership`), when the parent forwarded a team root\n * (see `asyncExecution.ts`'s TEAM MEMBERSHIP ENV FORWARDING section) - a\n * child that cannot be messaged, asked, or seen on the shared task board by\n * its siblings is a real gap, but it is not a reason to fail the run itself.\n * A registration failure is recorded (`onTeamRegistrationFailed`), not\n * thrown, and everything else proceeds exactly as if no team root had been\n * present at all.\n *\n * DISPOSAL RIDES THE ONE FINALIZE PATH, NOT A SECOND ONE:\n * The registration's `dispose()` is called from inside the SAME callback\n * passed to `terminalPersistence.begin()` that `RunnerReporting` already\n * owns, immediately after `mutateTerminalStatus` - not from a second,\n * independent cleanup site. `TerminalPersistenceService`'s own `finalized`\n * boolean is still the only thing deciding whether that callback runs at\n * all, so a child that finalizes for any reason (completion, signal,\n * uncaught exception) also leaves the team exactly once, for the same\n * reason there is exactly one finalize.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { currentRunConfigPath, currentRunsDir } from '../../shared/paths.ts';\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport type { TeamMemberContext } from '../../intercom/nativeTeamChannel.ts';\nimport { handshakePathFor, type AsyncRunStatus, type LaunchConfig } from './asyncExecution.ts';\nimport type { IRunnerReporting } from './runnerReporting.ts';\nimport type { IRunnerTeamMembership, RunnerTeamRegistration } from './runnerTeamMembership.ts';\nimport { type ICoalescedStatusWriter } from './statusWriter.ts';\nimport type { ITerminalPersistenceService } from './terminalPersistence.ts';\n\nconst STATUS_FILE_NAME = 'status.json';\n\nexport type RunnerLaunchConfig = LaunchConfig;\n\nexport interface RunnerBootstrapResult {\n runId: string;\n launchConfig: RunnerLaunchConfig;\n /** This run's own bound team membership, when it joined one. Same instance `registerTeamMembership` already holds. */\n teamContext: TeamMemberContext | undefined;\n}\n\nexport interface IRunnerBootstrap {\n /**\n * Read the launch config, open status, wire finalize, and signal ready.\n * Throws (after writing an error handshake) on any failure to reach that\n * point - a bootstrap that cannot even establish this run's identity has\n * nothing else it can safely do.\n */\n bootstrap(runId?: string, sessionFile?: string): RunnerBootstrapResult;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\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\n@injectable()\nexport class RunnerBootstrap implements IRunnerBootstrap {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter)\n private readonly statusWriter: ICoalescedStatusWriter<AsyncRunStatus>,\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.RunnerTeamMembership) private readonly teamMembership: IRunnerTeamMembership,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n protected resolveRunIdFromEnv(): string | undefined {\n const value = process.env[SUBAGENT_RUN_ID_ENV];\n return value?.trim() ? value : undefined;\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 writeHandshake(filePath: string, payload: { state: 'ready' } | { state: 'error'; error: string }): void {\n writeAtomicJson(filePath, payload);\n }\n\n /** Recorded, not thrown - see the module header's TEAM MEMBERSHIP IS ADDITIVE section. */\n lastTeamRegistrationError: { runId: string; error: unknown; at: number } | undefined;\n\n protected onTeamRegistrationFailed(runId: string, error: unknown): void {\n this.lastTeamRegistrationError = { runId, error, at: this.now() };\n }\n\n /**\n * Join the native team as a new member for this run, if the parent\n * forwarded a team root. `undefined` when this run is not part of a team\n * - a silent no-op, not a failure (see `asyncExecution.ts`'s TEAM\n * MEMBERSHIP ENV FORWARDING section).\n */\n protected registerTeamMembership(\n launchConfig: RunnerLaunchConfig,\n fanoutIndex: number | undefined,\n ): RunnerTeamRegistration | undefined {\n const root = this.teamMembership.readRoot();\n if (!root) return undefined;\n return this.teamMembership.register({\n root,\n role: 'subagent',\n agent: launchConfig.agent,\n runId: launchConfig.runId,\n childIndex: launchConfig.childIndex,\n // Only a fan-out child gets a `-N` suffix on its member name; a\n // standalone run has no sibling to disambiguate against.\n fanoutIndex,\n task: { id: launchConfig.runId, subject: launchConfig.task },\n pid: process.pid,\n });\n }\n\n protected readExistingStatus(runId: string): AsyncRunStatus | undefined {\n // Best-effort: the parent already wrote an initial status.json for this\n // run (`AsyncSubagentSpawner.spawn()`); reading it back preserves\n // parent-decided fields (`fanoutIndex`) rather than this process\n // reconstructing them from nothing.\n const raw = this.readFile(path.join(currentRunsDir(), runId, STATUS_FILE_NAME));\n if (raw === undefined) return undefined;\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRecord(parsed) ? (parsed as unknown as AsyncRunStatus) : undefined;\n } catch {\n return undefined;\n }\n }\n\n private readLaunchConfig(runId: string): RunnerLaunchConfig {\n const configPath = currentRunConfigPath(runId);\n const raw = this.readFile(configPath);\n if (raw === undefined) {\n throw new Error(`No launch config found for run '${runId}' at '${configPath}'.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new Error(`Launch config for run '${runId}' is not valid JSON: ${(error as Error).message}`);\n }\n if (!isRecord(parsed) || typeof parsed.runId !== 'string' || parsed.runId !== runId) {\n throw new Error(`Launch config for run '${runId}' is missing or has a mismatched 'runId'.`);\n }\n return parsed as unknown as RunnerLaunchConfig;\n }\n\n bootstrap(runId?: string, sessionFile?: string): RunnerBootstrapResult {\n const resolvedRunId = runId ?? this.resolveRunIdFromEnv();\n if (!resolvedRunId) {\n // Nothing identifies this run yet, so there is no handshake path to\n // write an error to either; this can only surface as a thrown error.\n throw new Error(`No run id available: '${SUBAGENT_RUN_ID_ENV}' is not set and none was supplied.`);\n }\n\n let launchConfig: RunnerLaunchConfig;\n try {\n launchConfig = this.readLaunchConfig(resolvedRunId);\n } catch (error) {\n this.writeHandshake(handshakePathFor(resolvedRunId), {\n state: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n\n try {\n const now = this.now();\n const existing = this.readExistingStatus(resolvedRunId);\n const fanoutIndex = existing?.fanoutIndex;\n this.statusWriter.open(resolvedRunId, {\n ...existing,\n version: 1,\n runId: resolvedRunId,\n agent: launchConfig.agent,\n state: 'running',\n startedAt: existing?.startedAt ?? now,\n lastUpdate: now,\n ...(sessionFile ? { sessionFile } : {}),\n ...(fanoutIndex === undefined ? {} : { fanoutIndex }),\n });\n\n let teamRegistration: RunnerTeamRegistration | undefined;\n try {\n teamRegistration = this.registerTeamMembership(launchConfig, fanoutIndex);\n } catch (error) {\n // Additive, never fatal - see the module header's TEAM MEMBERSHIP\n // IS ADDITIVE section.\n this.onTeamRegistrationFailed(resolvedRunId, error);\n }\n\n this.terminalPersistence.begin(resolvedRunId, (status, trigger) => {\n this.reporting.mutateTerminalStatus(status, trigger);\n // Disposal rides this SAME callback, not a second cleanup site - see\n // the module header's DISPOSAL RIDES THE ONE FINALIZE PATH section.\n teamRegistration?.dispose();\n });\n\n this.writeHandshake(launchConfig.handshakePath, { state: 'ready' });\n\n return {\n runId: resolvedRunId,\n launchConfig,\n teamContext: teamRegistration?.context,\n };\n } catch (error) {\n this.writeHandshake(launchConfig.handshakePath, {\n state: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -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{SUBAGENT_RUN_ID_ENV as a,init_env as o}from"../../env.mjs";import{DOOM_TEAM_TYPES as s,init_types as c}from"../../container/types.mjs";import{__decorateMetadata as l,init_decorateMetadata as u}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as d,init_decorateParam as f}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentRunConfigPath as p,currentRunsDir as m,init_paths as h}from"../../shared/paths.mjs";import{handshakePathFor as g,init_asyncExecution as _}from"./asyncExecution.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 typeof e==`object`&&!!e&&!Array.isArray(e)}function C(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}var w,T,E=e((()=>{h(),t(),c(),o(),_(),u(),f(),i(),w=`status.json`,T=class{constructor(e,t,n,r){this.statusWriter=e,this.terminalPersistence=t,this.reporting=n,this.teamMembership=r}now(){return Date.now()}resolveRunIdFromEnv(){let e=process.env[a];return e?.trim()?e:void 0}readFile(e){try{return v.readFileSync(e,`utf-8`)}catch(e){if(C(e)===`ENOENT`)return;throw e}}writeHandshake(e,t){n(e,t)}lastTeamRegistrationError;onTeamRegistrationFailed(e,t){this.lastTeamRegistrationError={runId:e,error:t,at:this.now()}}registerTeamMembership(e,t){let n=this.teamMembership.readRoot();if(n)return this.teamMembership.register({root:n,role:`subagent`,agent:e.agent,runId:e.runId,childIndex:e.childIndex,fanoutIndex:t,task:{id:e.runId,subject:e.task},pid:process.pid})}readExistingStatus(e){let t=this.readFile(y.join(m(),e,w));if(t!==void 0)try{let e=JSON.parse(t);return S(e)?e:void 0}catch{return}}readLaunchConfig(e){let t=p(e),n=this.readFile(t);if(n===void 0)throw Error(`No launch config found for run '${e}' at '${t}'.`);let r;try{r=JSON.parse(n)}catch(t){throw Error(`Launch config for run '${e}' is not valid JSON: ${t.message}`)}if(!S(r)||typeof r.runId!=`string`||r.runId!==e)throw Error(`Launch config for run '${e}' is missing or has a mismatched 'runId'.`);return r}bootstrap(e,t){let n=e??this.resolveRunIdFromEnv();if(!n)throw Error(`No run id available: '${a}' is not set and none was supplied.`);let r;try{r=this.readLaunchConfig(n)}catch(e){throw this.writeHandshake(g(n),{state:`error`,error:e instanceof Error?e.message:String(e)}),e}try{let e=this.now(),i=this.readExistingStatus(n),a=i?.fanoutIndex;this.statusWriter.open(n,{...i,version:1,runId:n,agent:r.agent,state:`running`,startedAt:i?.startedAt??e,lastUpdate:e,...t?{sessionFile:t}:{},...a===void 0?{}:{fanoutIndex:a}});let o;try{o=this.registerTeamMembership(r,a)}catch(e){this.onTeamRegistrationFailed(n,e)}return this.terminalPersistence.begin(n,(e,t)=>{this.reporting.mutateTerminalStatus(e,t),o?.dispose()}),this.writeHandshake(r.handshakePath,{state:`ready`}),{runId:n,launchConfig:r,teamContext:o?.context}}catch(e){throw this.writeHandshake(r.handshakePath,{state:`error`,error:e instanceof Error?e.message:String(e)}),e}}},T=r([x(),d(0,b(s.CoalescedStatusWriter)),d(1,b(s.TerminalPersistenceService)),d(2,b(s.RunnerReporting)),d(3,b(s.RunnerTeamMembership)),l(`design:paramtypes`,[Object,Object,Object,Object])],T)}));E();export{T as RunnerBootstrap,E as init_runnerBootstrap};
|
|
2
|
+
//# sourceMappingURL=runnerBootstrap.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerBootstrap.mjs","names":[],"sources":["../../../src/runs/background/runnerBootstrap.ts"],"sourcesContent":["/**\n * The child-side half of the spawn handshake: reads the launch config the\n * parent (`AsyncSubagentSpawner`) wrote, opens this run's status, wires\n * `RunnerReporting`'s finalize callback into `TerminalPersistenceService`,\n * and signals the parent that this process is ready to proceed.\n *\n * \"Nothing works until this exists\" (per the task brief): `SpawnHandshake`\n * on the parent side blocks on exactly the file this class writes at the\n * end of `bootstrap()`.\n *\n * WHY THE RUN ID COMES FROM THE ENVIRONMENT, NOT AN ARGUMENT:\n * `SUBAGENT_RUN_ID_ENV` is already the wire contract `buildPiArgs` encodes a\n * run's id into (see `env.ts`); this process discovers its own identity the\n * same way every other piece of this package's child-side machinery would\n * (`SUBAGENT_CHILD_INDEX_ENV`, etc.), rather than requiring whatever calls\n * `bootstrap()` to already know it and pass it in redundantly. An explicit\n * override is still accepted for tests and for a caller that has a genuine\n * reason to know better.\n *\n * WHY A BOOTSTRAP FAILURE REPORTS THROUGH THE HANDSHAKE'S `error` FIELD\n * ONLY:\n * The predecessor invented a second file (`runner-startup.json`, `state:\n * 'error'`) for exactly this case, which is part of why a reader had to\n * check three separate files to know whether a run was actually finished\n * (see `asyncExecution.ts`'s finalization diagnosis). `SpawnHandshake`\n * already has a vocabulary for this - `{state:'error', error}` at the SAME\n * path it watches for `{state:'ready'}` - so a bootstrap failure uses that,\n * not a parallel channel.\n *\n * WHAT REMAINS UNPROVEN UNTIL `subagentPromptRuntime.ts` LANDS:\n * This class is complete and independently testable, but nothing in this\n * package yet calls it for a real spawned child - that entry point is a\n * separate, concurrent port (see `asyncExecution.ts`'s header). Until then,\n * `bootstrap()`'s only proof of correctness is its own unit tests against\n * the documented contract, not an end-to-end run.\n *\n * TEAM MEMBERSHIP IS ADDITIVE, NEVER FATAL:\n * `bootstrap()` also joins this run's `runId` to the native team as a new\n * member (`RunnerTeamMembership`), when the parent forwarded a team root\n * (see `asyncExecution.ts`'s TEAM MEMBERSHIP ENV FORWARDING section) - a\n * child that cannot be messaged, asked, or seen on the shared task board by\n * its siblings is a real gap, but it is not a reason to fail the run itself.\n * A registration failure is recorded (`onTeamRegistrationFailed`), not\n * thrown, and everything else proceeds exactly as if no team root had been\n * present at all.\n *\n * DISPOSAL RIDES THE ONE FINALIZE PATH, NOT A SECOND ONE:\n * The registration's `dispose()` is called from inside the SAME callback\n * passed to `terminalPersistence.begin()` that `RunnerReporting` already\n * owns, immediately after `mutateTerminalStatus` - not from a second,\n * independent cleanup site. `TerminalPersistenceService`'s own `finalized`\n * boolean is still the only thing deciding whether that callback runs at\n * all, so a child that finalizes for any reason (completion, signal,\n * uncaught exception) also leaves the team exactly once, for the same\n * reason there is exactly one finalize.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport { currentRunConfigPath, currentRunsDir } from '../../shared/paths.ts';\nimport { writeAtomicJson } from '../../shared/atomicJson.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport { SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport type { TeamMemberContext } from '../../intercom/nativeTeamChannel.ts';\nimport { handshakePathFor, type AsyncRunStatus, type LaunchConfig } from './asyncExecution.ts';\nimport type { IRunnerReporting } from './runnerReporting.ts';\nimport type { IRunnerTeamMembership, RunnerTeamRegistration } from './runnerTeamMembership.ts';\nimport { type ICoalescedStatusWriter } from './statusWriter.ts';\nimport type { ITerminalPersistenceService } from './terminalPersistence.ts';\n\nconst STATUS_FILE_NAME = 'status.json';\n\nexport type RunnerLaunchConfig = LaunchConfig;\n\nexport interface RunnerBootstrapResult {\n runId: string;\n launchConfig: RunnerLaunchConfig;\n /** This run's own bound team membership, when it joined one. Same instance `registerTeamMembership` already holds. */\n teamContext: TeamMemberContext | undefined;\n}\n\nexport interface IRunnerBootstrap {\n /**\n * Read the launch config, open status, wire finalize, and signal ready.\n * Throws (after writing an error handshake) on any failure to reach that\n * point - a bootstrap that cannot even establish this run's identity has\n * nothing else it can safely do.\n */\n bootstrap(runId?: string, sessionFile?: string): RunnerBootstrapResult;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\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\n@injectable()\nexport class RunnerBootstrap implements IRunnerBootstrap {\n constructor(\n @inject(DOOM_TEAM_TYPES.CoalescedStatusWriter)\n private readonly statusWriter: ICoalescedStatusWriter<AsyncRunStatus>,\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.RunnerTeamMembership) private readonly teamMembership: IRunnerTeamMembership,\n ) {}\n\n protected now(): number {\n return Date.now();\n }\n\n protected resolveRunIdFromEnv(): string | undefined {\n const value = process.env[SUBAGENT_RUN_ID_ENV];\n return value?.trim() ? value : undefined;\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 writeHandshake(filePath: string, payload: { state: 'ready' } | { state: 'error'; error: string }): void {\n writeAtomicJson(filePath, payload);\n }\n\n /** Recorded, not thrown - see the module header's TEAM MEMBERSHIP IS ADDITIVE section. */\n lastTeamRegistrationError: { runId: string; error: unknown; at: number } | undefined;\n\n protected onTeamRegistrationFailed(runId: string, error: unknown): void {\n this.lastTeamRegistrationError = { runId, error, at: this.now() };\n }\n\n /**\n * Join the native team as a new member for this run, if the parent\n * forwarded a team root. `undefined` when this run is not part of a team\n * - a silent no-op, not a failure (see `asyncExecution.ts`'s TEAM\n * MEMBERSHIP ENV FORWARDING section).\n */\n protected registerTeamMembership(\n launchConfig: RunnerLaunchConfig,\n fanoutIndex: number | undefined,\n ): RunnerTeamRegistration | undefined {\n const root = this.teamMembership.readRoot();\n if (!root) return undefined;\n return this.teamMembership.register({\n root,\n role: 'subagent',\n agent: launchConfig.agent,\n runId: launchConfig.runId,\n childIndex: launchConfig.childIndex,\n // Only a fan-out child gets a `-N` suffix on its member name; a\n // standalone run has no sibling to disambiguate against.\n fanoutIndex,\n task: { id: launchConfig.runId, subject: launchConfig.task },\n pid: process.pid,\n });\n }\n\n protected readExistingStatus(runId: string): AsyncRunStatus | undefined {\n // Best-effort: the parent already wrote an initial status.json for this\n // run (`AsyncSubagentSpawner.spawn()`); reading it back preserves\n // parent-decided fields (`fanoutIndex`) rather than this process\n // reconstructing them from nothing.\n const raw = this.readFile(path.join(currentRunsDir(), runId, STATUS_FILE_NAME));\n if (raw === undefined) return undefined;\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRecord(parsed) ? (parsed as unknown as AsyncRunStatus) : undefined;\n } catch {\n return undefined;\n }\n }\n\n private readLaunchConfig(runId: string): RunnerLaunchConfig {\n const configPath = currentRunConfigPath(runId);\n const raw = this.readFile(configPath);\n if (raw === undefined) {\n throw new Error(`No launch config found for run '${runId}' at '${configPath}'.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new Error(`Launch config for run '${runId}' is not valid JSON: ${(error as Error).message}`);\n }\n if (!isRecord(parsed) || typeof parsed.runId !== 'string' || parsed.runId !== runId) {\n throw new Error(`Launch config for run '${runId}' is missing or has a mismatched 'runId'.`);\n }\n return parsed as unknown as RunnerLaunchConfig;\n }\n\n bootstrap(runId?: string, sessionFile?: string): RunnerBootstrapResult {\n const resolvedRunId = runId ?? this.resolveRunIdFromEnv();\n if (!resolvedRunId) {\n // Nothing identifies this run yet, so there is no handshake path to\n // write an error to either; this can only surface as a thrown error.\n throw new Error(`No run id available: '${SUBAGENT_RUN_ID_ENV}' is not set and none was supplied.`);\n }\n\n let launchConfig: RunnerLaunchConfig;\n try {\n launchConfig = this.readLaunchConfig(resolvedRunId);\n } catch (error) {\n this.writeHandshake(handshakePathFor(resolvedRunId), {\n state: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n\n try {\n const now = this.now();\n const existing = this.readExistingStatus(resolvedRunId);\n const fanoutIndex = existing?.fanoutIndex;\n this.statusWriter.open(resolvedRunId, {\n ...existing,\n version: 1,\n runId: resolvedRunId,\n agent: launchConfig.agent,\n state: 'running',\n startedAt: existing?.startedAt ?? now,\n lastUpdate: now,\n ...(sessionFile ? { sessionFile } : {}),\n ...(fanoutIndex === undefined ? {} : { fanoutIndex }),\n });\n\n let teamRegistration: RunnerTeamRegistration | undefined;\n try {\n teamRegistration = this.registerTeamMembership(launchConfig, fanoutIndex);\n } catch (error) {\n // Additive, never fatal - see the module header's TEAM MEMBERSHIP\n // IS ADDITIVE section.\n this.onTeamRegistrationFailed(resolvedRunId, error);\n }\n\n this.terminalPersistence.begin(resolvedRunId, (status, trigger) => {\n this.reporting.mutateTerminalStatus(status, trigger);\n // Disposal rides this SAME callback, not a second cleanup site - see\n // the module header's DISPOSAL RIDES THE ONE FINALIZE PATH section.\n teamRegistration?.dispose();\n });\n\n this.writeHandshake(launchConfig.handshakePath, { state: 'ready' });\n\n return {\n runId: resolvedRunId,\n launchConfig,\n teamContext: teamRegistration?.context,\n };\n } catch (error) {\n this.writeHandshake(launchConfig.handshakePath, {\n state: 'error',\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n"],"mappings":"07BA6FA,SAAS,EAAS,EAAkD,CAClE,OAAO,OAAO,GAAU,YAAY,GAAkB,CAAC,MAAM,QAAQ,EAAM,CAG7E,SAAS,EAAU,EAAoC,CACrD,OAAO,OAAO,GAAU,UAAY,GAAkB,SAAU,EAC3D,EAAgC,KACjC,IAAA,wBAvCuE,IAChB,IACF,IACR,IAE4C,aAMzF,EAAmB,gBAgClB,KAAkD,CACvD,YACE,EAEA,EAEA,EACA,EACA,CALiB,KAAA,aAAA,EAEA,KAAA,oBAAA,EACyC,KAAA,UAAA,EACK,KAAA,eAAA,EAGjE,KAAwB,CACtB,OAAO,KAAK,KAAK,CAGnB,qBAAoD,CAClD,IAAM,EAAQ,QAAQ,IAAI,GAC1B,OAAO,GAAO,MAAM,CAAG,EAAQ,IAAA,GAGjC,SAAmB,EAAsC,CACvD,GAAI,CACF,OAAO,EAAG,aAAa,EAAU,QAAQ,OAClC,EAAO,CACd,GAAI,EAAU,EAAM,GAAK,SAAU,OACnC,MAAM,GAIV,eAAyB,EAAkB,EAAuE,CAChH,EAAgB,EAAU,EAAQ,CAIpC,0BAEA,yBAAmC,EAAe,EAAsB,CACtE,KAAK,0BAA4B,CAAE,QAAO,QAAO,GAAI,KAAK,KAAK,CAAE,CASnE,uBACE,EACA,EACoC,CACpC,IAAM,EAAO,KAAK,eAAe,UAAU,CACtC,KACL,OAAO,KAAK,eAAe,SAAS,CAClC,OACA,KAAM,WACN,MAAO,EAAa,MACpB,MAAO,EAAa,MACpB,WAAY,EAAa,WAGzB,cACA,KAAM,CAAE,GAAI,EAAa,MAAO,QAAS,EAAa,KAAM,CAC5D,IAAK,QAAQ,IACd,CAAC,CAGJ,mBAA6B,EAA2C,CAKtE,IAAM,EAAM,KAAK,SAAS,EAAK,KAAK,GAAgB,CAAE,EAAO,EAAiB,CAAC,CAC3E,OAAQ,IAAA,GACZ,GAAI,CACF,IAAM,EAAkB,KAAK,MAAM,EAAI,CACvC,OAAO,EAAS,EAAO,CAAI,EAAuC,IAAA,QAC5D,CACN,QAIJ,iBAAyB,EAAmC,CAC1D,IAAM,EAAa,EAAqB,EAAM,CACxC,EAAM,KAAK,SAAS,EAAW,CACrC,GAAI,IAAQ,IAAA,GACV,MAAU,MAAM,mCAAmC,EAAM,QAAQ,EAAW,IAAI,CAElF,IAAI,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAI,OACjB,EAAO,CACd,MAAU,MAAM,0BAA0B,EAAM,uBAAwB,EAAgB,UAAU,CAEpG,GAAI,CAAC,EAAS,EAAO,EAAI,OAAO,EAAO,OAAU,UAAY,EAAO,QAAU,EAC5E,MAAU,MAAM,0BAA0B,EAAM,2CAA2C,CAE7F,OAAO,EAGT,UAAU,EAAgB,EAA6C,CACrE,IAAM,EAAgB,GAAS,KAAK,qBAAqB,CACzD,GAAI,CAAC,EAGH,MAAU,MAAM,yBAAyB,EAAoB,qCAAqC,CAGpG,IAAI,EACJ,GAAI,CACF,EAAe,KAAK,iBAAiB,EAAc,OAC5C,EAAO,CAKd,MAJA,KAAK,eAAe,EAAiB,EAAc,CAAE,CACnD,MAAO,QACP,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC9D,CAAC,CACI,EAGR,GAAI,CACF,IAAM,EAAM,KAAK,KAAK,CAChB,EAAW,KAAK,mBAAmB,EAAc,CACjD,EAAc,GAAU,YAC9B,KAAK,aAAa,KAAK,EAAe,CACpC,GAAG,EACH,QAAS,EACT,MAAO,EACP,MAAO,EAAa,MACpB,MAAO,UACP,UAAW,GAAU,WAAa,EAClC,WAAY,EACZ,GAAI,EAAc,CAAE,cAAa,CAAG,EAAE,CACtC,GAAI,IAAgB,IAAA,GAAY,EAAE,CAAG,CAAE,cAAa,CACrD,CAAC,CAEF,IAAI,EACJ,GAAI,CACF,EAAmB,KAAK,uBAAuB,EAAc,EAAY,OAClE,EAAO,CAGd,KAAK,yBAAyB,EAAe,EAAM,CAYrD,OATA,KAAK,oBAAoB,MAAM,GAAgB,EAAQ,IAAY,CACjE,KAAK,UAAU,qBAAqB,EAAQ,EAAQ,CAGpD,GAAkB,SAAS,EAC3B,CAEF,KAAK,eAAe,EAAa,cAAe,CAAE,MAAO,QAAS,CAAC,CAE5D,CACL,MAAO,EACP,eACA,YAAa,GAAkB,QAChC,OACM,EAAO,CAKd,MAJA,KAAK,eAAe,EAAa,cAAe,CAC9C,MAAO,QACP,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC9D,CAAC,CACI,UAlKX,GAAY,KAGR,EAAO,EAAgB,sBAAsB,CAAA,KAE7C,EAAO,EAAgB,2BAA2B,CAAA,KAElD,EAAO,EAAgB,gBAAgB,CAAA,KACvC,EAAO,EAAgB,qBAAqB,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(`../../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`),i=require(`./asyncExecution.cjs`);let a=require(`inversify`),o=class{constructor(e,t,n,r){this.controlChannel=e,this.terminalPersistence=t,this.reporting=n,this.statusWriter=r}exitProcess(e){process.exit(e)}start(e,t={}){let n={onInterrupt:()=>this.interrupt(),onStop:()=>this.stop(),onTimeout:()=>this.timeout(),...t.onSteer?{onSteer:t.onSteer}:{}};return this.controlChannel.watch(i.runDirFor(e),n)}setActivity(e){this.statusWriter.update(t=>{t.activityState=e,t.lastUpdate=Date.now()})}setProgress(e){e.tokens===void 0&&e.currentTool===void 0&&e.toolCount===void 0||this.statusWriter.update(t=>{e.tokens!==void 0&&(t.tokens=e.tokens),e.currentTool!==void 0&&(t.currentTool=e.currentTool),e.toolCount!==void 0&&(t.toolCount=e.toolCount),t.lastUpdate=Date.now()})}complete(e,t){this.reporting.prepareResult({success:e,summary:t}),this.terminalPersistence.finalize()}interrupt(){this.reporting.prepareResult({success:!1,summary:`Interrupted before completion.`}),this.terminalPersistence.finalize()}stop(){this.reporting.prepareResult({success:!1,state:`stopped`,summary:`Stopped before completion.`});try{this.terminalPersistence.finalize()}finally{this.exitProcess(0)}}timeout(){this.reporting.prepareResult({success:!1,summary:`Timed out before completion.`}),this.terminalPersistence.finalize()}};o=e.__decorate([(0,a.injectable)(),r.__decorateParam(0,(0,a.inject)(t.DOOM_TEAM_TYPES.ControlChannelWatcher)),r.__decorateParam(1,(0,a.inject)(t.DOOM_TEAM_TYPES.TerminalPersistenceService)),r.__decorateParam(2,(0,a.inject)(t.DOOM_TEAM_TYPES.RunnerReporting)),r.__decorateParam(3,(0,a.inject)(t.DOOM_TEAM_TYPES.CoalescedStatusWriter)),n.__decorateMetadata(`design:paramtypes`,[Object,Object,Object,Object])],o),Object.defineProperty(exports,`RunnerExecution`,{enumerable:!0,get:function(){return o}});
|
|
2
|
+
//# sourceMappingURL=runnerExecution.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runnerExecution.cjs","names":["runDirFor","DOOM_TEAM_TYPES"],"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":"oZA+EO,EAAA,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,MAAMA,EAAAA,UAAU,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,qCAlE1B,kCAGDC,EAAAA,gBAAgB,sBAAsB,CAAA,kCACtCA,EAAAA,gBAAgB,2BAA2B,CAAA,kCAE3CA,EAAAA,gBAAgB,gBAAgB,CAAA,kCAChCA,EAAAA,gBAAgB,sBAAsB,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{init_asyncExecution as l,runDirFor as u}from"./asyncExecution.mjs";import{inject as d,injectable as f}from"inversify";var p,m=e((()=>{i(),l(),o(),c(),n(),p=class{constructor(e,t,n,r){this.controlChannel=e,this.terminalPersistence=t,this.reporting=n,this.statusWriter=r}exitProcess(e){process.exit(e)}start(e,t={}){let n={onInterrupt:()=>this.interrupt(),onStop:()=>this.stop(),onTimeout:()=>this.timeout(),...t.onSteer?{onSteer:t.onSteer}:{}};return this.controlChannel.watch(u(e),n)}setActivity(e){this.statusWriter.update(t=>{t.activityState=e,t.lastUpdate=Date.now()})}setProgress(e){e.tokens===void 0&&e.currentTool===void 0&&e.toolCount===void 0||this.statusWriter.update(t=>{e.tokens!==void 0&&(t.tokens=e.tokens),e.currentTool!==void 0&&(t.currentTool=e.currentTool),e.toolCount!==void 0&&(t.toolCount=e.toolCount),t.lastUpdate=Date.now()})}complete(e,t){this.reporting.prepareResult({success:e,summary:t}),this.terminalPersistence.finalize()}interrupt(){this.reporting.prepareResult({success:!1,summary:`Interrupted before completion.`}),this.terminalPersistence.finalize()}stop(){this.reporting.prepareResult({success:!1,state:`stopped`,summary:`Stopped before completion.`});try{this.terminalPersistence.finalize()}finally{this.exitProcess(0)}}timeout(){this.reporting.prepareResult({success:!1,summary:`Timed out before completion.`}),this.terminalPersistence.finalize()}},p=t([f(),s(0,d(r.ControlChannelWatcher)),s(1,d(r.TerminalPersistenceService)),s(2,d(r.RunnerReporting)),s(3,d(r.CoalescedStatusWriter)),a(`design:paramtypes`,[Object,Object,Object,Object])],p)}));m();export{p as RunnerExecution,m as init_runnerExecution};
|
|
2
|
+
//# sourceMappingURL=runnerExecution.mjs.map
|