clinkx 0.3.0 → 1.0.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/CHANGELOG.md +33 -0
- package/LICENSE +10 -0
- package/README.md +82 -26
- package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
- package/clinkx-workflows/dist/allowed-roots.js +5 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
- package/clinkx-workflows/dist/artifacts.d.ts +20 -1
- package/clinkx-workflows/dist/artifacts.js +38 -9
- package/clinkx-workflows/dist/artifacts.js.map +1 -1
- package/clinkx-workflows/dist/backend.d.ts +4 -0
- package/clinkx-workflows/dist/citation-check.d.ts +40 -0
- package/clinkx-workflows/dist/citation-check.js +348 -0
- package/clinkx-workflows/dist/citation-check.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +2 -1
- package/clinkx-workflows/dist/clink-client.js +17 -4
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +145 -16
- package/clinkx-workflows/dist/config.js +384 -26
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.d.ts +172 -0
- package/clinkx-workflows/dist/contained-file.js +483 -0
- package/clinkx-workflows/dist/contained-file.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
- package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
- package/clinkx-workflows/dist/context-cache-key.js +91 -0
- package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
- package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
- package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
- package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
- package/clinkx-workflows/dist/context-preflight.d.ts +184 -0
- package/clinkx-workflows/dist/context-preflight.js +91 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +1 -1
- package/clinkx-workflows/dist/council-expander.js +2 -1
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +165 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js +105 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js +480 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +36 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +1047 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +222 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +1178 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +8936 -0
- package/clinkx-workflows/dist/dashboard/contracts.js +1157 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
- package/clinkx-workflows/dist/dashboard/generation.js +41 -0
- package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/index.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/index.js +16 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +101 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js +748 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +303 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +581 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +110 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
- package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +126 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +75 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +1110 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js +431 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2611 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +480 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +522 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +6 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +367 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +25 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
- package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
- package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
- package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
- package/clinkx-workflows/dist/delegate-validate.d.ts +78 -0
- package/clinkx-workflows/dist/delegate-validate.js +97 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
- package/clinkx-workflows/dist/engine.d.ts +141 -0
- package/clinkx-workflows/dist/engine.js +882 -28
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +2 -0
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +1 -1
- package/clinkx-workflows/dist/fingerprint.js +1 -1
- package/clinkx-workflows/dist/index.d.ts +19 -0
- package/clinkx-workflows/dist/index.js +15 -0
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/loader.d.ts +9 -3
- package/clinkx-workflows/dist/loader.js +92 -21
- package/clinkx-workflows/dist/loader.js.map +1 -1
- package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
- package/clinkx-workflows/dist/plan-review/admission.js +290 -0
- package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
- package/clinkx-workflows/dist/plan-review/contract.js +289 -0
- package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
- package/clinkx-workflows/dist/plan-review/digest.js +241 -0
- package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
- package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
- package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
- package/clinkx-workflows/dist/plan-review/expander.js +243 -0
- package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
- package/clinkx-workflows/dist/plan-review/projector.js +437 -0
- package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
- package/clinkx-workflows/dist/plan-review/publication.js +182 -0
- package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
- package/clinkx-workflows/dist/plan-review/resume.js +122 -0
- package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
- package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
- package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
- package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
- package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
- package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
- package/clinkx-workflows/dist/post-processors/index.js +21 -0
- package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
- package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
- package/clinkx-workflows/dist/prompt-budget.js +7 -1
- package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.js +2 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
- package/clinkx-workflows/dist/run-summary.d.ts +13 -0
- package/clinkx-workflows/dist/run-summary.js +29 -1
- package/clinkx-workflows/dist/run-summary.js.map +1 -1
- package/clinkx-workflows/dist/schema.d.ts +76 -21
- package/clinkx-workflows/dist/schema.js +10 -1
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
- package/clinkx-workflows/dist/seed-terms.js +176 -0
- package/clinkx-workflows/dist/seed-terms.js.map +1 -0
- package/clinkx-workflows/dist/server.d.ts +5 -2
- package/clinkx-workflows/dist/server.js +82 -52
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +18 -20
- package/clinkx-workflows/dist/shutdown.js +43 -61
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +38 -0
- package/clinkx-workflows/dist/state.d.ts +170 -3
- package/clinkx-workflows/dist/state.js +1275 -28
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/transport.d.ts +15 -0
- package/clinkx-workflows/dist/transport.js +19 -10
- package/clinkx-workflows/dist/transport.js.map +1 -1
- package/clinkx-workflows/dist/types.d.ts +14 -2
- package/clinkx-workflows/dist/validators/council.d.ts +318 -85
- package/clinkx-workflows/dist/validators/council.js +52 -13
- package/clinkx-workflows/dist/validators/council.js.map +1 -1
- package/clinkx-workflows/dist/validators/index.d.ts +1 -0
- package/clinkx-workflows/dist/validators/index.js +3 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
- package/clinkx-workflows/dist/validators/plan-review.js +316 -0
- package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
- package/clinkx-workflows/dist/workflow-receipt.js +188 -4
- package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
- package/clinkx-workflows/dist/workflow-tools.d.ts +48 -3
- package/clinkx-workflows/dist/workflow-tools.js +1402 -59
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
- package/clinkx-workflows/dist/workspace-signature.js +388 -0
- package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +2 -1
- package/clinkx-workflows/templates/council/code-review-chairman.txt +13 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +4 -1
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +19 -3
- package/clinkx-workflows/templates/council/debug-debugger.txt +2 -1
- package/clinkx-workflows/templates/council/discover-analyst.txt +2 -1
- package/clinkx-workflows/templates/council/member.txt +2 -1
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +68 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +51 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +60 -0
- package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
- package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
- package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
- package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
- package/clinkx-workflows/workflows/council-answer.yaml +97 -8
- package/clinkx-workflows/workflows/council-code-review.yaml +103 -8
- package/clinkx-workflows/workflows/council-debug.yaml +97 -8
- package/clinkx-workflows/workflows/council-default.yaml +97 -8
- package/clinkx-workflows/workflows/council-discover.yaml +97 -8
- package/clinkx-workflows/workflows/council-plan-review.yaml +250 -0
- package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
- package/conf/adapters/agy.json +49 -0
- package/conf/adapters/claude.json +5 -0
- package/conf/adapters/codex-app-server.json +44 -0
- package/conf/adapters/codex.json +7 -2
- package/conf/adapters/glm.json +5 -0
- package/conf/adapters/grok.json +60 -0
- package/conf/adapters/hapi/agy.json +52 -0
- package/conf/adapters/hapi/claude.json +5 -0
- package/conf/adapters/hapi/codex.json +7 -2
- package/conf/adapters/hapi/glm.json +5 -0
- package/dist/activity-journal.d.ts +42 -0
- package/dist/activity-journal.js +381 -0
- package/dist/activity-journal.js.map +1 -0
- package/dist/allowed-roots.d.ts +41 -0
- package/dist/allowed-roots.js +125 -3
- package/dist/allowed-roots.js.map +1 -1
- package/dist/codex/activity-session-log.d.ts +14 -0
- package/dist/codex/activity-session-log.js +206 -0
- package/dist/codex/activity-session-log.js.map +1 -0
- package/dist/codex/agent-tools.d.ts +23 -0
- package/dist/codex/agent-tools.js +1270 -0
- package/dist/codex/agent-tools.js.map +1 -0
- package/dist/codex/app-server-client.d.ts +76 -0
- package/dist/codex/app-server-client.js +582 -0
- package/dist/codex/app-server-client.js.map +1 -0
- package/dist/codex/app-server-registry.d.ts +389 -0
- package/dist/codex/app-server-registry.js +1407 -0
- package/dist/codex/app-server-registry.js.map +1 -0
- package/dist/codex/client-types.d.ts +109 -0
- package/dist/codex/client-types.js +2 -0
- package/dist/codex/client-types.js.map +1 -0
- package/dist/codex/protocol.d.ts +5375 -0
- package/dist/codex/protocol.js +614 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/codex/session.d.ts +263 -0
- package/dist/codex/session.js +466 -0
- package/dist/codex/session.js.map +1 -0
- package/dist/config.d.ts +185 -19
- package/dist/config.js +82 -16
- package/dist/config.js.map +1 -1
- package/dist/context-builder/anchors.d.ts +1 -0
- package/dist/context-builder/anchors.js +3 -0
- package/dist/context-builder/anchors.js.map +1 -1
- package/dist/context-builder/bundle.d.ts +35 -2
- package/dist/context-builder/bundle.js +342 -2417
- package/dist/context-builder/bundle.js.map +1 -1
- package/dist/context-builder/codemap.d.ts +5 -0
- package/dist/context-builder/codemap.js +387 -13
- package/dist/context-builder/codemap.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +12 -0
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/context-run-catalog.d.ts +17 -0
- package/dist/context-builder/context-run-catalog.js +99 -0
- package/dist/context-builder/context-run-catalog.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +37 -0
- package/dist/context-builder/delegate-packet.js +359 -67
- package/dist/context-builder/delegate-packet.js.map +1 -1
- package/dist/context-builder/delegate-validation.d.ts +10 -1
- package/dist/context-builder/delegate-validation.js +153 -41
- package/dist/context-builder/delegate-validation.js.map +1 -1
- package/dist/context-builder/delegate.d.ts +2 -0
- package/dist/context-builder/delegate.js +1 -0
- package/dist/context-builder/delegate.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +11 -2
- package/dist/context-builder/diagnostics.js +39 -6
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery.d.ts +29 -1
- package/dist/context-builder/discovery.js +475 -175
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/globs.js +10 -10
- package/dist/context-builder/globs.js.map +1 -1
- package/dist/context-builder/handler.d.ts +26 -2
- package/dist/context-builder/handler.js +1158 -2416
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/lifecycle-contract.d.ts +424 -0
- package/dist/context-builder/lifecycle-contract.js +523 -0
- package/dist/context-builder/lifecycle-contract.js.map +1 -0
- package/dist/context-builder/lifecycle-journal.d.ts +62 -0
- package/dist/context-builder/lifecycle-journal.js +88 -0
- package/dist/context-builder/lifecycle-journal.js.map +1 -0
- package/dist/context-builder/lifecycle-observer.d.ts +102 -0
- package/dist/context-builder/lifecycle-observer.js +258 -0
- package/dist/context-builder/lifecycle-observer.js.map +1 -0
- package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
- package/dist/context-builder/lifecycle-reconcile.js +191 -0
- package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
- package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
- package/dist/context-builder/lifecycle-recovery.js +14 -0
- package/dist/context-builder/lifecycle-recovery.js.map +1 -0
- package/dist/context-builder/path-signals.js +9 -5
- package/dist/context-builder/path-signals.js.map +1 -1
- package/dist/context-builder/progress.d.ts +3 -1
- package/dist/context-builder/progress.js +23 -0
- package/dist/context-builder/progress.js.map +1 -1
- package/dist/context-builder/receipt.d.ts +6 -4
- package/dist/context-builder/receipt.js +12 -1
- package/dist/context-builder/receipt.js.map +1 -1
- package/dist/context-builder/refine-handler.d.ts +14 -0
- package/dist/context-builder/refine-handler.js +790 -0
- package/dist/context-builder/refine-handler.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +84 -0
- package/dist/context-builder/review-coverage.js +437 -0
- package/dist/context-builder/review-coverage.js.map +1 -0
- package/dist/context-builder/schema.d.ts +239 -1
- package/dist/context-builder/schema.js +112 -6
- package/dist/context-builder/schema.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +28 -0
- package/dist/context-builder/secrets.js +1473 -44
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/selection.d.ts +7 -3
- package/dist/context-builder/selection.js +200 -738
- package/dist/context-builder/selection.js.map +1 -1
- package/dist/context-builder/session-log-identity.d.ts +1 -0
- package/dist/context-builder/session-log-identity.js +66 -0
- package/dist/context-builder/session-log-identity.js.map +1 -0
- package/dist/context-builder/slice-range-math.d.ts +16 -0
- package/dist/context-builder/slice-range-math.js +137 -0
- package/dist/context-builder/slice-range-math.js.map +1 -0
- package/dist/context-builder/status.d.ts +22 -1
- package/dist/context-builder/status.js +59 -4
- package/dist/context-builder/status.js.map +1 -1
- package/dist/context-builder/storage.d.ts +35 -2
- package/dist/context-builder/storage.js +271 -14
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/token-estimate.d.ts +13 -2
- package/dist/context-builder/token-estimate.js +77 -4
- package/dist/context-builder/token-estimate.js.map +1 -1
- package/dist/context-builder/versions.d.ts +29 -0
- package/dist/context-builder/versions.js +30 -0
- package/dist/context-builder/versions.js.map +1 -0
- package/dist/context-builder/workflow-runner.d.ts +69 -0
- package/dist/context-builder/workflow-runner.js +1051 -0
- package/dist/context-builder/workflow-runner.js.map +1 -0
- package/dist/context-builder/write-watchdog.d.ts +14 -0
- package/dist/context-builder/write-watchdog.js +38 -0
- package/dist/context-builder/write-watchdog.js.map +1 -0
- package/dist/dashboard-adapter-catalog.d.ts +9 -0
- package/dist/dashboard-adapter-catalog.js +46 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -0
- package/dist/dashboard-cli.d.ts +79 -0
- package/dist/dashboard-cli.js +585 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-companion/client.d.ts +36 -0
- package/dist/dashboard-companion/client.js +210 -0
- package/dist/dashboard-companion/client.js.map +1 -0
- package/dist/dashboard-companion/config.d.ts +8 -0
- package/dist/dashboard-companion/config.js +52 -0
- package/dist/dashboard-companion/config.js.map +1 -0
- package/dist/dashboard-companion/http-proxy.d.ts +10 -0
- package/dist/dashboard-companion/http-proxy.js +234 -0
- package/dist/dashboard-companion/http-proxy.js.map +1 -0
- package/dist/dashboard-companion/protocol.d.ts +80 -0
- package/dist/dashboard-companion/protocol.js +455 -0
- package/dist/dashboard-companion/protocol.js.map +1 -0
- package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
- package/dist/dashboard-companion/standalone-assets.js +72 -0
- package/dist/dashboard-companion/standalone-assets.js.map +1 -0
- package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
- package/dist/dashboard-companion/standalone-entry.js +17 -0
- package/dist/dashboard-companion/standalone-entry.js.map +1 -0
- package/dist/dashboard-gateway/config.d.ts +9 -0
- package/dist/dashboard-gateway/config.js +44 -0
- package/dist/dashboard-gateway/config.js.map +1 -0
- package/dist/dashboard-gateway/index.d.ts +11 -0
- package/dist/dashboard-gateway/index.js +755 -0
- package/dist/dashboard-gateway/index.js.map +1 -0
- package/dist/dashboard-gateway/main.d.ts +1 -0
- package/dist/dashboard-gateway/main.js +25 -0
- package/dist/dashboard-gateway/main.js.map +1 -0
- package/dist/dashboard-pairing-token.d.ts +1 -0
- package/dist/dashboard-pairing-token.js +5 -0
- package/dist/dashboard-pairing-token.js.map +1 -0
- package/dist/elaris/client.d.ts +73 -0
- package/dist/elaris/client.js +825 -0
- package/dist/elaris/client.js.map +1 -0
- package/dist/elaris/config.d.ts +18 -0
- package/dist/elaris/config.js +75 -0
- package/dist/elaris/config.js.map +1 -0
- package/dist/elaris/errors.d.ts +41 -0
- package/dist/elaris/errors.js +205 -0
- package/dist/elaris/errors.js.map +1 -0
- package/dist/elaris/github-repo.d.ts +18 -0
- package/dist/elaris/github-repo.js +89 -0
- package/dist/elaris/github-repo.js.map +1 -0
- package/dist/elaris/index.d.ts +10 -0
- package/dist/elaris/index.js +11 -0
- package/dist/elaris/index.js.map +1 -0
- package/dist/elaris/redact.d.ts +89 -0
- package/dist/elaris/redact.js +423 -0
- package/dist/elaris/redact.js.map +1 -0
- package/dist/elaris/run-id.d.ts +5 -0
- package/dist/elaris/run-id.js +20 -0
- package/dist/elaris/run-id.js.map +1 -0
- package/dist/elaris/status.d.ts +45 -0
- package/dist/elaris/status.js +122 -0
- package/dist/elaris/status.js.map +1 -0
- package/dist/elaris/tool-schemas.d.ts +314 -0
- package/dist/elaris/tool-schemas.js +208 -0
- package/dist/elaris/tool-schemas.js.map +1 -0
- package/dist/elaris/tools.d.ts +73 -0
- package/dist/elaris/tools.js +1065 -0
- package/dist/elaris/tools.js.map +1 -0
- package/dist/elaris/types.d.ts +1541 -0
- package/dist/elaris/types.js +252 -0
- package/dist/elaris/types.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -1
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +10 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/local-clink-backend.d.ts +3 -0
- package/dist/local-clink-backend.js +4 -0
- package/dist/local-clink-backend.js.map +1 -1
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +18 -0
- package/dist/package-info.js.map +1 -0
- package/dist/parsers/codex-jsonl.js +48 -3
- package/dist/parsers/codex-jsonl.js.map +1 -1
- package/dist/parsers/extract.d.ts +10 -1
- package/dist/parsers/extract.js +11 -3
- package/dist/parsers/extract.js.map +1 -1
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.js +129 -8
- package/dist/pipeline.js.map +1 -1
- package/dist/run-dir.d.ts +24 -0
- package/dist/run-dir.js +201 -2
- package/dist/run-dir.js.map +1 -1
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +184 -15
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +4 -4
- package/dist/safety.js +4 -4
- package/dist/schema.d.ts +7 -7
- package/dist/server.d.ts +1 -0
- package/dist/server.js +359 -17
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +18 -0
- package/dist/session-log-stream.js +319 -0
- package/dist/session-log-stream.js.map +1 -0
- package/package.json +46 -5
- package/conf/adapters/gemini.json +0 -44
- package/conf/adapters/hapi/gemini.json +0 -47
- package/dist/parsers/gemini-json.d.ts +0 -2
- package/dist/parsers/gemini-json.js +0 -72
- package/dist/parsers/gemini-json.js.map +0 -1
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
import { ExecutionError, InvalidParamsError } from "../errors.js";
|
|
2
|
+
import { Semaphore } from "../concurrency.js";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import { CodexAppServerClient as RealCodexAppServerClient, sandboxPolicyFromMode, } from "./app-server-client.js";
|
|
5
|
+
import { CODEX_EVENT_AGENT_MESSAGE_DELTA, CODEX_EVENT_ITEM_COMPLETED, CODEX_EVENT_ITEM_PLAN_DELTA, CODEX_EVENT_ITEM_STARTED, CODEX_EVENT_THREAD_TOKEN_USAGE_UPDATED, CODEX_EVENT_TURN_COMPLETED, CODEX_EVENT_TURN_DIFF_UPDATED, CODEX_EVENT_TURN_PLAN_UPDATED, CODEX_EVENT_TURN_STARTED, CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL, CODEX_REQUEST_FILE_CHANGE_APPROVAL, CODEX_REQUEST_PERMISSIONS_APPROVAL, } from "./protocol.js";
|
|
6
|
+
import { SessionRecord, } from "./session.js";
|
|
7
|
+
/** Typed sentinel for a withBudget timeout so callers can gate on the type,
|
|
8
|
+
* not a fragile message substring (review fix). */
|
|
9
|
+
class BudgetExceededError extends ExecutionError {
|
|
10
|
+
constructor(message = "app-server RPC budget exceeded") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "BudgetExceededError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const DEFAULTS = {
|
|
16
|
+
maxActiveTurns: 5,
|
|
17
|
+
maxRetainedSessions: 20,
|
|
18
|
+
sessionTtlSeconds: 1800,
|
|
19
|
+
eventBuffer: 500,
|
|
20
|
+
turnTimeoutSeconds: 600,
|
|
21
|
+
approvalTimeoutSeconds: 120,
|
|
22
|
+
maxAgentDepth: 2,
|
|
23
|
+
maxResponseChars: 20_000,
|
|
24
|
+
maxEventTextChars: 2_000,
|
|
25
|
+
interruptBudgetSeconds: 10,
|
|
26
|
+
terminalWaitBudgetSeconds: 10,
|
|
27
|
+
startBudgetSeconds: 60,
|
|
28
|
+
};
|
|
29
|
+
function safeParseInt(val, fallback) {
|
|
30
|
+
if (val == null || val === "")
|
|
31
|
+
return fallback;
|
|
32
|
+
const n = Number(val);
|
|
33
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
34
|
+
}
|
|
35
|
+
// Allow 0 (disables a timeout) but reject negatives / non-numbers.
|
|
36
|
+
function safeParseNonNegInt(val, fallback) {
|
|
37
|
+
if (val == null || val === "")
|
|
38
|
+
return fallback;
|
|
39
|
+
const n = Number(val);
|
|
40
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
41
|
+
}
|
|
42
|
+
function createDeferred() {
|
|
43
|
+
let resolve;
|
|
44
|
+
const promise = new Promise((res) => {
|
|
45
|
+
resolve = res;
|
|
46
|
+
});
|
|
47
|
+
return { promise, resolve };
|
|
48
|
+
}
|
|
49
|
+
function extractThreadId(result) {
|
|
50
|
+
// The result schemas are .passthrough() (additive vendor fields), whose index
|
|
51
|
+
// signature defeats union narrowing — read the id fields via a typed view.
|
|
52
|
+
const r = result;
|
|
53
|
+
if (typeof r.thread?.id === "string")
|
|
54
|
+
return r.thread.id;
|
|
55
|
+
if (typeof r.threadId === "string")
|
|
56
|
+
return r.threadId;
|
|
57
|
+
throw new ExecutionError("Codex app-server returned a thread result without a thread id.");
|
|
58
|
+
}
|
|
59
|
+
function extractTurnId(result) {
|
|
60
|
+
const r = result;
|
|
61
|
+
if (typeof r.turn?.id === "string")
|
|
62
|
+
return r.turn.id;
|
|
63
|
+
if (typeof r.turnId === "string")
|
|
64
|
+
return r.turnId;
|
|
65
|
+
throw new ExecutionError("Codex app-server returned a turn result without a turn id.");
|
|
66
|
+
}
|
|
67
|
+
function approvalTypeFromMethod(method) {
|
|
68
|
+
switch (method) {
|
|
69
|
+
case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL:
|
|
70
|
+
return "commandExecution";
|
|
71
|
+
case CODEX_REQUEST_FILE_CHANGE_APPROVAL:
|
|
72
|
+
return "fileChange";
|
|
73
|
+
case CODEX_REQUEST_PERMISSIONS_APPROVAL:
|
|
74
|
+
return "permissions";
|
|
75
|
+
default:
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function registryHealthFromClient(activeLeases, retainedSessionCount, clientHealth) {
|
|
80
|
+
return {
|
|
81
|
+
activeLeases,
|
|
82
|
+
retainedSessionCount,
|
|
83
|
+
appServerProtocol: {
|
|
84
|
+
acceptedVersion: clientHealth.acceptedVersion,
|
|
85
|
+
minVersion: clientHealth.minVersion,
|
|
86
|
+
maxVersion: clientHealth.maxVersion,
|
|
87
|
+
strictVersion: clientHealth.strictVersion,
|
|
88
|
+
},
|
|
89
|
+
childRespawnCount: clientHealth.childRespawnCount,
|
|
90
|
+
recentFailures: {
|
|
91
|
+
count: clientHealth.recentFailures.length,
|
|
92
|
+
samples: [...clientHealth.recentFailures],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function pendingApprovalFromEvent(event, type, requestedAtMs) {
|
|
97
|
+
const base = {
|
|
98
|
+
id: event.id,
|
|
99
|
+
type,
|
|
100
|
+
requestedAtMs,
|
|
101
|
+
};
|
|
102
|
+
switch (event.method) {
|
|
103
|
+
case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL: {
|
|
104
|
+
const params = event.params;
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
reason: params.reason ?? null,
|
|
108
|
+
...(hasOwn(params, "command") ? { command: params.command ?? null } : {}),
|
|
109
|
+
...(hasOwn(params, "cwd") ? { cwd: params.cwd ?? null } : {}),
|
|
110
|
+
...(hasOwn(params, "networkApprovalContext")
|
|
111
|
+
? { networkApprovalContext: params.networkApprovalContext ?? null }
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
case CODEX_REQUEST_FILE_CHANGE_APPROVAL: {
|
|
116
|
+
const params = event.params;
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
reason: params.reason ?? null,
|
|
120
|
+
...(hasOwn(params, "grantRoot")
|
|
121
|
+
? { grantRoot: params.grantRoot ?? null }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case CODEX_REQUEST_PERMISSIONS_APPROVAL: {
|
|
126
|
+
const params = event.params;
|
|
127
|
+
return {
|
|
128
|
+
...base,
|
|
129
|
+
reason: params.reason ?? null,
|
|
130
|
+
cwd: params.cwd,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
reason: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function hasOwn(value, key) {
|
|
141
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
142
|
+
}
|
|
143
|
+
function asRecord(value) {
|
|
144
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
145
|
+
? value
|
|
146
|
+
: null;
|
|
147
|
+
}
|
|
148
|
+
function stringField(value, key) {
|
|
149
|
+
return typeof value[key] === "string" ? value[key] : null;
|
|
150
|
+
}
|
|
151
|
+
function joinedStringField(value, key) {
|
|
152
|
+
const field = value[key];
|
|
153
|
+
if (typeof field === "string")
|
|
154
|
+
return field;
|
|
155
|
+
if (!Array.isArray(field))
|
|
156
|
+
return null;
|
|
157
|
+
const strings = field.filter((entry) => typeof entry === "string");
|
|
158
|
+
return strings.length > 0 ? strings.join("\n") : null;
|
|
159
|
+
}
|
|
160
|
+
function turnInputText(input) {
|
|
161
|
+
const parts = [];
|
|
162
|
+
for (const entry of input) {
|
|
163
|
+
const item = asRecord(entry);
|
|
164
|
+
if (item != null &&
|
|
165
|
+
item["type"] === "text" &&
|
|
166
|
+
typeof item["text"] === "string") {
|
|
167
|
+
parts.push(item["text"]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
171
|
+
}
|
|
172
|
+
function toolIdentity(item) {
|
|
173
|
+
const type = stringField(item, "type");
|
|
174
|
+
switch (type) {
|
|
175
|
+
case "commandExecution":
|
|
176
|
+
return {
|
|
177
|
+
toolName: type,
|
|
178
|
+
summary: stringField(item, "command") ?? type,
|
|
179
|
+
};
|
|
180
|
+
case "fileChange": {
|
|
181
|
+
const count = Array.isArray(item["changes"]) ? item["changes"].length : 0;
|
|
182
|
+
return {
|
|
183
|
+
toolName: type,
|
|
184
|
+
summary: count > 0
|
|
185
|
+
? `${count} file change${count === 1 ? "" : "s"}`
|
|
186
|
+
: type,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case "mcpToolCall": {
|
|
190
|
+
const server = stringField(item, "server");
|
|
191
|
+
const tool = stringField(item, "tool");
|
|
192
|
+
const toolName = server != null && tool != null ? `${server}/${tool}` : tool ?? type;
|
|
193
|
+
return { toolName, summary: toolName };
|
|
194
|
+
}
|
|
195
|
+
case "dynamicToolCall": {
|
|
196
|
+
const namespace = stringField(item, "namespace");
|
|
197
|
+
const tool = stringField(item, "tool");
|
|
198
|
+
const toolName = namespace != null && tool != null
|
|
199
|
+
? `${namespace}/${tool}`
|
|
200
|
+
: tool ?? type;
|
|
201
|
+
return { toolName, summary: toolName };
|
|
202
|
+
}
|
|
203
|
+
case "collabAgentToolCall": {
|
|
204
|
+
const toolName = stringField(item, "tool") ?? type;
|
|
205
|
+
return { toolName, summary: stringField(item, "prompt") ?? toolName };
|
|
206
|
+
}
|
|
207
|
+
case "webSearch":
|
|
208
|
+
return { toolName: type, summary: stringField(item, "query") ?? type };
|
|
209
|
+
case "imageView":
|
|
210
|
+
return { toolName: type, summary: stringField(item, "path") ?? type };
|
|
211
|
+
default:
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function toolOutcome(item) {
|
|
216
|
+
const status = stringField(item, "status");
|
|
217
|
+
if (status === "failed")
|
|
218
|
+
return "failed";
|
|
219
|
+
if (status === "declined" ||
|
|
220
|
+
status === "cancelled" ||
|
|
221
|
+
status === "interrupted") {
|
|
222
|
+
return "cancelled";
|
|
223
|
+
}
|
|
224
|
+
const exitCode = item["exitCode"];
|
|
225
|
+
if (typeof exitCode === "number" && exitCode !== 0)
|
|
226
|
+
return "failed";
|
|
227
|
+
return "succeeded";
|
|
228
|
+
}
|
|
229
|
+
function itemObservation(itemValue, phase) {
|
|
230
|
+
const item = asRecord(itemValue);
|
|
231
|
+
if (item == null)
|
|
232
|
+
return null;
|
|
233
|
+
const itemId = stringField(item, "id");
|
|
234
|
+
const type = stringField(item, "type");
|
|
235
|
+
if (itemId == null || type == null)
|
|
236
|
+
return null;
|
|
237
|
+
if (phase === "completed" && type === "agentMessage") {
|
|
238
|
+
const content = stringField(item, "text");
|
|
239
|
+
if (content == null)
|
|
240
|
+
return null;
|
|
241
|
+
return {
|
|
242
|
+
kind: "assistant",
|
|
243
|
+
itemId,
|
|
244
|
+
content,
|
|
245
|
+
phase: stringField(item, "phase"),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (phase === "completed" && type === "reasoning") {
|
|
249
|
+
const summary = joinedStringField(item, "summary");
|
|
250
|
+
if (summary == null)
|
|
251
|
+
return null;
|
|
252
|
+
return {
|
|
253
|
+
kind: "reasoning",
|
|
254
|
+
itemId,
|
|
255
|
+
summary,
|
|
256
|
+
content: joinedStringField(item, "content"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const tool = toolIdentity(item);
|
|
260
|
+
if (tool == null)
|
|
261
|
+
return null;
|
|
262
|
+
if (phase === "started") {
|
|
263
|
+
return {
|
|
264
|
+
kind: "tool_call",
|
|
265
|
+
toolName: tool.toolName,
|
|
266
|
+
callId: itemId,
|
|
267
|
+
summary: tool.summary,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const resultSummary = stringField(item, "aggregatedOutput") ??
|
|
271
|
+
stringField(asRecord(item["error"]) ?? {}, "message") ??
|
|
272
|
+
tool.summary;
|
|
273
|
+
return {
|
|
274
|
+
kind: "tool_result",
|
|
275
|
+
toolName: tool.toolName,
|
|
276
|
+
callId: itemId,
|
|
277
|
+
outcome: toolOutcome(item),
|
|
278
|
+
summary: resultSummary,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function approvalAction(approval) {
|
|
282
|
+
return (approval.command ?? approval.grantRoot ?? approval.reason ?? approval.type);
|
|
283
|
+
}
|
|
284
|
+
function optionalTokenCount(value) {
|
|
285
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
286
|
+
}
|
|
287
|
+
function mapTurnStatus(status) {
|
|
288
|
+
switch (status) {
|
|
289
|
+
case "completed":
|
|
290
|
+
return "completed";
|
|
291
|
+
case "interrupted":
|
|
292
|
+
case "cancelled":
|
|
293
|
+
return "interrupted";
|
|
294
|
+
case "failed":
|
|
295
|
+
return "failed";
|
|
296
|
+
default:
|
|
297
|
+
return "failed";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function resolveRegistryConfig(override) {
|
|
301
|
+
const env = process.env;
|
|
302
|
+
const base = {
|
|
303
|
+
maxActiveTurns: safeParseInt(env["CLINKX_MAX_ACTIVE_TURNS"], DEFAULTS.maxActiveTurns),
|
|
304
|
+
maxRetainedSessions: safeParseInt(env["CLINKX_MAX_RETAINED_SESSIONS"], DEFAULTS.maxRetainedSessions),
|
|
305
|
+
sessionTtlMs: safeParseNonNegInt(env["CLINKX_SESSION_TTL_SECONDS"], DEFAULTS.sessionTtlSeconds) * 1000,
|
|
306
|
+
eventBuffer: safeParseInt(env["CLINKX_SESSION_EVENT_BUFFER"], DEFAULTS.eventBuffer),
|
|
307
|
+
turnTimeoutMs: safeParseNonNegInt(env["CLINKX_TURN_TIMEOUT_SECONDS"], DEFAULTS.turnTimeoutSeconds) * 1000,
|
|
308
|
+
approvalTimeoutMs: safeParseNonNegInt(env["CLINKX_APPROVAL_TIMEOUT_SECONDS"], DEFAULTS.approvalTimeoutSeconds) * 1000,
|
|
309
|
+
maxAgentDepth: safeParseInt(env["CLINKX_MAX_AGENT_DEPTH"], DEFAULTS.maxAgentDepth),
|
|
310
|
+
currentDepth: safeParseNonNegInt(env["CLINKX_AGENT_DEPTH"], 0),
|
|
311
|
+
maxResponseChars: safeParseInt(env["CLINKX_MAX_RESPONSE_CHARS"], DEFAULTS.maxResponseChars),
|
|
312
|
+
maxEventTextChars: safeParseInt(env["CLINKX_SESSION_EVENT_TEXT_MAX"], DEFAULTS.maxEventTextChars),
|
|
313
|
+
interruptBudgetMs: safeParseInt(env["CLINKX_TURN_INTERRUPT_BUDGET_SECONDS"], DEFAULTS.interruptBudgetSeconds) * 1000,
|
|
314
|
+
terminalWaitBudgetMs: safeParseInt(env["CLINKX_TERMINAL_WAIT_BUDGET_SECONDS"], DEFAULTS.terminalWaitBudgetSeconds) * 1000,
|
|
315
|
+
startBudgetMs: safeParseInt(env["CLINKX_APP_SERVER_START_BUDGET_SECONDS"], DEFAULTS.startBudgetSeconds) * 1000,
|
|
316
|
+
};
|
|
317
|
+
return { ...base, ...override };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Env marker to ADD to the spawned app-server child so the recursion guard
|
|
321
|
+
* survives the env sanitizer (2-6). Single source for both the registry's
|
|
322
|
+
* {@link AppServerRegistry.childEnvMarker} and the default-singleton spawn.
|
|
323
|
+
*/
|
|
324
|
+
function childEnvMarkerFor(currentDepth) {
|
|
325
|
+
return { CLINKX_AGENT_DEPTH: String(currentDepth + 1) };
|
|
326
|
+
}
|
|
327
|
+
const defaultSetTimer = (cb, ms) => {
|
|
328
|
+
const handle = setTimeout(cb, ms);
|
|
329
|
+
// Don't keep the STDIO server alive on dangling turn timers.
|
|
330
|
+
handle.unref?.();
|
|
331
|
+
return handle;
|
|
332
|
+
};
|
|
333
|
+
const defaultClearTimer = (handle) => {
|
|
334
|
+
clearTimeout(handle);
|
|
335
|
+
};
|
|
336
|
+
const DEFAULT_APP_SERVER_SPAWN_CONFIG = {
|
|
337
|
+
command: "codex",
|
|
338
|
+
args: ["app-server"],
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Registry & lifecycle for long-lived Codex app-server sessions.
|
|
342
|
+
*
|
|
343
|
+
* CORE responsibilities (Phase 2): the active-turn lease (the SOLE concurrency
|
|
344
|
+
* gate — the request {@link Semaphore} is never acquired here), session record
|
|
345
|
+
* accounting, the bounded event ring, retained-cap + TTL reaping, turn and
|
|
346
|
+
* pending-approval timeouts with the shared interrupt → terminal-wait →
|
|
347
|
+
* process-group-kill + respawn escalation path, crash semantics, the recursion
|
|
348
|
+
* depth guard, and a standalone {@link shutdown}.
|
|
349
|
+
*
|
|
350
|
+
* Respawn ownership belongs to the client: the registry only calls
|
|
351
|
+
* {@link CodexAppServerClient.ensureReady} and the client's guarded
|
|
352
|
+
* {@link CodexAppServerClient.killProcessGroup}.
|
|
353
|
+
*/
|
|
354
|
+
export class AppServerRegistry {
|
|
355
|
+
client;
|
|
356
|
+
observer;
|
|
357
|
+
clock;
|
|
358
|
+
setTimer;
|
|
359
|
+
clearTimer;
|
|
360
|
+
config;
|
|
361
|
+
lease;
|
|
362
|
+
sessions = new Map();
|
|
363
|
+
/**
|
|
364
|
+
* Thread ids with an in-flight resume reservation. Held synchronously across
|
|
365
|
+
* startSession's awaited RPCs so two concurrent resumes of the same thread
|
|
366
|
+
* cannot both pass the in-flight check before either becomes visible as a
|
|
367
|
+
* running session (review fix A — closes the deferred-insertion race).
|
|
368
|
+
*/
|
|
369
|
+
reservedResumeThreads = new Set();
|
|
370
|
+
unsubscribeProcessDied;
|
|
371
|
+
constructor(deps) {
|
|
372
|
+
this.client = deps.client;
|
|
373
|
+
this.observer = deps.observer;
|
|
374
|
+
this.clock = deps.clock ?? { now: () => Date.now() };
|
|
375
|
+
this.setTimer = deps.setTimer ?? defaultSetTimer;
|
|
376
|
+
this.clearTimer = deps.clearTimer ?? defaultClearTimer;
|
|
377
|
+
this.config = resolveRegistryConfig(deps.config);
|
|
378
|
+
this.lease = new Semaphore(this.config.maxActiveTurns);
|
|
379
|
+
this.unsubscribeProcessDied = this.client.onProcessDied((event) => this.handleProcessDied(event));
|
|
380
|
+
}
|
|
381
|
+
now() {
|
|
382
|
+
return this.clock.now();
|
|
383
|
+
}
|
|
384
|
+
get activeLeases() {
|
|
385
|
+
return this.lease.active;
|
|
386
|
+
}
|
|
387
|
+
get sessionCount() {
|
|
388
|
+
return this.sessions.size;
|
|
389
|
+
}
|
|
390
|
+
health() {
|
|
391
|
+
return registryHealthFromClient(this.activeLeases, this.sessionCount, this.client.getHealth());
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Env marker to ADD to the spawned app-server child so the recursion guard
|
|
395
|
+
* survives the env sanitizer (2-6). The client owns actual env injection
|
|
396
|
+
* into the AppServerSpawnConfig; the registry only supplies the marker.
|
|
397
|
+
*/
|
|
398
|
+
childEnvMarker() {
|
|
399
|
+
return childEnvMarkerFor(this.config.currentDepth);
|
|
400
|
+
}
|
|
401
|
+
require(sessionId) {
|
|
402
|
+
const runtime = this.sessions.get(sessionId);
|
|
403
|
+
if (runtime == null) {
|
|
404
|
+
throw new InvalidParamsError(`Unknown session: ${sessionId}`);
|
|
405
|
+
}
|
|
406
|
+
return runtime;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Start a new (or resumed) session and its first turn. Returns once the turn
|
|
410
|
+
* is RUNNING (lease held); the turn proceeds asynchronously and the lease is
|
|
411
|
+
* released only on provable terminality — held even if the MCP call detaches
|
|
412
|
+
* (2-3). Use {@link waitForTurn} to await completion without consuming a lease.
|
|
413
|
+
*/
|
|
414
|
+
async startSession(opts) {
|
|
415
|
+
// Opportunistic reap before admitting new work.
|
|
416
|
+
this.reapIdle();
|
|
417
|
+
// Recursion guard (2-6): refuse beyond the depth ceiling.
|
|
418
|
+
if (this.config.currentDepth >= this.config.maxAgentDepth) {
|
|
419
|
+
throw new ExecutionError(`Agent recursion depth limit reached (${this.config.currentDepth}/${this.config.maxAgentDepth}). ` +
|
|
420
|
+
"Refusing to start a nested Codex app-server session.");
|
|
421
|
+
}
|
|
422
|
+
if (this.sessions.has(opts.sessionId)) {
|
|
423
|
+
throw new InvalidParamsError(`Session already exists: ${opts.sessionId}`);
|
|
424
|
+
}
|
|
425
|
+
// Resume reservation (review fix A): claim the resumed thread SYNCHRONOUSLY
|
|
426
|
+
// — before any await — so two concurrent resumes of the same thread cannot
|
|
427
|
+
// both pass this check during startSession's awaited RPC sequence. The
|
|
428
|
+
// authoritative guard lives here, not in the caller's preflight.
|
|
429
|
+
const resumeThreadId = opts.resumeThreadId;
|
|
430
|
+
if (resumeThreadId != null) {
|
|
431
|
+
const heldByRunning = [...this.sessions.values()].some((rt) => rt.record.threadId === resumeThreadId && !rt.record.isIdle());
|
|
432
|
+
if (this.reservedResumeThreads.has(resumeThreadId) || heldByRunning) {
|
|
433
|
+
throw new InvalidParamsError(`Cannot resume thread ${resumeThreadId}: a turn is already in flight on it. ` +
|
|
434
|
+
"Wait for that turn to complete before resuming.");
|
|
435
|
+
}
|
|
436
|
+
this.reservedResumeThreads.add(resumeThreadId);
|
|
437
|
+
}
|
|
438
|
+
let leaseHeld = false;
|
|
439
|
+
let runtime;
|
|
440
|
+
try {
|
|
441
|
+
// Active-turn lease: the SOLE gate, fail-fast when full (2-3). Acquired
|
|
442
|
+
// INSIDE the try — and after the synchronous reservation-add with no await
|
|
443
|
+
// between — so a pool-full throw here is caught below and releases the
|
|
444
|
+
// reservation (otherwise a saturated resume would leak it permanently).
|
|
445
|
+
this.lease.acquire();
|
|
446
|
+
leaseHeld = true;
|
|
447
|
+
// A silent child must not hang startSession forever holding the lease
|
|
448
|
+
// (review fix 3): bound the whole pre-turn RPC sequence with a budget.
|
|
449
|
+
const established = await this.withBudget((async () => {
|
|
450
|
+
await this.client.ensureReady();
|
|
451
|
+
let threadId;
|
|
452
|
+
if (opts.resumeThreadId != null) {
|
|
453
|
+
const resumed = await this.client.threadResume({
|
|
454
|
+
threadId: opts.resumeThreadId,
|
|
455
|
+
});
|
|
456
|
+
threadId = extractThreadId(resumed);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const started = await this.client.threadStart(this.buildThreadStartParams(opts));
|
|
460
|
+
threadId = extractThreadId(started);
|
|
461
|
+
}
|
|
462
|
+
const origin = this.config.currentDepth === 0 ? "root" : "nested";
|
|
463
|
+
const record = new SessionRecord({
|
|
464
|
+
sessionId: opts.sessionId,
|
|
465
|
+
threadId,
|
|
466
|
+
sandbox: opts.sandbox,
|
|
467
|
+
approval: opts.approval,
|
|
468
|
+
origin,
|
|
469
|
+
depth: this.config.currentDepth,
|
|
470
|
+
createdAtMs: this.now(),
|
|
471
|
+
eventBuffer: this.config.eventBuffer,
|
|
472
|
+
maxEventTextChars: this.config.maxEventTextChars,
|
|
473
|
+
maxFinalTextChars: this.config.maxResponseChars,
|
|
474
|
+
cwd: opts.cwd,
|
|
475
|
+
model: opts.model,
|
|
476
|
+
});
|
|
477
|
+
const rt = {
|
|
478
|
+
record,
|
|
479
|
+
hasLease: true,
|
|
480
|
+
finalized: false,
|
|
481
|
+
unsubscribe: undefined,
|
|
482
|
+
turnTimer: undefined,
|
|
483
|
+
approvalTimer: undefined,
|
|
484
|
+
terminal: createDeferred(),
|
|
485
|
+
attention: createDeferred(),
|
|
486
|
+
turnTimeoutMs: opts.turnTimeoutMs,
|
|
487
|
+
activityObservation: opts.activityObservation,
|
|
488
|
+
sessionObserver: opts.sessionObserver,
|
|
489
|
+
observerTail: Promise.resolve(),
|
|
490
|
+
nextObservationSequence: 0,
|
|
491
|
+
};
|
|
492
|
+
runtime = rt;
|
|
493
|
+
this.sessions.set(opts.sessionId, rt);
|
|
494
|
+
this.observeUserInput(rt, opts.input, this.now());
|
|
495
|
+
// Subscribe before turn/start so early deltas are not lost.
|
|
496
|
+
rt.unsubscribe = this.client.on({ threadId }, (event) => {
|
|
497
|
+
this.handleEvent(rt, event);
|
|
498
|
+
});
|
|
499
|
+
const turnResult = await this.client.turnStart(this.buildTurnStartParams(threadId, opts));
|
|
500
|
+
return { runtime: rt, turnId: extractTurnId(turnResult) };
|
|
501
|
+
})(), this.config.startBudgetMs);
|
|
502
|
+
const { runtime: rt, turnId } = established;
|
|
503
|
+
this.bindSessionObserverTurn(rt, turnId);
|
|
504
|
+
// turnStart flushes buffered provisional events before returning; a fast
|
|
505
|
+
// turn can fully finalize during that flush. Only establish the running
|
|
506
|
+
// turn if it has not already gone terminal (review fix 2).
|
|
507
|
+
if (!rt.finalized && rt.record.activeTurnId == null) {
|
|
508
|
+
rt.record.beginTurn(turnId, this.now());
|
|
509
|
+
this.scheduleTurnTimeout(rt);
|
|
510
|
+
this.observeRunning(rt);
|
|
511
|
+
this.observeLifecycle(rt, "running");
|
|
512
|
+
// Lease intentionally retained — the turn is now running.
|
|
513
|
+
leaseHeld = false;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// Already finalized via turn/completed during the flush: the lease was
|
|
517
|
+
// released by finalizeTurn. Do not release it again.
|
|
518
|
+
leaseHeld = false;
|
|
519
|
+
}
|
|
520
|
+
// The session is now in the sessions map (running, or terminal if it
|
|
521
|
+
// finished during the flush). Subsequent resumes are gated by the
|
|
522
|
+
// in-flight session check, so the synchronous reservation can be released.
|
|
523
|
+
if (resumeThreadId != null) {
|
|
524
|
+
this.reservedResumeThreads.delete(resumeThreadId);
|
|
525
|
+
}
|
|
526
|
+
return rt.record.snapshot();
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
// Release the resume reservation on every failure path so it never leaks.
|
|
530
|
+
if (resumeThreadId != null) {
|
|
531
|
+
this.reservedResumeThreads.delete(resumeThreadId);
|
|
532
|
+
}
|
|
533
|
+
// Failed before the turn was established: tear down, unregister, and
|
|
534
|
+
// release the lease (the session never reached a running turn). Guard the
|
|
535
|
+
// rare race where finalizeTurn already ran during the turnStart
|
|
536
|
+
// provisional flush (a fast turn went terminal) and the IIFE then threw
|
|
537
|
+
// afterwards (e.g. extractTurnId on a malformed turn result): that path
|
|
538
|
+
// already completed the turn and released the lease via finalizeTurn, so
|
|
539
|
+
// neither relabel the completed turn as "failed" nor double-release the
|
|
540
|
+
// lease. Once the runtime exists, runtime.hasLease/finalized are the
|
|
541
|
+
// source of truth (finalizeTurn flips them together); the local leaseHeld
|
|
542
|
+
// only covers a throw before the runtime was created.
|
|
543
|
+
if (runtime != null) {
|
|
544
|
+
if (!runtime.finalized) {
|
|
545
|
+
runtime.unsubscribe?.();
|
|
546
|
+
this.clearTimers(runtime);
|
|
547
|
+
runtime.finalized = true;
|
|
548
|
+
const failureReason = err instanceof Error ? err.message : String(err);
|
|
549
|
+
const now = this.now();
|
|
550
|
+
runtime.record.completeTurn("failed", { failureReason }, now);
|
|
551
|
+
this.observeTerminal(runtime, "failed");
|
|
552
|
+
this.observeError(runtime, failureReason, now, null);
|
|
553
|
+
this.observeLifecycle(runtime, "failed", now, null);
|
|
554
|
+
runtime.terminal.resolve();
|
|
555
|
+
this.sessions.delete(opts.sessionId);
|
|
556
|
+
if (runtime.hasLease) {
|
|
557
|
+
this.lease.release();
|
|
558
|
+
runtime.hasLease = false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (leaseHeld) {
|
|
563
|
+
// Acquired the lease but threw before the runtime existed
|
|
564
|
+
// (ensureReady/threadResume/threadStart, or a pre-runtime budget
|
|
565
|
+
// timeout): release the lease taken at the top of the try.
|
|
566
|
+
this.lease.release();
|
|
567
|
+
}
|
|
568
|
+
// On a startup-budget timeout the child is presumed silent/hung: recycle
|
|
569
|
+
// it so the leaked pending request + provisional buffer in the client get
|
|
570
|
+
// cleared by handleChildClose (review fix 3).
|
|
571
|
+
if (err instanceof BudgetExceededError) {
|
|
572
|
+
await this.client
|
|
573
|
+
.killProcessGroup("startSession budget exceeded")
|
|
574
|
+
.catch(() => { });
|
|
575
|
+
// The kill terminated the SHARED child, so every OTHER running session
|
|
576
|
+
// is dead too. The client suppresses onProcessDied for intentional
|
|
577
|
+
// kills, so fan out here or their leases leak (review fix). The
|
|
578
|
+
// triggering session was already torn down + removed above.
|
|
579
|
+
this.finalizeCoActiveAfterSharedKill(undefined, "startSession budget exceeded");
|
|
580
|
+
}
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
buildThreadStartParams(opts) {
|
|
585
|
+
const params = { sandbox: opts.sandbox };
|
|
586
|
+
if (opts.model != null)
|
|
587
|
+
params.model = opts.model;
|
|
588
|
+
if (opts.cwd != null)
|
|
589
|
+
params.cwd = opts.cwd;
|
|
590
|
+
if (opts.approvalPolicy != null)
|
|
591
|
+
params.approvalPolicy = opts.approvalPolicy;
|
|
592
|
+
return params;
|
|
593
|
+
}
|
|
594
|
+
buildTurnStartParams(threadId, opts) {
|
|
595
|
+
const params = { threadId, input: opts.input };
|
|
596
|
+
if (opts.cwd != null)
|
|
597
|
+
params.cwd = opts.cwd;
|
|
598
|
+
if (opts.model != null)
|
|
599
|
+
params.model = opts.model;
|
|
600
|
+
if (opts.approvalPolicy != null)
|
|
601
|
+
params.approvalPolicy = opts.approvalPolicy;
|
|
602
|
+
// turn/start requires the TAGGED-OBJECT sandboxPolicy, never the string.
|
|
603
|
+
// When the caller omits it, derive from the thread's sandbox mode so the
|
|
604
|
+
// client does not silently downgrade to read-only (review fix 8).
|
|
605
|
+
params.sandboxPolicy =
|
|
606
|
+
opts.sandboxPolicy ??
|
|
607
|
+
sandboxPolicyFromMode(opts.sandbox, opts.cwd != null ? { writableRoots: [opts.cwd] } : undefined);
|
|
608
|
+
if (opts.outputSchema != null)
|
|
609
|
+
params.outputSchema = opts.outputSchema;
|
|
610
|
+
return params;
|
|
611
|
+
}
|
|
612
|
+
/** Snapshot without consuming an active-turn lease (2-4). */
|
|
613
|
+
poll(sessionId) {
|
|
614
|
+
return this.require(sessionId).record.snapshot();
|
|
615
|
+
}
|
|
616
|
+
list(filter) {
|
|
617
|
+
const all = [...this.sessions.values()].map((rt) => rt.record.snapshot());
|
|
618
|
+
if (filter?.status != null) {
|
|
619
|
+
return all.filter((s) => s.status === filter.status);
|
|
620
|
+
}
|
|
621
|
+
return all;
|
|
622
|
+
}
|
|
623
|
+
/** Bounded event-log read with a monotonic cursor; never consumes a lease (2-4,2-5). */
|
|
624
|
+
getLog(sessionId, cursor, limit) {
|
|
625
|
+
return this.require(sessionId).record.events.read(cursor, limit);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Discovery passthrough for `model/list`. Spawns/initializes the app-server
|
|
629
|
+
* child if needed (via the client) but never starts a session or consumes a
|
|
630
|
+
* lease. Bounded by the start budget like other pre-turn RPCs.
|
|
631
|
+
*/
|
|
632
|
+
async listModels(params) {
|
|
633
|
+
return this.withBudget(this.client.modelList(params), this.config.startBudgetMs, "model/list RPC budget exceeded");
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Await the active turn's terminality WITHOUT consuming a lease. An optional
|
|
637
|
+
* `timeoutMs` bounds the wait only — the underlying turn keeps running and
|
|
638
|
+
* retains its lease on wait-timeout.
|
|
639
|
+
*/
|
|
640
|
+
async waitForTurn(sessionId, opts) {
|
|
641
|
+
const runtime = this.require(sessionId);
|
|
642
|
+
if (runtime.finalized || runtime.record.status !== "running") {
|
|
643
|
+
return runtime.record.snapshot();
|
|
644
|
+
}
|
|
645
|
+
if (runtime.record.pendingApprovals.size > 0) {
|
|
646
|
+
return runtime.record.snapshot();
|
|
647
|
+
}
|
|
648
|
+
await this.raceTurnWait(runtime, opts?.timeoutMs, opts?.signal);
|
|
649
|
+
return runtime.record.snapshot();
|
|
650
|
+
}
|
|
651
|
+
/** Reply to a pending approval. The turn continues; the lease is NOT released. */
|
|
652
|
+
async respondApproval(sessionId, approvalId, decision) {
|
|
653
|
+
const runtime = this.require(sessionId);
|
|
654
|
+
const pending = runtime.record.getPendingApproval(approvalId);
|
|
655
|
+
if (pending == null) {
|
|
656
|
+
throw new InvalidParamsError(`stale or unknown approval: ${String(approvalId)}`);
|
|
657
|
+
}
|
|
658
|
+
this.clearApprovalTimer(runtime);
|
|
659
|
+
try {
|
|
660
|
+
// Defensive: permissions approvals are auto-handled on arrival and never
|
|
661
|
+
// surfaced, so this path normally only sees command/file. If a permissions
|
|
662
|
+
// pending somehow reaches here, use the empty-grant shape (not {decision}).
|
|
663
|
+
await this.withBudget(pending.type === "permissions"
|
|
664
|
+
? this.client.respondPermissionsApproval(approvalId)
|
|
665
|
+
: this.client.respondApproval({ id: approvalId, decision }), this.config.interruptBudgetMs);
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
// Restore the auto-decline/escalation backstop the timer provides before
|
|
669
|
+
// surfacing the failure (review fix 4).
|
|
670
|
+
if (runtime.record.pendingApprovals.size > 0) {
|
|
671
|
+
this.scheduleApprovalTimeout(runtime);
|
|
672
|
+
}
|
|
673
|
+
throw err;
|
|
674
|
+
}
|
|
675
|
+
const now = this.now();
|
|
676
|
+
runtime.record.clearPendingApproval(approvalId, now);
|
|
677
|
+
this.observeApprovalResolution(runtime, approvalId, decision, now);
|
|
678
|
+
if (runtime.record.pendingApprovals.size > 0) {
|
|
679
|
+
this.scheduleApprovalTimeout(runtime);
|
|
680
|
+
}
|
|
681
|
+
return runtime.record.snapshot();
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Steer the currently active turn. This never starts a new turn and never
|
|
685
|
+
* acquires a new lease; it targets the active turn id captured in the record.
|
|
686
|
+
*/
|
|
687
|
+
async steer(sessionId, input) {
|
|
688
|
+
const runtime = this.require(sessionId);
|
|
689
|
+
if (runtime.finalized || runtime.record.status !== "running") {
|
|
690
|
+
throw new InvalidParamsError(`No active turn for session: ${sessionId}`);
|
|
691
|
+
}
|
|
692
|
+
const turnId = runtime.record.activeTurnId;
|
|
693
|
+
if (turnId == null) {
|
|
694
|
+
throw new InvalidParamsError(`No active turn for session: ${sessionId}`);
|
|
695
|
+
}
|
|
696
|
+
await this.withBudget(this.client.turnSteer({
|
|
697
|
+
threadId: runtime.record.threadId,
|
|
698
|
+
expectedTurnId: turnId,
|
|
699
|
+
input,
|
|
700
|
+
}), this.config.interruptBudgetMs, "turnSteer RPC budget exceeded");
|
|
701
|
+
this.observeUserInput(runtime, input, this.now());
|
|
702
|
+
return runtime.record.snapshot();
|
|
703
|
+
}
|
|
704
|
+
/** Explicit cancel → interrupted snapshot (decision 4). */
|
|
705
|
+
async interrupt(sessionId) {
|
|
706
|
+
const runtime = this.require(sessionId);
|
|
707
|
+
if (runtime.finalized || runtime.record.status !== "running") {
|
|
708
|
+
return runtime.record.snapshot();
|
|
709
|
+
}
|
|
710
|
+
await this.terminateTurn(runtime, "interrupted", "explicit interrupt");
|
|
711
|
+
return runtime.record.snapshot();
|
|
712
|
+
}
|
|
713
|
+
/** Remove a session. Running sessions are skipped unless force:true is supplied. */
|
|
714
|
+
async cleanup(sessionId, opts = {}) {
|
|
715
|
+
const runtime = this.require(sessionId);
|
|
716
|
+
if (!runtime.finalized && runtime.record.status === "running") {
|
|
717
|
+
if (opts.force !== true) {
|
|
718
|
+
return { sessionId, cleanedUp: false, skippedActive: true };
|
|
719
|
+
}
|
|
720
|
+
await this.terminateTurn(runtime, "interrupted", "session cleanup");
|
|
721
|
+
}
|
|
722
|
+
this.evict(sessionId, runtime, "cleanup");
|
|
723
|
+
return { sessionId, cleanedUp: true, skippedActive: false };
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Standalone shutdown: interrupt every active turn, then kill the app-server
|
|
727
|
+
* child by process group via the client's guarded kill.
|
|
728
|
+
*/
|
|
729
|
+
async shutdown(reason = "shutdown") {
|
|
730
|
+
const running = [...this.sessions.values()].filter((rt) => !rt.finalized && rt.record.status === "running");
|
|
731
|
+
for (const runtime of running) {
|
|
732
|
+
await this.terminateTurn(runtime, "interrupted", reason);
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
await this.client.killProcessGroup(reason);
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
logger.error({ err, reason }, "app-server-registry: killProcessGroup during shutdown failed");
|
|
739
|
+
}
|
|
740
|
+
for (const [id, runtime] of [...this.sessions]) {
|
|
741
|
+
this.clearTimers(runtime);
|
|
742
|
+
runtime.unsubscribe?.();
|
|
743
|
+
runtime.unsubscribe = undefined;
|
|
744
|
+
this.sessions.delete(id);
|
|
745
|
+
}
|
|
746
|
+
this.unsubscribeProcessDied();
|
|
747
|
+
}
|
|
748
|
+
/** TTL + retained-cap reaping of idle sessions (2-4). Running sessions are never reaped. */
|
|
749
|
+
reapIdle() {
|
|
750
|
+
const now = this.now();
|
|
751
|
+
if (this.config.sessionTtlMs > 0) {
|
|
752
|
+
for (const [id, runtime] of [...this.sessions]) {
|
|
753
|
+
if (runtime.record.status === "running")
|
|
754
|
+
continue;
|
|
755
|
+
if (now - runtime.record.lastActivityAtMs >= this.config.sessionTtlMs) {
|
|
756
|
+
this.evict(id, runtime, "ttl");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const idle = [...this.sessions.entries()].filter(([, rt]) => rt.record.status !== "running");
|
|
761
|
+
const overflow = idle.length - this.config.maxRetainedSessions;
|
|
762
|
+
if (overflow > 0) {
|
|
763
|
+
idle.sort((a, b) => a[1].record.lastActivityAtMs - b[1].record.lastActivityAtMs);
|
|
764
|
+
for (let i = 0; i < overflow; i++) {
|
|
765
|
+
const entry = idle[i];
|
|
766
|
+
if (entry == null)
|
|
767
|
+
break;
|
|
768
|
+
this.evict(entry[0], entry[1], "retained-cap");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// --- internal lifecycle --------------------------------------------------
|
|
773
|
+
handleEvent(runtime, event) {
|
|
774
|
+
if (runtime.finalized)
|
|
775
|
+
return;
|
|
776
|
+
const now = this.now();
|
|
777
|
+
runtime.record.appendEvent(event, now);
|
|
778
|
+
switch (event.method) {
|
|
779
|
+
case CODEX_EVENT_TURN_STARTED: {
|
|
780
|
+
const turnId = event.params.turn.id;
|
|
781
|
+
if (runtime.record.activeTurnId == null) {
|
|
782
|
+
runtime.record.beginTurn(turnId, now);
|
|
783
|
+
this.scheduleTurnTimeout(runtime);
|
|
784
|
+
this.observeRunning(runtime);
|
|
785
|
+
this.observeLifecycle(runtime, "running", now);
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case CODEX_EVENT_AGENT_MESSAGE_DELTA: {
|
|
790
|
+
// Streaming assistant text. Accumulate into the bounded running preview
|
|
791
|
+
// so a poller can watch a turn mid-stream without a second get_log; the
|
|
792
|
+
// delta is also retained verbatim (capped) in the ring via appendEvent.
|
|
793
|
+
runtime.record.appendAssistantDelta(event.params.delta, event.params.turnId, now);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
case CODEX_EVENT_ITEM_STARTED: {
|
|
797
|
+
const observation = itemObservation(event.params.item, "started");
|
|
798
|
+
if (observation != null) {
|
|
799
|
+
this.runSessionObservation(runtime, observation, now);
|
|
800
|
+
}
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case CODEX_EVENT_ITEM_COMPLETED: {
|
|
804
|
+
const item = event.params.item;
|
|
805
|
+
if (item.type === "agentMessage" && typeof item.text === "string") {
|
|
806
|
+
// Cache the latest final-answer text; capped at assignment so any
|
|
807
|
+
// terminal path exposes a bounded value (review fix 7).
|
|
808
|
+
runtime.record.setFinalText(item.text);
|
|
809
|
+
}
|
|
810
|
+
const observation = itemObservation(event.params.item, "completed");
|
|
811
|
+
if (observation != null) {
|
|
812
|
+
this.runSessionObservation(runtime, observation, now);
|
|
813
|
+
}
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
case CODEX_EVENT_THREAD_TOKEN_USAGE_UPDATED: {
|
|
817
|
+
runtime.record.recordTokenUsage(event.params.tokenUsage, now);
|
|
818
|
+
const last = event.params.tokenUsage.last;
|
|
819
|
+
this.runSessionObservation(runtime, {
|
|
820
|
+
kind: "usage",
|
|
821
|
+
inputTokens: optionalTokenCount(last.inputTokens),
|
|
822
|
+
cachedInputTokens: optionalTokenCount(last.cachedInputTokens),
|
|
823
|
+
outputTokens: optionalTokenCount(last.outputTokens),
|
|
824
|
+
reasoningTokens: optionalTokenCount(last.reasoningOutputTokens),
|
|
825
|
+
incomplete: runtime.record.tokenUsage.incomplete,
|
|
826
|
+
}, now);
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case CODEX_EVENT_TURN_COMPLETED: {
|
|
830
|
+
const status = mapTurnStatus(event.params.turn.status);
|
|
831
|
+
// finalText already cached from item/completed (if any).
|
|
832
|
+
const finalText = runtime.record.finalText;
|
|
833
|
+
this.finalizeTurn(runtime, status, { finalText });
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case CODEX_EVENT_TURN_DIFF_UPDATED: {
|
|
837
|
+
runtime.record.setLatestDiff(event.params.diff, now);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case CODEX_EVENT_TURN_PLAN_UPDATED: {
|
|
841
|
+
runtime.record.setLatestPlan(event.params.plan, event.params.explanation ?? null, now);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
case CODEX_EVENT_ITEM_PLAN_DELTA:
|
|
845
|
+
// Streaming plan text; no snapshot field — it is already retained in the
|
|
846
|
+
// event ring via appendEvent above (get_log surfaces it).
|
|
847
|
+
break;
|
|
848
|
+
case CODEX_REQUEST_PERMISSIONS_APPROVAL: {
|
|
849
|
+
// Permissions (sandbox-escalation) approvals are never surfaced: under
|
|
850
|
+
// the read-only / approval:never posture we auto-decline on arrival with
|
|
851
|
+
// a protocol-valid empty grant (grants nothing). Best-effort and bounded;
|
|
852
|
+
// a failure here cannot stall the turn beyond the wall-clock timeout.
|
|
853
|
+
const pending = pendingApprovalFromEvent(event, "permissions", now);
|
|
854
|
+
this.observeApprovalRequest(runtime, pending, now);
|
|
855
|
+
void this.autoDeclinePermissionsApproval(runtime, event.id);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case CODEX_REQUEST_COMMAND_EXECUTION_APPROVAL:
|
|
859
|
+
case CODEX_REQUEST_FILE_CHANGE_APPROVAL: {
|
|
860
|
+
const type = approvalTypeFromMethod(event.method);
|
|
861
|
+
if (type == null)
|
|
862
|
+
break;
|
|
863
|
+
const pending = pendingApprovalFromEvent(event, type, now);
|
|
864
|
+
runtime.record.setPendingApproval(pending, now);
|
|
865
|
+
this.notifyAttention(runtime);
|
|
866
|
+
this.scheduleApprovalTimeout(runtime);
|
|
867
|
+
this.observeApprovalRequest(runtime, pending, now);
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
default:
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Finalize a turn on PROVABLE terminality. Idempotent. Clears timers,
|
|
876
|
+
* unsubscribes, releases the lease, and resolves the terminal deferred.
|
|
877
|
+
*/
|
|
878
|
+
finalizeTurn(runtime, status, opts) {
|
|
879
|
+
if (runtime.finalized)
|
|
880
|
+
return;
|
|
881
|
+
runtime.finalized = true;
|
|
882
|
+
this.clearTimers(runtime);
|
|
883
|
+
runtime.unsubscribe?.();
|
|
884
|
+
runtime.unsubscribe = undefined;
|
|
885
|
+
const turnId = runtime.record.activeTurnId ?? null;
|
|
886
|
+
const now = this.now();
|
|
887
|
+
runtime.record.completeTurn(status, opts, now);
|
|
888
|
+
this.observeTerminal(runtime, status);
|
|
889
|
+
if (status === "failed" || status === "timeout") {
|
|
890
|
+
this.observeError(runtime, runtime.record.failureReason ??
|
|
891
|
+
(status === "timeout"
|
|
892
|
+
? "Codex app-server turn timed out."
|
|
893
|
+
: "Codex app-server turn failed."), now, turnId);
|
|
894
|
+
}
|
|
895
|
+
this.observeLifecycle(runtime, status, now, turnId);
|
|
896
|
+
if (runtime.hasLease) {
|
|
897
|
+
this.lease.release();
|
|
898
|
+
runtime.hasLease = false;
|
|
899
|
+
}
|
|
900
|
+
runtime.terminal.resolve();
|
|
901
|
+
}
|
|
902
|
+
runSessionObservation(runtime, semantic, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
|
|
903
|
+
const observer = runtime.sessionObserver ?? this.observer;
|
|
904
|
+
if (observer == null)
|
|
905
|
+
return;
|
|
906
|
+
// Capture sequence and identity synchronously at the semantic reduction
|
|
907
|
+
// point. The async continuation receives only this immutable snapshot.
|
|
908
|
+
const observation = {
|
|
909
|
+
...semantic,
|
|
910
|
+
sequence: runtime.nextObservationSequence++,
|
|
911
|
+
sessionId: runtime.record.sessionId,
|
|
912
|
+
threadId: runtime.record.threadId,
|
|
913
|
+
turnId,
|
|
914
|
+
atMs,
|
|
915
|
+
};
|
|
916
|
+
const deliver = async () => {
|
|
917
|
+
try {
|
|
918
|
+
await observer(observation);
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
logger.debug({
|
|
922
|
+
err: err instanceof Error ? err.message : String(err),
|
|
923
|
+
sessionId: observation.sessionId,
|
|
924
|
+
observationKind: observation.kind,
|
|
925
|
+
sequence: observation.sequence,
|
|
926
|
+
}, "app-server-registry: session observation failed open");
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
runtime.observerTail = runtime.observerTail.then(deliver, deliver);
|
|
930
|
+
}
|
|
931
|
+
bindSessionObserverTurn(runtime, turnId) {
|
|
932
|
+
const observer = runtime.sessionObserver ?? this.observer;
|
|
933
|
+
if (observer?.bindTurn == null)
|
|
934
|
+
return;
|
|
935
|
+
const binding = Object.freeze({
|
|
936
|
+
sessionId: runtime.record.sessionId,
|
|
937
|
+
threadId: runtime.record.threadId,
|
|
938
|
+
turnId,
|
|
939
|
+
});
|
|
940
|
+
const deliver = async () => {
|
|
941
|
+
try {
|
|
942
|
+
await observer.bindTurn(binding);
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
logger.debug({ sessionId: binding.sessionId }, "app-server-registry: session turn binding failed open");
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
runtime.observerTail = runtime.observerTail.then(deliver, deliver);
|
|
949
|
+
}
|
|
950
|
+
observeUserInput(runtime, input, atMs = this.now()) {
|
|
951
|
+
const content = turnInputText(input);
|
|
952
|
+
if (content == null)
|
|
953
|
+
return;
|
|
954
|
+
this.runSessionObservation(runtime, { kind: "user", content }, atMs, runtime.record.activeTurnId ?? null);
|
|
955
|
+
}
|
|
956
|
+
observeApprovalRequest(runtime, approval, atMs = this.now()) {
|
|
957
|
+
this.runSessionObservation(runtime, {
|
|
958
|
+
kind: "approval_request",
|
|
959
|
+
approvalId: approval.id,
|
|
960
|
+
approvalType: approval.type,
|
|
961
|
+
action: approvalAction(approval),
|
|
962
|
+
}, atMs);
|
|
963
|
+
}
|
|
964
|
+
observeApprovalResolution(runtime, approvalId, decision, atMs = this.now()) {
|
|
965
|
+
this.runSessionObservation(runtime, {
|
|
966
|
+
kind: "approval_resolution",
|
|
967
|
+
approvalId,
|
|
968
|
+
decision,
|
|
969
|
+
}, atMs);
|
|
970
|
+
}
|
|
971
|
+
observeError(runtime, message, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
|
|
972
|
+
this.runSessionObservation(runtime, {
|
|
973
|
+
kind: "error",
|
|
974
|
+
code: "app_server_session_failed",
|
|
975
|
+
message,
|
|
976
|
+
retryable: false,
|
|
977
|
+
}, atMs, turnId);
|
|
978
|
+
}
|
|
979
|
+
observeLifecycle(runtime, status, atMs = this.now(), turnId = runtime.record.activeTurnId ?? null) {
|
|
980
|
+
this.runSessionObservation(runtime, {
|
|
981
|
+
kind: "lifecycle",
|
|
982
|
+
status,
|
|
983
|
+
failureReason: runtime.record.failureReason ?? null,
|
|
984
|
+
}, atMs, turnId);
|
|
985
|
+
}
|
|
986
|
+
observeRunning(runtime) {
|
|
987
|
+
const observation = runtime.activityObservation;
|
|
988
|
+
if (observation == null)
|
|
989
|
+
return;
|
|
990
|
+
this.runActivityObservation(runtime, "running", () => observation.transition("running"));
|
|
991
|
+
}
|
|
992
|
+
observeTerminal(runtime, status) {
|
|
993
|
+
const observation = runtime.activityObservation;
|
|
994
|
+
if (observation == null)
|
|
995
|
+
return;
|
|
996
|
+
const completion = status === "completed"
|
|
997
|
+
? { status: "succeeded", outcomeKind: "success" }
|
|
998
|
+
: status === "interrupted"
|
|
999
|
+
? { status: "cancelled", outcomeKind: "cancelled" }
|
|
1000
|
+
: { status: "failed", outcomeKind: "error" };
|
|
1001
|
+
this.runActivityObservation(runtime, "terminal", () => observation.complete(completion));
|
|
1002
|
+
}
|
|
1003
|
+
runActivityObservation(runtime, boundary, operation) {
|
|
1004
|
+
const report = (err) => {
|
|
1005
|
+
logger.debug({
|
|
1006
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1007
|
+
sessionId: runtime.record.sessionId,
|
|
1008
|
+
}, `app-server-registry: dashboard ${boundary} observation failed open`);
|
|
1009
|
+
};
|
|
1010
|
+
try {
|
|
1011
|
+
void operation().catch(report);
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
report(err);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Shared terminality/escalation path (2-8 / 2-9):
|
|
1019
|
+
* 1. bounded interrupt RPC
|
|
1020
|
+
* 2. bounded wait for terminal turn/completed
|
|
1021
|
+
* 3. on hang/no-terminal → process-group kill + respawn + mark terminal
|
|
1022
|
+
*
|
|
1023
|
+
* The lease is released only via {@link finalizeTurn}, i.e. only once the
|
|
1024
|
+
* turn is provably terminal.
|
|
1025
|
+
*/
|
|
1026
|
+
async terminateTurn(runtime, statusOnKill, reason) {
|
|
1027
|
+
if (runtime.finalized)
|
|
1028
|
+
return;
|
|
1029
|
+
const threadId = runtime.record.threadId;
|
|
1030
|
+
const turnId = runtime.record.activeTurnId;
|
|
1031
|
+
if (turnId != null) {
|
|
1032
|
+
try {
|
|
1033
|
+
await this.withBudget(this.client.turnInterrupt({ threadId, turnId }), this.config.interruptBudgetMs);
|
|
1034
|
+
const reachedTerminal = await this.waitTerminalWithin(runtime, this.config.terminalWaitBudgetMs);
|
|
1035
|
+
if (reachedTerminal)
|
|
1036
|
+
return; // finalized via turn/completed event
|
|
1037
|
+
}
|
|
1038
|
+
catch (err) {
|
|
1039
|
+
logger.warn({ err, reason, sessionId: runtime.record.sessionId }, "app-server-registry: interrupt RPC failed or timed out — escalating");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (runtime.finalized)
|
|
1043
|
+
return;
|
|
1044
|
+
await this.forceKill(runtime, statusOnKill, reason);
|
|
1045
|
+
}
|
|
1046
|
+
async forceKill(runtime, status, reason) {
|
|
1047
|
+
if (runtime.finalized)
|
|
1048
|
+
return;
|
|
1049
|
+
try {
|
|
1050
|
+
await this.client.killProcessGroup(reason);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
logger.error({ err, reason, sessionId: runtime.record.sessionId }, "app-server-registry: killProcessGroup failed during escalation");
|
|
1054
|
+
}
|
|
1055
|
+
// Release leases FIRST — the kill terminated the SHARED child, so every
|
|
1056
|
+
// in-flight turn is dead. The client suppresses onProcessDied for
|
|
1057
|
+
// intentional kills, so we must finalize here or leases leak forever
|
|
1058
|
+
// (review fix). This must NOT depend on the respawn: a silent respawned
|
|
1059
|
+
// child would otherwise block escalation before any lease is released.
|
|
1060
|
+
// Future startSession calls re-ensureReady themselves.
|
|
1061
|
+
this.finalizeTurn(runtime, status, { failureReason: reason });
|
|
1062
|
+
this.finalizeCoActiveAfterSharedKill(runtime, reason);
|
|
1063
|
+
try {
|
|
1064
|
+
// Best-effort respawn ownership is the client's — registry only nudges
|
|
1065
|
+
// it, and bounds it so a silent child cannot hang escalation.
|
|
1066
|
+
await this.withBudget(this.client.ensureReady(), this.config.startBudgetMs);
|
|
1067
|
+
}
|
|
1068
|
+
catch (err) {
|
|
1069
|
+
logger.error({ err, sessionId: runtime.record.sessionId }, "app-server-registry: ensureReady (respawn) failed during escalation");
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Finalize every co-active running session after the SHARED child was killed.
|
|
1074
|
+
* The client suppresses onProcessDied for intentional kills, so without this
|
|
1075
|
+
* their leases leak (review fix). finalizeTurn is idempotent; `except` is the
|
|
1076
|
+
* triggering runtime, handled by the caller.
|
|
1077
|
+
*/
|
|
1078
|
+
finalizeCoActiveAfterSharedKill(except, reason) {
|
|
1079
|
+
for (const other of this.sessions.values()) {
|
|
1080
|
+
if (other === except)
|
|
1081
|
+
continue;
|
|
1082
|
+
if (other.finalized || other.record.status !== "running")
|
|
1083
|
+
continue;
|
|
1084
|
+
this.finalizeTurn(other, "failed", {
|
|
1085
|
+
failureReason: `shared app-server killed: ${reason}`,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
async onTurnTimeout(runtime) {
|
|
1090
|
+
if (runtime.finalized)
|
|
1091
|
+
return;
|
|
1092
|
+
logger.warn({ sessionId: runtime.record.sessionId }, "app-server-registry: turn wall-clock timeout — attempting interrupt");
|
|
1093
|
+
await this.terminateTurn(runtime, "timeout", "turn wall-clock timeout");
|
|
1094
|
+
}
|
|
1095
|
+
async onApprovalTimeout(runtime) {
|
|
1096
|
+
if (runtime.finalized)
|
|
1097
|
+
return;
|
|
1098
|
+
const pendingApprovals = [...runtime.record.pendingApprovals.values()];
|
|
1099
|
+
if (pendingApprovals.length === 0)
|
|
1100
|
+
return;
|
|
1101
|
+
// Auto-decline is NOT terminal (2-9): the turn may continue, and the
|
|
1102
|
+
// decline RPC can hang. Bound it, then await terminality, then escalate.
|
|
1103
|
+
try {
|
|
1104
|
+
for (const pending of pendingApprovals) {
|
|
1105
|
+
if (runtime.finalized)
|
|
1106
|
+
return;
|
|
1107
|
+
// Defensive: permissions approvals are auto-handled on arrival, so this
|
|
1108
|
+
// path normally only sees command/file. Route any stray permissions
|
|
1109
|
+
// pending to the empty-grant shape rather than {decision}.
|
|
1110
|
+
await this.withBudget(pending.type === "permissions"
|
|
1111
|
+
? this.client.respondPermissionsApproval(pending.id)
|
|
1112
|
+
: this.client.respondApproval({
|
|
1113
|
+
id: pending.id,
|
|
1114
|
+
decision: "decline",
|
|
1115
|
+
}), this.config.interruptBudgetMs);
|
|
1116
|
+
const now = this.now();
|
|
1117
|
+
runtime.record.clearPendingApproval(pending.id, now);
|
|
1118
|
+
this.observeApprovalResolution(runtime, pending.id, "decline", now);
|
|
1119
|
+
}
|
|
1120
|
+
const reachedTerminal = await this.waitTerminalWithin(runtime, this.config.terminalWaitBudgetMs);
|
|
1121
|
+
if (reachedTerminal)
|
|
1122
|
+
return; // finalized via turn/completed event
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
logger.warn({ err, sessionId: runtime.record.sessionId }, "app-server-registry: auto-decline RPC failed or timed out — escalating");
|
|
1126
|
+
}
|
|
1127
|
+
if (runtime.finalized)
|
|
1128
|
+
return;
|
|
1129
|
+
await this.forceKill(runtime, "failed", "pending-approval timeout");
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Auto-decline a permissions (sandbox-escalation) approval the moment it
|
|
1133
|
+
* arrives, with a protocol-valid empty grant. Never surfaced to the user
|
|
1134
|
+
* (read-only / approval:never posture). Bounded: the decline RPC is capped by
|
|
1135
|
+
* `interruptBudgetMs`.
|
|
1136
|
+
*
|
|
1137
|
+
* On decline failure/timeout we do NOT merely log: Codex's child is blocked on
|
|
1138
|
+
* a response that never lands, and if the operator disabled the wall-clock
|
|
1139
|
+
* turn timeout (`CLINKX_TURN_TIMEOUT_SECONDS=0`) there is no backstop until
|
|
1140
|
+
* idle reap. So we escalate the turn directly under a bounded budget,
|
|
1141
|
+
* independent of the global turn timeout, mirroring the command/file
|
|
1142
|
+
* decline-failure recovery in {@link onApprovalTimeout}. terminateTurn is
|
|
1143
|
+
* idempotent and no-ops if the turn already finalized, so this never
|
|
1144
|
+
* double-finalizes.
|
|
1145
|
+
*/
|
|
1146
|
+
async autoDeclinePermissionsApproval(runtime, id) {
|
|
1147
|
+
try {
|
|
1148
|
+
await this.withBudget(this.client.respondPermissionsApproval(id), this.config.interruptBudgetMs);
|
|
1149
|
+
this.observeApprovalResolution(runtime, id, "decline", this.now());
|
|
1150
|
+
logger.debug({ sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: auto-declined permissions approval (empty grant)");
|
|
1151
|
+
}
|
|
1152
|
+
catch (err) {
|
|
1153
|
+
logger.warn({ err, sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: permissions auto-decline failed — escalating turn");
|
|
1154
|
+
// Failure-safe backstop: the child is wedged awaiting a response we could
|
|
1155
|
+
// not deliver. Escalate independently of the (possibly disabled) turn
|
|
1156
|
+
// wall-clock timeout. Idempotent; bounded by interrupt/terminal budgets.
|
|
1157
|
+
await this.terminateTurn(runtime, "failed", "permissions auto-decline failed").catch((escalationErr) => {
|
|
1158
|
+
logger.error({ err: escalationErr, sessionId: runtime.record.sessionId, approvalId: id }, "app-server-registry: escalation after permissions auto-decline failure errored");
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
handleProcessDied(event) {
|
|
1163
|
+
const reason = "app-server process died" +
|
|
1164
|
+
(event.signal != null ? ` (signal ${event.signal})` : "") +
|
|
1165
|
+
(event.exitCode != null ? ` (exit ${event.exitCode})` : "");
|
|
1166
|
+
for (const runtime of this.sessions.values()) {
|
|
1167
|
+
// Crash split (2-10): only active/in-flight turns are unrecoverable.
|
|
1168
|
+
// Idle/completed sessions keep their resumable threadId untouched.
|
|
1169
|
+
if (!runtime.finalized && runtime.record.status === "running") {
|
|
1170
|
+
this.finalizeTurn(runtime, "failed", { failureReason: reason });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// --- timers --------------------------------------------------------------
|
|
1175
|
+
scheduleTurnTimeout(runtime) {
|
|
1176
|
+
// Honor an explicit per-session override (review fix B); fall back to the
|
|
1177
|
+
// global default/backstop. A value of 0 disables the timeout, preserving
|
|
1178
|
+
// the global's disabled semantics exactly.
|
|
1179
|
+
const turnTimeoutMs = runtime.turnTimeoutMs ?? this.config.turnTimeoutMs;
|
|
1180
|
+
if (turnTimeoutMs <= 0)
|
|
1181
|
+
return;
|
|
1182
|
+
if (runtime.turnTimer != null)
|
|
1183
|
+
this.clearTimer(runtime.turnTimer);
|
|
1184
|
+
runtime.turnTimer = this.setTimer(() => {
|
|
1185
|
+
void this.onTurnTimeout(runtime);
|
|
1186
|
+
}, turnTimeoutMs);
|
|
1187
|
+
}
|
|
1188
|
+
scheduleApprovalTimeout(runtime) {
|
|
1189
|
+
if (this.config.approvalTimeoutMs <= 0)
|
|
1190
|
+
return;
|
|
1191
|
+
this.clearApprovalTimer(runtime);
|
|
1192
|
+
runtime.approvalTimer = this.setTimer(() => {
|
|
1193
|
+
void this.onApprovalTimeout(runtime);
|
|
1194
|
+
}, this.config.approvalTimeoutMs);
|
|
1195
|
+
}
|
|
1196
|
+
clearApprovalTimer(runtime) {
|
|
1197
|
+
if (runtime.approvalTimer != null) {
|
|
1198
|
+
this.clearTimer(runtime.approvalTimer);
|
|
1199
|
+
runtime.approvalTimer = undefined;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
clearTimers(runtime) {
|
|
1203
|
+
if (runtime.turnTimer != null) {
|
|
1204
|
+
this.clearTimer(runtime.turnTimer);
|
|
1205
|
+
runtime.turnTimer = undefined;
|
|
1206
|
+
}
|
|
1207
|
+
this.clearApprovalTimer(runtime);
|
|
1208
|
+
}
|
|
1209
|
+
evict(sessionId, runtime, why) {
|
|
1210
|
+
this.clearTimers(runtime);
|
|
1211
|
+
runtime.unsubscribe?.();
|
|
1212
|
+
runtime.unsubscribe = undefined;
|
|
1213
|
+
this.sessions.delete(sessionId);
|
|
1214
|
+
logger.debug({ sessionId, why }, "app-server-registry: evicted session");
|
|
1215
|
+
}
|
|
1216
|
+
// --- bounded-wait helpers ------------------------------------------------
|
|
1217
|
+
/** Reject if `promise` does not settle within `ms`. */
|
|
1218
|
+
withBudget(promise, ms, message) {
|
|
1219
|
+
if (ms <= 0)
|
|
1220
|
+
return promise;
|
|
1221
|
+
return new Promise((resolve, reject) => {
|
|
1222
|
+
const handle = this.setTimer(() => {
|
|
1223
|
+
reject(new BudgetExceededError(message));
|
|
1224
|
+
}, ms);
|
|
1225
|
+
promise.then((value) => {
|
|
1226
|
+
this.clearTimer(handle);
|
|
1227
|
+
resolve(value);
|
|
1228
|
+
}, (err) => {
|
|
1229
|
+
this.clearTimer(handle);
|
|
1230
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
/** Resolve `true` if the terminal deferred settles within `ms`, else `false`. */
|
|
1235
|
+
waitTerminalWithin(runtime, ms) {
|
|
1236
|
+
if (runtime.finalized)
|
|
1237
|
+
return Promise.resolve(true);
|
|
1238
|
+
if (ms <= 0)
|
|
1239
|
+
return Promise.resolve(false);
|
|
1240
|
+
return new Promise((resolve) => {
|
|
1241
|
+
let settled = false;
|
|
1242
|
+
const handle = this.setTimer(() => {
|
|
1243
|
+
if (settled)
|
|
1244
|
+
return;
|
|
1245
|
+
settled = true;
|
|
1246
|
+
resolve(false);
|
|
1247
|
+
}, ms);
|
|
1248
|
+
void runtime.terminal.promise.then(() => {
|
|
1249
|
+
if (settled)
|
|
1250
|
+
return;
|
|
1251
|
+
settled = true;
|
|
1252
|
+
this.clearTimer(handle);
|
|
1253
|
+
resolve(true);
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
notifyAttention(runtime) {
|
|
1258
|
+
const attention = runtime.attention;
|
|
1259
|
+
runtime.attention = createDeferred();
|
|
1260
|
+
attention.resolve();
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Await terminality, a wait-local timeout, or newly requested human attention.
|
|
1264
|
+
* All outcomes are lease-free: a pending approval returns a running snapshot.
|
|
1265
|
+
*/
|
|
1266
|
+
raceTurnWait(runtime, timeoutMs, signal) {
|
|
1267
|
+
return new Promise((resolve) => {
|
|
1268
|
+
let settled = false;
|
|
1269
|
+
let handle;
|
|
1270
|
+
let onAbort;
|
|
1271
|
+
const done = () => {
|
|
1272
|
+
if (settled)
|
|
1273
|
+
return;
|
|
1274
|
+
settled = true;
|
|
1275
|
+
if (handle != null)
|
|
1276
|
+
this.clearTimer(handle);
|
|
1277
|
+
if (onAbort != null && signal != null) {
|
|
1278
|
+
signal.removeEventListener("abort", onAbort);
|
|
1279
|
+
}
|
|
1280
|
+
resolve();
|
|
1281
|
+
};
|
|
1282
|
+
if (signal?.aborted) {
|
|
1283
|
+
// Already aborted: resolve immediately without arming a timer.
|
|
1284
|
+
resolve();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (timeoutMs != null && timeoutMs > 0) {
|
|
1288
|
+
handle = this.setTimer(done, timeoutMs);
|
|
1289
|
+
}
|
|
1290
|
+
if (signal != null) {
|
|
1291
|
+
onAbort = done;
|
|
1292
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1293
|
+
}
|
|
1294
|
+
void runtime.terminal.promise.then(done);
|
|
1295
|
+
void runtime.attention.promise.then(done);
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
let appServerRegistrySingleton;
|
|
1300
|
+
let appServerRegistrySingletonSpawn;
|
|
1301
|
+
let appServerRegistryTestOverrides;
|
|
1302
|
+
/**
|
|
1303
|
+
* Derive the registry spawn request from a resolved app_server adapter:
|
|
1304
|
+
* `app_server.command` (falling back to the adapter's top-level `command`) and
|
|
1305
|
+
* `app_server.args`.
|
|
1306
|
+
*/
|
|
1307
|
+
export function spawnRequestFor(adapter) {
|
|
1308
|
+
return {
|
|
1309
|
+
command: adapter.app_server?.command ?? adapter.command,
|
|
1310
|
+
args: adapter.app_server?.args,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function resolveSpawnRequest(request) {
|
|
1314
|
+
return {
|
|
1315
|
+
command: request?.command ?? DEFAULT_APP_SERVER_SPAWN_CONFIG.command,
|
|
1316
|
+
args: request?.args ?? [...DEFAULT_APP_SERVER_SPAWN_CONFIG.args],
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function defaultAppServerSpawnConfig(spawn, env) {
|
|
1320
|
+
return {
|
|
1321
|
+
command: spawn.command,
|
|
1322
|
+
args: [...spawn.args],
|
|
1323
|
+
env,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function createDefaultAppServerRegistry(spawn) {
|
|
1327
|
+
// Resolve config first so the spawned child and the registry share the same
|
|
1328
|
+
// depth: the child must receive CLINKX_AGENT_DEPTH=currentDepth+1 so the
|
|
1329
|
+
// recursion guard survives the env sanitizer (review fix 5).
|
|
1330
|
+
const config = resolveRegistryConfig();
|
|
1331
|
+
const childEnv = childEnvMarkerFor(config.currentDepth);
|
|
1332
|
+
const client = appServerRegistryTestOverrides?.client ??
|
|
1333
|
+
new RealCodexAppServerClient(defaultAppServerSpawnConfig(spawn, childEnv));
|
|
1334
|
+
const registryFactory = appServerRegistryTestOverrides?.registryFactory ??
|
|
1335
|
+
((deps) => new AppServerRegistry(deps));
|
|
1336
|
+
return registryFactory({ client, config });
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Get the process-wide app-server registry, creating it on first call.
|
|
1340
|
+
*
|
|
1341
|
+
* The registry is a singleton: one long-lived `codex app-server` process
|
|
1342
|
+
* multiplexes every session. The adapter's `app_server.command/args` therefore
|
|
1343
|
+
* take effect only at FIRST creation. A later adapter requesting a spawn config
|
|
1344
|
+
* that conflicts with the live one is rejected with {@link InvalidParamsError}
|
|
1345
|
+
* (-32602) rather than silently ignored — building a registry-per-config map
|
|
1346
|
+
* would be over-engineering for a singleton-by-design subsystem.
|
|
1347
|
+
*/
|
|
1348
|
+
export function getAppServerRegistry(spawn) {
|
|
1349
|
+
const resolved = resolveSpawnRequest(spawn);
|
|
1350
|
+
if (appServerRegistrySingleton == null) {
|
|
1351
|
+
appServerRegistrySingleton = createDefaultAppServerRegistry(resolved);
|
|
1352
|
+
appServerRegistrySingletonSpawn = resolved;
|
|
1353
|
+
return appServerRegistrySingleton;
|
|
1354
|
+
}
|
|
1355
|
+
// A no-arg call (e.g. clink_agent_manage) is a pure accessor — it never
|
|
1356
|
+
// imposes a spawn config, so it cannot conflict with the live one.
|
|
1357
|
+
if (spawn == null) {
|
|
1358
|
+
return appServerRegistrySingleton;
|
|
1359
|
+
}
|
|
1360
|
+
const live = appServerRegistrySingletonSpawn;
|
|
1361
|
+
if (live != null &&
|
|
1362
|
+
(live.command !== resolved.command ||
|
|
1363
|
+
live.args.length !== resolved.args.length ||
|
|
1364
|
+
live.args.some((arg, i) => arg !== resolved.args[i]))) {
|
|
1365
|
+
throw new InvalidParamsError(`Codex app-server spawn config conflict: the app-server process is already ` +
|
|
1366
|
+
`running as "${live.command} ${live.args.join(" ")}" but this adapter requests ` +
|
|
1367
|
+
`"${resolved.command} ${resolved.args.join(" ")}". The app-server registry is a ` +
|
|
1368
|
+
`single shared process; all app_server adapters must agree on command/args.`);
|
|
1369
|
+
}
|
|
1370
|
+
return appServerRegistrySingleton;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Return the live process-wide registry WITHOUT creating it. Read-only manage
|
|
1374
|
+
* ops (clink_agent_manage) must not lock in a default spawn config before a run
|
|
1375
|
+
* has chosen one — otherwise a later run with a custom app_server.command would
|
|
1376
|
+
* spuriously hit the spawn-config conflict (-32602). See {@link getAppServerRegistry}.
|
|
1377
|
+
*/
|
|
1378
|
+
export function peekAppServerRegistry() {
|
|
1379
|
+
return appServerRegistrySingleton;
|
|
1380
|
+
}
|
|
1381
|
+
export async function shutdownAppServerRegistry(reason) {
|
|
1382
|
+
const registry = appServerRegistrySingleton;
|
|
1383
|
+
if (registry == null)
|
|
1384
|
+
return;
|
|
1385
|
+
await registry.shutdown(reason);
|
|
1386
|
+
appServerRegistrySingleton = undefined;
|
|
1387
|
+
appServerRegistrySingletonSpawn = undefined;
|
|
1388
|
+
}
|
|
1389
|
+
/** The spawn command/args the live singleton was created with (test inspection). */
|
|
1390
|
+
export function appServerRegistrySpawnForTest() {
|
|
1391
|
+
return appServerRegistrySingletonSpawn;
|
|
1392
|
+
}
|
|
1393
|
+
export function configureAppServerRegistryForTest(overrides) {
|
|
1394
|
+
appServerRegistryTestOverrides = overrides;
|
|
1395
|
+
appServerRegistrySingleton = undefined;
|
|
1396
|
+
appServerRegistrySingletonSpawn = undefined;
|
|
1397
|
+
}
|
|
1398
|
+
export async function resetAppServerRegistryForTest(reason = "test reset") {
|
|
1399
|
+
const registry = appServerRegistrySingleton;
|
|
1400
|
+
appServerRegistrySingleton = undefined;
|
|
1401
|
+
appServerRegistrySingletonSpawn = undefined;
|
|
1402
|
+
appServerRegistryTestOverrides = undefined;
|
|
1403
|
+
if (registry != null) {
|
|
1404
|
+
await registry.shutdown(reason);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
//# sourceMappingURL=app-server-registry.js.map
|