toolroll 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
package/dist/exec.js
ADDED
|
@@ -0,0 +1,1841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running external commands.
|
|
3
|
+
*
|
|
4
|
+
* Two rules define this module. Arguments are passed as an array and never
|
|
5
|
+
* through a shell, so a directory named `; rm -rf ~` is a string and not a
|
|
6
|
+
* command. And nothing here rejects: a failed command is a value. Discovery
|
|
7
|
+
* walks repos it has never seen, and one broken repo must not end the scan.
|
|
8
|
+
*/
|
|
9
|
+
import { linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced } from "./agent-fence.js";
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { observeProcessTree, stopProcessTree, readProcessObservationFailure } from "./process-tree.js";
|
|
14
|
+
import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
15
|
+
import { createContainer, currentContainment } from "./containment.js";
|
|
16
|
+
import { claudeLimitsOf, noteLimits } from "./provider-limits.js";
|
|
17
|
+
/** Shell convention for "could not be executed": the refusal before any target ran. */
|
|
18
|
+
export const CONTAINMENT_REFUSED_CODE = 126;
|
|
19
|
+
/** Live provider children, for the deterministic stop. Registered only when `processGroup` was set. */
|
|
20
|
+
const liveProviders = new Set();
|
|
21
|
+
/** Live children by owner tag (v52): the exact-attempt stop's handle. A
|
|
22
|
+
* child is registered here beside liveProviders when its options named an
|
|
23
|
+
* owner, and dropped the moment it closes. */
|
|
24
|
+
const ownedChildren = new Map();
|
|
25
|
+
function registerOwned(owner, child) {
|
|
26
|
+
if (owner === undefined)
|
|
27
|
+
return;
|
|
28
|
+
let set = ownedChildren.get(owner);
|
|
29
|
+
if (set === undefined) {
|
|
30
|
+
set = new Set();
|
|
31
|
+
ownedChildren.set(owner, set);
|
|
32
|
+
}
|
|
33
|
+
set.add(child);
|
|
34
|
+
child.once("close", () => {
|
|
35
|
+
const owned = ownedChildren.get(owner);
|
|
36
|
+
if (owned === undefined)
|
|
37
|
+
return;
|
|
38
|
+
owned.delete(child);
|
|
39
|
+
if (owned.size === 0)
|
|
40
|
+
ownedChildren.delete(owner);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** The native OS object behind a live child, when it has one. */
|
|
44
|
+
const containers = new WeakMap();
|
|
45
|
+
/** Thrown BEFORE any spawn when required containment cannot be had. */
|
|
46
|
+
export class ContainmentRefusal extends Error {
|
|
47
|
+
name = "ContainmentRefusal";
|
|
48
|
+
}
|
|
49
|
+
/** A spawn callback refused custody AFTER the child existed: the caller
|
|
50
|
+
* must reap exactly this child before answering. */
|
|
51
|
+
class SpawnCustodyFailure extends Error {
|
|
52
|
+
child;
|
|
53
|
+
reason;
|
|
54
|
+
name = "SpawnCustodyFailure";
|
|
55
|
+
constructor(child, reason) {
|
|
56
|
+
super(String(reason));
|
|
57
|
+
this.child = child;
|
|
58
|
+
this.reason = reason;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The child a failed spawn road left behind, if any. */
|
|
62
|
+
function rejectedChild(error, child) {
|
|
63
|
+
return error instanceof SpawnCustodyFailure ? error.child : child;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The ONE spawn road for containable children (buffered, streaming and
|
|
67
|
+
* held transports): consult the pinned policy, make this spawn's OS
|
|
68
|
+
* object, spawn the target INSIDE it, and register the object beside the
|
|
69
|
+
* handle. Under observed containment this is exactly the old spawn. A
|
|
70
|
+
* required policy that cannot be met throws ContainmentRefusal here —
|
|
71
|
+
* nothing has been spawned, so the caller's reap is a no-op and its
|
|
72
|
+
* result says `refused` in the policy's words.
|
|
73
|
+
*/
|
|
74
|
+
function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
75
|
+
// The agent fence wraps the target itself (sandbox-exec then execs it, same pid).
|
|
76
|
+
if (bag.fence !== undefined && bag.fence.length > 0 && macosFenceAvailable())
|
|
77
|
+
({ file, args } = macosFenced(file, args, bag.fence));
|
|
78
|
+
else if (bag.fence !== undefined && bag.fence.length > 0 && linuxFenceAvailable())
|
|
79
|
+
({ file, args } = linuxFenced(file, args, bag.fence));
|
|
80
|
+
// The policy refusal comes first: nothing is reserved, recorded or
|
|
81
|
+
// spawned for a spawn that cannot be contained as required.
|
|
82
|
+
const effective = contain ? currentContainment() : null;
|
|
83
|
+
if (effective !== null && effective.refusal !== null)
|
|
84
|
+
throw new ContainmentRefusal(effective.refusal);
|
|
85
|
+
if (bag.beforeSpawn?.() === false)
|
|
86
|
+
throw new Error("the attempt stopped before this process could spawn");
|
|
87
|
+
const made = effective === null ? { container: null } : createContainer(effective, label);
|
|
88
|
+
if ("refused" in made)
|
|
89
|
+
throw new ContainmentRefusal(made.refused);
|
|
90
|
+
const container = made.container;
|
|
91
|
+
try {
|
|
92
|
+
if (container !== null)
|
|
93
|
+
bag.onContainer?.({ backend: container.backend, id: container.id, ...(container.identity ? { identity: container.identity } : {}) });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
container?.release();
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
let launch;
|
|
100
|
+
try {
|
|
101
|
+
launch = container === null ? null : container.launch(file, args);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
container?.release();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
let child;
|
|
108
|
+
try {
|
|
109
|
+
child = spawn(launch === null ? file : launch.file, launch === null ? [...args] : launch.args, {
|
|
110
|
+
shell: false,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
detached: spawnOptions.detached,
|
|
113
|
+
stdio: [...spawnOptions.stdio, ...(launch === null ? [] : launch.extraStdio)],
|
|
114
|
+
...(spawnOptions.cwd === undefined ? {} : { cwd: spawnOptions.cwd }),
|
|
115
|
+
...(spawnOptions.env === undefined ? {} : { env: spawnOptions.env }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// The OS spawn itself threw: this invocation made no target process.
|
|
120
|
+
try {
|
|
121
|
+
if (container !== null)
|
|
122
|
+
bag.onContainerEmpty?.();
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
container?.release();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
if (container !== null) {
|
|
129
|
+
containers.set(child, container);
|
|
130
|
+
// A descendant may keep stdout/stderr open after the root exits. Begin
|
|
131
|
+
// cleanup at root exit; waiting for `close` would wait for that descendant.
|
|
132
|
+
child.once("exit", () => {
|
|
133
|
+
void settleContainer(container, bag).then(empty => {
|
|
134
|
+
if (!empty) {
|
|
135
|
+
child.stdout?.destroy();
|
|
136
|
+
child.stderr?.destroy();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
child.once("error", () => { void settleContainer(container, bag); });
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (child.pid !== undefined) {
|
|
144
|
+
bag.onSpawn?.(child.pid);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw new SpawnCustodyFailure(child, error);
|
|
149
|
+
}
|
|
150
|
+
const attached = launch === null || child.pid === undefined ? Promise.resolve({ ok: true }) : launch.attach(child);
|
|
151
|
+
return { child, container, attached };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The settlement every containable transport runs at the root's exit:
|
|
155
|
+
* members that outlived the root (a setsid'd helper, a double fork) are
|
|
156
|
+
* killed and the OS asked for its empty state, bounded. The answer is
|
|
157
|
+
* the proof the custody record keeps — `false` leaves the record
|
|
158
|
+
* unproven and every stop/resume fence closed.
|
|
159
|
+
*/
|
|
160
|
+
const containerSettlements = new WeakMap();
|
|
161
|
+
function settleContainer(container, bag, timeoutMs = 5_000) {
|
|
162
|
+
const existing = containerSettlements.get(container);
|
|
163
|
+
if (existing !== undefined)
|
|
164
|
+
return existing;
|
|
165
|
+
const settling = finishContainer(container, bag, timeoutMs);
|
|
166
|
+
containerSettlements.set(container, settling);
|
|
167
|
+
return settling;
|
|
168
|
+
}
|
|
169
|
+
async function finishContainer(container, bag, timeoutMs) {
|
|
170
|
+
let empty = container.populated() === false;
|
|
171
|
+
try {
|
|
172
|
+
if (!empty)
|
|
173
|
+
empty = await container.kill(timeoutMs);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
empty = false;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
if (empty)
|
|
180
|
+
bag.onContainerEmpty?.();
|
|
181
|
+
else
|
|
182
|
+
bag.onUnknown?.();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Custody that cannot be written stays unproven; the fences hold.
|
|
186
|
+
empty = false;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
container.release();
|
|
190
|
+
}
|
|
191
|
+
catch { /* proof already determines the result */ }
|
|
192
|
+
return empty;
|
|
193
|
+
}
|
|
194
|
+
/** The result's containment field for a spawn that ran inside an object. */
|
|
195
|
+
function containmentOf(container, empty, attachFailure) {
|
|
196
|
+
if (attachFailure !== null)
|
|
197
|
+
return { containment: { refused: attachFailure } };
|
|
198
|
+
if (container === null || empty === null)
|
|
199
|
+
return {};
|
|
200
|
+
return { containment: { backend: container.backend, id: container.id, empty } };
|
|
201
|
+
}
|
|
202
|
+
/** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
|
|
203
|
+
function spawnStream(file, args, options, stdio, childEnv) {
|
|
204
|
+
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
205
|
+
}
|
|
206
|
+
/** The refusal-before-spawn shape shared by every containable transport. */
|
|
207
|
+
function refusedResult(error) {
|
|
208
|
+
if (error instanceof ContainmentRefusal) {
|
|
209
|
+
return { code: CONTAINMENT_REFUSED_CODE, stdout: "", stderr: error.message, timedOut: false, notFound: false, containment: { refused: error.message } };
|
|
210
|
+
}
|
|
211
|
+
return { code: 1, stdout: "", stderr: String(error instanceof SpawnCustodyFailure ? error.reason : error), timedOut: false, notFound: false };
|
|
212
|
+
}
|
|
213
|
+
const databaseOwners = new WeakMap();
|
|
214
|
+
let memoryDatabaseOwner = 0;
|
|
215
|
+
/** Run IDs are local to a database. Connections to the same file share
|
|
216
|
+
* custody; separate files and separate in-memory databases never do. */
|
|
217
|
+
export function runOwnerTag(store, runId) {
|
|
218
|
+
let owner = databaseOwners.get(store.handle);
|
|
219
|
+
if (owner === undefined) {
|
|
220
|
+
const main = store.handle.prepare("PRAGMA database_list").all().find(row => row["name"] === "main");
|
|
221
|
+
const file = main?.["file"];
|
|
222
|
+
owner = typeof file === "string" && file !== "" ? `file:${realpathSync(file)}` : `memory:${++memoryDatabaseOwner}`;
|
|
223
|
+
databaseOwners.set(store.handle, owner);
|
|
224
|
+
}
|
|
225
|
+
return JSON.stringify([owner, runId]);
|
|
226
|
+
}
|
|
227
|
+
/** How many live children this process still tracks under an owner tag. */
|
|
228
|
+
// Zero is local to this process registry. Recovery must also inspect durable
|
|
229
|
+
// witnesses and its authenticated OS census; this count is not an absence proof.
|
|
230
|
+
export function ownedProcessCount(owner) {
|
|
231
|
+
return ownedChildren.get(owner)?.size ?? 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The exact-attempt stop (v52): SIGKILL the process group of every live
|
|
235
|
+
* child registered under the owner tag — and only those. Returns how many
|
|
236
|
+
* were signalled. Idempotent: a child already gone was dropped at close,
|
|
237
|
+
* and a tag nobody registered kills nothing. This is the ONLY kill road a
|
|
238
|
+
* task action may take; the global sweep below is the watch's shutdown.
|
|
239
|
+
*/
|
|
240
|
+
export function terminateOwnedProcesses(owner) {
|
|
241
|
+
const owned = ownedChildren.get(owner);
|
|
242
|
+
if (owned === undefined)
|
|
243
|
+
return 0;
|
|
244
|
+
let terminated = 0;
|
|
245
|
+
for (const child of owned) {
|
|
246
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
247
|
+
continue;
|
|
248
|
+
killGroup(child);
|
|
249
|
+
terminated += 1;
|
|
250
|
+
}
|
|
251
|
+
return terminated;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Live held-session SUPERVISORS (v6 W7). Not in liveProviders: SIGKILLing a
|
|
255
|
+
* supervisor's group would orphan the agent living in its own fresh group.
|
|
256
|
+
* The sweep's contract here is SIGTERM first — the supervisor's handler
|
|
257
|
+
* fences its child (EOF → grace → group SIGKILL) — and SIGKILL only on a
|
|
258
|
+
* repeat sweep, which is the one road that can orphan an agent and is why
|
|
259
|
+
* it is the escalation, never the opener.
|
|
260
|
+
*/
|
|
261
|
+
const heldSupervisors = new Map();
|
|
262
|
+
/** SIGKILL the child's whole process group; fall back to the child alone. */
|
|
263
|
+
function killGroup(child) {
|
|
264
|
+
const container = containers.get(child);
|
|
265
|
+
if (container !== undefined) {
|
|
266
|
+
// The OS object reaches every member at once — detached helpers
|
|
267
|
+
// included — and its empty state is awaited by the transport's
|
|
268
|
+
// settlement, never assumed here.
|
|
269
|
+
void container.kill().catch(() => { });
|
|
270
|
+
// The Windows helper is the job's owner and reporter: it terminates the
|
|
271
|
+
// job on the order above and answers "empty"; killing it instead would
|
|
272
|
+
// close the handle (kill-on-close still ends the job) but lose the proof.
|
|
273
|
+
if (container.backend !== "job-object") {
|
|
274
|
+
try {
|
|
275
|
+
child.kill("SIGKILL");
|
|
276
|
+
}
|
|
277
|
+
catch { /* already gone */ }
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (stopProcessTree(child))
|
|
282
|
+
return;
|
|
283
|
+
const pid = child.pid;
|
|
284
|
+
if (pid !== undefined && process.platform === "win32") {
|
|
285
|
+
// Windows has no process groups to signal (Codex M5-M8 audit, IV-6):
|
|
286
|
+
// taskkill /T walks the tree, so a harness's shell grandchildren die
|
|
287
|
+
// with it instead of writing to the worktree after "stopped". Untested
|
|
288
|
+
// on physical Windows, like the daemon — stated, not hidden.
|
|
289
|
+
try {
|
|
290
|
+
spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => { });
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// taskkill missing or refused — the direct kill below still runs.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (pid !== undefined && process.platform !== "win32") {
|
|
297
|
+
try {
|
|
298
|
+
process.kill(-pid, "SIGKILL");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// No group of ours (not detached, or already gone) — fall through.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
child.kill("SIGKILL");
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Already gone.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* The hard stop (M6.12): SIGKILL every live provider's process group.
|
|
314
|
+
* Called by the watch when its stop grace expires — never on the first
|
|
315
|
+
* signal, which stays graceful. Late output cannot commit or publish:
|
|
316
|
+
* the killed process exits nonzero, the build finalizes as the failure
|
|
317
|
+
* it is, and every commit sits behind fences the corpse cannot pass.
|
|
318
|
+
*/
|
|
319
|
+
export function terminateLiveProviders() {
|
|
320
|
+
let terminated = 0;
|
|
321
|
+
for (const child of liveProviders) {
|
|
322
|
+
killGroup(child);
|
|
323
|
+
terminated += 1;
|
|
324
|
+
}
|
|
325
|
+
// Held supervisors take the graceful road first (v6 W7): SIGTERM makes
|
|
326
|
+
// the supervisor fence its own agent race-free through the handle only
|
|
327
|
+
// it holds. A second sweep escalates to SIGKILL — the documented
|
|
328
|
+
// residual hole, reachable only by double-escalation, with the startup
|
|
329
|
+
// orphan fence paging for anything that lingers.
|
|
330
|
+
for (const [supervisor, stage] of heldSupervisors) {
|
|
331
|
+
try {
|
|
332
|
+
if (stage === "fresh") {
|
|
333
|
+
supervisor.kill("SIGTERM");
|
|
334
|
+
heldSupervisors.set(supervisor, "termed");
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// With a native OS object the escalation reaches the agent too:
|
|
338
|
+
// the object's kill is the one road that cannot orphan it.
|
|
339
|
+
killGroup(supervisor);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Already gone.
|
|
344
|
+
}
|
|
345
|
+
terminated += 1;
|
|
346
|
+
}
|
|
347
|
+
return terminated;
|
|
348
|
+
}
|
|
349
|
+
/** The one place a child's environment is decided (audit IV-5). */
|
|
350
|
+
/**
|
|
351
|
+
* Chat credentials never reach a child process (Codex chat v3 review,
|
|
352
|
+
* change 10): ANTHROPIC_API_KEY is the chat key AND would silently flip
|
|
353
|
+
* the claude harness from subscription auth to API billing if inherited.
|
|
354
|
+
* A caller that genuinely needs it re-supplies it via `env` explicitly.
|
|
355
|
+
* (OPENROUTER_API_KEY is not listed: the openrouter BUILD adapter reads
|
|
356
|
+
* it from the child env by design, and the agent-facing shell excludes
|
|
357
|
+
* it separately via the provider's own config.)
|
|
358
|
+
*/
|
|
359
|
+
const CHAT_KEYS_NEVER_INHERITED = ["ANTHROPIC_API_KEY"];
|
|
360
|
+
function resolveChildEnv(options) {
|
|
361
|
+
const { env, omitEnv, envAllowlist } = options;
|
|
362
|
+
if (envAllowlist !== undefined) {
|
|
363
|
+
const picked = {};
|
|
364
|
+
for (const name of envAllowlist) {
|
|
365
|
+
const value = process.env[name];
|
|
366
|
+
if (value !== undefined)
|
|
367
|
+
picked[name] = value;
|
|
368
|
+
}
|
|
369
|
+
Object.assign(picked, env ?? {});
|
|
370
|
+
for (const name of omitEnv ?? [])
|
|
371
|
+
delete picked[name];
|
|
372
|
+
for (const name of CHAT_KEYS_NEVER_INHERITED) {
|
|
373
|
+
if (env?.[name] === undefined)
|
|
374
|
+
delete picked[name];
|
|
375
|
+
}
|
|
376
|
+
return picked;
|
|
377
|
+
}
|
|
378
|
+
const merged = { ...process.env, ...(env ?? {}) };
|
|
379
|
+
for (const name of omitEnv ?? [])
|
|
380
|
+
delete merged[name];
|
|
381
|
+
for (const name of CHAT_KEYS_NEVER_INHERITED) {
|
|
382
|
+
if (env?.[name] === undefined)
|
|
383
|
+
delete merged[name];
|
|
384
|
+
}
|
|
385
|
+
return merged;
|
|
386
|
+
}
|
|
387
|
+
/** Conventions from the shell and from coreutils `timeout(1)`. */
|
|
388
|
+
export const NOT_FOUND_CODE = 127;
|
|
389
|
+
export const TIMEOUT_CODE = 124;
|
|
390
|
+
/** Output too large to hold. Not a timeout, and must not be reported as one. */
|
|
391
|
+
/** A spawn callback records custody after the child exists. If that write
|
|
392
|
+
* fails, stop and reap the owned child before reporting failure; otherwise
|
|
393
|
+
* an unrecorded provider could keep writing after its caller retries. */
|
|
394
|
+
function reapRejectedSpawn(child, group) {
|
|
395
|
+
if (child === undefined)
|
|
396
|
+
return Promise.resolve();
|
|
397
|
+
return new Promise(resolve => {
|
|
398
|
+
if (group)
|
|
399
|
+
liveProviders.add(child);
|
|
400
|
+
child.once("close", () => {
|
|
401
|
+
liveProviders.delete(child);
|
|
402
|
+
const container = containers.get(child);
|
|
403
|
+
if (container === undefined)
|
|
404
|
+
resolve();
|
|
405
|
+
else
|
|
406
|
+
void settleContainer(container, {}).then(() => resolve());
|
|
407
|
+
});
|
|
408
|
+
child.on("error", () => { });
|
|
409
|
+
child.stdout?.resume();
|
|
410
|
+
child.stderr?.resume();
|
|
411
|
+
if (group)
|
|
412
|
+
killGroup(child);
|
|
413
|
+
else {
|
|
414
|
+
try {
|
|
415
|
+
child.kill("SIGKILL");
|
|
416
|
+
}
|
|
417
|
+
catch { /* The child must still exit before failure is returned. */ }
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
export const OVERFLOW_CODE = 125;
|
|
422
|
+
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
423
|
+
export const DEFAULT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
424
|
+
/**
|
|
425
|
+
* A streaming process may run for hours while it is making observable
|
|
426
|
+
* progress. This controller keeps the ordinary hard timeout available for
|
|
427
|
+
* bounded work, while allowing provider callers to choose an activity-based
|
|
428
|
+
* watchdog with no wall-clock deadline. Both roads share the same kill and
|
|
429
|
+
* settlement semantics.
|
|
430
|
+
*/
|
|
431
|
+
function streamWatchdog(options, onTimeout) {
|
|
432
|
+
const hardMs = options.timeoutMs ?? (options.idleTimeoutMs === undefined ? DEFAULT_TIMEOUT_MS : undefined);
|
|
433
|
+
const idleMs = options.idleTimeoutMs;
|
|
434
|
+
let fired = false;
|
|
435
|
+
let hardTimer;
|
|
436
|
+
let idleTimer;
|
|
437
|
+
const fire = () => {
|
|
438
|
+
if (fired)
|
|
439
|
+
return;
|
|
440
|
+
fired = true;
|
|
441
|
+
onTimeout();
|
|
442
|
+
};
|
|
443
|
+
const armIdle = () => {
|
|
444
|
+
if (idleMs === undefined)
|
|
445
|
+
return;
|
|
446
|
+
if (idleTimer !== undefined)
|
|
447
|
+
clearTimeout(idleTimer);
|
|
448
|
+
idleTimer = setTimeout(fire, idleMs);
|
|
449
|
+
idleTimer.unref?.();
|
|
450
|
+
};
|
|
451
|
+
if (hardMs !== undefined) {
|
|
452
|
+
hardTimer = setTimeout(fire, hardMs);
|
|
453
|
+
hardTimer.unref?.();
|
|
454
|
+
}
|
|
455
|
+
armIdle();
|
|
456
|
+
return {
|
|
457
|
+
touch: armIdle,
|
|
458
|
+
stop: () => {
|
|
459
|
+
if (hardTimer !== undefined)
|
|
460
|
+
clearTimeout(hardTimer);
|
|
461
|
+
if (idleTimer !== undefined)
|
|
462
|
+
clearTimeout(idleTimer);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const MAX_BUFFER_ERROR = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
467
|
+
/**
|
|
468
|
+
* A spawn the OS refused for the moment — no free process slot or file
|
|
469
|
+
* descriptor (a loaded machine, a fork storm from a parallel test suite) —
|
|
470
|
+
* is not the command failing: the command never ran. Left as an exit 1, a
|
|
471
|
+
* `git symbolic-ref` that never started reads as "no branch checked out"
|
|
472
|
+
* and an unattended pass refuses on a lie. So the two buffered transports
|
|
473
|
+
* retry EXACTLY this class, bounded, before answering.
|
|
474
|
+
*/
|
|
475
|
+
const TRANSIENT_SPAWN_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE"]);
|
|
476
|
+
export const SPAWN_RETRY_DELAYS_MS = [50, 100, 200, 400, 800];
|
|
477
|
+
export function isTransientSpawnFailure(error) {
|
|
478
|
+
return error !== null && error !== undefined && typeof error.code === "string" && TRANSIENT_SPAWN_CODES.has(error.code);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Retry `attempt` while it reports a transient spawn failure, sleeping the
|
|
482
|
+
* given delays between tries; the last answer stands when they run out.
|
|
483
|
+
* A non-transient answer — success, a real exit code, not found, timeout —
|
|
484
|
+
* is returned at once: only the never-started case is retried.
|
|
485
|
+
*/
|
|
486
|
+
export async function retryTransientSpawn(attempt, delays = SPAWN_RETRY_DELAYS_MS, sleep = ms => new Promise(resolve => setTimeout(resolve, ms))) {
|
|
487
|
+
let last = await attempt();
|
|
488
|
+
for (const delay of delays) {
|
|
489
|
+
if (!last.transient)
|
|
490
|
+
return last.result;
|
|
491
|
+
await sleep(delay);
|
|
492
|
+
last = await attempt();
|
|
493
|
+
}
|
|
494
|
+
return last.result;
|
|
495
|
+
}
|
|
496
|
+
export function run(file, args, options = {}) {
|
|
497
|
+
const { cwd, timeoutMs = DEFAULT_TIMEOUT_MS, maxBuffer = DEFAULT_MAX_BUFFER } = options;
|
|
498
|
+
const childEnv = resolveChildEnv(options);
|
|
499
|
+
// A provider run needs its own process group; execFile cannot give one,
|
|
500
|
+
// so the buffered path detours through spawn with identical semantics.
|
|
501
|
+
if (options.processGroup === true) {
|
|
502
|
+
return retryTransientSpawn(() => runBufferedGroup(file, args, {
|
|
503
|
+
timeoutMs,
|
|
504
|
+
maxBuffer,
|
|
505
|
+
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
506
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
507
|
+
...(childEnv === undefined ? {} : { childEnv }),
|
|
508
|
+
...(options.onSpawn === undefined ? {} : { onSpawn: options.onSpawn }),
|
|
509
|
+
...(options.owner === undefined ? {} : { owner: options.owner }),
|
|
510
|
+
...(options.beforeSpawn === undefined ? {} : { beforeSpawn: options.beforeSpawn }),
|
|
511
|
+
...(options.onStdout === undefined ? {} : { onStdout: options.onStdout }),
|
|
512
|
+
...(options.onStderr === undefined ? {} : { onStderr: options.onStderr }),
|
|
513
|
+
...(options.fence === undefined ? {} : { fence: options.fence }),
|
|
514
|
+
...(options.onDescendant === undefined ? {} : { onDescendant: options.onDescendant }),
|
|
515
|
+
...(options.onDescendantWriteFailure === undefined ? {} : { onDescendantWriteFailure: options.onDescendantWriteFailure }),
|
|
516
|
+
...(options.onDescendantExit === undefined ? {} : { onDescendantExit: options.onDescendantExit }),
|
|
517
|
+
...(options.onUnknown === undefined ? {} : { onUnknown: options.onUnknown }),
|
|
518
|
+
...(options.onObservationFailure === undefined ? {} : { onObservationFailure: options.onObservationFailure }),
|
|
519
|
+
...(options.onContainer === undefined ? {} : { onContainer: options.onContainer }),
|
|
520
|
+
...(options.onContainerEmpty === undefined ? {} : { onContainerEmpty: options.onContainerEmpty }),
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
return retryTransientSpawn(() => new Promise(resolve => {
|
|
524
|
+
execFile(file, [...args], {
|
|
525
|
+
cwd,
|
|
526
|
+
timeout: timeoutMs,
|
|
527
|
+
maxBuffer,
|
|
528
|
+
encoding: "utf8",
|
|
529
|
+
shell: false,
|
|
530
|
+
windowsHide: true,
|
|
531
|
+
...(childEnv === undefined ? {} : { env: childEnv }),
|
|
532
|
+
}, (error, stdout, stderr) => {
|
|
533
|
+
if (error === null) {
|
|
534
|
+
resolve({ result: { code: 0, stdout, stderr, timedOut: false, notFound: false }, transient: false });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
resolve({
|
|
538
|
+
result: describeFailure(error, stdout, stderr),
|
|
539
|
+
transient: isTransientSpawnFailure(error),
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The buffered transport for process-group providers (claude): spawn
|
|
546
|
+
* detached so the harness and everything it launches share one killable
|
|
547
|
+
* group, collect output up to the same maxBuffer contract as execFile,
|
|
548
|
+
* and register as a live provider for the watch's hard stop.
|
|
549
|
+
*/
|
|
550
|
+
function runBufferedGroup(file, args, bag) {
|
|
551
|
+
return new Promise(resolve => {
|
|
552
|
+
let child;
|
|
553
|
+
let container = null;
|
|
554
|
+
let attached;
|
|
555
|
+
try {
|
|
556
|
+
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
557
|
+
child = spawned.child;
|
|
558
|
+
container = spawned.container;
|
|
559
|
+
attached = spawned.attached;
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const left = rejectedChild(error, child);
|
|
563
|
+
void reapRejectedSpawn(left, true).then(() => {
|
|
564
|
+
resolve({
|
|
565
|
+
result: refusedResult(error),
|
|
566
|
+
transient: left === undefined && isTransientSpawnFailure(error),
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
liveProviders.add(child);
|
|
572
|
+
registerOwned(bag.owner, child);
|
|
573
|
+
if (container === null)
|
|
574
|
+
observeProcessTree(child, bag);
|
|
575
|
+
let stdout = "";
|
|
576
|
+
let stderr = "";
|
|
577
|
+
let timedOut = false;
|
|
578
|
+
let overflowed = false;
|
|
579
|
+
let inputFailed = false;
|
|
580
|
+
let notFound = false;
|
|
581
|
+
let attachFailure = null;
|
|
582
|
+
void attached.then(outcome => { if (!outcome.ok)
|
|
583
|
+
attachFailure = outcome.detail; });
|
|
584
|
+
const timer = setTimeout(() => {
|
|
585
|
+
timedOut = true;
|
|
586
|
+
killGroup(child);
|
|
587
|
+
}, bag.timeoutMs);
|
|
588
|
+
child.stdout?.setEncoding("utf8");
|
|
589
|
+
child.stdout?.on("data", (chunk) => {
|
|
590
|
+
stdout += chunk;
|
|
591
|
+
if (stdout.length > bag.maxBuffer) {
|
|
592
|
+
overflowed = true;
|
|
593
|
+
killGroup(child);
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
bag.onStdout?.(chunk);
|
|
597
|
+
}
|
|
598
|
+
catch { /* a listener never breaks the run */ }
|
|
599
|
+
});
|
|
600
|
+
child.stderr?.setEncoding("utf8");
|
|
601
|
+
child.stderr?.on("data", (chunk) => {
|
|
602
|
+
if (stderr.length < bag.maxBuffer)
|
|
603
|
+
stderr += chunk.slice(0, bag.maxBuffer - stderr.length);
|
|
604
|
+
try {
|
|
605
|
+
bag.onStderr?.(chunk);
|
|
606
|
+
}
|
|
607
|
+
catch { /* a listener never breaks the run */ }
|
|
608
|
+
});
|
|
609
|
+
let settled = false;
|
|
610
|
+
let transient = false;
|
|
611
|
+
const finish = (code) => {
|
|
612
|
+
if (settled)
|
|
613
|
+
return;
|
|
614
|
+
settled = true;
|
|
615
|
+
clearTimeout(timer);
|
|
616
|
+
liveProviders.delete(child);
|
|
617
|
+
// The OS object is settled BEFORE the answer: a root that exited is
|
|
618
|
+
// not an empty container until the OS says so.
|
|
619
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, bag);
|
|
620
|
+
void settlement.then(empty => {
|
|
621
|
+
resolve({
|
|
622
|
+
result: {
|
|
623
|
+
code: notFound ? NOT_FOUND_CODE : overflowed ? OVERFLOW_CODE : timedOut ? TIMEOUT_CODE : inputFailed ? 1 : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : (code ?? 1),
|
|
624
|
+
stdout,
|
|
625
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
626
|
+
// Overflow also killed the child; it is not a timeout and must not read as one.
|
|
627
|
+
timedOut: timedOut && !overflowed,
|
|
628
|
+
notFound,
|
|
629
|
+
...containmentOf(container, empty, attachFailure),
|
|
630
|
+
},
|
|
631
|
+
transient,
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
child.on("error", error => {
|
|
636
|
+
notFound = error.code === "ENOENT";
|
|
637
|
+
transient = isTransientSpawnFailure(error);
|
|
638
|
+
if (stderr === "")
|
|
639
|
+
stderr = String(error);
|
|
640
|
+
finish(null);
|
|
641
|
+
});
|
|
642
|
+
child.on("close", code => finish(code));
|
|
643
|
+
if (bag.stdin !== undefined && child.stdin !== null) {
|
|
644
|
+
child.stdin.on("error", () => {
|
|
645
|
+
inputFailed = true;
|
|
646
|
+
stderr = "prompt input failed: the provider closed its input before delivery completed";
|
|
647
|
+
killGroup(child);
|
|
648
|
+
});
|
|
649
|
+
child.stdin.end(bag.stdin, "utf8");
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Turn an execFile error into a result. The order matters: a maxBuffer overflow
|
|
655
|
+
* also arrives with `killed: true`, so it has to be recognised before the
|
|
656
|
+
* timeout check or a truncated `git log` reads as a hung one.
|
|
657
|
+
*/
|
|
658
|
+
function describeFailure(error, stdout, stderr) {
|
|
659
|
+
const notFound = error.code === "ENOENT";
|
|
660
|
+
const overflowed = error.code === MAX_BUFFER_ERROR;
|
|
661
|
+
const timedOut = !overflowed && error.killed === true;
|
|
662
|
+
return {
|
|
663
|
+
code: resolveCode(error, { notFound, overflowed, timedOut }),
|
|
664
|
+
stdout,
|
|
665
|
+
stderr: stderr === "" ? error.message : stderr,
|
|
666
|
+
timedOut,
|
|
667
|
+
notFound,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function resolveCode(error, flags) {
|
|
671
|
+
if (typeof error.code === "number")
|
|
672
|
+
return error.code;
|
|
673
|
+
if (flags.notFound)
|
|
674
|
+
return NOT_FOUND_CODE;
|
|
675
|
+
if (flags.overflowed)
|
|
676
|
+
return OVERFLOW_CODE;
|
|
677
|
+
if (flags.timedOut)
|
|
678
|
+
return TIMEOUT_CODE;
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* The streaming transport for JSONL-emitting providers.
|
|
683
|
+
*
|
|
684
|
+
* A long agent session can write far more event stream than any fixed
|
|
685
|
+
* buffer should hold, and the lines that matter — the session identity,
|
|
686
|
+
* the terminal usage — arrive LAST. Buffering-and-overflowing would kill
|
|
687
|
+
* the process at 8 MiB and lose exactly the facts a paid run must not
|
|
688
|
+
* lose (Codex provider review, high finding 2). So stdout is consumed
|
|
689
|
+
* incrementally and only the load-bearing lines are retained:
|
|
690
|
+
*
|
|
691
|
+
* - a bounded identity witness for `thread.started` (first usable id,
|
|
692
|
+
* first conflicting id, and last event), plus the LAST `turn.completed`
|
|
693
|
+
* and `turn.failed` line
|
|
694
|
+
* - the LAST `item.completed` line carrying an agent_message
|
|
695
|
+
*
|
|
696
|
+
* each capped per line; everything else is counted and dropped. The
|
|
697
|
+
* result's stdout is the retained lines joined — a synthetic, bounded
|
|
698
|
+
* envelope the parser reads exactly like test fixtures.
|
|
699
|
+
*/
|
|
700
|
+
const JSONL_LINE_CAP = 64 * 1024;
|
|
701
|
+
const JSONL_STDERR_CAP = 64 * 1024;
|
|
702
|
+
/** Canonical identity at the transport boundary. Never persist or expose
|
|
703
|
+
* provider framing whitespace as part of a resumable session id. */
|
|
704
|
+
function transportSessionId(value) {
|
|
705
|
+
if (typeof value !== "string")
|
|
706
|
+
return null;
|
|
707
|
+
const id = value.trim();
|
|
708
|
+
return id === "" ? null : id;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Provider replies are nested inside their JSONL envelope. A 64 KiB reply
|
|
712
|
+
* therefore needs appreciably more than 64 KiB on the wire once quotes,
|
|
713
|
+
* backslashes, and control characters are escaped. 512 KiB carries that
|
|
714
|
+
* worst useful case while remaining a hard, per-event memory boundary.
|
|
715
|
+
*/
|
|
716
|
+
export const JSONL_EVENT_HARD_CAP = 512 * 1024;
|
|
717
|
+
const JSONL_OVERFLOW_PREFIX_CAP = 4 * 1024;
|
|
718
|
+
/**
|
|
719
|
+
* Split a byte stream into JSONL records without ever accumulating an
|
|
720
|
+
* unbounded partial line. The callback is synchronous so the same fixed
|
|
721
|
+
* storage can be reused for the next record. Counting the original bytes
|
|
722
|
+
* (rather than JavaScript characters) makes the overflow receipt exact.
|
|
723
|
+
*/
|
|
724
|
+
function boundedJsonlFramer(cap, onLine, inspectDiscriminants = false) {
|
|
725
|
+
const retained = Buffer.allocUnsafe(cap);
|
|
726
|
+
let retainedBytes = 0;
|
|
727
|
+
let originalBytes = 0;
|
|
728
|
+
let nonWhitespace = false;
|
|
729
|
+
let discriminants = inspectDiscriminants ? jsonlDiscriminants() : null;
|
|
730
|
+
const append = (part) => {
|
|
731
|
+
discriminants?.push(part);
|
|
732
|
+
originalBytes += part.length;
|
|
733
|
+
if (!nonWhitespace) {
|
|
734
|
+
for (const byte of part) {
|
|
735
|
+
if (byte !== 0x09 && byte !== 0x0a && byte !== 0x0d && byte !== 0x20) {
|
|
736
|
+
nonWhitespace = true;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (retainedBytes < cap) {
|
|
742
|
+
retainedBytes += part.copy(retained, retainedBytes, 0, cap - retainedBytes);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
const emit = () => {
|
|
746
|
+
onLine({
|
|
747
|
+
prefix: retained.subarray(0, retainedBytes),
|
|
748
|
+
bytesOriginal: originalBytes,
|
|
749
|
+
overflowed: originalBytes > cap,
|
|
750
|
+
...(discriminants === null ? {} : { discriminants: discriminants.finish() }),
|
|
751
|
+
});
|
|
752
|
+
retainedBytes = 0;
|
|
753
|
+
originalBytes = 0;
|
|
754
|
+
nonWhitespace = false;
|
|
755
|
+
discriminants = inspectDiscriminants ? jsonlDiscriminants() : null;
|
|
756
|
+
};
|
|
757
|
+
return {
|
|
758
|
+
push(chunk) {
|
|
759
|
+
let start = 0;
|
|
760
|
+
for (;;) {
|
|
761
|
+
const newline = chunk.indexOf(0x0a, start);
|
|
762
|
+
if (newline === -1) {
|
|
763
|
+
append(chunk.subarray(start));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
append(chunk.subarray(start, newline));
|
|
767
|
+
emit();
|
|
768
|
+
start = newline + 1;
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
finish() {
|
|
772
|
+
// Match the old `partial.trim() !== ""` rule: a final unterminated
|
|
773
|
+
// whitespace-only record is not an event and needs no receipt.
|
|
774
|
+
if (originalBytes > 0 && nonWhitespace)
|
|
775
|
+
emit();
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function overflowReceiptText(transport, line) {
|
|
780
|
+
const rawPrefix = line.prefix.subarray(0, Math.min(line.prefix.length, JSONL_OVERFLOW_PREFIX_CAP));
|
|
781
|
+
return JSON.stringify({
|
|
782
|
+
type: "standing-orders.stream-event-overflow",
|
|
783
|
+
transport,
|
|
784
|
+
message: `The provider emitted a JSONL event larger than the ${JSONL_EVENT_HARD_CAP}-byte safety limit.`,
|
|
785
|
+
eventBytesOriginal: line.bytesOriginal,
|
|
786
|
+
hardCapBytes: JSONL_EVENT_HARD_CAP,
|
|
787
|
+
prefixBytes: rawPrefix.length,
|
|
788
|
+
prefix: rawPrefix.toString("utf8"),
|
|
789
|
+
// The readable prefix can end halfway through a UTF-8 character. This
|
|
790
|
+
// companion is the exact retained byte prefix for forensic evidence.
|
|
791
|
+
prefixBase64: rawPrefix.toString("base64"),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function malformedReceiptText(transport, line) {
|
|
795
|
+
const rawPrefix = line.prefix.subarray(0, Math.min(line.prefix.length, JSONL_OVERFLOW_PREFIX_CAP));
|
|
796
|
+
return JSON.stringify({
|
|
797
|
+
type: "standing-orders.stream-event-malformed",
|
|
798
|
+
transport,
|
|
799
|
+
message: "The provider emitted malformed JSON for a load-bearing stream event after initialization.",
|
|
800
|
+
eventBytesOriginal: line.bytesOriginal,
|
|
801
|
+
prefixBytes: rawPrefix.length,
|
|
802
|
+
prefix: rawPrefix.toString("utf8"),
|
|
803
|
+
prefixBase64: rawPrefix.toString("base64"),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function codexOverflowLine(line) {
|
|
807
|
+
return JSON.stringify({
|
|
808
|
+
type: "turn.failed",
|
|
809
|
+
error: {
|
|
810
|
+
type: "standing-orders.stream-event-overflow",
|
|
811
|
+
message: overflowReceiptText("codex", line),
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function codexMalformedLine(line) {
|
|
816
|
+
return JSON.stringify({
|
|
817
|
+
type: "turn.failed",
|
|
818
|
+
error: {
|
|
819
|
+
type: "standing-orders.stream-event-malformed",
|
|
820
|
+
message: malformedReceiptText("codex", line),
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function claudeOverflowLine(line) {
|
|
825
|
+
return JSON.stringify({
|
|
826
|
+
type: "result",
|
|
827
|
+
subtype: "standing-orders-stream-event-overflow",
|
|
828
|
+
is_error: true,
|
|
829
|
+
result: overflowReceiptText("claude", line),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function geminiOverflowLine(line) {
|
|
833
|
+
return JSON.stringify({
|
|
834
|
+
type: "result",
|
|
835
|
+
status: "standing-orders-stream-event-overflow",
|
|
836
|
+
error: { message: overflowReceiptText("gemini", line) },
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function geminiMalformedLine(line) {
|
|
840
|
+
return JSON.stringify({
|
|
841
|
+
type: "result",
|
|
842
|
+
status: "standing-orders-stream-event-malformed",
|
|
843
|
+
error: { message: malformedReceiptText("gemini", line) },
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Read only the leading, top-level `type` discriminant from an incomplete
|
|
848
|
+
* JSON object. Provider JSONL dialects put `type` first. Anchoring the match
|
|
849
|
+
* means content later in an oversized event can never spoof classification.
|
|
850
|
+
*/
|
|
851
|
+
function leadingJsonObjectType(prefix) {
|
|
852
|
+
const match = /^\s*\{\s*"type"\s*:\s*"([A-Za-z0-9._-]+)"/.exec(prefix.toString("utf8", 0, Math.min(prefix.length, 256)));
|
|
853
|
+
return match?.[1] ?? null;
|
|
854
|
+
}
|
|
855
|
+
/** Codex emits `item` immediately after the outer type. Only an agent
|
|
856
|
+
* message is load-bearing; completed command/tool telemetry is droppable. */
|
|
857
|
+
function leadingCodexCompletedItemType(prefix) {
|
|
858
|
+
const match = /^\s*\{\s*"type"\s*:\s*"item\.completed"\s*,\s*"item"\s*:\s*\{\s*"type"\s*:\s*"([A-Za-z0-9._-]+)"/.exec(prefix.toString("utf8", 0, Math.min(prefix.length, 512)));
|
|
859
|
+
return match?.[1] ?? null;
|
|
860
|
+
}
|
|
861
|
+
function leadingClaudeSystemSubtype(prefix) {
|
|
862
|
+
const match = /^\s*\{\s*"type"\s*:\s*"system"\s*,\s*"subtype"\s*:\s*"([A-Za-z0-9._-]+)"/.exec(prefix.toString("utf8", 0, Math.min(prefix.length, 512)));
|
|
863
|
+
return match?.[1] ?? null;
|
|
864
|
+
}
|
|
865
|
+
function malformedCodexEventIsLoadBearing(prefix) {
|
|
866
|
+
const type = leadingJsonObjectType(prefix);
|
|
867
|
+
if (type === "thread.started" || type === "turn.completed" || type === "turn.failed")
|
|
868
|
+
return true;
|
|
869
|
+
if (type !== "item.completed")
|
|
870
|
+
return false;
|
|
871
|
+
const itemType = leadingCodexCompletedItemType(prefix);
|
|
872
|
+
return itemType === null || itemType === "agent_message";
|
|
873
|
+
}
|
|
874
|
+
function malformedGeminiEventIsLoadBearing(prefix) {
|
|
875
|
+
const type = leadingJsonObjectType(prefix);
|
|
876
|
+
// These two records authorize the session and the successful terminal.
|
|
877
|
+
// Malformed messages remain diagnostics only; startup prose, tool traffic,
|
|
878
|
+
// and warnings retain the transport's established noise tolerance.
|
|
879
|
+
return type === "init" || type === "result";
|
|
880
|
+
}
|
|
881
|
+
export function runStreamJsonl(file, args, options = {}) {
|
|
882
|
+
const { cwd } = options;
|
|
883
|
+
const childEnv = resolveChildEnv(options);
|
|
884
|
+
return new Promise(resolve => {
|
|
885
|
+
let child;
|
|
886
|
+
let container = null;
|
|
887
|
+
let attachFailure = null;
|
|
888
|
+
try {
|
|
889
|
+
const spawned = spawnStream(file, args, options, [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], childEnv);
|
|
890
|
+
child = spawned.child;
|
|
891
|
+
container = spawned.container;
|
|
892
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
893
|
+
attachFailure = outcome.detail; });
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (options.processGroup === true)
|
|
900
|
+
liveProviders.add(child);
|
|
901
|
+
registerOwned(options.owner, child);
|
|
902
|
+
if (options.processGroup === true && container === null)
|
|
903
|
+
observeProcessTree(child, options);
|
|
904
|
+
let startedLine = null;
|
|
905
|
+
let startedIdentityLine = null;
|
|
906
|
+
let conflictingStartedLine = null;
|
|
907
|
+
let startedSessionId = null;
|
|
908
|
+
let completedLine = null;
|
|
909
|
+
let failedLine = null;
|
|
910
|
+
let overflowLine = null;
|
|
911
|
+
let malformedLine = null;
|
|
912
|
+
let lastMessage = null;
|
|
913
|
+
let stderr = "";
|
|
914
|
+
let timedOut = false;
|
|
915
|
+
let notFound = false;
|
|
916
|
+
let inputFailed = false;
|
|
917
|
+
const keep = (framed) => {
|
|
918
|
+
if (framed.overflowed) {
|
|
919
|
+
// Inspect structural keys across the complete byte stream, including
|
|
920
|
+
// keys AFTER discarded command output. Quoted or nested types cannot
|
|
921
|
+
// spoof these discriminants, and malformed/ambiguous input stays red.
|
|
922
|
+
const type = framed.discriminants?.type ?? null;
|
|
923
|
+
if (type !== null && !["thread.started", "turn.completed", "turn.failed", "item.completed"].includes(type)) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (type === "item.completed") {
|
|
927
|
+
const itemType = framed.discriminants?.itemType;
|
|
928
|
+
if (itemType !== undefined && itemType !== null && ["command_execution", "mcp_tool_call", "web_search", "file_change", "todo_list"].includes(itemType))
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// A load-bearing or unclassifiable hard-cap breach is independently
|
|
932
|
+
// retained as failure. Once those bytes were discarded, a later
|
|
933
|
+
// ordinary message must never turn the run green.
|
|
934
|
+
if (overflowLine === null)
|
|
935
|
+
overflowLine = codexOverflowLine(framed);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const line = framed.prefix.toString("utf8");
|
|
939
|
+
try {
|
|
940
|
+
const event = JSON.parse(line);
|
|
941
|
+
const type = String(event["type"] ?? "");
|
|
942
|
+
if (type === "thread.started") {
|
|
943
|
+
// Keep the last init for the existing transport contract and a
|
|
944
|
+
// bounded pair of earlier witnesses so a conflict cannot hide.
|
|
945
|
+
startedLine = line;
|
|
946
|
+
const id = transportSessionId(event["thread_id"]);
|
|
947
|
+
if (id !== null) {
|
|
948
|
+
// Retain only enough events to prove the identity contract:
|
|
949
|
+
// one usable id when the first init was blank, and one conflict.
|
|
950
|
+
if (startedSessionId === null) {
|
|
951
|
+
startedSessionId = id;
|
|
952
|
+
startedIdentityLine = line;
|
|
953
|
+
}
|
|
954
|
+
else if (id !== startedSessionId && conflictingStartedLine === null) {
|
|
955
|
+
conflictingStartedLine = line;
|
|
956
|
+
}
|
|
957
|
+
if (options.onSessionId !== undefined) {
|
|
958
|
+
try {
|
|
959
|
+
options.onSessionId(id);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
// A registry that cannot be written must not kill the turn.
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else if (type === "turn.completed") {
|
|
968
|
+
completedLine = line;
|
|
969
|
+
}
|
|
970
|
+
else if (type === "turn.failed") {
|
|
971
|
+
// Retain failure independently from completion: seeing either at
|
|
972
|
+
// any point is load-bearing, while one slot per kind stays bounded.
|
|
973
|
+
failedLine = line;
|
|
974
|
+
}
|
|
975
|
+
else if (type === "item.completed") {
|
|
976
|
+
const item = event["item"];
|
|
977
|
+
if (item !== undefined && String(item["type"] ?? "") === "agent_message") {
|
|
978
|
+
lastMessage = line;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (options.onStreamEvent !== undefined) {
|
|
982
|
+
try {
|
|
983
|
+
options.onStreamEvent(event);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// Observational only: a broken listener never touches the run.
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
// Startup prose and ordinary malformed telemetry remain ignorable.
|
|
992
|
+
// Once initialization is proven, however, a recognizable control or
|
|
993
|
+
// answer record cannot disappear merely because its JSON was cut.
|
|
994
|
+
if (startedLine !== null &&
|
|
995
|
+
malformedLine === null &&
|
|
996
|
+
malformedCodexEventIsLoadBearing(framed.prefix)) {
|
|
997
|
+
malformedLine = codexMalformedLine(framed);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
const framing = boundedJsonlFramer(JSONL_EVENT_HARD_CAP, keep, true);
|
|
1002
|
+
const watchdog = streamWatchdog(options, () => {
|
|
1003
|
+
timedOut = true;
|
|
1004
|
+
if (options.processGroup === true)
|
|
1005
|
+
killGroup(child);
|
|
1006
|
+
else
|
|
1007
|
+
child.kill("SIGKILL");
|
|
1008
|
+
});
|
|
1009
|
+
child.stdout?.on("data", (chunk) => {
|
|
1010
|
+
watchdog.touch();
|
|
1011
|
+
framing.push(chunk);
|
|
1012
|
+
});
|
|
1013
|
+
child.stderr?.setEncoding("utf8");
|
|
1014
|
+
child.stderr?.on("data", (chunk) => {
|
|
1015
|
+
watchdog.touch();
|
|
1016
|
+
if (stderr.length < JSONL_STDERR_CAP)
|
|
1017
|
+
stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
|
|
1018
|
+
});
|
|
1019
|
+
let settled = false;
|
|
1020
|
+
const finish = (code) => {
|
|
1021
|
+
if (settled)
|
|
1022
|
+
return;
|
|
1023
|
+
settled = true;
|
|
1024
|
+
watchdog.stop();
|
|
1025
|
+
liveProviders.delete(child);
|
|
1026
|
+
framing.finish();
|
|
1027
|
+
const lines = [
|
|
1028
|
+
startedIdentityLine,
|
|
1029
|
+
conflictingStartedLine,
|
|
1030
|
+
startedLine,
|
|
1031
|
+
completedLine,
|
|
1032
|
+
failedLine,
|
|
1033
|
+
overflowLine,
|
|
1034
|
+
malformedLine,
|
|
1035
|
+
lastMessage,
|
|
1036
|
+
]
|
|
1037
|
+
.filter((line) => line !== null)
|
|
1038
|
+
.filter((line, index, all) => all.indexOf(line) === index);
|
|
1039
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
1040
|
+
void settlement.then(empty => resolve({
|
|
1041
|
+
code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : inputFailed ? 1 : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : (code ?? 1),
|
|
1042
|
+
stdout: lines.join("\n"),
|
|
1043
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
1044
|
+
timedOut,
|
|
1045
|
+
notFound,
|
|
1046
|
+
...containmentOf(container, empty, attachFailure),
|
|
1047
|
+
}));
|
|
1048
|
+
};
|
|
1049
|
+
// A failed spawn fires 'error' and may never fire 'close' — both routes
|
|
1050
|
+
// settle, exactly once.
|
|
1051
|
+
child.on("error", error => {
|
|
1052
|
+
notFound = error.code === "ENOENT";
|
|
1053
|
+
if (stderr === "")
|
|
1054
|
+
stderr = String(error);
|
|
1055
|
+
finish(null);
|
|
1056
|
+
});
|
|
1057
|
+
child.on("close", code => finish(code));
|
|
1058
|
+
if (options.stdin !== undefined && child.stdin !== null) {
|
|
1059
|
+
// Even an exit-0 child cannot prove a review if prompt delivery failed.
|
|
1060
|
+
// Handle EPIPE without crashing the supervisor, then wait for close.
|
|
1061
|
+
child.stdin.on("error", error => {
|
|
1062
|
+
inputFailed = true;
|
|
1063
|
+
stderr = `review prompt input failed: ${String(error)}`.slice(0, JSONL_STDERR_CAP);
|
|
1064
|
+
if (options.processGroup === true)
|
|
1065
|
+
killGroup(child);
|
|
1066
|
+
else
|
|
1067
|
+
child.kill("SIGKILL");
|
|
1068
|
+
});
|
|
1069
|
+
child.stdin.end(options.stdin, "utf8");
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* The streaming transport for gemini's stream-json dialect (Phase 3 D1/A7).
|
|
1075
|
+
*
|
|
1076
|
+
* Retention: a bounded `init` identity witness (the first init signal, first
|
|
1077
|
+
* usable session id, and first conflicting id), the FIRST
|
|
1078
|
+
* `severity:"error"` error event (diagnostics),
|
|
1079
|
+
* the LAST `result` event (tokens + structural status), and the assistant's
|
|
1080
|
+
* text — which gemini emits only as many `message` delta lines — ASSEMBLED
|
|
1081
|
+
* into one line of a dedicated internal schema the real CLI cannot emit:
|
|
1082
|
+
*
|
|
1083
|
+
* {"type":"synthetic_message","content":"...","truncated":true?}
|
|
1084
|
+
*
|
|
1085
|
+
* Assembly rules (round-3 f9): only `role:"assistant"` events with
|
|
1086
|
+
* `delta === true` concatenate; a non-delta assistant message REPLACES the
|
|
1087
|
+
* buffer (full-message semantics); non-string content is dropped; the cap
|
|
1088
|
+
* is enforced on the SERIALIZED UTF-8 BYTES of the complete synthetic line
|
|
1089
|
+
* (marker included), so it can never violate the runner's own line
|
|
1090
|
+
* discipline. Everything else — user echoes, tool_use, tool_result,
|
|
1091
|
+
* warnings — is dropped. Timeout, process-group, kill, and spawn
|
|
1092
|
+
* semantics match `runStreamJsonl`.
|
|
1093
|
+
*/
|
|
1094
|
+
export function runGeminiStreamJsonl(file, args, options = {}) {
|
|
1095
|
+
const { cwd } = options;
|
|
1096
|
+
const childEnv = resolveChildEnv(options);
|
|
1097
|
+
return new Promise(resolve => {
|
|
1098
|
+
let child;
|
|
1099
|
+
let container = null;
|
|
1100
|
+
let attachFailure = null;
|
|
1101
|
+
try {
|
|
1102
|
+
const spawned = spawnStream(file, args, options, ["ignore", "pipe", "pipe"], childEnv);
|
|
1103
|
+
child = spawned.child;
|
|
1104
|
+
container = spawned.container;
|
|
1105
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
1106
|
+
attachFailure = outcome.detail; });
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (options.processGroup === true)
|
|
1113
|
+
liveProviders.add(child);
|
|
1114
|
+
registerOwned(options.owner, child);
|
|
1115
|
+
if (options.processGroup === true && container === null)
|
|
1116
|
+
observeProcessTree(child, options);
|
|
1117
|
+
let initLine = null;
|
|
1118
|
+
let initIdentityLine = null;
|
|
1119
|
+
let conflictingInitLine = null;
|
|
1120
|
+
let initSessionId = null;
|
|
1121
|
+
let errorLine = null;
|
|
1122
|
+
let resultLine = null;
|
|
1123
|
+
let message = "";
|
|
1124
|
+
let messageTruncated = false;
|
|
1125
|
+
let stderr = "";
|
|
1126
|
+
let timedOut = false;
|
|
1127
|
+
let notFound = false;
|
|
1128
|
+
let overflowLine = null;
|
|
1129
|
+
let malformedLine = null;
|
|
1130
|
+
const keep = (framed) => {
|
|
1131
|
+
if (framed.overflowed) {
|
|
1132
|
+
const type = leadingJsonObjectType(framed.prefix);
|
|
1133
|
+
// Tool plumbing is never a terminal or answer. All load-bearing and
|
|
1134
|
+
// unknown oversized events latch a terminal protocol failure so a
|
|
1135
|
+
// later small success cannot hide discarded provider bytes.
|
|
1136
|
+
if (type !== "tool_use" && type !== "tool_result") {
|
|
1137
|
+
if (overflowLine === null)
|
|
1138
|
+
overflowLine = geminiOverflowLine(framed);
|
|
1139
|
+
}
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const line = framed.prefix.toString("utf8");
|
|
1143
|
+
let event;
|
|
1144
|
+
try {
|
|
1145
|
+
event = JSON.parse(line);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
if (initLine !== null &&
|
|
1149
|
+
malformedLine === null &&
|
|
1150
|
+
malformedGeminiEventIsLoadBearing(framed.prefix)) {
|
|
1151
|
+
malformedLine = geminiMalformedLine(framed);
|
|
1152
|
+
}
|
|
1153
|
+
return; // startup prose and non-load-bearing malformed noise are dropped
|
|
1154
|
+
}
|
|
1155
|
+
if (event === null || typeof event !== "object")
|
|
1156
|
+
return;
|
|
1157
|
+
const type = String(event["type"] ?? "");
|
|
1158
|
+
if (type === "init") {
|
|
1159
|
+
if (initLine === null)
|
|
1160
|
+
initLine = line;
|
|
1161
|
+
const id = transportSessionId(event["session_id"]);
|
|
1162
|
+
let identityWitness = false;
|
|
1163
|
+
if (id !== null) {
|
|
1164
|
+
if (initSessionId === null) {
|
|
1165
|
+
initSessionId = id;
|
|
1166
|
+
initIdentityLine = line;
|
|
1167
|
+
identityWitness = true;
|
|
1168
|
+
}
|
|
1169
|
+
else if (id !== initSessionId && conflictingInitLine === null) {
|
|
1170
|
+
conflictingInitLine = line;
|
|
1171
|
+
identityWitness = true;
|
|
1172
|
+
}
|
|
1173
|
+
if (identityWitness && options.onSessionId !== undefined) {
|
|
1174
|
+
try {
|
|
1175
|
+
options.onSessionId(id);
|
|
1176
|
+
}
|
|
1177
|
+
catch {
|
|
1178
|
+
// A registry that cannot be written must not kill the turn.
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
else if (type === "result") {
|
|
1184
|
+
resultLine = line; // last one wins
|
|
1185
|
+
}
|
|
1186
|
+
else if (type === "error") {
|
|
1187
|
+
if (errorLine === null && String(event["severity"] ?? "") === "error")
|
|
1188
|
+
errorLine = line;
|
|
1189
|
+
}
|
|
1190
|
+
else if (type === "message" && String(event["role"] ?? "") === "assistant") {
|
|
1191
|
+
const content = event["content"];
|
|
1192
|
+
if (typeof content !== "string")
|
|
1193
|
+
return; // non-string content: dropped
|
|
1194
|
+
if (event["delta"] === true) {
|
|
1195
|
+
if (!messageTruncated) {
|
|
1196
|
+
message += content;
|
|
1197
|
+
if (Buffer.byteLength(message, "utf8") > JSONL_LINE_CAP)
|
|
1198
|
+
messageTruncated = true;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
// A full (non-delta) assistant message replaces the buffer.
|
|
1203
|
+
message = content;
|
|
1204
|
+
messageTruncated = Buffer.byteLength(message, "utf8") > JSONL_LINE_CAP;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (options.onStreamEvent !== undefined) {
|
|
1208
|
+
try {
|
|
1209
|
+
options.onStreamEvent(event);
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
// Observational only: a broken listener never touches the run.
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
const framing = boundedJsonlFramer(JSONL_EVENT_HARD_CAP, keep);
|
|
1217
|
+
const watchdog = streamWatchdog(options, () => {
|
|
1218
|
+
timedOut = true;
|
|
1219
|
+
if (options.processGroup === true)
|
|
1220
|
+
killGroup(child);
|
|
1221
|
+
else
|
|
1222
|
+
child.kill("SIGKILL");
|
|
1223
|
+
});
|
|
1224
|
+
child.stdout?.on("data", (chunk) => {
|
|
1225
|
+
watchdog.touch();
|
|
1226
|
+
framing.push(chunk);
|
|
1227
|
+
});
|
|
1228
|
+
child.stderr?.setEncoding("utf8");
|
|
1229
|
+
child.stderr?.on("data", (chunk) => {
|
|
1230
|
+
watchdog.touch();
|
|
1231
|
+
if (stderr.length < JSONL_STDERR_CAP)
|
|
1232
|
+
stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
|
|
1233
|
+
});
|
|
1234
|
+
let settled = false;
|
|
1235
|
+
const finish = (code) => {
|
|
1236
|
+
if (settled)
|
|
1237
|
+
return;
|
|
1238
|
+
settled = true;
|
|
1239
|
+
watchdog.stop();
|
|
1240
|
+
liveProviders.delete(child);
|
|
1241
|
+
framing.finish();
|
|
1242
|
+
const lines = [initLine, initIdentityLine, conflictingInitLine]
|
|
1243
|
+
.filter((line) => line !== null)
|
|
1244
|
+
.filter((line, index, all) => all.indexOf(line) === index);
|
|
1245
|
+
if (errorLine !== null)
|
|
1246
|
+
lines.push(errorLine);
|
|
1247
|
+
if (message !== "")
|
|
1248
|
+
lines.push(syntheticMessageLine(message, messageTruncated));
|
|
1249
|
+
if (resultLine !== null)
|
|
1250
|
+
lines.push(resultLine);
|
|
1251
|
+
// Keep the overflow last: gemini's contract is last-result-wins, and
|
|
1252
|
+
// no later small event may erase evidence that a record was discarded.
|
|
1253
|
+
if (overflowLine !== null)
|
|
1254
|
+
lines.push(overflowLine);
|
|
1255
|
+
// A recognizable, malformed terminal is just as conclusive as an
|
|
1256
|
+
// oversized one. Keep it last so last-result-wins cannot erase it.
|
|
1257
|
+
if (malformedLine !== null)
|
|
1258
|
+
lines.push(malformedLine);
|
|
1259
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
1260
|
+
void settlement.then(empty => resolve({
|
|
1261
|
+
code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : (code ?? 1),
|
|
1262
|
+
stdout: lines.join("\n"),
|
|
1263
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
1264
|
+
timedOut,
|
|
1265
|
+
notFound,
|
|
1266
|
+
...containmentOf(container, empty, attachFailure),
|
|
1267
|
+
}));
|
|
1268
|
+
};
|
|
1269
|
+
child.on("error", error => {
|
|
1270
|
+
notFound = error.code === "ENOENT";
|
|
1271
|
+
if (stderr === "")
|
|
1272
|
+
stderr = String(error);
|
|
1273
|
+
finish(null);
|
|
1274
|
+
});
|
|
1275
|
+
child.on("close", code => finish(code));
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const TRUNCATION_MARKER = "…[truncated]";
|
|
1279
|
+
/**
|
|
1280
|
+
* One serialized synthetic line, guaranteed ≤ JSONL_LINE_CAP in UTF-8
|
|
1281
|
+
* bytes — the trim loop measures the COMPLETE serialized line (escaping
|
|
1282
|
+
* and marker included), so pathological escaping cannot smuggle it past
|
|
1283
|
+
* the cap (round-3 f9).
|
|
1284
|
+
*/
|
|
1285
|
+
function syntheticMessageLine(content, truncated) {
|
|
1286
|
+
let body = content;
|
|
1287
|
+
let marked = truncated;
|
|
1288
|
+
for (;;) {
|
|
1289
|
+
const line = JSON.stringify(marked
|
|
1290
|
+
? { type: "synthetic_message", content: body + TRUNCATION_MARKER, truncated: true }
|
|
1291
|
+
: { type: "synthetic_message", content: body });
|
|
1292
|
+
const over = Buffer.byteLength(line, "utf8") - JSONL_LINE_CAP;
|
|
1293
|
+
if (over <= 0)
|
|
1294
|
+
return line;
|
|
1295
|
+
marked = true;
|
|
1296
|
+
// Cut at least the overage in UTF-16 units; the loop re-measures.
|
|
1297
|
+
const cut = Math.max(1, over);
|
|
1298
|
+
body = body.slice(0, Math.max(0, body.length - cut));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* The streaming transport for claude's stream-json dialect (arc 1).
|
|
1303
|
+
*
|
|
1304
|
+
* Same rationale as `runStreamJsonl`: a long agent session writes far more
|
|
1305
|
+
* event stream than any fixed buffer should hold, and the line that matters
|
|
1306
|
+
* — the terminal result carrying session, usage, and dollars — arrives
|
|
1307
|
+
* LAST. The 8 MiB buffered kill would destroy exactly that accounting
|
|
1308
|
+
* envelope, so stdout is consumed incrementally and only the load-bearing
|
|
1309
|
+
* lines are retained:
|
|
1310
|
+
*
|
|
1311
|
+
* - a bounded identity witness for `system`/`init`: the first init signal,
|
|
1312
|
+
* the first usable session id, and one conflicting id
|
|
1313
|
+
* - the FIRST primary `result` event, selected STRUCTURALLY (finding 10):
|
|
1314
|
+
* origin absent or `origin.kind === "human"` — an allowlist, so a
|
|
1315
|
+
* background task's result (or any future origin kind) can never be
|
|
1316
|
+
* mistaken for the main query's accounting. Later results are dropped.
|
|
1317
|
+
*
|
|
1318
|
+
* The result's stdout is the retained lines joined — a synthetic, bounded
|
|
1319
|
+
* envelope `claudeParse` reads exactly like test fixtures. Timeout,
|
|
1320
|
+
* process-group, kill, and spawn semantics match `runStreamJsonl`.
|
|
1321
|
+
*/
|
|
1322
|
+
function primaryResultOrigin(event) {
|
|
1323
|
+
const origin = event["origin"];
|
|
1324
|
+
if (origin === undefined || origin === null)
|
|
1325
|
+
return true;
|
|
1326
|
+
if (typeof origin === "object" && String(origin["kind"] ?? "") === "human")
|
|
1327
|
+
return true;
|
|
1328
|
+
return false; // task-notification, channel, peer, coordinator, unknown: never the envelope
|
|
1329
|
+
}
|
|
1330
|
+
export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
1331
|
+
const { cwd } = options;
|
|
1332
|
+
const childEnv = resolveChildEnv(options);
|
|
1333
|
+
return new Promise(resolve => {
|
|
1334
|
+
let child;
|
|
1335
|
+
let container = null;
|
|
1336
|
+
let attachFailure = null;
|
|
1337
|
+
try {
|
|
1338
|
+
const spawned = spawnStream(file, args, options, ["ignore", "pipe", "pipe"], childEnv);
|
|
1339
|
+
child = spawned.child;
|
|
1340
|
+
container = spawned.container;
|
|
1341
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
1342
|
+
attachFailure = outcome.detail; });
|
|
1343
|
+
}
|
|
1344
|
+
catch (error) {
|
|
1345
|
+
void reapRejectedSpawn(rejectedChild(error, child), options.processGroup === true).then(() => resolve(refusedResult(error)));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (options.processGroup === true)
|
|
1349
|
+
liveProviders.add(child);
|
|
1350
|
+
registerOwned(options.owner, child);
|
|
1351
|
+
if (options.processGroup === true && container === null)
|
|
1352
|
+
observeProcessTree(child, options);
|
|
1353
|
+
let initLine = null;
|
|
1354
|
+
let initIdentityLine = null;
|
|
1355
|
+
let conflictingInitLine = null;
|
|
1356
|
+
let initSessionId = null;
|
|
1357
|
+
let resultLine = null;
|
|
1358
|
+
let overflowLine = null;
|
|
1359
|
+
let receiptFired = false;
|
|
1360
|
+
let stderr = "";
|
|
1361
|
+
let timedOut = false;
|
|
1362
|
+
let notFound = false;
|
|
1363
|
+
// Latch BEFORE invoking (finding 13): a throwing listener has already
|
|
1364
|
+
// consumed its one firing, and the stream goes on unharmed.
|
|
1365
|
+
const fireReceipt = () => {
|
|
1366
|
+
if (receiptFired || options.onReceipt === undefined) {
|
|
1367
|
+
receiptFired = true;
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
receiptFired = true;
|
|
1371
|
+
try {
|
|
1372
|
+
options.onReceipt();
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
// A receipt that cannot be recorded must not alter the run; the
|
|
1376
|
+
// note stays honestly unreceipted and re-attaches later.
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
const keep = (framed) => {
|
|
1380
|
+
if (framed.overflowed) {
|
|
1381
|
+
// An oversized result is the first primary candidate and therefore
|
|
1382
|
+
// closes the slot fail-closed; an oversized non-result is merely a
|
|
1383
|
+
// fallback that a later parseable primary result can supersede.
|
|
1384
|
+
// Claude's parser trusts the first primary result. When a line is
|
|
1385
|
+
// too large to classify, reserve that first slot fail-closed rather
|
|
1386
|
+
// than let a later small result hide emitted bytes we discarded.
|
|
1387
|
+
const type = leadingJsonObjectType(framed.prefix);
|
|
1388
|
+
if (type !== null && type !== "result") {
|
|
1389
|
+
if (type !== "system")
|
|
1390
|
+
return;
|
|
1391
|
+
const subtype = leadingClaudeSystemSubtype(framed.prefix);
|
|
1392
|
+
if (subtype !== null && subtype !== "init")
|
|
1393
|
+
return;
|
|
1394
|
+
// An init too large to inspect may conceal a conflicting session
|
|
1395
|
+
// identity. Latch it independently from the primary result so the
|
|
1396
|
+
// same event set fails regardless of whether it arrived before or
|
|
1397
|
+
// after the terminal, while a normal terminal can still account
|
|
1398
|
+
// for the turn's usage.
|
|
1399
|
+
if (overflowLine === null)
|
|
1400
|
+
overflowLine = claudeOverflowLine(framed);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (resultLine === null)
|
|
1404
|
+
resultLine = claudeOverflowLine(framed);
|
|
1405
|
+
else if (overflowLine === null)
|
|
1406
|
+
overflowLine = claudeOverflowLine(framed);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const line = framed.prefix.toString("utf8");
|
|
1410
|
+
let event;
|
|
1411
|
+
try {
|
|
1412
|
+
event = JSON.parse(line);
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
return; // not JSON: not an event; dropped
|
|
1416
|
+
}
|
|
1417
|
+
if (event === null || typeof event !== "object")
|
|
1418
|
+
return;
|
|
1419
|
+
const type = String(event["type"] ?? "");
|
|
1420
|
+
if (type === "system" && String(event["subtype"] ?? "") === "init") {
|
|
1421
|
+
if (initLine === null)
|
|
1422
|
+
initLine = line;
|
|
1423
|
+
const id = transportSessionId(event["session_id"]);
|
|
1424
|
+
let identityWitness = false;
|
|
1425
|
+
if (id !== null) {
|
|
1426
|
+
if (initSessionId === null) {
|
|
1427
|
+
initSessionId = id;
|
|
1428
|
+
initIdentityLine = line;
|
|
1429
|
+
identityWitness = true;
|
|
1430
|
+
}
|
|
1431
|
+
else if (id !== initSessionId && conflictingInitLine === null) {
|
|
1432
|
+
conflictingInitLine = line;
|
|
1433
|
+
identityWitness = true;
|
|
1434
|
+
}
|
|
1435
|
+
if (identityWitness && options.onSessionId !== undefined) {
|
|
1436
|
+
try {
|
|
1437
|
+
options.onSessionId(id);
|
|
1438
|
+
}
|
|
1439
|
+
catch {
|
|
1440
|
+
// A registry that cannot be written must not kill the turn.
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
else if (type === "assistant") {
|
|
1446
|
+
// Top-level only (finding 11): assistant events carry no origin;
|
|
1447
|
+
// parent_tool_use_id is the correlation field, and a subagent's
|
|
1448
|
+
// words prove nothing about the main prompt.
|
|
1449
|
+
const parent = event["parent_tool_use_id"];
|
|
1450
|
+
if (parent === undefined || parent === null)
|
|
1451
|
+
fireReceipt();
|
|
1452
|
+
}
|
|
1453
|
+
else if (type === "rate_limit_event") {
|
|
1454
|
+
// v105: the plan's usage windows, as Claude says them each turn (the Tasks page's limits).
|
|
1455
|
+
noteLimits(claudeLimitsOf(event));
|
|
1456
|
+
}
|
|
1457
|
+
else if (type === "result" && resultLine === null && primaryResultOrigin(event)) {
|
|
1458
|
+
resultLine = line;
|
|
1459
|
+
// Fallback receipt (finding 11): a SUCCESSFUL main-query completion
|
|
1460
|
+
// entails the prompt ran even if the stream elided assistant
|
|
1461
|
+
// events. Error results never fire — startup death is not delivery.
|
|
1462
|
+
if (event["is_error"] !== true && String(event["subtype"] ?? "") === "success")
|
|
1463
|
+
fireReceipt();
|
|
1464
|
+
}
|
|
1465
|
+
if (options.onStreamEvent !== undefined) {
|
|
1466
|
+
try {
|
|
1467
|
+
options.onStreamEvent(event);
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
// Observational only: a broken listener never touches the run.
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const framing = boundedJsonlFramer(JSONL_EVENT_HARD_CAP, keep);
|
|
1475
|
+
const watchdog = streamWatchdog(options, () => {
|
|
1476
|
+
timedOut = true;
|
|
1477
|
+
if (options.processGroup === true)
|
|
1478
|
+
killGroup(child);
|
|
1479
|
+
else
|
|
1480
|
+
child.kill("SIGKILL");
|
|
1481
|
+
});
|
|
1482
|
+
child.stdout?.on("data", (chunk) => {
|
|
1483
|
+
watchdog.touch();
|
|
1484
|
+
framing.push(chunk);
|
|
1485
|
+
});
|
|
1486
|
+
child.stderr?.setEncoding("utf8");
|
|
1487
|
+
child.stderr?.on("data", (chunk) => {
|
|
1488
|
+
watchdog.touch();
|
|
1489
|
+
if (stderr.length < JSONL_STDERR_CAP)
|
|
1490
|
+
stderr += chunk.slice(0, JSONL_STDERR_CAP - stderr.length);
|
|
1491
|
+
});
|
|
1492
|
+
let settled = false;
|
|
1493
|
+
const finish = (code) => {
|
|
1494
|
+
if (settled)
|
|
1495
|
+
return;
|
|
1496
|
+
settled = true;
|
|
1497
|
+
watchdog.stop();
|
|
1498
|
+
liveProviders.delete(child);
|
|
1499
|
+
framing.finish();
|
|
1500
|
+
const lines = [initLine, initIdentityLine, conflictingInitLine, resultLine, overflowLine]
|
|
1501
|
+
.filter((one) => one !== null)
|
|
1502
|
+
.filter((line, index, all) => all.indexOf(line) === index);
|
|
1503
|
+
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
1504
|
+
void settlement.then(empty => resolve({
|
|
1505
|
+
code: notFound ? NOT_FOUND_CODE : timedOut ? TIMEOUT_CODE : attachFailure !== null || empty === false ? CONTAINMENT_REFUSED_CODE : (code ?? 1),
|
|
1506
|
+
stdout: lines.join("\n"),
|
|
1507
|
+
stderr: empty === false ? `${stderr}\nNative process containment could not prove all descendants exited.`.trim() : attachFailure !== null && stderr === "" ? attachFailure : stderr,
|
|
1508
|
+
timedOut,
|
|
1509
|
+
notFound,
|
|
1510
|
+
...containmentOf(container, empty, attachFailure),
|
|
1511
|
+
}));
|
|
1512
|
+
};
|
|
1513
|
+
child.on("error", error => {
|
|
1514
|
+
notFound = error.code === "ENOENT";
|
|
1515
|
+
if (stderr === "")
|
|
1516
|
+
stderr = String(error);
|
|
1517
|
+
finish(null);
|
|
1518
|
+
});
|
|
1519
|
+
child.on("close", code => finish(code));
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
// ---- the held session transport (Parity II Phase 2, spec v2 S0a + v6 W9) ----
|
|
1523
|
+
/**
|
|
1524
|
+
* Where the supervisor script lives: a plain-.mjs sibling of this module in
|
|
1525
|
+
* BOTH layouts (src/ under tsx, dist/ after build — postbuild copies it).
|
|
1526
|
+
*/
|
|
1527
|
+
export function supervisorPath() {
|
|
1528
|
+
return fileURLToPath(new URL("./supervisor.mjs", import.meta.url));
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Darwin's sun_path is 104 bytes including the terminator; Linux allows a
|
|
1532
|
+
* hair more. Refusing at spawn beats failing inside bind() with a raw
|
|
1533
|
+
* ENAMETOOLONG after custody was already recorded.
|
|
1534
|
+
*/
|
|
1535
|
+
export const HELD_SOCKET_PATH_LIMIT = 103;
|
|
1536
|
+
export function heldSocketPathProblem(path) {
|
|
1537
|
+
const bytes = Buffer.byteLength(path, "utf8");
|
|
1538
|
+
if (bytes > HELD_SOCKET_PATH_LIMIT) {
|
|
1539
|
+
return `the control socket path is ${bytes} bytes — the platform limit is ${HELD_SOCKET_PATH_LIMIT}: ${path}`;
|
|
1540
|
+
}
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Hold one claude process open across stream-json turns, under a
|
|
1545
|
+
* supervisor whose parenthood makes the eventual kill provable. Returns
|
|
1546
|
+
* once the supervisor's control frame settles the spawn either way.
|
|
1547
|
+
*
|
|
1548
|
+
* No timers live here: the per-turn wall deadline, liveness lapse, and
|
|
1549
|
+
* absolute expiry are the coordinator's exact timers (v2 S1d) — this
|
|
1550
|
+
* transport only moves bytes and counts the protocol's own marks.
|
|
1551
|
+
*/
|
|
1552
|
+
export function startClaudeHeldSession(file, args, options) {
|
|
1553
|
+
const pathProblem = heldSocketPathProblem(options.socketPath);
|
|
1554
|
+
if (pathProblem !== null) {
|
|
1555
|
+
return Promise.resolve({ ok: false, reason: "socket-path", message: pathProblem });
|
|
1556
|
+
}
|
|
1557
|
+
const childEnv = resolveChildEnv(options) ?? { ...process.env };
|
|
1558
|
+
const events = options.events ?? {};
|
|
1559
|
+
return new Promise(resolveStart => {
|
|
1560
|
+
let supervisor;
|
|
1561
|
+
let container = null;
|
|
1562
|
+
let attachFailure = null;
|
|
1563
|
+
try {
|
|
1564
|
+
// The supervisor spawns INSIDE the OS object, so the agent it holds
|
|
1565
|
+
// and every tool the agent launches are members too: one kill reaches
|
|
1566
|
+
// the whole hold, and the object's empty state proves the hold over.
|
|
1567
|
+
const spawned = spawnContained(process.execPath, [supervisorPath(), file, ...args], {
|
|
1568
|
+
cwd: options.cwd,
|
|
1569
|
+
env: {
|
|
1570
|
+
...childEnv,
|
|
1571
|
+
SO_HELD_SOCKET: options.socketPath,
|
|
1572
|
+
SO_HELD_COOKIE: options.cookie,
|
|
1573
|
+
SO_HELD_GRACE_MS: String(options.graceMs ?? 10_000),
|
|
1574
|
+
SO_HELD_DIAGNOSTIC_FD: currentContainment().mode === "observed" ? "3" : "",
|
|
1575
|
+
},
|
|
1576
|
+
stdio: currentContainment().mode === "observed" ? ["pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
|
|
1577
|
+
detached: false,
|
|
1578
|
+
}, options.owner ?? "held", {
|
|
1579
|
+
beforeSpawn: options.beforeSpawn,
|
|
1580
|
+
onSpawn: options.onSpawn,
|
|
1581
|
+
onContainer: options.onContainer,
|
|
1582
|
+
onContainerEmpty: options.onContainerEmpty,
|
|
1583
|
+
onUnknown: options.onUnknown,
|
|
1584
|
+
fence: options.fence,
|
|
1585
|
+
});
|
|
1586
|
+
supervisor = spawned.child;
|
|
1587
|
+
container = spawned.container;
|
|
1588
|
+
void spawned.attached.then(outcome => { if (!outcome.ok)
|
|
1589
|
+
attachFailure = outcome.detail; });
|
|
1590
|
+
if (container === null)
|
|
1591
|
+
observeProcessTree(supervisor, options);
|
|
1592
|
+
}
|
|
1593
|
+
catch (error) {
|
|
1594
|
+
const left = rejectedChild(error, supervisor);
|
|
1595
|
+
const refused = () => resolveStart({ ok: false, reason: "spawn-failed", message: String(error instanceof SpawnCustodyFailure ? error.reason : error) });
|
|
1596
|
+
if (left === undefined)
|
|
1597
|
+
refused();
|
|
1598
|
+
else {
|
|
1599
|
+
left.once("close", refused);
|
|
1600
|
+
left.on("error", () => { });
|
|
1601
|
+
killGroup(left);
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
// This pipe belongs to the supervisor, not the agent's relayed stream.
|
|
1606
|
+
// Native containment uses its existing attachment descriptors instead.
|
|
1607
|
+
if (container === null) {
|
|
1608
|
+
const diagnostics = supervisor.stdio[3];
|
|
1609
|
+
let buffer = "", reportedFault = false;
|
|
1610
|
+
const diagnosticFault = () => {
|
|
1611
|
+
if (reportedFault)
|
|
1612
|
+
return;
|
|
1613
|
+
reportedFault = true;
|
|
1614
|
+
// This channel carries diagnostics only. Its failure cannot invent
|
|
1615
|
+
// lost child identity; genuine supervisor uncertainty still exits126.
|
|
1616
|
+
try {
|
|
1617
|
+
options.onObservationFailure?.({ phase: "supervisor", operation: "diagnostic-read", code: "SUPERVISOR_DIAGNOSTIC", rootPid: supervisor.pid ?? null, at: new Date().toISOString(), identityUnknown: false });
|
|
1618
|
+
}
|
|
1619
|
+
catch { }
|
|
1620
|
+
};
|
|
1621
|
+
if (diagnostics !== null && diagnostics !== undefined && "on" in diagnostics) {
|
|
1622
|
+
diagnostics.on("data", (chunk) => {
|
|
1623
|
+
buffer += chunk.toString("utf8");
|
|
1624
|
+
if (buffer.length > 16_384) {
|
|
1625
|
+
buffer = "";
|
|
1626
|
+
diagnosticFault();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
let cut;
|
|
1630
|
+
while ((cut = buffer.indexOf("\n")) !== -1) {
|
|
1631
|
+
const line = buffer.slice(0, cut);
|
|
1632
|
+
buffer = buffer.slice(cut + 1);
|
|
1633
|
+
let failure = null;
|
|
1634
|
+
try {
|
|
1635
|
+
failure = readProcessObservationFailure(JSON.parse(line));
|
|
1636
|
+
}
|
|
1637
|
+
catch { }
|
|
1638
|
+
if (failure === null) {
|
|
1639
|
+
diagnosticFault();
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
options.onObservationFailure?.(failure);
|
|
1644
|
+
}
|
|
1645
|
+
catch { /* No diagnostic can certify exit or clear an existing witness. */ }
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
diagnostics.on("error", diagnosticFault);
|
|
1649
|
+
diagnostics.on("end", () => { if (buffer !== "")
|
|
1650
|
+
diagnosticFault(); });
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
heldSupervisors.set(supervisor, "fresh");
|
|
1654
|
+
let settledStart = false;
|
|
1655
|
+
let agentPgid = null;
|
|
1656
|
+
let initSeq = 0;
|
|
1657
|
+
let resultSeq = 0;
|
|
1658
|
+
let sessionSeen = false;
|
|
1659
|
+
let partial = "";
|
|
1660
|
+
let exitResolve = () => { };
|
|
1661
|
+
const exited = new Promise(pass => {
|
|
1662
|
+
exitResolve = pass;
|
|
1663
|
+
});
|
|
1664
|
+
const readyTimer = setTimeout(() => {
|
|
1665
|
+
if (settledStart)
|
|
1666
|
+
return;
|
|
1667
|
+
settledStart = true;
|
|
1668
|
+
try {
|
|
1669
|
+
killGroup(supervisor);
|
|
1670
|
+
}
|
|
1671
|
+
catch {
|
|
1672
|
+
// Already gone.
|
|
1673
|
+
}
|
|
1674
|
+
resolveStart({ ok: false, reason: "ready-timeout", message: "the supervisor never sent its control frame" });
|
|
1675
|
+
}, options.readyTimeoutMs ?? 10_000);
|
|
1676
|
+
const handle = {
|
|
1677
|
+
supervisorPid: null,
|
|
1678
|
+
agentPgid: null,
|
|
1679
|
+
writeTurn(json) {
|
|
1680
|
+
const stdin = supervisor.stdin;
|
|
1681
|
+
if (stdin === null || stdin.destroyed || !stdin.writable)
|
|
1682
|
+
return false;
|
|
1683
|
+
try {
|
|
1684
|
+
stdin.write(json.endsWith("\n") ? json : `${json}\n`);
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
catch {
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
endInput() {
|
|
1692
|
+
try {
|
|
1693
|
+
supervisor.stdin?.end();
|
|
1694
|
+
}
|
|
1695
|
+
catch {
|
|
1696
|
+
// Already closed.
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
terminate() {
|
|
1700
|
+
try {
|
|
1701
|
+
supervisor.kill("SIGTERM");
|
|
1702
|
+
heldSupervisors.set(supervisor, "termed");
|
|
1703
|
+
}
|
|
1704
|
+
catch {
|
|
1705
|
+
// Already gone.
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
killHard() {
|
|
1709
|
+
try {
|
|
1710
|
+
killGroup(supervisor);
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
// Already gone.
|
|
1714
|
+
}
|
|
1715
|
+
},
|
|
1716
|
+
exited,
|
|
1717
|
+
};
|
|
1718
|
+
const keep = (line) => {
|
|
1719
|
+
if (line.length > JSONL_LINE_CAP)
|
|
1720
|
+
return;
|
|
1721
|
+
let event;
|
|
1722
|
+
try {
|
|
1723
|
+
event = JSON.parse(line);
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (event === null || typeof event !== "object")
|
|
1729
|
+
return;
|
|
1730
|
+
// The control frame settles the start, exactly once, before any
|
|
1731
|
+
// agent byte — the two-hop handshake (v6 W9).
|
|
1732
|
+
if (!settledStart && typeof event["so_supervisor"] === "string") {
|
|
1733
|
+
settledStart = true;
|
|
1734
|
+
clearTimeout(readyTimer);
|
|
1735
|
+
if (event["so_supervisor"] === "ready") {
|
|
1736
|
+
agentPgid = typeof event["agentPgid"] === "number" ? event["agentPgid"] : null;
|
|
1737
|
+
handle.agentPgid = agentPgid;
|
|
1738
|
+
handle.supervisorPid =
|
|
1739
|
+
typeof event["supervisorPid"] === "number" ? event["supervisorPid"] : (supervisor.pid ?? null);
|
|
1740
|
+
resolveStart({ ok: true, handle });
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
const message = typeof event["message"] === "string" ? event["message"] : "spawn failed";
|
|
1744
|
+
resolveStart({ ok: false, reason: "spawn-failed", message });
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof event["so_supervisor"] === "string")
|
|
1749
|
+
return; // never two frames; drop strays
|
|
1750
|
+
const type = String(event["type"] ?? "");
|
|
1751
|
+
if (type === "system" && String(event["subtype"] ?? "") === "init") {
|
|
1752
|
+
initSeq += 1;
|
|
1753
|
+
if (!sessionSeen && events.onSessionId !== undefined) {
|
|
1754
|
+
const id = transportSessionId(event["session_id"]);
|
|
1755
|
+
if (id !== null) {
|
|
1756
|
+
sessionSeen = true;
|
|
1757
|
+
try {
|
|
1758
|
+
events.onSessionId(id);
|
|
1759
|
+
}
|
|
1760
|
+
catch {
|
|
1761
|
+
// A registry that cannot be written must not kill the turn.
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
events.onTurnInit?.(initSeq);
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
// Observational.
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
else if (type === "rate_limit_event") {
|
|
1773
|
+
noteLimits(claudeLimitsOf(event));
|
|
1774
|
+
}
|
|
1775
|
+
else if (type === "result" && primaryResultOrigin(event)) {
|
|
1776
|
+
resultSeq += 1;
|
|
1777
|
+
try {
|
|
1778
|
+
events.onTurnResult?.(resultSeq, event);
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
// Observational.
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
events.onStreamEvent?.(event);
|
|
1786
|
+
}
|
|
1787
|
+
catch {
|
|
1788
|
+
// Observational only, arc-1 rule unchanged.
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
supervisor.stdout?.setEncoding("utf8");
|
|
1792
|
+
supervisor.stdout?.on("data", (chunk) => {
|
|
1793
|
+
partial += chunk;
|
|
1794
|
+
let cut = partial.indexOf("\n");
|
|
1795
|
+
while (cut !== -1) {
|
|
1796
|
+
keep(partial.slice(0, cut));
|
|
1797
|
+
partial = partial.slice(cut + 1);
|
|
1798
|
+
cut = partial.indexOf("\n");
|
|
1799
|
+
}
|
|
1800
|
+
if (partial.length > JSONL_LINE_CAP * 2)
|
|
1801
|
+
partial = partial.slice(-JSONL_LINE_CAP);
|
|
1802
|
+
});
|
|
1803
|
+
supervisor.on("error", error => {
|
|
1804
|
+
heldSupervisors.delete(supervisor);
|
|
1805
|
+
if (!settledStart) {
|
|
1806
|
+
settledStart = true;
|
|
1807
|
+
clearTimeout(readyTimer);
|
|
1808
|
+
resolveStart({ ok: false, reason: "spawn-failed", message: String(error) });
|
|
1809
|
+
}
|
|
1810
|
+
exitResolve({ code: null });
|
|
1811
|
+
});
|
|
1812
|
+
supervisor.on("close", code => {
|
|
1813
|
+
heldSupervisors.delete(supervisor);
|
|
1814
|
+
if (partial.trim() !== "")
|
|
1815
|
+
keep(partial);
|
|
1816
|
+
// The POSIX supervisor could not prove its observed tree drained.
|
|
1817
|
+
// Persist uncertainty before any onExit handler can release custody.
|
|
1818
|
+
if (container === null && code === CONTAINMENT_REFUSED_CODE) {
|
|
1819
|
+
try {
|
|
1820
|
+
options.onUnknown?.();
|
|
1821
|
+
}
|
|
1822
|
+
catch { /* the observer must not prevent settlement */ }
|
|
1823
|
+
}
|
|
1824
|
+
const settlement = container === null ? Promise.resolve(true) : settleContainer(container, options);
|
|
1825
|
+
void settlement.then(empty => {
|
|
1826
|
+
if (!settledStart) {
|
|
1827
|
+
settledStart = true;
|
|
1828
|
+
clearTimeout(readyTimer);
|
|
1829
|
+
resolveStart({ ok: false, reason: "spawn-failed", message: attachFailure ?? "the supervisor exited before its control frame" });
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
events.onExit?.({ code: empty ? code : CONTAINMENT_REFUSED_CODE });
|
|
1833
|
+
}
|
|
1834
|
+
catch {
|
|
1835
|
+
// Observational.
|
|
1836
|
+
}
|
|
1837
|
+
exitResolve({ code: empty ? code : CONTAINMENT_REFUSED_CODE });
|
|
1838
|
+
});
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
}
|