@agimon-ai/doompi-team 0.0.1-alpha.2 → 0.0.1-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -17
- package/dist/adapters/asyncJobTracker.cjs +2 -0
- package/dist/adapters/asyncJobTracker.cjs.map +1 -0
- package/dist/adapters/asyncJobTracker.mjs +2 -0
- package/dist/adapters/asyncJobTracker.mjs.map +1 -0
- package/dist/adapters/atomicJson.cjs +2 -0
- package/dist/adapters/atomicJson.cjs.map +1 -0
- package/dist/adapters/atomicJson.mjs +2 -0
- package/dist/adapters/atomicJson.mjs.map +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/adapters/pollScheduler.cjs +2 -0
- package/dist/adapters/pollScheduler.cjs.map +1 -0
- package/dist/adapters/pollScheduler.mjs +2 -0
- package/dist/adapters/pollScheduler.mjs.map +1 -0
- package/dist/adapters/process/childTranscript.cjs +5 -0
- package/dist/adapters/process/childTranscript.cjs.map +1 -0
- package/dist/adapters/process/childTranscript.mjs +5 -0
- package/dist/adapters/process/childTranscript.mjs.map +1 -0
- package/dist/adapters/process/env.cjs +2 -0
- package/dist/adapters/process/env.cjs.map +1 -0
- package/dist/adapters/process/env.d.cts +69 -0
- package/dist/adapters/process/env.d.cts.map +1 -0
- package/dist/adapters/process/env.d.mts +69 -0
- package/dist/adapters/process/env.d.mts.map +1 -0
- package/dist/adapters/process/env.mjs +2 -0
- package/dist/adapters/process/env.mjs.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.cjs +2 -0
- package/dist/adapters/process/sdkRunnerEntry.cjs.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.d.cts +5 -0
- package/dist/adapters/process/sdkRunnerEntry.d.cts.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.d.mts +5 -0
- package/dist/adapters/process/sdkRunnerEntry.d.mts.map +1 -0
- package/dist/adapters/process/sdkRunnerEntry.mjs +2 -0
- package/dist/adapters/process/sdkRunnerEntry.mjs.map +1 -0
- package/dist/adapters/processTerminal.cjs +2 -0
- package/dist/adapters/processTerminal.cjs.map +1 -0
- package/dist/adapters/processTerminal.mjs +2 -0
- package/dist/adapters/processTerminal.mjs.map +1 -0
- package/dist/adapters/resultWatcher.cjs +2 -0
- package/dist/adapters/resultWatcher.cjs.map +1 -0
- package/dist/adapters/resultWatcher.mjs +2 -0
- package/dist/adapters/resultWatcher.mjs.map +1 -0
- package/dist/adapters/runIdResolver.cjs +2 -0
- package/dist/adapters/runIdResolver.cjs.map +1 -0
- package/dist/adapters/runIdResolver.mjs +2 -0
- package/dist/adapters/runIdResolver.mjs.map +1 -0
- package/dist/adapters/runRegistry.cjs +2 -0
- package/dist/adapters/runRegistry.cjs.map +1 -0
- package/dist/adapters/runRegistry.mjs +2 -0
- package/dist/adapters/runRegistry.mjs.map +1 -0
- package/dist/adapters/scopeOwner.cjs +2 -0
- package/dist/adapters/scopeOwner.cjs.map +1 -0
- package/dist/adapters/scopeOwner.mjs +2 -0
- package/dist/adapters/scopeOwner.mjs.map +1 -0
- package/dist/adapters/staleRunReconciler.cjs +2 -0
- package/dist/adapters/staleRunReconciler.cjs.map +1 -0
- package/dist/adapters/staleRunReconciler.mjs +2 -0
- package/dist/adapters/staleRunReconciler.mjs.map +1 -0
- package/dist/adapters/statusReader.cjs +2 -0
- package/dist/adapters/statusReader.cjs.map +1 -0
- package/dist/adapters/statusReader.mjs +2 -0
- package/dist/adapters/statusReader.mjs.map +1 -0
- package/dist/adapters/suspendedRuns.cjs +6 -0
- package/dist/adapters/suspendedRuns.cjs.map +1 -0
- package/dist/adapters/suspendedRuns.mjs +6 -0
- package/dist/adapters/suspendedRuns.mjs.map +1 -0
- package/dist/api/backgroundWork.cjs +1 -1
- package/dist/api/backgroundWork.d.cts +2 -2
- package/dist/api/backgroundWork.d.mts +2 -2
- package/dist/api/backgroundWork.mjs +1 -1
- package/dist/api/capabilityCeiling.cjs +1 -1
- package/dist/api/capabilityCeiling.d.cts +3 -3
- package/dist/api/capabilityCeiling.d.mts +3 -3
- package/dist/api/capabilityCeiling.mjs +1 -2
- package/dist/api/delegation.cjs +1 -1
- package/dist/api/delegation.d.cts +1 -1
- package/dist/api/delegation.d.mts +1 -1
- package/dist/api/delegation.mjs +1 -1
- package/dist/api/teamSnapshot.cjs +1 -1
- package/dist/api/teamSnapshot.d.cts +2 -2
- package/dist/api/teamSnapshot.d.mts +2 -2
- package/dist/api/teamSnapshot.mjs +1 -2
- package/dist/commands/slash/chainExpression.cjs.map +1 -0
- package/dist/commands/slash/chainExpression.mjs.map +1 -0
- package/dist/commands/slash/doctor.cjs +5 -0
- package/dist/commands/slash/doctor.cjs.map +1 -0
- package/dist/commands/slash/doctor.mjs +5 -0
- package/dist/{slash/doctor.cjs.map → commands/slash/doctor.mjs.map} +1 -1
- package/dist/{slash → commands/slash}/slashCommands.cjs +1 -1
- package/dist/commands/slash/slashCommands.cjs.map +1 -0
- package/dist/{slash → commands/slash}/slashCommands.mjs +2 -2
- package/dist/commands/slash/slashCommands.mjs.map +1 -0
- package/dist/commands/slash/spawnRequestMapping.cjs.map +1 -0
- package/dist/commands/slash/spawnRequestMapping.mjs.map +1 -0
- package/dist/commands/slash/subagentLaunch.cjs +2 -0
- package/dist/commands/slash/subagentLaunch.cjs.map +1 -0
- package/dist/commands/slash/subagentLaunch.mjs +2 -0
- package/dist/commands/slash/subagentLaunch.mjs.map +1 -0
- package/dist/container/globalBridge.cjs.map +1 -1
- package/dist/container/globalBridge.d.cts +1 -1
- package/dist/container/globalBridge.d.mts +1 -1
- package/dist/container/globalBridge.mjs.map +1 -1
- package/dist/container/index.cjs +1 -1
- package/dist/container/index.cjs.map +1 -1
- package/dist/container/index.d.cts +2 -1
- package/dist/container/index.d.cts.map +1 -1
- package/dist/container/index.d.mts +2 -1
- package/dist/container/index.d.mts.map +1 -1
- package/dist/container/index.mjs +1 -1
- package/dist/container/index.mjs.map +1 -1
- package/dist/container/modules/agents.cjs +1 -1
- package/dist/container/modules/agents.cjs.map +1 -1
- package/dist/container/modules/agents.mjs +1 -1
- package/dist/container/modules/agents.mjs.map +1 -1
- package/dist/container/modules/core.cjs +1 -1
- package/dist/container/modules/core.cjs.map +1 -1
- package/dist/container/modules/core.mjs +1 -1
- package/dist/container/modules/core.mjs.map +1 -1
- package/dist/container/modules/extensions.cjs +1 -1
- package/dist/container/modules/extensions.cjs.map +1 -1
- package/dist/container/modules/extensions.mjs +1 -1
- package/dist/container/modules/extensions.mjs.map +1 -1
- package/dist/container/modules/intercom.cjs +1 -1
- package/dist/container/modules/intercom.cjs.map +1 -1
- package/dist/container/modules/intercom.mjs +1 -1
- package/dist/container/modules/intercom.mjs.map +1 -1
- package/dist/container/modules/runs.cjs +1 -1
- package/dist/container/modules/runs.cjs.map +1 -1
- package/dist/container/modules/runs.mjs +1 -1
- package/dist/container/modules/runs.mjs.map +1 -1
- package/dist/container/types.cjs +1 -1
- package/dist/container/types.cjs.map +1 -1
- package/dist/container/types.d.cts +1 -0
- package/dist/container/types.d.cts.map +1 -1
- package/dist/container/types.d.mts +1 -0
- package/dist/container/types.d.mts.map +1 -1
- package/dist/container/types.mjs +1 -1
- package/dist/container/types.mjs.map +1 -1
- package/dist/env.cjs +1 -2
- package/dist/env.d.cts +2 -65
- package/dist/env.d.mts +2 -65
- package/dist/env.mjs +1 -1
- package/dist/env.mjs.map +1 -1
- package/dist/extensions/config.cjs.map +1 -1
- package/dist/extensions/config.mjs.map +1 -1
- package/dist/extensions/delegationBridge.cjs.map +1 -1
- package/dist/extensions/delegationBridge.mjs.map +1 -1
- package/dist/extensions/doom.cjs +1 -1
- package/dist/extensions/doom.cjs.map +1 -1
- package/dist/extensions/doom.mjs +1 -1
- package/dist/extensions/doom.mjs.map +1 -1
- package/dist/extensions/fablePlanBridge.cjs +1 -1
- package/dist/extensions/fablePlanBridge.cjs.map +1 -1
- package/dist/extensions/fablePlanBridge.mjs +1 -1
- package/dist/extensions/fablePlanBridge.mjs.map +1 -1
- package/dist/extensions/managementActions.cjs +1 -1
- package/dist/extensions/managementActions.cjs.map +1 -1
- package/dist/extensions/managementActions.mjs +1 -1
- package/dist/extensions/managementActions.mjs.map +1 -1
- package/dist/extensions/operationJournal.cjs +1 -1
- package/dist/extensions/operationJournal.cjs.map +1 -1
- package/dist/extensions/operationJournal.mjs +1 -1
- package/dist/extensions/operationJournal.mjs.map +1 -1
- package/dist/extensions/orchestratorPrompt.cjs +5 -6
- package/dist/extensions/orchestratorPrompt.cjs.map +1 -1
- package/dist/extensions/orchestratorPrompt.mjs +5 -6
- package/dist/extensions/orchestratorPrompt.mjs.map +1 -1
- package/dist/extensions/schemas.mjs.map +1 -1
- package/dist/extensions/spawnPlan.cjs +3 -2
- package/dist/extensions/spawnPlan.cjs.map +1 -1
- package/dist/extensions/spawnPlan.mjs +3 -2
- package/dist/extensions/spawnPlan.mjs.map +1 -1
- package/dist/extensions/standard.cjs +1 -1
- package/dist/extensions/standard.cjs.map +1 -1
- package/dist/extensions/standard.mjs +1 -1
- package/dist/extensions/standard.mjs.map +1 -1
- package/dist/extensions/statusViews.cjs +1 -1
- package/dist/extensions/statusViews.cjs.map +1 -1
- package/dist/extensions/statusViews.mjs +2 -2
- package/dist/extensions/statusViews.mjs.map +1 -1
- package/dist/extensions/subagentPromptRuntime.cjs +11 -11
- package/dist/extensions/subagentPromptRuntime.cjs.map +1 -1
- package/dist/extensions/subagentPromptRuntime.d.cts.map +1 -1
- package/dist/extensions/subagentPromptRuntime.d.mts.map +1 -1
- package/dist/extensions/subagentPromptRuntime.mjs +5 -5
- package/dist/extensions/subagentPromptRuntime.mjs.map +1 -1
- package/dist/extensions/subagentTool.cjs +5 -6
- package/dist/extensions/subagentTool.cjs.map +1 -1
- package/dist/extensions/subagentTool.mjs +5 -6
- package/dist/extensions/subagentTool.mjs.map +1 -1
- package/dist/extensions/toolDescription.cjs +1 -1
- package/dist/extensions/toolDescription.cjs.map +1 -1
- package/dist/extensions/toolDescription.mjs +1 -1
- package/dist/extensions/toolDescription.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/runs/background/cliRunnerEntry.cjs +1 -2
- package/dist/runs/background/cliRunnerEntry.mjs +1 -2
- package/dist/runs/sdkRunnerEntry.cjs +1 -2
- package/dist/runs/sdkRunnerEntry.d.cts +2 -1
- package/dist/runs/sdkRunnerEntry.d.mts +2 -1
- package/dist/runs/sdkRunnerEntry.mjs +1 -2
- package/dist/schemas/team/backgroundWork.cjs +2 -0
- package/dist/schemas/team/backgroundWork.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/backgroundWork.d.cts +2 -2
- package/dist/schemas/team/backgroundWork.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/backgroundWork.d.mts +2 -2
- package/dist/schemas/team/backgroundWork.d.mts.map +1 -0
- package/dist/schemas/team/backgroundWork.mjs +2 -0
- package/dist/{protocol/backgroundWork.cjs.map → schemas/team/backgroundWork.mjs.map} +1 -1
- package/dist/schemas/team/capabilityCeiling.cjs +2 -0
- package/dist/schemas/team/capabilityCeiling.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/capabilityCeiling.d.cts +3 -3
- package/dist/schemas/team/capabilityCeiling.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/capabilityCeiling.d.mts +3 -3
- package/dist/schemas/team/capabilityCeiling.d.mts.map +1 -0
- package/dist/schemas/team/capabilityCeiling.mjs +2 -0
- package/dist/schemas/team/capabilityCeiling.mjs.map +1 -0
- package/dist/schemas/team/delegation.cjs.map +1 -0
- package/dist/{protocol → schemas/team}/delegation.d.cts +1 -1
- package/dist/schemas/team/delegation.d.cts.map +1 -0
- package/dist/{protocol → schemas/team}/delegation.d.mts +1 -1
- package/dist/schemas/team/delegation.d.mts.map +1 -0
- package/dist/schemas/team/delegation.mjs.map +1 -0
- package/dist/schemas/team/packageConfig.cjs +2 -0
- package/dist/schemas/team/packageConfig.cjs.map +1 -0
- package/dist/schemas/team/packageConfig.mjs +2 -0
- package/dist/schemas/team/packageConfig.mjs.map +1 -0
- package/dist/services/agents/discovery.cjs +2 -0
- package/dist/services/agents/discovery.cjs.map +1 -0
- package/dist/services/agents/discovery.mjs +2 -0
- package/dist/services/agents/discovery.mjs.map +1 -0
- package/dist/services/agents/frontmatter.cjs.map +1 -0
- package/dist/{agents → services/agents}/frontmatter.mjs +1 -1
- package/dist/services/agents/frontmatter.mjs.map +1 -0
- package/dist/services/agents/loader.cjs +2 -0
- package/dist/services/agents/loader.cjs.map +1 -0
- package/dist/services/agents/loader.mjs +2 -0
- package/dist/services/agents/loader.mjs.map +1 -0
- package/dist/services/agents/memory.cjs +3 -0
- package/dist/services/agents/memory.cjs.map +1 -0
- package/dist/{agents → services/agents}/memory.mjs +1 -1
- package/dist/services/agents/memory.mjs.map +1 -0
- package/dist/services/agents/projectRoot.cjs +2 -0
- package/dist/services/agents/projectRoot.cjs.map +1 -0
- package/dist/services/agents/projectRoot.mjs +2 -0
- package/dist/services/agents/projectRoot.mjs.map +1 -0
- package/dist/services/agents/settings.cjs +2 -0
- package/dist/services/agents/settings.cjs.map +1 -0
- package/dist/services/agents/settings.mjs +2 -0
- package/dist/services/agents/settings.mjs.map +1 -0
- package/dist/services/agents/skills.cjs +0 -0
- package/dist/services/agents/skills.cjs.map +1 -0
- package/dist/services/agents/skills.mjs +5 -0
- package/dist/services/agents/skills.mjs.map +1 -0
- package/dist/{agents → services/agents}/types.d.cts +4 -4
- package/dist/services/agents/types.d.cts.map +1 -0
- package/dist/{agents → services/agents}/types.d.mts +4 -4
- package/dist/services/agents/types.d.mts.map +1 -0
- package/dist/services/api/backgroundWork.cjs +1 -0
- package/dist/services/api/backgroundWork.d.cts +1 -0
- package/dist/services/api/backgroundWork.d.mts +1 -0
- package/dist/services/api/backgroundWork.mjs +1 -0
- package/dist/services/api/capabilityCeiling.cjs +1 -0
- package/dist/services/api/capabilityCeiling.d.cts +2 -0
- package/dist/services/api/capabilityCeiling.d.mts +2 -0
- package/dist/services/api/capabilityCeiling.mjs +2 -0
- package/dist/services/api/capabilityCeiling.mjs.map +1 -0
- package/dist/services/api/teamSnapshot.cjs +1 -0
- package/dist/services/api/teamSnapshot.d.cts +1 -0
- package/dist/services/api/teamSnapshot.d.mts +1 -0
- package/dist/services/api/teamSnapshot.mjs +2 -0
- package/dist/services/api/teamSnapshot.mjs.map +1 -0
- package/dist/services/intercom/intercomBridge.cjs.map +1 -0
- package/dist/services/intercom/intercomBridge.mjs.map +1 -0
- package/dist/services/intercom/nativeTeamChannel.cjs +5 -0
- package/dist/services/intercom/nativeTeamChannel.cjs.map +1 -0
- package/dist/{intercom → services/intercom}/nativeTeamChannel.d.cts +1 -1
- package/dist/services/intercom/nativeTeamChannel.d.cts.map +1 -0
- package/dist/{intercom → services/intercom}/nativeTeamChannel.d.mts +1 -1
- package/dist/services/intercom/nativeTeamChannel.d.mts.map +1 -0
- package/dist/services/intercom/nativeTeamChannel.mjs +5 -0
- package/dist/services/intercom/nativeTeamChannel.mjs.map +1 -0
- package/dist/services/intercom/supervisorControlChannel.cjs +2 -0
- package/dist/services/intercom/supervisorControlChannel.cjs.map +1 -0
- package/dist/{intercom → services/intercom}/supervisorControlChannel.mjs +1 -1
- package/dist/services/intercom/supervisorControlChannel.mjs.map +1 -0
- package/dist/services/runs/background/asyncExecution.cjs +2 -0
- package/dist/services/runs/background/asyncExecution.cjs.map +1 -0
- package/dist/services/runs/background/asyncExecution.mjs +2 -0
- package/dist/services/runs/background/asyncExecution.mjs.map +1 -0
- package/dist/services/runs/background/asyncJobTracker.cjs +1 -0
- package/dist/services/runs/background/asyncJobTracker.mjs +2 -0
- package/dist/services/runs/background/asyncJobTracker.mjs.map +1 -0
- package/dist/services/runs/background/cliRunnerEntry.cjs +2 -0
- package/dist/services/runs/background/cliRunnerEntry.cjs.map +1 -0
- package/dist/services/runs/background/cliRunnerEntry.mjs +2 -0
- package/dist/services/runs/background/cliRunnerEntry.mjs.map +1 -0
- package/dist/services/runs/background/completionBatcher.cjs.map +1 -0
- package/dist/services/runs/background/completionBatcher.mjs +2 -0
- package/dist/services/runs/background/completionBatcher.mjs.map +1 -0
- package/dist/{runs → services/runs}/background/notify.cjs +1 -1
- package/dist/services/runs/background/notify.cjs.map +1 -0
- package/dist/{runs → services/runs}/background/notify.mjs +1 -1
- package/dist/services/runs/background/notify.mjs.map +1 -0
- package/dist/services/runs/background/parentWatchdog.cjs.map +1 -0
- package/dist/services/runs/background/parentWatchdog.mjs.map +1 -0
- package/dist/services/runs/background/processTerminal.cjs +1 -0
- package/dist/services/runs/background/processTerminal.mjs +2 -0
- package/dist/services/runs/background/processTerminal.mjs.map +1 -0
- package/dist/services/runs/background/resultWatcher.cjs +1 -0
- package/dist/services/runs/background/resultWatcher.mjs +2 -0
- package/dist/services/runs/background/resultWatcher.mjs.map +1 -0
- package/dist/services/runs/background/runIdResolver.cjs +1 -0
- package/dist/services/runs/background/runIdResolver.mjs +2 -0
- package/dist/services/runs/background/runIdResolver.mjs.map +1 -0
- package/dist/services/runs/background/runnerBootstrap.cjs +2 -0
- package/dist/services/runs/background/runnerBootstrap.cjs.map +1 -0
- package/dist/services/runs/background/runnerBootstrap.mjs +2 -0
- package/dist/services/runs/background/runnerBootstrap.mjs.map +1 -0
- package/dist/services/runs/background/runnerExecution.cjs +2 -0
- package/dist/services/runs/background/runnerExecution.cjs.map +1 -0
- package/dist/services/runs/background/runnerExecution.mjs +2 -0
- package/dist/services/runs/background/runnerExecution.mjs.map +1 -0
- package/dist/{runs → services/runs}/background/runnerReporting.cjs +2 -2
- package/dist/services/runs/background/runnerReporting.cjs.map +1 -0
- package/dist/{runs → services/runs}/background/runnerReporting.mjs +2 -2
- package/dist/services/runs/background/runnerReporting.mjs.map +1 -0
- package/dist/services/runs/background/runnerTeamMembership.cjs +2 -0
- package/dist/services/runs/background/runnerTeamMembership.cjs.map +1 -0
- package/dist/services/runs/background/runnerTeamMembership.mjs +2 -0
- package/dist/services/runs/background/runnerTeamMembership.mjs.map +1 -0
- package/dist/services/runs/background/spawnHandshake.cjs +2 -0
- package/dist/services/runs/background/spawnHandshake.cjs.map +1 -0
- package/dist/services/runs/background/spawnHandshake.mjs +2 -0
- package/dist/services/runs/background/spawnHandshake.mjs.map +1 -0
- package/dist/services/runs/background/staleRunReconciler.cjs +1 -0
- package/dist/services/runs/background/staleRunReconciler.mjs +2 -0
- package/dist/services/runs/background/staleRunReconciler.mjs.map +1 -0
- package/dist/services/runs/background/statusReader.cjs +1 -0
- package/dist/services/runs/background/statusReader.mjs +2 -0
- package/dist/services/runs/background/statusReader.mjs.map +1 -0
- package/dist/services/runs/background/statusWriter.cjs +2 -0
- package/dist/services/runs/background/statusWriter.cjs.map +1 -0
- package/dist/services/runs/background/statusWriter.mjs +2 -0
- package/dist/services/runs/background/statusWriter.mjs.map +1 -0
- package/dist/services/runs/background/subagentWait.cjs +2 -0
- package/dist/services/runs/background/subagentWait.cjs.map +1 -0
- package/dist/services/runs/background/subagentWait.mjs +2 -0
- package/dist/services/runs/background/subagentWait.mjs.map +1 -0
- package/dist/{runs/background/terminalPersistence.mjs → services/runs/background/terminalPersistence.cjs} +2 -2
- package/dist/services/runs/background/terminalPersistence.cjs.map +1 -0
- package/dist/services/runs/background/terminalPersistence.mjs +2 -0
- package/dist/services/runs/background/terminalPersistence.mjs.map +1 -0
- package/dist/services/runs/extensionErrorTelemetry.cjs +2 -0
- package/dist/services/runs/extensionErrorTelemetry.cjs.map +1 -0
- package/dist/services/runs/extensionErrorTelemetry.mjs +2 -0
- package/dist/services/runs/extensionErrorTelemetry.mjs.map +1 -0
- package/dist/services/runs/registry/processGroup.cjs.map +1 -0
- package/dist/services/runs/registry/processGroup.mjs.map +1 -0
- package/dist/services/runs/registry/runRegistry.cjs +1 -0
- package/dist/services/runs/registry/runRegistry.mjs +2 -0
- package/dist/services/runs/registry/runRegistry.mjs.map +1 -0
- package/dist/services/runs/registry/sessionLifecycle.cjs +2 -0
- package/dist/services/runs/registry/sessionLifecycle.cjs.map +1 -0
- package/dist/services/runs/registry/sessionLifecycle.mjs +2 -0
- package/dist/services/runs/registry/sessionLifecycle.mjs.map +1 -0
- package/dist/services/runs/registry/suspendedRuns.cjs +1 -0
- package/dist/services/runs/registry/suspendedRuns.mjs +2 -0
- package/dist/services/runs/registry/suspendedRuns.mjs.map +1 -0
- package/dist/services/runs/sdkRunnerEntry.cjs +1 -0
- package/dist/services/runs/sdkRunnerEntry.mjs +1 -0
- package/dist/services/runs/shared/claudeFableProfile.cjs +4 -0
- package/dist/services/runs/shared/claudeFableProfile.cjs.map +1 -0
- package/dist/services/runs/shared/claudeFableProfile.mjs +4 -0
- package/dist/services/runs/shared/claudeFableProfile.mjs.map +1 -0
- package/dist/services/runs/shared/depthGuard.cjs +2 -0
- package/dist/services/runs/shared/depthGuard.cjs.map +1 -0
- package/dist/services/runs/shared/depthGuard.mjs +2 -0
- package/dist/services/runs/shared/depthGuard.mjs.map +1 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs +2 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.cjs.map +1 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs +2 -0
- package/dist/services/runs/shared/mcpDirectToolAllowlist.mjs.map +1 -0
- package/dist/services/runs/shared/modelFallback.cjs +2 -0
- package/dist/services/runs/shared/modelFallback.cjs.map +1 -0
- package/dist/services/runs/shared/modelFallback.mjs +2 -0
- package/dist/services/runs/shared/modelFallback.mjs.map +1 -0
- package/dist/services/runs/shared/modelScope.cjs +2 -0
- package/dist/services/runs/shared/modelScope.cjs.map +1 -0
- package/dist/services/runs/shared/modelScope.mjs +2 -0
- package/dist/services/runs/shared/modelScope.mjs.map +1 -0
- package/dist/services/runs/shared/nestedPath.cjs +2 -0
- package/dist/services/runs/shared/nestedPath.cjs.map +1 -0
- package/dist/services/runs/shared/nestedPath.mjs +2 -0
- package/dist/services/runs/shared/nestedPath.mjs.map +1 -0
- package/dist/services/runs/shared/piArgs.cjs +2 -0
- package/dist/services/runs/shared/piArgs.cjs.map +1 -0
- package/dist/services/runs/shared/piArgs.mjs +2 -0
- package/dist/services/runs/shared/piArgs.mjs.map +1 -0
- package/dist/services/runs/shared/piSpawn.cjs +2 -0
- package/dist/services/runs/shared/piSpawn.cjs.map +1 -0
- package/dist/services/runs/shared/piSpawn.mjs +2 -0
- package/dist/services/runs/shared/piSpawn.mjs.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.cjs +2 -0
- package/dist/services/runs/shared/runWithConcurrency.cjs.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.d.cts +25 -0
- package/dist/services/runs/shared/runWithConcurrency.d.cts.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.d.mts +25 -0
- package/dist/services/runs/shared/runWithConcurrency.d.mts.map +1 -0
- package/dist/services/runs/shared/runWithConcurrency.mjs +2 -0
- package/dist/services/runs/shared/runWithConcurrency.mjs.map +1 -0
- package/dist/services/runs/shared/runtimeRegistry.cjs +2 -0
- package/dist/services/runs/shared/runtimeRegistry.cjs.map +1 -0
- package/dist/services/runs/shared/runtimeRegistry.mjs +2 -0
- package/dist/services/runs/shared/runtimeRegistry.mjs.map +1 -0
- package/dist/services/runs/shared/structuredOutput.cjs +2 -0
- package/dist/{runs → services/runs}/shared/structuredOutput.cjs.map +1 -1
- package/dist/services/runs/shared/structuredOutput.mjs +2 -0
- package/dist/{runs → services/runs}/shared/structuredOutput.mjs.map +1 -1
- package/dist/services/runs/shared/toolAvailability.cjs +2 -0
- package/dist/services/runs/shared/toolAvailability.cjs.map +1 -0
- package/dist/services/runs/shared/toolAvailability.mjs +2 -0
- package/dist/services/runs/shared/toolAvailability.mjs.map +1 -0
- package/dist/services/runs/shared/toolBudget.cjs +2 -0
- package/dist/services/runs/shared/toolBudget.cjs.map +1 -0
- package/dist/services/runs/shared/toolBudget.mjs +2 -0
- package/dist/services/runs/shared/toolBudget.mjs.map +1 -0
- package/dist/shared/artifacts.cjs.map +1 -1
- package/dist/shared/artifacts.mjs.map +1 -1
- package/dist/shared/atomicJson.cjs +1 -2
- package/dist/shared/atomicJson.mjs +1 -1
- package/dist/shared/atomicJson.mjs.map +1 -1
- package/dist/shared/childTranscript.cjs +1 -5
- package/dist/shared/childTranscript.mjs +1 -4
- package/dist/shared/childTranscript.mjs.map +1 -1
- package/dist/shared/fileSystemRetry.cjs +1 -1
- package/dist/shared/fileSystemRetry.cjs.map +1 -1
- package/dist/shared/fileSystemRetry.mjs +1 -1
- package/dist/shared/fileSystemRetry.mjs.map +1 -1
- package/dist/shared/formatters.cjs.map +1 -1
- package/dist/shared/formatters.mjs.map +1 -1
- package/dist/shared/paths.cjs +1 -1
- package/dist/shared/paths.cjs.map +1 -1
- package/dist/shared/paths.mjs +1 -1
- package/dist/shared/paths.mjs.map +1 -1
- package/dist/shared/pollScheduler.cjs +1 -2
- package/dist/shared/pollScheduler.mjs +1 -1
- package/dist/shared/pollScheduler.mjs.map +1 -1
- package/dist/shared/scopeOwner.cjs +1 -2
- package/dist/shared/scopeOwner.mjs +1 -1
- package/dist/shared/scopeOwner.mjs.map +1 -1
- package/dist/tui/agentCatalog.cjs +1 -1
- package/dist/tui/agentCatalog.cjs.map +1 -1
- package/dist/tui/agentCatalog.mjs +1 -1
- package/dist/tui/agentCatalog.mjs.map +1 -1
- package/dist/tui/agentResourceProjection.cjs +1 -1
- package/dist/tui/agentResourceProjection.cjs.map +1 -1
- package/dist/tui/agentResourceProjection.mjs +1 -1
- package/dist/tui/agentResourceProjection.mjs.map +1 -1
- package/dist/tui/completionNotice.cjs +1 -1
- package/dist/tui/completionNotice.cjs.map +1 -1
- package/dist/tui/completionNotice.mjs +1 -1
- package/dist/tui/completionNotice.mjs.map +1 -1
- package/dist/tui/fleet.cjs +2 -2
- package/dist/tui/fleet.cjs.map +1 -1
- package/dist/tui/fleet.mjs +2 -2
- package/dist/tui/fleet.mjs.map +1 -1
- package/dist/tui/fleetAgentView.cjs.map +1 -1
- package/dist/tui/fleetAgentView.mjs.map +1 -1
- package/dist/tui/fleetStatus.cjs +1 -1
- package/dist/tui/fleetStatus.cjs.map +1 -1
- package/dist/tui/fleetStatus.mjs +1 -1
- package/dist/tui/fleetStatus.mjs.map +1 -1
- package/dist/tui/fleetTranscript.cjs +1 -1
- package/dist/tui/fleetTranscript.cjs.map +1 -1
- package/dist/tui/fleetTranscript.mjs +1 -1
- package/dist/tui/fleetTranscript.mjs.map +1 -1
- package/dist/tui/register.cjs +1 -1
- package/dist/tui/register.cjs.map +1 -1
- package/dist/tui/register.mjs +1 -1
- package/dist/tui/register.mjs.map +1 -1
- package/dist/tui/subagentToolRender.cjs +3 -0
- package/dist/tui/subagentToolRender.cjs.map +1 -0
- package/dist/tui/subagentToolRender.mjs +3 -0
- package/dist/tui/subagentToolRender.mjs.map +1 -0
- package/dist/{types.d.cts → types/index.d.cts} +2 -2
- package/dist/types/index.d.cts.map +1 -0
- package/dist/{types.d.mts → types/index.d.mts} +2 -2
- package/dist/types/index.d.mts.map +1 -0
- package/package.json +23 -16
- package/dist/agents/discovery.cjs +0 -2
- package/dist/agents/discovery.cjs.map +0 -1
- package/dist/agents/discovery.mjs +0 -2
- package/dist/agents/discovery.mjs.map +0 -1
- package/dist/agents/frontmatter.cjs.map +0 -1
- package/dist/agents/frontmatter.mjs.map +0 -1
- package/dist/agents/loader.cjs +0 -2
- package/dist/agents/loader.cjs.map +0 -1
- package/dist/agents/loader.mjs +0 -2
- package/dist/agents/loader.mjs.map +0 -1
- package/dist/agents/memory.cjs +0 -3
- package/dist/agents/memory.cjs.map +0 -1
- package/dist/agents/memory.mjs.map +0 -1
- package/dist/agents/projectRoot.cjs +0 -2
- package/dist/agents/projectRoot.cjs.map +0 -1
- package/dist/agents/projectRoot.mjs +0 -2
- package/dist/agents/projectRoot.mjs.map +0 -1
- package/dist/agents/settings.cjs +0 -2
- package/dist/agents/settings.cjs.map +0 -1
- package/dist/agents/settings.mjs +0 -2
- package/dist/agents/settings.mjs.map +0 -1
- package/dist/agents/skills.cjs +0 -0
- package/dist/agents/skills.cjs.map +0 -1
- package/dist/agents/skills.mjs +0 -5
- package/dist/agents/skills.mjs.map +0 -1
- package/dist/agents/types.d.cts.map +0 -1
- package/dist/agents/types.d.mts.map +0 -1
- package/dist/api/capabilityCeiling.mjs.map +0 -1
- package/dist/api/teamSnapshot.mjs.map +0 -1
- package/dist/env.cjs.map +0 -1
- package/dist/env.d.cts.map +0 -1
- package/dist/env.d.mts.map +0 -1
- package/dist/extension.cjs +0 -2
- package/dist/extension.cjs.map +0 -1
- package/dist/extension.mjs +0 -2
- package/dist/extension.mjs.map +0 -1
- package/dist/intercom/intercomBridge.cjs.map +0 -1
- package/dist/intercom/intercomBridge.mjs.map +0 -1
- package/dist/intercom/nativeTeamChannel.cjs +0 -5
- package/dist/intercom/nativeTeamChannel.cjs.map +0 -1
- package/dist/intercom/nativeTeamChannel.d.cts.map +0 -1
- package/dist/intercom/nativeTeamChannel.d.mts.map +0 -1
- package/dist/intercom/nativeTeamChannel.mjs +0 -5
- package/dist/intercom/nativeTeamChannel.mjs.map +0 -1
- package/dist/intercom/supervisorControlChannel.cjs +0 -2
- package/dist/intercom/supervisorControlChannel.cjs.map +0 -1
- package/dist/intercom/supervisorControlChannel.mjs.map +0 -1
- package/dist/protocol/backgroundWork.cjs +0 -2
- package/dist/protocol/backgroundWork.d.cts.map +0 -1
- package/dist/protocol/backgroundWork.d.mts.map +0 -1
- package/dist/protocol/backgroundWork.mjs +0 -2
- package/dist/protocol/backgroundWork.mjs.map +0 -1
- package/dist/protocol/capabilityCeiling.cjs +0 -2
- package/dist/protocol/capabilityCeiling.cjs.map +0 -1
- package/dist/protocol/capabilityCeiling.d.cts.map +0 -1
- package/dist/protocol/capabilityCeiling.d.mts.map +0 -1
- package/dist/protocol/capabilityCeiling.mjs +0 -2
- package/dist/protocol/capabilityCeiling.mjs.map +0 -1
- package/dist/protocol/delegation.cjs.map +0 -1
- package/dist/protocol/delegation.d.cts.map +0 -1
- package/dist/protocol/delegation.d.mts.map +0 -1
- package/dist/protocol/delegation.mjs.map +0 -1
- package/dist/runs/background/asyncExecution.cjs +0 -2
- package/dist/runs/background/asyncExecution.cjs.map +0 -1
- package/dist/runs/background/asyncExecution.mjs +0 -2
- package/dist/runs/background/asyncExecution.mjs.map +0 -1
- package/dist/runs/background/asyncJobTracker.cjs +0 -2
- package/dist/runs/background/asyncJobTracker.cjs.map +0 -1
- package/dist/runs/background/asyncJobTracker.mjs +0 -2
- package/dist/runs/background/asyncJobTracker.mjs.map +0 -1
- package/dist/runs/background/cliRunnerEntry.cjs.map +0 -1
- package/dist/runs/background/cliRunnerEntry.mjs.map +0 -1
- package/dist/runs/background/completionBatcher.cjs.map +0 -1
- package/dist/runs/background/completionBatcher.mjs +0 -2
- package/dist/runs/background/completionBatcher.mjs.map +0 -1
- package/dist/runs/background/notify.cjs.map +0 -1
- package/dist/runs/background/notify.mjs.map +0 -1
- package/dist/runs/background/parentWatchdog.cjs.map +0 -1
- package/dist/runs/background/parentWatchdog.mjs.map +0 -1
- package/dist/runs/background/processTerminal.cjs +0 -2
- package/dist/runs/background/processTerminal.cjs.map +0 -1
- package/dist/runs/background/processTerminal.mjs +0 -2
- package/dist/runs/background/processTerminal.mjs.map +0 -1
- package/dist/runs/background/resultWatcher.cjs +0 -2
- package/dist/runs/background/resultWatcher.cjs.map +0 -1
- package/dist/runs/background/resultWatcher.mjs +0 -2
- package/dist/runs/background/resultWatcher.mjs.map +0 -1
- package/dist/runs/background/runIdResolver.cjs +0 -2
- package/dist/runs/background/runIdResolver.cjs.map +0 -1
- package/dist/runs/background/runIdResolver.mjs +0 -2
- package/dist/runs/background/runIdResolver.mjs.map +0 -1
- package/dist/runs/background/runnerBootstrap.cjs +0 -2
- package/dist/runs/background/runnerBootstrap.cjs.map +0 -1
- package/dist/runs/background/runnerBootstrap.mjs +0 -2
- package/dist/runs/background/runnerBootstrap.mjs.map +0 -1
- package/dist/runs/background/runnerExecution.cjs +0 -2
- package/dist/runs/background/runnerExecution.cjs.map +0 -1
- package/dist/runs/background/runnerExecution.mjs +0 -2
- package/dist/runs/background/runnerExecution.mjs.map +0 -1
- package/dist/runs/background/runnerReporting.cjs.map +0 -1
- package/dist/runs/background/runnerReporting.mjs.map +0 -1
- package/dist/runs/background/runnerTeamMembership.cjs +0 -2
- package/dist/runs/background/runnerTeamMembership.cjs.map +0 -1
- package/dist/runs/background/runnerTeamMembership.mjs +0 -2
- package/dist/runs/background/runnerTeamMembership.mjs.map +0 -1
- package/dist/runs/background/spawnHandshake.cjs +0 -2
- package/dist/runs/background/spawnHandshake.cjs.map +0 -1
- package/dist/runs/background/spawnHandshake.mjs +0 -2
- package/dist/runs/background/spawnHandshake.mjs.map +0 -1
- package/dist/runs/background/staleRunReconciler.cjs +0 -2
- package/dist/runs/background/staleRunReconciler.cjs.map +0 -1
- package/dist/runs/background/staleRunReconciler.mjs +0 -2
- package/dist/runs/background/staleRunReconciler.mjs.map +0 -1
- package/dist/runs/background/statusReader.cjs +0 -2
- package/dist/runs/background/statusReader.cjs.map +0 -1
- package/dist/runs/background/statusReader.mjs +0 -2
- package/dist/runs/background/statusReader.mjs.map +0 -1
- package/dist/runs/background/statusWriter.cjs +0 -2
- package/dist/runs/background/statusWriter.cjs.map +0 -1
- package/dist/runs/background/statusWriter.mjs +0 -2
- package/dist/runs/background/statusWriter.mjs.map +0 -1
- package/dist/runs/background/subagentWait.cjs +0 -2
- package/dist/runs/background/subagentWait.cjs.map +0 -1
- package/dist/runs/background/subagentWait.mjs +0 -2
- package/dist/runs/background/subagentWait.mjs.map +0 -1
- package/dist/runs/background/terminalPersistence.cjs +0 -2
- package/dist/runs/background/terminalPersistence.cjs.map +0 -1
- package/dist/runs/background/terminalPersistence.mjs.map +0 -1
- package/dist/runs/registry/processGroup.cjs.map +0 -1
- package/dist/runs/registry/processGroup.mjs.map +0 -1
- package/dist/runs/registry/runRegistry.cjs +0 -2
- package/dist/runs/registry/runRegistry.cjs.map +0 -1
- package/dist/runs/registry/runRegistry.mjs +0 -2
- package/dist/runs/registry/runRegistry.mjs.map +0 -1
- package/dist/runs/registry/sessionLifecycle.cjs +0 -2
- package/dist/runs/registry/sessionLifecycle.cjs.map +0 -1
- package/dist/runs/registry/sessionLifecycle.mjs +0 -2
- package/dist/runs/registry/sessionLifecycle.mjs.map +0 -1
- package/dist/runs/registry/suspendedRuns.cjs +0 -4
- package/dist/runs/registry/suspendedRuns.cjs.map +0 -1
- package/dist/runs/registry/suspendedRuns.mjs +0 -4
- package/dist/runs/registry/suspendedRuns.mjs.map +0 -1
- package/dist/runs/sdkRunnerEntry.cjs.map +0 -1
- package/dist/runs/sdkRunnerEntry.mjs.map +0 -1
- package/dist/runs/shared/claudeFableProfile.cjs +0 -4
- package/dist/runs/shared/claudeFableProfile.cjs.map +0 -1
- package/dist/runs/shared/claudeFableProfile.mjs +0 -4
- package/dist/runs/shared/claudeFableProfile.mjs.map +0 -1
- package/dist/runs/shared/depthGuard.cjs +0 -2
- package/dist/runs/shared/depthGuard.cjs.map +0 -1
- package/dist/runs/shared/depthGuard.mjs +0 -2
- package/dist/runs/shared/depthGuard.mjs.map +0 -1
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs +0 -2
- package/dist/runs/shared/mcpDirectToolAllowlist.cjs.map +0 -1
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs +0 -2
- package/dist/runs/shared/mcpDirectToolAllowlist.mjs.map +0 -1
- package/dist/runs/shared/modelFallback.cjs +0 -2
- package/dist/runs/shared/modelFallback.cjs.map +0 -1
- package/dist/runs/shared/modelFallback.mjs +0 -2
- package/dist/runs/shared/modelFallback.mjs.map +0 -1
- package/dist/runs/shared/modelScope.cjs +0 -2
- package/dist/runs/shared/modelScope.cjs.map +0 -1
- package/dist/runs/shared/modelScope.mjs +0 -2
- package/dist/runs/shared/modelScope.mjs.map +0 -1
- package/dist/runs/shared/nestedPath.cjs +0 -2
- package/dist/runs/shared/nestedPath.cjs.map +0 -1
- package/dist/runs/shared/nestedPath.mjs +0 -2
- package/dist/runs/shared/nestedPath.mjs.map +0 -1
- package/dist/runs/shared/piArgs.cjs +0 -2
- package/dist/runs/shared/piArgs.cjs.map +0 -1
- package/dist/runs/shared/piArgs.mjs +0 -2
- package/dist/runs/shared/piArgs.mjs.map +0 -1
- package/dist/runs/shared/piSpawn.cjs +0 -2
- package/dist/runs/shared/piSpawn.cjs.map +0 -1
- package/dist/runs/shared/piSpawn.mjs +0 -2
- package/dist/runs/shared/piSpawn.mjs.map +0 -1
- package/dist/runs/shared/runWithConcurrency.cjs +0 -2
- package/dist/runs/shared/runWithConcurrency.cjs.map +0 -1
- package/dist/runs/shared/runWithConcurrency.mjs +0 -2
- package/dist/runs/shared/runWithConcurrency.mjs.map +0 -1
- package/dist/runs/shared/runtimeRegistry.cjs +0 -2
- package/dist/runs/shared/runtimeRegistry.cjs.map +0 -1
- package/dist/runs/shared/runtimeRegistry.mjs +0 -2
- package/dist/runs/shared/runtimeRegistry.mjs.map +0 -1
- package/dist/runs/shared/structuredOutput.cjs +0 -2
- package/dist/runs/shared/structuredOutput.mjs +0 -2
- package/dist/runs/shared/toolAvailability.cjs +0 -2
- package/dist/runs/shared/toolAvailability.cjs.map +0 -1
- package/dist/runs/shared/toolAvailability.mjs +0 -2
- package/dist/runs/shared/toolAvailability.mjs.map +0 -1
- package/dist/runs/shared/toolBudget.cjs +0 -2
- package/dist/runs/shared/toolBudget.cjs.map +0 -1
- package/dist/runs/shared/toolBudget.mjs +0 -2
- package/dist/runs/shared/toolBudget.mjs.map +0 -1
- package/dist/shared/atomicJson.cjs.map +0 -1
- package/dist/shared/childTranscript.cjs.map +0 -1
- package/dist/shared/pollScheduler.cjs.map +0 -1
- package/dist/shared/scopeOwner.cjs.map +0 -1
- package/dist/slash/chainExpression.cjs.map +0 -1
- package/dist/slash/chainExpression.mjs.map +0 -1
- package/dist/slash/doctor.cjs +0 -5
- package/dist/slash/doctor.mjs +0 -5
- package/dist/slash/doctor.mjs.map +0 -1
- package/dist/slash/slashCommands.cjs.map +0 -1
- package/dist/slash/slashCommands.mjs.map +0 -1
- package/dist/slash/spawnRequestMapping.cjs.map +0 -1
- package/dist/slash/spawnRequestMapping.mjs.map +0 -1
- package/dist/slash/subagentLaunch.cjs +0 -2
- package/dist/slash/subagentLaunch.cjs.map +0 -1
- package/dist/slash/subagentLaunch.mjs +0 -2
- package/dist/slash/subagentLaunch.mjs.map +0 -1
- package/dist/types.d.cts.map +0 -1
- package/dist/types.d.mts.map +0 -1
- /package/dist/{slash → commands/slash}/chainExpression.cjs +0 -0
- /package/dist/{slash → commands/slash}/chainExpression.mjs +0 -0
- /package/dist/{slash → commands/slash}/spawnRequestMapping.cjs +0 -0
- /package/dist/{slash → commands/slash}/spawnRequestMapping.mjs +0 -0
- /package/dist/{protocol → schemas/team}/delegation.cjs +0 -0
- /package/dist/{protocol → schemas/team}/delegation.mjs +0 -0
- /package/dist/{agents → services/agents}/frontmatter.cjs +0 -0
- /package/dist/{intercom → services/intercom}/intercomBridge.cjs +0 -0
- /package/dist/{intercom → services/intercom}/intercomBridge.mjs +0 -0
- /package/dist/{runs → services/runs}/background/completionBatcher.cjs +0 -0
- /package/dist/{runs → services/runs}/background/parentWatchdog.cjs +0 -0
- /package/dist/{runs → services/runs}/background/parentWatchdog.mjs +0 -0
- /package/dist/{runs → services/runs}/registry/processGroup.cjs +0 -0
- /package/dist/{runs → services/runs}/registry/processGroup.mjs +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"asyncJobTracker.cjs","names":["path","currentRunsDir","STATUS_FILE_NAME","fs","parseAsyncRunStatus","DOOM_TEAM_TYPES"],"sources":["../../../src/runs/background/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job aged past `retentionMs` is simply\n * dropped the next time `run()` walks the map. One clock, not one timer per\n * job\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport type { ActivityState } from '../../types.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { IPollScheduler } from '../../shared/pollScheduler.ts';\nimport { parseAsyncRunStatus } from './statusReader.ts';\nimport { STATUS_FILE_NAME } from './statusWriter.ts';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** How long a terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport interface ITrackedAsyncJobs {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every currently tracked job (in-flight, or terminal within its retention window). */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n}\n\nexport interface IAsyncJobTracker extends ITrackedAsyncJobs {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): ITrackedAsyncJobs;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n}\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: ITrackedAsyncJobs, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\n@injectable()\nexport class AsyncJobTracker implements IAsyncJobTracker {\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. See `pollScheduler.ts` and\n * `agents/discovery.ts` for the same pattern.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\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 private readonly sessions = new Map<string, SessionJobs>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n forSession(sessionId: string): ITrackedAsyncJobs {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n const raw = this.readFile(statusPathFor(runId));\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const session = this.session(sessionId);\n const existing = session.jobs.get(runId);\n const read = this.readStatus(runId);\n if (read === undefined) {\n // Not readable yet (the run has not started) or a transient torn/\n // mid-write read (see readStatus's own comment): neither is evidence\n // to overwrite whatever was already known, so an existing entry is\n // left exactly as it was rather than blanked out for one tick.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return false;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) session.terminalAt.delete(runId);\n\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n /** One scheduler tick: refresh every tracked job's status, then evict anything past its retention window. */\n protected run(): boolean {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (this.refresh(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n changed = true;\n }\n if (session.jobs.size === 0) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n session?.jobs.delete(runId);\n session?.terminalAt.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n start(): void {\n this.stop();\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.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{DOOM_TEAM_TYPES as r,init_types as i}from"../../container/types.mjs";import{__decorateMetadata as a,init_decorateMetadata as o}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateMetadata.mjs";import{__decorateParam as s,init_decorateParam as c}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorateParam.mjs";import{currentRunsDir as l,init_paths as u}from"../../shared/paths.mjs";import{STATUS_FILE_NAME as d,init_statusWriter as f}from"./statusWriter.mjs";import{init_statusReader as p,parseAsyncRunStatus as m}from"./statusReader.mjs";import*as h from"node:fs";import*as g from"node:path";import{inject as _,injectable as v}from"inversify";function y(e,t){let n=e.list().map(e=>e.runId).filter(e=>e===t||e.startsWith(t)),r=n.find(e=>e===t);if(r)return r;if(n.length===1)return n[0];throw n.length===0?Error(`No current-session run found for '${t}'.`):Error(`Multiple current-session runs match '${t}'. Use a longer id.`)}function b(e){return g.join(l(),e,d)}function x(e){return typeof e==`object`&&e&&`code`in e?e.code:void 0}function S(e){return x(e)===`ENOENT`}function C(e){return typeof e==`string`&&k.has(e)?e:void 0}var w,T,E,D,O,k,A,j=e((()=>{u(),i(),p(),f(),o(),c(),n(),w=new Set([`complete`,`completed`,`failed`,`paused`,`stopped`]),T=1e4,E=250,D=`async-job-tracker`,O=`__unscoped__`,k=new Set([`starting`,`working`,`tool`,`waiting_for_reply`,`needs_attention`,`finalizing`,`active_long_running`]),A=class{constructor(e){this.scheduler=e}pollIntervalMs=E;retentionMs=T;now(){return Date.now()}readFile(e){try{return h.readFileSync(e,`utf-8`)}catch(e){if(S(e))return;throw e}}sessions=new Map;unregisterPoll;session(e){let t=this.sessions.get(e);return t||(t={jobs:new Map,terminalAt:new Map},this.sessions.set(e,t)),t}forSession(e){if(!e.trim())throw Error(`Pi session identity is required to track subagent runs.`);return{track:t=>this.trackInSession(e,t),untrack:t=>this.untrackInSession(e,t),list:()=>this.listInSession(e),get:t=>this.getInSession(e,t),reset:()=>this.resetSession(e)}}readStatus(e){let t=this.readFile(b(e));if(t===void 0)return;let n=m(t,b(e));if(n.kind!==`ok`)return;let r=n.status;return{agent:r.agent,status:r.state,startedAt:r.startedAt,updatedAt:r.lastUpdate,error:r.error,activityState:C(r.activityState),attentionReason:r.attentionReason,runtime:r.runtime,tokens:r.tokens,currentTool:r.currentTool,toolCount:r.toolCount}}refresh(e,t){let n=this.session(e),r=n.jobs.get(t),i=this.readStatus(t);if(i===void 0)return r||n.jobs.set(t,{runId:t,status:void 0}),!1;let a={runId:t,...i};n.jobs.set(t,a);let o=r?.status!==void 0&&w.has(r.status),s=a.status!==void 0&&w.has(a.status);return s&&!o&&n.terminalAt.set(t,this.now()),s||n.terminalAt.delete(t),!r||r.status!==a.status||r.updatedAt!==a.updatedAt||r.tokens!==a.tokens||r.currentTool!==a.currentTool||r.toolCount!==a.toolCount}run(){let e=!1,t=this.now();for(let[n,r]of this.sessions){for(let t of r.jobs.keys())this.refresh(n,t)&&(e=!0);for(let[n,i]of r.terminalAt)t-i<=this.retentionMs||(r.jobs.delete(n),r.terminalAt.delete(n),e=!0);r.jobs.size===0&&this.sessions.delete(n)}return e}track(e){this.trackInSession(O,e)}untrack(e){this.untrackInSession(O,e)}list(){return this.listInSession(O)}get(e){return this.getInSession(O,e)}reset(){this.resetSession(O)}trackInSession(e,t){this.refresh(e,t)}untrackInSession(e,t){let n=this.sessions.get(e);n?.jobs.delete(t),n?.terminalAt.delete(t),n?.jobs.size===0&&this.sessions.delete(e)}listInSession(e){return[...this.sessions.get(e)?.jobs.values()??[]]}getInSession(e,t){return this.sessions.get(e)?.jobs.get(t)}resetSession(e){this.sessions.delete(e)}start(){this.stop(),this.unregisterPoll=this.scheduler.register({id:D,intervalMs:this.pollIntervalMs,run:()=>this.run()})}stop(){this.unregisterPoll?.(),this.unregisterPoll=void 0,this.sessions.clear()}},A=t([v(),s(0,_(r.PollScheduler)),a(`design:paramtypes`,[Object])],A)}));j();export{A as AsyncJobTracker,w as TERMINAL_ASYNC_JOB_STATES,j as init_asyncJobTracker,y as resolveTrackedRunId};
|
|
2
|
-
//# sourceMappingURL=asyncJobTracker.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"asyncJobTracker.mjs","names":[],"sources":["../../../src/runs/background/asyncJobTracker.ts"],"sourcesContent":["/**\n * Live, in-memory registry of the async runs this session currently cares\n * about, refreshed from `status.json` on a `PollScheduler` tick.\n *\n * WHY THIS IS SMALLER THAN THE PREDECESSOR:\n * `doom-pi-subagents/src/runs/background/asyncJobTracker.ts` is 590 lines\n * doing four different jobs: this registry, TUI widget rendering\n * (`renderWidget`/`widgetRenderKey`/`ctx.ui.requestRender`), forwarding\n * control events and steering notices by byte-range-tailing each run's\n * `events.jsonl`, and projecting nested/parallel-group run trees. Only the\n * registry is ported. The other three have no consumer in this package yet:\n * TUI rendering is `G8` (slash commands + TUI), and nested/chain/parallel run\n * modes are `G7`+ (this package is currently single-run async-only - see\n * `spawnHandshake.ts`'s header). Porting rendering or event-tailing logic\n * with nothing to render to or forward to would be untested, unreachable\n * code; it is deferred to whichever later port actually needs it, not\n * dropped silently - this note is where to look for it.\n *\n * DESIGN PATTERNS:\n * - The in-memory map is a pure CACHE, never authoritative on its own:\n * `status.json` (owned by `CoalescedStatusWriter` elsewhere) is the only\n * source of truth. `track()` reads it synchronously immediately, so a\n * caller that already knows a run's id sees *something* right away rather\n * than waiting out the first poll\n * - Restart survivability follows from the point above rather than a second\n * persistence layer: this package has not built session-scoped run\n * ownership yet (which ids \"belong\" to a restarted session is not\n * something this module can determine on its own - see the AVOID entry),\n * so recovery is the caller re-`track()`-ing whatever ids it still cares\n * about from ITS OWN durable record. The very next poll tick repopulates\n * that job's status straight from disk. There is no separate store to keep\n * in sync, and therefore no window where memory could diverge from disk\n * that a restart would not immediately close\n * - Registered with `PollScheduler`, not a private timer: this polls\n * potentially many jobs' status files on an unbounded, periodic cadence\n * with no low-latency requirement - the exact shape `PollScheduler` exists\n * for. Contrast `completionBatcher.ts`/`spawnHandshake.ts`, which own\n * their own timers because they are short-lived, single-shot, and need\n * sub-200ms responsiveness `PollScheduler`'s floor cannot give them; this\n * registry needs neither\n * - Retention-then-eviction happens on the SAME poll tick as the status\n * refresh, rather than the predecessor's per-job `setTimeout`\n * (`scheduleCleanup`): a terminal job aged past `retentionMs` is simply\n * dropped the next time `run()` walks the map. One clock, not one timer per\n * job\n *\n * AVOID:\n * - Scanning `currentRunsDir()` to guess which run ids \"belong\" to this session on\n * startup. This package has no session-scoped run ownership concept yet;\n * inventing one here to answer \"what should I restore\" would be a guess,\n * not a fact this module has standing to assert. Restoration is the\n * caller's job (see the design note above)\n * - Growing `terminalStates` independently of `staleRunReconciler.ts`'s own\n * (unexported) copy; both must treat the same state strings as terminal,\n * or a job could be evicted here while the reconciler still thinks it is\n * in flight, or vice versa\n *\n * `activityState`/`attentionReason` (added for `subagentWait.ts`):\n * `DeliverableGuard.evaluate()` computes `activityState: 'needs_attention'`\n * and a `reason` (e.g. `'missing-deliverable'`), but returns it rather than\n * writing it anywhere - it has no standing to touch `status.json` itself (see\n * that module's header). Something upstream of this tracker - the run's own\n * runner main loop, not yet built - is responsible for folding that result\n * into `status.json` through `CoalescedStatusWriter`. This tracker's job is\n * only to read whatever ends up there, the same as every other field: it does\n * not compute attention, detect it, or decide when a run needs it.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { inject, injectable } from 'inversify';\n\nimport type { ActivityState } from '../../types.ts';\nimport { currentRunsDir } from '../../shared/paths.ts';\nimport { DOOM_TEAM_TYPES } from '../../container/types.ts';\nimport type { IPollScheduler } from '../../shared/pollScheduler.ts';\nimport { parseAsyncRunStatus } from './statusReader.ts';\nimport { STATUS_FILE_NAME } from './statusWriter.ts';\n\n/**\n * States that mean \"already finished\"; matches `staleRunReconciler.ts`'s\n * own (unexported) set. Exported so a caller outside this module (the slash\n * layer's poll-until-terminal helper) can recognize the same terminal set\n * `TrackedAsyncJob.status` uses, rather than declaring a third copy of this\n * literal.\n */\nexport const TERMINAL_ASYNC_JOB_STATES = new Set(['complete', 'completed', 'failed', 'paused', 'stopped']);\n\n/** How long a terminal job stays queryable before eviction. */\nconst DEFAULT_RETENTION_MS = 10_000;\n/** How often tracked jobs' status is refreshed from disk. */\nconst DEFAULT_POLL_INTERVAL_MS = 250;\n\nconst POLL_SUBSCRIBER_ID = 'async-job-tracker';\n\n/**\n * The minimum shape this module reads. A run's actual `status.json` carries\n * far more (this package has no shared status type yet - see\n * `staleRunReconciler.ts`'s `ReconcilableStatus` for the same caveat);\n * everything else is simply not read here.\n */\nexport interface TrackedAsyncJob {\n runId: string;\n /** Named agent executing this run. Absent until the first readable status. */\n agent?: string;\n /** `status.json`'s `state` field. `undefined` until the first successful read. */\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n /** `status.json`'s `activityState` field, e.g. `'needs_attention'`. See the module doc. */\n activityState?: ActivityState;\n /** `status.json`'s `reason` field, present alongside `activityState` (e.g. `'missing-deliverable'`). */\n attentionReason?: string;\n /** Which runtime is executing this run. Absent means the default `pi` path. */\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nexport interface ITrackedAsyncJobs {\n /**\n * Start tracking `runId`: reads its current status synchronously (so a\n * caller sees something immediately) and adds it to the poll rotation.\n * Calling this again for an id already tracked simply re-reads it now,\n * the same idempotent-refresh idiom as `CoalescedStatusWriter.open()`.\n */\n track(runId: string): void;\n /** Stop tracking `runId` immediately, without waiting out its retention window. */\n untrack(runId: string): void;\n /** Every currently tracked job (in-flight, or terminal within its retention window). */\n list(): TrackedAsyncJob[];\n get(runId: string): TrackedAsyncJob | undefined;\n /** Drop every tracked job. */\n reset(): void;\n}\n\nexport interface IAsyncJobTracker extends ITrackedAsyncJobs {\n /** Return the isolated run collection owned by one Pi session context. */\n forSession(sessionId: string): ITrackedAsyncJobs;\n /** Register with `PollScheduler`. Call once; a second call is a reset, same as `ResultWatcher.start()`. */\n start(): void;\n /** Unregister from `PollScheduler` and drop every tracked job. */\n stop(): void;\n}\n\n/** Resolve an exact id or unique prefix only inside one Pi session's run collection. */\nexport function resolveTrackedRunId(jobs: ITrackedAsyncJobs, id: string): string {\n const matches = jobs\n .list()\n .map((job) => job.runId)\n .filter((runId) => runId === id || runId.startsWith(id));\n const exact = matches.find((runId) => runId === id);\n if (exact) return exact;\n if (matches.length === 1) return matches[0]!;\n if (matches.length === 0) throw new Error(`No current-session run found for '${id}'.`);\n throw new Error(`Multiple current-session runs match '${id}'. Use a longer id.`);\n}\n\ninterface SessionJobs {\n jobs: Map<string, TrackedAsyncJob>;\n terminalAt: Map<string, number>;\n}\n\nconst UNSCOPED_SESSION = '__unscoped__';\n\nfunction statusPathFor(runId: string): string {\n return path.join(currentRunsDir(), runId, STATUS_FILE_NAME);\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null && 'code' in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return errorCode(error) === 'ENOENT';\n}\n\ninterface ReadStatusResult {\n agent: string;\n status: string | undefined;\n startedAt?: number;\n updatedAt?: number;\n error?: string;\n activityState?: ActivityState;\n attentionReason?: string;\n runtime?: string;\n tokens?: number;\n currentTool?: string;\n toolCount?: number;\n}\n\nconst VALID_ACTIVITY_STATES: ReadonlySet<string> = new Set<ActivityState>([\n 'starting',\n 'working',\n 'tool',\n 'waiting_for_reply',\n 'needs_attention',\n 'finalizing',\n 'active_long_running',\n]);\n\nfunction asActivityState(value: unknown): ActivityState | undefined {\n return typeof value === 'string' && VALID_ACTIVITY_STATES.has(value) ? (value as ActivityState) : undefined;\n}\n\n@injectable()\nexport class AsyncJobTracker implements IAsyncJobTracker {\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. See `pollScheduler.ts` and\n * `agents/discovery.ts` for the same pattern.\n */\n protected readonly pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS;\n protected readonly retentionMs: number = DEFAULT_RETENTION_MS;\n\n protected now(): number {\n return Date.now();\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 private readonly sessions = new Map<string, SessionJobs>();\n private unregisterPoll: (() => void) | undefined;\n\n private session(sessionId: string): SessionJobs {\n let session = this.sessions.get(sessionId);\n if (!session) {\n session = { jobs: new Map(), terminalAt: new Map() };\n this.sessions.set(sessionId, session);\n }\n return session;\n }\n\n forSession(sessionId: string): ITrackedAsyncJobs {\n if (!sessionId.trim()) throw new Error('Pi session identity is required to track subagent runs.');\n return {\n track: (runId) => this.trackInSession(sessionId, runId),\n untrack: (runId) => this.untrackInSession(sessionId, runId),\n list: () => this.listInSession(sessionId),\n get: (runId) => this.getInSession(sessionId, runId),\n reset: () => this.resetSession(sessionId),\n };\n }\n\n private readStatus(runId: string): ReadStatusResult | undefined {\n const raw = this.readFile(statusPathFor(runId));\n if (raw === undefined) return undefined;\n const result = parseAsyncRunStatus(raw, statusPathFor(runId));\n if (result.kind !== 'ok') return undefined;\n const status = result.status;\n return {\n agent: status.agent,\n status: status.state,\n startedAt: status.startedAt,\n updatedAt: status.lastUpdate,\n error: status.error,\n activityState: asActivityState(status.activityState),\n attentionReason: status.attentionReason,\n runtime: status.runtime,\n tokens: status.tokens,\n currentTool: status.currentTool,\n toolCount: status.toolCount,\n };\n }\n\n private refresh(sessionId: string, runId: string): boolean {\n const session = this.session(sessionId);\n const existing = session.jobs.get(runId);\n const read = this.readStatus(runId);\n if (read === undefined) {\n // Not readable yet (the run has not started) or a transient torn/\n // mid-write read (see readStatus's own comment): neither is evidence\n // to overwrite whatever was already known, so an existing entry is\n // left exactly as it was rather than blanked out for one tick.\n if (existing) return false;\n session.jobs.set(runId, { runId, status: undefined });\n return false;\n }\n\n const job: TrackedAsyncJob = { runId, ...read };\n session.jobs.set(runId, job);\n\n const wasTerminal = existing?.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(existing.status);\n const isTerminal = job.status !== undefined && TERMINAL_ASYNC_JOB_STATES.has(job.status);\n if (isTerminal && !wasTerminal) session.terminalAt.set(runId, this.now());\n if (!isTerminal) session.terminalAt.delete(runId);\n\n return (\n !existing ||\n existing.status !== job.status ||\n existing.updatedAt !== job.updatedAt ||\n existing.tokens !== job.tokens ||\n existing.currentTool !== job.currentTool ||\n existing.toolCount !== job.toolCount\n );\n }\n\n /** One scheduler tick: refresh every tracked job's status, then evict anything past its retention window. */\n protected run(): boolean {\n let changed = false;\n const now = this.now();\n for (const [sessionId, session] of this.sessions) {\n for (const runId of session.jobs.keys()) {\n if (this.refresh(sessionId, runId)) changed = true;\n }\n for (const [runId, terminatedAt] of session.terminalAt) {\n if (now - terminatedAt <= this.retentionMs) continue;\n session.jobs.delete(runId);\n session.terminalAt.delete(runId);\n changed = true;\n }\n if (session.jobs.size === 0) this.sessions.delete(sessionId);\n }\n\n return changed;\n }\n\n track(runId: string): void {\n this.trackInSession(UNSCOPED_SESSION, runId);\n }\n\n untrack(runId: string): void {\n this.untrackInSession(UNSCOPED_SESSION, runId);\n }\n\n list(): TrackedAsyncJob[] {\n return this.listInSession(UNSCOPED_SESSION);\n }\n\n get(runId: string): TrackedAsyncJob | undefined {\n return this.getInSession(UNSCOPED_SESSION, runId);\n }\n\n reset(): void {\n this.resetSession(UNSCOPED_SESSION);\n }\n\n private trackInSession(sessionId: string, runId: string): void {\n this.refresh(sessionId, runId);\n }\n\n private untrackInSession(sessionId: string, runId: string): void {\n const session = this.sessions.get(sessionId);\n session?.jobs.delete(runId);\n session?.terminalAt.delete(runId);\n if (session?.jobs.size === 0) this.sessions.delete(sessionId);\n }\n\n private listInSession(sessionId: string): TrackedAsyncJob[] {\n return [...(this.sessions.get(sessionId)?.jobs.values() ?? [])];\n }\n\n private getInSession(sessionId: string, runId: string): TrackedAsyncJob | undefined {\n return this.sessions.get(sessionId)?.jobs.get(runId);\n }\n\n private resetSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n start(): void {\n this.stop();\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.unregisterPoll?.();\n this.unregisterPoll = undefined;\n this.sessions.clear();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cliRunnerEntry.cjs","names":["fs","currentRunConfigPath","SUBAGENT_RUN_ID_ENV","adoptSessionScopeFromEnv","SUBAGENT_ROOT_SESSION_ENV","CoalescedStatusWriter","readAsyncRunStatus","CLAUDE_FABLE_PROFILE","startParentWatchdog","parseClaudeFableOutput"],"sources":["../../../src/runs/background/cliRunnerEntry.ts"],"sourcesContent":["/**\n * The detached child process for an external agent CLI.\n *\n * Counterpart to `sdkRunnerEntry.ts`, which does the same job for the\n * in-process Pi runtime. Both are spawned by `AsyncSubagentSpawner`, both write\n * the same `status.json` and the same terminal result, and both signal the same\n * handshake - so `AsyncJobTracker`, `ResultWatcher`, the fleet view and\n * folded `subagent` wait/status handling cannot tell them apart, which is the point.\n *\n * WHAT AN EXTERNAL CHILD DOES NOT GET, STATED PLAINLY:\n * No intercom, no steering, no structured output. Those\n * are delivered by `subagentPromptRuntime.ts`, which Pi loads in-process into\n * a Pi child; an arbitrary CLI has nowhere to load it. Layer tool exclusions\n * are therefore best effort for external runtimes. An external child is a\n * one-shot worker: it receives a prompt and returns output. Stop and interrupt\n * still work, because they are process-group signals rather than cooperative\n * file requests.\n *\n * WHY THE OUTPUT IS RECORDED, NOT PARSED:\n * A generic runner cannot know what a given CLI's output means. Trailing stdout\n * is the result; anything more specific would either be wrong for a CLI nobody\n * tested or lock the config to the two that were.\n *\n * AVOID:\n * - Vendor-specific parsing here. If a runtime needs it, that is a different\n * module and a deliberate decision\n */\n\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../../shared/paths.ts';\nimport { writeAtomicJson, writePrivateAtomicJson } from '../../shared/atomicJson.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { startParentWatchdog } from './parentWatchdog.ts';\nimport type { CliLaunchConfig } from './cliLaunchConfig.ts';\nimport { readAsyncRunStatus } from './statusReader.ts';\nimport { CoalescedStatusWriter } from './statusWriter.ts';\nimport {\n CLAUDE_FABLE_PROFILE,\n cleanupClaudeFableLaunch,\n parseClaudeFableOutput,\n} from '../shared/claudeFableProfile.ts';\n\n/** How much trailing stdout is kept as the run's result. */\nconst MAX_RESULT_BYTES = 64 * 1024;\n/** How much stderr is kept for a failure message. */\nconst MAX_STDERR_BYTES = 8 * 1024;\nconst PARENT_LOST_MESSAGE = 'Parent process was lost.';\nconst FORCE_KILL_SIGNAL: NodeJS.Signals = 'SIGKILL';\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readLaunchConfig(runId: string): CliLaunchConfig {\n const parsed: unknown = JSON.parse(fs.readFileSync(currentRunConfigPath(runId), 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || typeof parsed.command !== 'string') {\n throw new Error(`CLI launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as CliLaunchConfig;\n}\n\n/** Keep only a UTF-8-safe trailing value within `limit` bytes, so a chatty CLI cannot exhaust memory. */\nfunction appendBounded(buffer: string, chunk: string, limit: number): string {\n let combined = buffer + chunk;\n while (Buffer.byteLength(combined, 'utf8') > limit && combined.length > 0) {\n const excess = Buffer.byteLength(combined, 'utf8') - limit;\n combined = combined.slice(Math.max(1, Math.ceil(excess / 2)));\n }\n return combined;\n}\n\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nasync function run(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-cli-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: CliLaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n // The vendor process emits its own spawn event before the ready handshake\n // is written below. Reading the config alone is not sufficient evidence\n // that the configured executable can start.\n } catch (error) {\n if (config?.handshakePath && !fs.existsSync(config.handshakePath)) {\n writeAtomicJson(config.handshakePath, { state: 'error', error: errorMessage(error) });\n }\n throw error;\n }\n\n const statusWriter = new CoalescedStatusWriter<AsyncRunStatus>();\n const now = Date.now();\n await childTelemetry?.recordEvent('doom_team.child_started', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n outcome: 'started',\n });\n statusWriter.open(\n runId,\n readAsyncRunStatus(runId) ?? {\n version: 1,\n runId,\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n cwd: config.cwd,\n runtime: config.runtime,\n state: 'queued',\n startedAt: now,\n lastUpdate: now,\n },\n );\n\n const profileLaunch = config.profile === CLAUDE_FABLE_PROFILE;\n if (config.profile !== undefined && !profileLaunch) throw new Error('Untrusted external profile.');\n if (profileLaunch && (!config.stdinPath || !config.profileResultPath)) {\n throw new Error('Fable profile launch config is incomplete.');\n }\n const stdinFd = profileLaunch ? fs.openSync(config.stdinPath!, 'r') : undefined;\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(config.command, config.args, {\n cwd: config.cwd,\n env: profileLaunch ? config.env : { ...process.env, ...config.env },\n stdio: [stdinFd ?? 'ignore', 'pipe', 'pipe'],\n // Its own group, so the watchdog below can take the CLI and anything it\n // spawned down together.\n detached: true,\n windowsHide: true,\n });\n } catch (error) {\n if (profileLaunch) {\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n }\n throw error;\n } finally {\n if (stdinFd !== undefined) fs.closeSync(stdinFd);\n }\n const cleanupProfile = (removeResult: boolean): void => {\n if (!profileLaunch) return;\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (removeResult && config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n };\n\n let requestedSignal: NodeJS.Signals | undefined;\n const stopVendor = (signal: NodeJS.Signals): void => {\n requestedSignal = signal;\n if (!child.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch (error) {\n process.stderr.write(`[doom-team cli runner] Could not signal vendor process: ${errorMessage(error)}\\n`);\n }\n };\n process.once('SIGTERM', stopVendor);\n process.once('SIGINT', stopVendor);\n\n try {\n await new Promise<void>((resolve, reject) => {\n child.once('spawn', resolve);\n child.once('error', reject);\n });\n writeAtomicJson(config.handshakePath, { state: 'ready' });\n statusWriter.updateSync((status) => {\n status.state = 'running';\n status.activityState = 'working';\n status.lastUpdate = Date.now();\n });\n } catch (error) {\n const message = errorMessage(error);\n writeAtomicJson(config.handshakePath, { state: 'error', error: message });\n statusWriter.updateSync((status) => {\n status.state = 'failed';\n status.error = message;\n status.lastUpdate = Date.now();\n status.endedAt = Date.now();\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: profileLaunch ? 'Fable Claude launch failed.' : message,\n });\n statusWriter.close();\n cleanupProfile(true);\n await finishTelemetry(\n 'doom_team.child_failed',\n { runtime: config.runtime, 'agent.name': config.agent, duration_ms: Date.now() - startedAt, outcome: 'failed' },\n error,\n );\n process.exit(1);\n }\n\n // A SIGKILLed parent cannot signal us, so terminate the vendor tree and\n // persist a terminal record before leaving when the watchdog can observe it.\n const stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n stopVendor(FORCE_KILL_SIGNAL);\n const endedAt = Date.now();\n statusWriter.updateSync((status) => {\n status.state = 'stopped';\n status.error = PARENT_LOST_MESSAGE;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: PARENT_LOST_MESSAGE,\n signal: FORCE_KILL_SIGNAL,\n });\n statusWriter.close();\n cleanupProfile(true);\n void finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n let stdout = '';\n let stderr = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout = appendBounded(stdout, chunk.toString('utf8'), MAX_RESULT_BYTES);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr = appendBounded(stderr, chunk.toString('utf8'), MAX_STDERR_BYTES);\n });\n\n const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {\n child.on('error', (error) => resolve({ code: null, signal: null, error }));\n child.on('close', (code, signal) => resolve({ code, signal }));\n });\n\n stopWatchdog();\n\n const stopped = requestedSignal !== undefined;\n let profileError: Error | undefined;\n if (profileLaunch && !stopped && exit.error === undefined && exit.code === 0) {\n try {\n const parsed = parseClaudeFableOutput(stdout);\n writePrivateAtomicJson(config.profileResultPath!, { text: parsed.text, outputBytes: parsed.outputBytes });\n } catch (error) {\n profileError = error instanceof Error ? error : new Error(String(error));\n }\n }\n const success = !stopped && exit.error === undefined && exit.code === 0 && profileError === undefined;\n const failureDetail = profileLaunch\n ? (profileError?.message ?? 'Fable Claude launch failed.')\n : stderr.trim() || `${config.runtime} exited with ${exit.signal ?? `code ${exit.code ?? 'unknown'}`}.`;\n const summary = stopped\n ? `Stopped by ${requestedSignal}.`\n : success\n ? profileLaunch\n ? 'Fable profile completed.'\n : stdout.trim()\n : (exit.error?.message ?? failureDetail);\n const endedAt = Date.now();\n\n statusWriter.updateSync((status) => {\n status.state = stopped ? 'stopped' : success ? 'completed' : 'failed';\n status.activityState = 'finalizing';\n status.summary = summary;\n if (!success) status.error = summary;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success,\n summary,\n ...(exit.code === null ? {} : { exitCode: exit.code }),\n ...(exit.signal ? { signal: exit.signal } : {}),\n });\n\n statusWriter.close();\n cleanupProfile(!success);\n await finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n 'runner.stdout_bytes': Buffer.byteLength(stdout, 'utf8'),\n 'runner.stderr_bytes': Buffer.byteLength(stderr, 'utf8'),\n 'runner.exit_code': exit.code ?? 0,\n outcome: stopped ? 'stopped' : success ? 'completed' : 'failed',\n });\n // The runner exit code reports transport health. Task success or failure is\n // encoded durably in status and result data before this process exits.\n process.exit(0);\n}\n\nvoid run().catch(async (error: unknown) => {\n await finishTelemetry('doom_team.child_failed', { outcome: 'failed' }, error);\n // Keep stdout reserved for vendor output. Stderr is captured in the run's\n // runner log and makes a transport failure diagnosable by the parent.\n process.stderr.write(`[doom-team cli runner] ${errorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cliRunnerEntry.mjs","names":[],"sources":["../../../src/runs/background/cliRunnerEntry.ts"],"sourcesContent":["/**\n * The detached child process for an external agent CLI.\n *\n * Counterpart to `sdkRunnerEntry.ts`, which does the same job for the\n * in-process Pi runtime. Both are spawned by `AsyncSubagentSpawner`, both write\n * the same `status.json` and the same terminal result, and both signal the same\n * handshake - so `AsyncJobTracker`, `ResultWatcher`, the fleet view and\n * folded `subagent` wait/status handling cannot tell them apart, which is the point.\n *\n * WHAT AN EXTERNAL CHILD DOES NOT GET, STATED PLAINLY:\n * No intercom, no steering, no structured output. Those\n * are delivered by `subagentPromptRuntime.ts`, which Pi loads in-process into\n * a Pi child; an arbitrary CLI has nowhere to load it. Layer tool exclusions\n * are therefore best effort for external runtimes. An external child is a\n * one-shot worker: it receives a prompt and returns output. Stop and interrupt\n * still work, because they are process-group signals rather than cooperative\n * file requests.\n *\n * WHY THE OUTPUT IS RECORDED, NOT PARSED:\n * A generic runner cannot know what a given CLI's output means. Trailing stdout\n * is the result; anything more specific would either be wrong for a CLI nobody\n * tested or lock the config to the two that were.\n *\n * AVOID:\n * - Vendor-specific parsing here. If a runtime needs it, that is a different\n * module and a deliberate decision\n */\n\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport { createDoomTelemetry, type DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { SUBAGENT_ROOT_SESSION_ENV, SUBAGENT_RUN_ID_ENV } from '../../env.ts';\nimport { adoptSessionScopeFromEnv, currentRunConfigPath } from '../../shared/paths.ts';\nimport { writeAtomicJson, writePrivateAtomicJson } from '../../shared/atomicJson.ts';\nimport type { AsyncRunStatus } from './asyncExecution.ts';\nimport { startParentWatchdog } from './parentWatchdog.ts';\nimport type { CliLaunchConfig } from './cliLaunchConfig.ts';\nimport { readAsyncRunStatus } from './statusReader.ts';\nimport { CoalescedStatusWriter } from './statusWriter.ts';\nimport {\n CLAUDE_FABLE_PROFILE,\n cleanupClaudeFableLaunch,\n parseClaudeFableOutput,\n} from '../shared/claudeFableProfile.ts';\n\n/** How much trailing stdout is kept as the run's result. */\nconst MAX_RESULT_BYTES = 64 * 1024;\n/** How much stderr is kept for a failure message. */\nconst MAX_STDERR_BYTES = 8 * 1024;\nconst PARENT_LOST_MESSAGE = 'Parent process was lost.';\nconst FORCE_KILL_SIGNAL: NodeJS.Signals = 'SIGKILL';\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readLaunchConfig(runId: string): CliLaunchConfig {\n const parsed: unknown = JSON.parse(fs.readFileSync(currentRunConfigPath(runId), 'utf8'));\n if (!isRecord(parsed) || parsed.runId !== runId || typeof parsed.command !== 'string') {\n throw new Error(`CLI launch config for run '${runId}' is invalid.`);\n }\n return parsed as unknown as CliLaunchConfig;\n}\n\n/** Keep only a UTF-8-safe trailing value within `limit` bytes, so a chatty CLI cannot exhaust memory. */\nfunction appendBounded(buffer: string, chunk: string, limit: number): string {\n let combined = buffer + chunk;\n while (Buffer.byteLength(combined, 'utf8') > limit && combined.length > 0) {\n const excess = Buffer.byteLength(combined, 'utf8') - limit;\n combined = combined.slice(Math.max(1, Math.ceil(excess / 2)));\n }\n return combined;\n}\n\nlet childTelemetry: DoomTelemetry | undefined;\n\nasync function finishTelemetry(event: string, attributes: Record<string, unknown>, error?: unknown): Promise<void> {\n const telemetry = childTelemetry;\n childTelemetry = undefined;\n if (!telemetry) return;\n if (error === undefined) await telemetry.recordEvent(event, attributes);\n else await telemetry.recordError(event, error, attributes);\n await telemetry.flush();\n await telemetry.shutdown();\n}\n\nasync function run(): Promise<void> {\n const startedAt = Date.now();\n childTelemetry = createDoomTelemetry({\n serviceName: 'doom-team-cli-child',\n packageName: '@agimon-ai/doompi-team',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();\n if (!runId) throw new Error(`${SUBAGENT_RUN_ID_ENV} is required.`);\n if (!adoptSessionScopeFromEnv()) {\n throw new Error(`${SUBAGENT_ROOT_SESSION_ENV} is required: a child cannot resolve its session scope without it.`);\n }\n\n let config: CliLaunchConfig | undefined;\n try {\n config = readLaunchConfig(runId);\n // The vendor process emits its own spawn event before the ready handshake\n // is written below. Reading the config alone is not sufficient evidence\n // that the configured executable can start.\n } catch (error) {\n if (config?.handshakePath && !fs.existsSync(config.handshakePath)) {\n writeAtomicJson(config.handshakePath, { state: 'error', error: errorMessage(error) });\n }\n throw error;\n }\n\n const statusWriter = new CoalescedStatusWriter<AsyncRunStatus>();\n const now = Date.now();\n await childTelemetry?.recordEvent('doom_team.child_started', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n outcome: 'started',\n });\n statusWriter.open(\n runId,\n readAsyncRunStatus(runId) ?? {\n version: 1,\n runId,\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n cwd: config.cwd,\n runtime: config.runtime,\n state: 'queued',\n startedAt: now,\n lastUpdate: now,\n },\n );\n\n const profileLaunch = config.profile === CLAUDE_FABLE_PROFILE;\n if (config.profile !== undefined && !profileLaunch) throw new Error('Untrusted external profile.');\n if (profileLaunch && (!config.stdinPath || !config.profileResultPath)) {\n throw new Error('Fable profile launch config is incomplete.');\n }\n const stdinFd = profileLaunch ? fs.openSync(config.stdinPath!, 'r') : undefined;\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(config.command, config.args, {\n cwd: config.cwd,\n env: profileLaunch ? config.env : { ...process.env, ...config.env },\n stdio: [stdinFd ?? 'ignore', 'pipe', 'pipe'],\n // Its own group, so the watchdog below can take the CLI and anything it\n // spawned down together.\n detached: true,\n windowsHide: true,\n });\n } catch (error) {\n if (profileLaunch) {\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n }\n throw error;\n } finally {\n if (stdinFd !== undefined) fs.closeSync(stdinFd);\n }\n const cleanupProfile = (removeResult: boolean): void => {\n if (!profileLaunch) return;\n cleanupClaudeFableLaunch({ cleanupPaths: config.cleanupPaths ?? [] });\n if (removeResult && config.profileResultPath) fs.rmSync(config.profileResultPath, { force: true });\n };\n\n let requestedSignal: NodeJS.Signals | undefined;\n const stopVendor = (signal: NodeJS.Signals): void => {\n requestedSignal = signal;\n if (!child.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch (error) {\n process.stderr.write(`[doom-team cli runner] Could not signal vendor process: ${errorMessage(error)}\\n`);\n }\n };\n process.once('SIGTERM', stopVendor);\n process.once('SIGINT', stopVendor);\n\n try {\n await new Promise<void>((resolve, reject) => {\n child.once('spawn', resolve);\n child.once('error', reject);\n });\n writeAtomicJson(config.handshakePath, { state: 'ready' });\n statusWriter.updateSync((status) => {\n status.state = 'running';\n status.activityState = 'working';\n status.lastUpdate = Date.now();\n });\n } catch (error) {\n const message = errorMessage(error);\n writeAtomicJson(config.handshakePath, { state: 'error', error: message });\n statusWriter.updateSync((status) => {\n status.state = 'failed';\n status.error = message;\n status.lastUpdate = Date.now();\n status.endedAt = Date.now();\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: profileLaunch ? 'Fable Claude launch failed.' : message,\n });\n statusWriter.close();\n cleanupProfile(true);\n await finishTelemetry(\n 'doom_team.child_failed',\n { runtime: config.runtime, 'agent.name': config.agent, duration_ms: Date.now() - startedAt, outcome: 'failed' },\n error,\n );\n process.exit(1);\n }\n\n // A SIGKILLed parent cannot signal us, so terminate the vendor tree and\n // persist a terminal record before leaving when the watchdog can observe it.\n const stopWatchdog = startParentWatchdog({\n onParentLost: () => {\n stopVendor(FORCE_KILL_SIGNAL);\n const endedAt = Date.now();\n statusWriter.updateSync((status) => {\n status.state = 'stopped';\n status.error = PARENT_LOST_MESSAGE;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success: false,\n summary: PARENT_LOST_MESSAGE,\n signal: FORCE_KILL_SIGNAL,\n });\n statusWriter.close();\n cleanupProfile(true);\n void finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n outcome: 'parent_lost',\n }).finally(() => process.exit(0));\n },\n });\n\n let stdout = '';\n let stderr = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout = appendBounded(stdout, chunk.toString('utf8'), MAX_RESULT_BYTES);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr = appendBounded(stderr, chunk.toString('utf8'), MAX_STDERR_BYTES);\n });\n\n const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {\n child.on('error', (error) => resolve({ code: null, signal: null, error }));\n child.on('close', (code, signal) => resolve({ code, signal }));\n });\n\n stopWatchdog();\n\n const stopped = requestedSignal !== undefined;\n let profileError: Error | undefined;\n if (profileLaunch && !stopped && exit.error === undefined && exit.code === 0) {\n try {\n const parsed = parseClaudeFableOutput(stdout);\n writePrivateAtomicJson(config.profileResultPath!, { text: parsed.text, outputBytes: parsed.outputBytes });\n } catch (error) {\n profileError = error instanceof Error ? error : new Error(String(error));\n }\n }\n const success = !stopped && exit.error === undefined && exit.code === 0 && profileError === undefined;\n const failureDetail = profileLaunch\n ? (profileError?.message ?? 'Fable Claude launch failed.')\n : stderr.trim() || `${config.runtime} exited with ${exit.signal ?? `code ${exit.code ?? 'unknown'}`}.`;\n const summary = stopped\n ? `Stopped by ${requestedSignal}.`\n : success\n ? profileLaunch\n ? 'Fable profile completed.'\n : stdout.trim()\n : (exit.error?.message ?? failureDetail);\n const endedAt = Date.now();\n\n statusWriter.updateSync((status) => {\n status.state = stopped ? 'stopped' : success ? 'completed' : 'failed';\n status.activityState = 'finalizing';\n status.summary = summary;\n if (!success) status.error = summary;\n status.lastUpdate = endedAt;\n status.endedAt = endedAt;\n });\n writeAtomicJson(config.resultPath, {\n runId,\n ...(config.internal ? { internal: true } : {}),\n ...(config.operationId ? { operationId: config.operationId } : {}),\n agent: config.agent,\n runtime: config.runtime,\n success,\n summary,\n ...(exit.code === null ? {} : { exitCode: exit.code }),\n ...(exit.signal ? { signal: exit.signal } : {}),\n });\n\n statusWriter.close();\n cleanupProfile(!success);\n await finishTelemetry('doom_team.child_finished', {\n runtime: config.runtime,\n 'agent.name': config.agent,\n duration_ms: Date.now() - startedAt,\n 'runner.stdout_bytes': Buffer.byteLength(stdout, 'utf8'),\n 'runner.stderr_bytes': Buffer.byteLength(stderr, 'utf8'),\n 'runner.exit_code': exit.code ?? 0,\n outcome: stopped ? 'stopped' : success ? 'completed' : 'failed',\n });\n // The runner exit code reports transport health. Task success or failure is\n // encoded durably in status and result data before this process exits.\n process.exit(0);\n}\n\nvoid run().catch(async (error: unknown) => {\n await finishTelemetry('doom_team.child_failed', { outcome: 'failed' }, error);\n // Keep stdout reserved for vendor output. Stderr is captured in the run's\n // runner log and makes a transport failure diagnosable by the parent.\n process.stderr.write(`[doom-team cli runner] ${errorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"completionBatcher.cjs","names":[],"sources":["../../../src/runs/background/completionBatcher.ts"],"sourcesContent":["/**\n * Debounced grouping for successful completion notifications.\n *\n * WHY THIS EXISTS:\n * A fan-out run's children tend to finish within milliseconds of each other.\n * Notifying once per child turns a five-way parallel run into five separate\n * interruptions for the operator instead of one. This holds a *successful*\n * completion briefly so siblings finishing in the same short window arrive as\n * a single grouped message.\n *\n * DESIGN PATTERNS (ported as-is from\n * `doom-pi-subagents/src/runs/background/completionBatcher.ts` - this\n * module's logic was not one of the flagged predecessor bugs):\n * - A hard `maxWaitMs` cap, measured from the first item in the group, means\n * a debounce that keeps getting reset by new arrivals cannot hold a group\n * open forever\n * - After a group emits, arrivals within `stragglerWindowMs` join a shorter\n * \"straggler\" group (`stragglerDebounceMs`/`stragglerMaxWaitMs`): a single\n * slow sibling still gets grouped with whoever else trails in soon after,\n * rather than waiting out the full debounce alone\n * - `push()` is the only entry point that arms timers; `flush()` and\n * `dispose()` both go through the same `clearTimers`/emit path, so there is\n * one place a leak could be introduced\n * - Disabled mode (`config.enabled === false`) degrades to \"emit immediately,\n * one item at a time\" rather than a special code path elsewhere: every\n * caller of `push()` behaves the same regardless of whether batching is on\n *\n * WHY THIS OWNS ITS OWN TIMERS RATHER THAN REGISTERING WITH `PollScheduler`:\n * Same reasoning as `spawnHandshake.ts`'s documented exception, and for the\n * same underlying reason: `PollScheduler`'s floor is 200ms, and this\n * batcher's default `debounceMs` is 150ms - below the floor before backoff\n * even starts growing it. Coupling a sub-200ms debounce to a shared tick that\n * is explicitly designed to back off toward 5s while idle would make the\n * debounce it exists to provide unreliable. A completion batcher is also\n * short-lived and per-group (bounded by `maxWaitMs`, at most ~1s), the same\n * shape as a handshake wait, not a long-lived background subscriber.\n *\n * AVOID:\n * - Calling `push()` for a failed or paused completion; this batcher has no\n * opinion on what should bypass it; that decision belongs to the caller\n * (`notify.ts`), which flushes and emits those immediately instead\n */\n\nexport interface CompletionBatchConfig {\n enabled?: boolean;\n /** Idle window after each arrival; resets on every new item. */\n debounceMs?: number;\n /** Hard cap measured from the first item in a group. */\n maxWaitMs?: number;\n /** Shorter idle window for straggler groups. */\n stragglerDebounceMs?: number;\n /** Shorter hard cap for straggler groups. */\n stragglerMaxWaitMs?: number;\n /** Arrivals within this window after an emit join a straggler group. */\n stragglerWindowMs?: number;\n}\n\nexport interface ResolvedCompletionBatchConfig {\n enabled: boolean;\n debounceMs: number;\n maxWaitMs: number;\n stragglerDebounceMs: number;\n stragglerMaxWaitMs: number;\n stragglerWindowMs: number;\n}\n\nexport const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {\n enabled: true,\n debounceMs: 150,\n maxWaitMs: 1000,\n stragglerDebounceMs: 75,\n stragglerMaxWaitMs: 400,\n stragglerWindowMs: 2000,\n};\n\nfunction parsePositiveInt(value: unknown): number | undefined {\n if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;\n return value;\n}\n\nexport function resolveCompletionBatchConfig(override?: CompletionBatchConfig): ResolvedCompletionBatchConfig {\n return {\n enabled: typeof override?.enabled === 'boolean' ? override.enabled : DEFAULT_COMPLETION_BATCH_CONFIG.enabled,\n debounceMs: parsePositiveInt(override?.debounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,\n maxWaitMs: parsePositiveInt(override?.maxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,\n stragglerDebounceMs:\n parsePositiveInt(override?.stragglerDebounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,\n stragglerMaxWaitMs:\n parsePositiveInt(override?.stragglerMaxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,\n stragglerWindowMs:\n parsePositiveInt(override?.stragglerWindowMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,\n };\n}\n\ntype TimerHandle = ReturnType<typeof setTimeout>;\n\ninterface TimerApi {\n setTimeout(handler: () => void, delayMs: number): TimerHandle;\n clearTimeout(handle: TimerHandle): void;\n}\n\nconst defaultTimers: TimerApi = {\n setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),\n clearTimeout: (handle) => clearTimeout(handle),\n};\n\nexport interface CompletionBatcherOptions<T> {\n config: ResolvedCompletionBatchConfig;\n emit: (items: T[]) => void;\n timers?: TimerApi;\n now?: () => number;\n}\n\nexport interface CompletionBatcher<T> {\n /** Add a batchable item. Emits immediately, alone, when batching is disabled. */\n push(item: T): void;\n /** Emit any held items immediately as a single group. No-op when nothing is held. */\n flush(): void;\n /** Clear timers and return items that were never emitted, without emitting them. */\n dispose(): T[];\n}\n\n/**\n * Create a completion batcher. Single-use per registration: it holds at most\n * one open group at a time. `flush()` forces emission (used by the caller to\n * drain a group before an immediate, unbatched emit for a failure/pause);\n * `dispose()` tears down timers for reload/shutdown without emitting.\n */\nexport function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {\n const timers = options.timers ?? defaultTimers;\n const now = options.now ?? Date.now;\n const config = options.config;\n\n if (!config.enabled) {\n return {\n push(item: T) {\n options.emit([item]);\n },\n flush() {\n /* nothing is ever held when disabled */\n },\n dispose() {\n return [];\n },\n };\n }\n\n let pending: T[] = [];\n let debounceTimer: TimerHandle | undefined;\n let maxWaitTimer: TimerHandle | undefined;\n let straggler = false;\n let lastEmitAt: number | undefined;\n\n const clearTimers = (): void => {\n if (debounceTimer !== undefined) {\n timers.clearTimeout(debounceTimer);\n debounceTimer = undefined;\n }\n if (maxWaitTimer !== undefined) {\n timers.clearTimeout(maxWaitTimer);\n maxWaitTimer = undefined;\n }\n };\n\n const emitGroup = (): void => {\n clearTimers();\n if (pending.length === 0) return;\n const items = pending;\n pending = [];\n lastEmitAt = now();\n options.emit(items);\n };\n\n return {\n push(item: T) {\n if (pending.length === 0) {\n straggler = lastEmitAt !== undefined && now() - lastEmitAt < config.stragglerWindowMs;\n }\n pending.push(item);\n\n if (debounceTimer !== undefined) timers.clearTimeout(debounceTimer);\n const debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;\n debounceTimer = timers.setTimeout(emitGroup, debounceDelay);\n debounceTimer.unref?.();\n\n if (maxWaitTimer === undefined) {\n const maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;\n maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);\n maxWaitTimer.unref?.();\n }\n },\n flush: emitGroup,\n dispose() {\n clearTimers();\n const abandoned = pending;\n pending = [];\n return abandoned;\n },\n };\n}\n"],"mappings":"AAkEA,MAAa,EAAiE,CAC5E,QAAS,GACT,WAAY,IACZ,UAAW,IACX,oBAAqB,GACrB,mBAAoB,IACpB,kBAAmB,IACpB,CAED,SAAS,EAAiB,EAAoC,CACxD,YAAO,GAAU,UAAY,CAAC,OAAO,SAAS,EAAM,EAAI,CAAC,OAAO,UAAU,EAAM,EAAI,EAAQ,GAChG,OAAO,EAGT,SAAgB,EAA6B,EAAiE,CAC5G,MAAO,CACL,QAAS,OAAO,GAAU,SAAY,UAAY,EAAS,QAAU,EAAgC,QACrG,WAAY,EAAiB,GAAU,WAAW,EAAI,EAAgC,WACtF,UAAW,EAAiB,GAAU,UAAU,EAAI,EAAgC,UACpF,oBACE,EAAiB,GAAU,oBAAoB,EAAI,EAAgC,oBACrF,mBACE,EAAiB,GAAU,mBAAmB,EAAI,EAAgC,mBACpF,kBACE,EAAiB,GAAU,kBAAkB,EAAI,EAAgC,kBACpF,CAUH,MAAM,EAA0B,CAC9B,YAAa,EAAS,IAAY,WAAW,EAAS,EAAQ,CAC9D,aAAe,GAAW,aAAa,EAAO,CAC/C,CAwBD,SAAgB,EAA2B,EAA4D,CACrG,IAAM,EAAS,EAAQ,QAAU,EAC3B,EAAM,EAAQ,KAAO,KAAK,IAC1B,EAAS,EAAQ,OAEvB,GAAI,CAAC,EAAO,QACV,MAAO,CACL,KAAK,EAAS,CACZ,EAAQ,KAAK,CAAC,EAAK,CAAC,EAEtB,OAAQ,GAGR,SAAU,CACR,MAAO,EAAE,EAEZ,CAGH,IAAI,EAAe,EAAE,CACjB,EACA,EACA,EAAY,GACZ,EAEE,MAA0B,CAC1B,IAAkB,IAAA,KACpB,EAAO,aAAa,EAAc,CAClC,EAAgB,IAAA,IAEd,IAAiB,IAAA,KACnB,EAAO,aAAa,EAAa,CACjC,EAAe,IAAA,KAIb,MAAwB,CAE5B,GADA,GAAa,CACT,EAAQ,SAAW,EAAG,OAC1B,IAAM,EAAQ,EACd,EAAU,EAAE,CACZ,EAAa,GAAK,CAClB,EAAQ,KAAK,EAAM,EAGrB,MAAO,CACL,KAAK,EAAS,CACR,EAAQ,SAAW,IACrB,EAAY,IAAe,IAAA,IAAa,GAAK,CAAG,EAAa,EAAO,mBAEtE,EAAQ,KAAK,EAAK,CAEd,IAAkB,IAAA,IAAW,EAAO,aAAa,EAAc,CACnE,IAAM,EAAgB,EAAY,EAAO,oBAAsB,EAAO,WAItE,GAHA,EAAgB,EAAO,WAAW,EAAW,EAAc,CAC3D,EAAc,SAAS,CAEnB,IAAiB,IAAA,GAAW,CAC9B,IAAM,EAAe,EAAY,EAAO,mBAAqB,EAAO,UACpE,EAAe,EAAO,WAAW,EAAW,EAAa,CACzD,EAAa,SAAS,GAG1B,MAAO,EACP,SAAU,CACR,GAAa,CACb,IAAM,EAAY,EAElB,MADA,GAAU,EAAE,CACL,GAEV"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";function t(e){if(!(typeof e!=`number`||!Number.isFinite(e)||!Number.isInteger(e)||e<1))return e}function n(e){return{enabled:typeof e?.enabled==`boolean`?e.enabled:i.enabled,debounceMs:t(e?.debounceMs)??i.debounceMs,maxWaitMs:t(e?.maxWaitMs)??i.maxWaitMs,stragglerDebounceMs:t(e?.stragglerDebounceMs)??i.stragglerDebounceMs,stragglerMaxWaitMs:t(e?.stragglerMaxWaitMs)??i.stragglerMaxWaitMs,stragglerWindowMs:t(e?.stragglerWindowMs)??i.stragglerWindowMs}}function r(e){let t=e.timers??a,n=e.now??Date.now,r=e.config;if(!r.enabled)return{push(t){e.emit([t])},flush(){},dispose(){return[]}};let i=[],o,s,c=!1,l,u=()=>{o!==void 0&&(t.clearTimeout(o),o=void 0),s!==void 0&&(t.clearTimeout(s),s=void 0)},d=()=>{if(u(),i.length===0)return;let t=i;i=[],l=n(),e.emit(t)};return{push(e){i.length===0&&(c=l!==void 0&&n()-l<r.stragglerWindowMs),i.push(e),o!==void 0&&t.clearTimeout(o);let a=c?r.stragglerDebounceMs:r.debounceMs;if(o=t.setTimeout(d,a),o.unref?.(),s===void 0){let e=c?r.stragglerMaxWaitMs:r.maxWaitMs;s=t.setTimeout(d,e),s.unref?.()}},flush:d,dispose(){u();let e=i;return i=[],e}}}var i,a,o=e((()=>{i={enabled:!0,debounceMs:150,maxWaitMs:1e3,stragglerDebounceMs:75,stragglerMaxWaitMs:400,stragglerWindowMs:2e3},a={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e)}}));o();export{r as createCompletionBatcher,o as init_completionBatcher,n as resolveCompletionBatchConfig};
|
|
2
|
-
//# sourceMappingURL=completionBatcher.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"completionBatcher.mjs","names":[],"sources":["../../../src/runs/background/completionBatcher.ts"],"sourcesContent":["/**\n * Debounced grouping for successful completion notifications.\n *\n * WHY THIS EXISTS:\n * A fan-out run's children tend to finish within milliseconds of each other.\n * Notifying once per child turns a five-way parallel run into five separate\n * interruptions for the operator instead of one. This holds a *successful*\n * completion briefly so siblings finishing in the same short window arrive as\n * a single grouped message.\n *\n * DESIGN PATTERNS (ported as-is from\n * `doom-pi-subagents/src/runs/background/completionBatcher.ts` - this\n * module's logic was not one of the flagged predecessor bugs):\n * - A hard `maxWaitMs` cap, measured from the first item in the group, means\n * a debounce that keeps getting reset by new arrivals cannot hold a group\n * open forever\n * - After a group emits, arrivals within `stragglerWindowMs` join a shorter\n * \"straggler\" group (`stragglerDebounceMs`/`stragglerMaxWaitMs`): a single\n * slow sibling still gets grouped with whoever else trails in soon after,\n * rather than waiting out the full debounce alone\n * - `push()` is the only entry point that arms timers; `flush()` and\n * `dispose()` both go through the same `clearTimers`/emit path, so there is\n * one place a leak could be introduced\n * - Disabled mode (`config.enabled === false`) degrades to \"emit immediately,\n * one item at a time\" rather than a special code path elsewhere: every\n * caller of `push()` behaves the same regardless of whether batching is on\n *\n * WHY THIS OWNS ITS OWN TIMERS RATHER THAN REGISTERING WITH `PollScheduler`:\n * Same reasoning as `spawnHandshake.ts`'s documented exception, and for the\n * same underlying reason: `PollScheduler`'s floor is 200ms, and this\n * batcher's default `debounceMs` is 150ms - below the floor before backoff\n * even starts growing it. Coupling a sub-200ms debounce to a shared tick that\n * is explicitly designed to back off toward 5s while idle would make the\n * debounce it exists to provide unreliable. A completion batcher is also\n * short-lived and per-group (bounded by `maxWaitMs`, at most ~1s), the same\n * shape as a handshake wait, not a long-lived background subscriber.\n *\n * AVOID:\n * - Calling `push()` for a failed or paused completion; this batcher has no\n * opinion on what should bypass it; that decision belongs to the caller\n * (`notify.ts`), which flushes and emits those immediately instead\n */\n\nexport interface CompletionBatchConfig {\n enabled?: boolean;\n /** Idle window after each arrival; resets on every new item. */\n debounceMs?: number;\n /** Hard cap measured from the first item in a group. */\n maxWaitMs?: number;\n /** Shorter idle window for straggler groups. */\n stragglerDebounceMs?: number;\n /** Shorter hard cap for straggler groups. */\n stragglerMaxWaitMs?: number;\n /** Arrivals within this window after an emit join a straggler group. */\n stragglerWindowMs?: number;\n}\n\nexport interface ResolvedCompletionBatchConfig {\n enabled: boolean;\n debounceMs: number;\n maxWaitMs: number;\n stragglerDebounceMs: number;\n stragglerMaxWaitMs: number;\n stragglerWindowMs: number;\n}\n\nexport const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {\n enabled: true,\n debounceMs: 150,\n maxWaitMs: 1000,\n stragglerDebounceMs: 75,\n stragglerMaxWaitMs: 400,\n stragglerWindowMs: 2000,\n};\n\nfunction parsePositiveInt(value: unknown): number | undefined {\n if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;\n return value;\n}\n\nexport function resolveCompletionBatchConfig(override?: CompletionBatchConfig): ResolvedCompletionBatchConfig {\n return {\n enabled: typeof override?.enabled === 'boolean' ? override.enabled : DEFAULT_COMPLETION_BATCH_CONFIG.enabled,\n debounceMs: parsePositiveInt(override?.debounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,\n maxWaitMs: parsePositiveInt(override?.maxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,\n stragglerDebounceMs:\n parsePositiveInt(override?.stragglerDebounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,\n stragglerMaxWaitMs:\n parsePositiveInt(override?.stragglerMaxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,\n stragglerWindowMs:\n parsePositiveInt(override?.stragglerWindowMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,\n };\n}\n\ntype TimerHandle = ReturnType<typeof setTimeout>;\n\ninterface TimerApi {\n setTimeout(handler: () => void, delayMs: number): TimerHandle;\n clearTimeout(handle: TimerHandle): void;\n}\n\nconst defaultTimers: TimerApi = {\n setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),\n clearTimeout: (handle) => clearTimeout(handle),\n};\n\nexport interface CompletionBatcherOptions<T> {\n config: ResolvedCompletionBatchConfig;\n emit: (items: T[]) => void;\n timers?: TimerApi;\n now?: () => number;\n}\n\nexport interface CompletionBatcher<T> {\n /** Add a batchable item. Emits immediately, alone, when batching is disabled. */\n push(item: T): void;\n /** Emit any held items immediately as a single group. No-op when nothing is held. */\n flush(): void;\n /** Clear timers and return items that were never emitted, without emitting them. */\n dispose(): T[];\n}\n\n/**\n * Create a completion batcher. Single-use per registration: it holds at most\n * one open group at a time. `flush()` forces emission (used by the caller to\n * drain a group before an immediate, unbatched emit for a failure/pause);\n * `dispose()` tears down timers for reload/shutdown without emitting.\n */\nexport function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {\n const timers = options.timers ?? defaultTimers;\n const now = options.now ?? Date.now;\n const config = options.config;\n\n if (!config.enabled) {\n return {\n push(item: T) {\n options.emit([item]);\n },\n flush() {\n /* nothing is ever held when disabled */\n },\n dispose() {\n return [];\n },\n };\n }\n\n let pending: T[] = [];\n let debounceTimer: TimerHandle | undefined;\n let maxWaitTimer: TimerHandle | undefined;\n let straggler = false;\n let lastEmitAt: number | undefined;\n\n const clearTimers = (): void => {\n if (debounceTimer !== undefined) {\n timers.clearTimeout(debounceTimer);\n debounceTimer = undefined;\n }\n if (maxWaitTimer !== undefined) {\n timers.clearTimeout(maxWaitTimer);\n maxWaitTimer = undefined;\n }\n };\n\n const emitGroup = (): void => {\n clearTimers();\n if (pending.length === 0) return;\n const items = pending;\n pending = [];\n lastEmitAt = now();\n options.emit(items);\n };\n\n return {\n push(item: T) {\n if (pending.length === 0) {\n straggler = lastEmitAt !== undefined && now() - lastEmitAt < config.stragglerWindowMs;\n }\n pending.push(item);\n\n if (debounceTimer !== undefined) timers.clearTimeout(debounceTimer);\n const debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;\n debounceTimer = timers.setTimeout(emitGroup, debounceDelay);\n debounceTimer.unref?.();\n\n if (maxWaitTimer === undefined) {\n const maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;\n maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);\n maxWaitTimer.unref?.();\n }\n },\n flush: emitGroup,\n dispose() {\n clearTimers();\n const abandoned = pending;\n pending = [];\n return abandoned;\n },\n };\n}\n"],"mappings":"gEA2EA,SAAS,EAAiB,EAAoC,CACxD,YAAO,GAAU,UAAY,CAAC,OAAO,SAAS,EAAM,EAAI,CAAC,OAAO,UAAU,EAAM,EAAI,EAAQ,GAChG,OAAO,EAGT,SAAgB,EAA6B,EAAiE,CAC5G,MAAO,CACL,QAAS,OAAO,GAAU,SAAY,UAAY,EAAS,QAAU,EAAgC,QACrG,WAAY,EAAiB,GAAU,WAAW,EAAI,EAAgC,WACtF,UAAW,EAAiB,GAAU,UAAU,EAAI,EAAgC,UACpF,oBACE,EAAiB,GAAU,oBAAoB,EAAI,EAAgC,oBACrF,mBACE,EAAiB,GAAU,mBAAmB,EAAI,EAAgC,mBACpF,kBACE,EAAiB,GAAU,kBAAkB,EAAI,EAAgC,kBACpF,CAqCH,SAAgB,EAA2B,EAA4D,CACrG,IAAM,EAAS,EAAQ,QAAU,EAC3B,EAAM,EAAQ,KAAO,KAAK,IAC1B,EAAS,EAAQ,OAEvB,GAAI,CAAC,EAAO,QACV,MAAO,CACL,KAAK,EAAS,CACZ,EAAQ,KAAK,CAAC,EAAK,CAAC,EAEtB,OAAQ,GAGR,SAAU,CACR,MAAO,EAAE,EAEZ,CAGH,IAAI,EAAe,EAAE,CACjB,EACA,EACA,EAAY,GACZ,EAEE,MAA0B,CAC1B,IAAkB,IAAA,KACpB,EAAO,aAAa,EAAc,CAClC,EAAgB,IAAA,IAEd,IAAiB,IAAA,KACnB,EAAO,aAAa,EAAa,CACjC,EAAe,IAAA,KAIb,MAAwB,CAE5B,GADA,GAAa,CACT,EAAQ,SAAW,EAAG,OAC1B,IAAM,EAAQ,EACd,EAAU,EAAE,CACZ,EAAa,GAAK,CAClB,EAAQ,KAAK,EAAM,EAGrB,MAAO,CACL,KAAK,EAAS,CACR,EAAQ,SAAW,IACrB,EAAY,IAAe,IAAA,IAAa,GAAK,CAAG,EAAa,EAAO,mBAEtE,EAAQ,KAAK,EAAK,CAEd,IAAkB,IAAA,IAAW,EAAO,aAAa,EAAc,CACnE,IAAM,EAAgB,EAAY,EAAO,oBAAsB,EAAO,WAItE,GAHA,EAAgB,EAAO,WAAW,EAAW,EAAc,CAC3D,EAAc,SAAS,CAEnB,IAAiB,IAAA,GAAW,CAC9B,IAAM,EAAe,EAAY,EAAO,mBAAqB,EAAO,UACpE,EAAe,EAAO,WAAW,EAAW,EAAa,CACzD,EAAa,SAAS,GAG1B,MAAO,EACP,SAAU,CACR,GAAa,CACb,IAAM,EAAY,EAElB,MADA,GAAU,EAAE,CACL,GAEV,mBApIU,EAAiE,CAC5E,QAAS,GACT,WAAY,IACZ,UAAW,IACX,oBAAqB,GACrB,mBAAoB,IACpB,kBAAmB,IACpB,CA4BK,EAA0B,CAC9B,YAAa,EAAS,IAAY,WAAW,EAAS,EAAQ,CAC9D,aAAe,GAAW,aAAa,EAAO,CAC/C"}
|
|
@@ -1 +0,0 @@
|
|
|
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":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{__esmMin as e}from"../../_virtual/_rolldown/runtime.mjs";import{__decorate as t,init_decorate as n}from"../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.mjs";import{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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|