@north-light/crouter 0.3.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/bin/crouter +4 -0
- package/bin/crtr +4 -0
- package/bin/crtrd +4 -0
- package/bin/runtime-generation-manifest.mjs +263 -0
- package/bin/runtime-selector.mjs +378 -0
- package/dist/api/__tests__/client.test.d.ts +1 -0
- package/dist/api/__tests__/client.test.js +274 -0
- package/dist/api/client.d.ts +249 -0
- package/dist/api/client.js +616 -0
- package/dist/api/dto/attach.d.ts +16 -0
- package/dist/api/dto/attach.js +13 -0
- package/dist/api/dto/broker.d.ts +45 -0
- package/dist/api/dto/broker.js +20 -0
- package/dist/api/dto/canvas.d.ts +253 -0
- package/dist/api/dto/canvas.js +2 -0
- package/dist/api/dto/common.d.ts +27 -0
- package/dist/api/dto/common.js +15 -0
- package/dist/api/dto/config.d.ts +19 -0
- package/dist/api/dto/config.js +3 -0
- package/dist/api/dto/crons.d.ts +124 -0
- package/dist/api/dto/crons.js +10 -0
- package/dist/api/dto/files.d.ts +11 -0
- package/dist/api/dto/files.js +7 -0
- package/dist/api/dto/focus.d.ts +24 -0
- package/dist/api/dto/focus.js +10 -0
- package/dist/api/dto/health.d.ts +41 -0
- package/dist/api/dto/health.js +2 -0
- package/dist/api/dto/human.d.ts +57 -0
- package/dist/api/dto/human.js +4 -0
- package/dist/api/dto/inbox.d.ts +105 -0
- package/dist/api/dto/inbox.js +10 -0
- package/dist/api/dto/lifecycle.d.ts +79 -0
- package/dist/api/dto/lifecycle.js +3 -0
- package/dist/api/dto/messages.d.ts +55 -0
- package/dist/api/dto/messages.js +2 -0
- package/dist/api/dto/modelauth.d.ts +41 -0
- package/dist/api/dto/modelauth.js +3 -0
- package/dist/api/dto/nodes.d.ts +209 -0
- package/dist/api/dto/nodes.js +3 -0
- package/dist/api/dto/profiles.d.ts +14 -0
- package/dist/api/dto/profiles.js +3 -0
- package/dist/api/dto/reports.d.ts +41 -0
- package/dist/api/dto/reports.js +2 -0
- package/dist/api/dto/subscriptions.d.ts +14 -0
- package/dist/api/dto/subscriptions.js +2 -0
- package/dist/api/dto/worktree.d.ts +19 -0
- package/dist/api/dto/worktree.js +6 -0
- package/dist/api/errors.d.ts +19 -0
- package/dist/api/errors.js +30 -0
- package/dist/api/index.d.ts +24 -0
- package/dist/api/index.js +25 -0
- package/dist/api/routes.d.ts +64 -0
- package/dist/api/routes.js +92 -0
- package/dist/build-root.d.ts +43 -0
- package/dist/build-root.js +96 -0
- package/dist/builtin-memory/00-runtime-base.md +51 -0
- package/dist/builtin-memory/01-spine/00-has-manager.md +23 -0
- package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +19 -0
- package/dist/builtin-memory/02-lifecycle/01-resident.md +10 -0
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +109 -0
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +19 -0
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +26 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +13 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +22 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +21 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +16 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +15 -0
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +11 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +17 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +17 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +17 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +19 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +17 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +20 -0
- package/dist/builtin-memory/05-kinds/product/00-base.md +25 -0
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +15 -0
- package/dist/builtin-memory/05-kinds/product/teardown.md +15 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +19 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +17 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +20 -0
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +15 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +13 -0
- package/dist/builtin-memory/design.md +62 -0
- package/dist/builtin-memory/development.md +120 -0
- package/dist/builtin-memory/internal/INDEX.md +26 -0
- package/dist/builtin-memory/internal/agent-shaping.md +77 -0
- package/dist/builtin-memory/internal/examples/INDEX.md +15 -0
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +100 -0
- package/dist/builtin-memory/internal/marketplaces.md +164 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +51 -0
- package/dist/builtin-memory/internal/plugins.md +249 -0
- package/dist/builtin-memory/internal/storage-tiers.md +41 -0
- package/dist/builtin-memory/planning.md +43 -0
- package/dist/builtin-memory/product.md +80 -0
- package/dist/builtin-memory/spec.md +98 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +34 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +47 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +49 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/crtr-commands.test.ts +93 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +194 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +2632 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +68 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +492 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +95 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/filters.json +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +191 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +448 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +119 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +195 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1232 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +255 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +47 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +19 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/package.json +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/LICENSE +13 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/README.md +172 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/bin.mjs +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/composer.js +219 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +225 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/Document.js +335 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/doc/directives.js +176 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/errors.js +57 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/index.js +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/log.js +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Alias.js +116 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/cst.js +98 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/parse/parser.js +975 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/public-api.js +102 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/tags.js +96 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/util.js +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/dist/visit.js +233 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/index.js +5 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/browser/package.json +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.d.ts +8 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/cli.mjs +201 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-collection.js +90 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-doc.js +45 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-node.js +112 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.d.ts +63 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/composer.js +224 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-end.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-flow-scalar.js +227 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/resolve-props.js +148 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.d.ts +141 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/Document.js +337 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/anchors.js +76 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/applyReviver.js +57 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/createNode.js +90 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.d.ts +49 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/doc/directives.js +178 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.d.ts +21 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/errors.js +62 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.d.ts +25 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/index.js +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.d.ts +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/log.js +19 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Alias.js +118 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Collection.js +151 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Node.js +40 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Pair.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/Scalar.js +27 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/identity.js +53 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/nodes/toJS.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/options.d.ts +350 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst-visit.js +99 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.d.ts +109 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/cst.js +112 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/lexer.js +723 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/line-counter.js +41 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.d.ts +84 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/parse/parser.js +980 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.d.ts +44 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/public-api.js +107 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/Schema.js +39 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/map.js +19 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/null.js +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/seq.js +19 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/common/string.js +16 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/bool.js +21 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/float.js +47 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/int.js +42 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/core/schema.js +25 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json/schema.js +64 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.d.ts +48 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/tags.js +99 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/types.d.ts +92 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringify.js +132 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.d.ts +4 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/test-events.js +134 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.d.ts +16 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/util.js +28 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.d.ts +102 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/dist/visit.js +236 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/package.json +97 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/node_modules/yaml/util.js +2 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +3385 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/package.json +25 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/tsconfig.json +10 -0
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +36 -0
- package/dist/builtin-pi-packages/pi-mode-switch/bin/mode +36 -0
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +449 -0
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +14 -0
- package/dist/builtin-pi-packages/pi-mode-switch/tsconfig.json +9 -0
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/core.mjs +657 -0
- package/dist/builtin-views/canvas/text.mjs +58 -0
- package/dist/builtin-views/canvas/tui.mjs +168 -0
- package/dist/builtin-views/canvas/web.jsx +121 -0
- package/dist/builtin-views/chat/core.mjs +684 -0
- package/dist/builtin-views/chat/text.mjs +101 -0
- package/dist/builtin-views/chat/tui.mjs +360 -0
- package/dist/builtin-views/chat/web.jsx +362 -0
- package/dist/builtin-views/git-pr/core.mjs +673 -0
- package/dist/builtin-views/git-pr/text.mjs +84 -0
- package/dist/builtin-views/git-pr/tui.mjs +301 -0
- package/dist/builtin-views/git-pr/web.jsx +216 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +175 -0
- package/dist/builtin-views/inbox/core.mjs +1273 -0
- package/dist/builtin-views/inbox/text.mjs +73 -0
- package/dist/builtin-views/inbox/tui.mjs +314 -0
- package/dist/builtin-views/inbox/web.jsx +188 -0
- package/dist/builtin-views/linkedin/core.mjs +906 -0
- package/dist/builtin-views/linkedin/text.mjs +69 -0
- package/dist/builtin-views/linkedin/tui.mjs +413 -0
- package/dist/builtin-views/linkedin/web.jsx +206 -0
- package/dist/builtin-views/prompt-review/core.mjs +735 -0
- package/dist/builtin-views/prompt-review/text.mjs +15 -0
- package/dist/builtin-views/prompt-review/tui.mjs +196 -0
- package/dist/builtin-views/prompt-review/web.jsx +484 -0
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.d.ts +1 -0
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.js +50 -0
- package/dist/builtin-views/workspace-sidebar/__tests__/core.test.ts +53 -0
- package/dist/builtin-views/workspace-sidebar/core.mjs +670 -0
- package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
- package/dist/builtin-views/workspace-sidebar/tui.mjs +141 -0
- package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +86 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +467 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +141 -0
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +191 -0
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +236 -0
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +55 -0
- package/dist/clients/attach/__tests__/crtr-output.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/crtr-output.test.js +2103 -0
- package/dist/clients/attach/__tests__/diagram.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/diagram.test.js +42 -0
- package/dist/clients/attach/__tests__/markdown-headings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/markdown-headings.test.js +27 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +133 -0
- package/dist/clients/attach/__tests__/prompt-preview.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/prompt-preview.test.js +87 -0
- package/dist/clients/attach/__tests__/ref-autocomplete.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +176 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +271 -0
- package/dist/clients/attach/__tests__/visible-paths.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/visible-paths.test.js +19 -0
- package/dist/clients/attach/chrome/bash-jobs.d.ts +9 -0
- package/dist/clients/attach/chrome/bash-jobs.js +43 -0
- package/dist/clients/attach/chrome/canvas-panels.d.ts +36 -0
- package/dist/clients/attach/chrome/canvas-panels.js +172 -0
- package/dist/clients/attach/chrome/completion-bell.d.ts +10 -0
- package/dist/clients/attach/chrome/completion-bell.js +89 -0
- package/dist/clients/attach/chrome/cron-wait.d.ts +3 -0
- package/dist/clients/attach/chrome/cron-wait.js +34 -0
- package/dist/clients/attach/chrome/git-info.d.ts +20 -0
- package/dist/clients/attach/chrome/git-info.js +44 -0
- package/dist/clients/attach/chrome/roster.d.ts +26 -0
- package/dist/clients/attach/chrome/roster.js +233 -0
- package/dist/clients/attach/chrome/status-line.d.ts +35 -0
- package/dist/clients/attach/chrome/status-line.js +133 -0
- package/dist/clients/attach/chrome/widgets.d.ts +9 -0
- package/dist/clients/attach/chrome/widgets.js +61 -0
- package/dist/clients/attach/command.d.ts +2 -0
- package/dist/clients/attach/command.js +96 -0
- package/dist/clients/attach/config.d.ts +91 -0
- package/dist/clients/attach/config.js +271 -0
- package/dist/clients/attach/input/clipboard-image.d.ts +55 -0
- package/dist/clients/attach/input/clipboard-image.js +237 -0
- package/dist/clients/attach/input/controller.d.ts +210 -0
- package/dist/clients/attach/input/controller.js +689 -0
- package/dist/clients/attach/input/ref-autocomplete.d.ts +75 -0
- package/dist/clients/attach/input/ref-autocomplete.js +135 -0
- package/dist/clients/attach/input/titled-editor.d.ts +209 -0
- package/dist/clients/attach/input/titled-editor.js +801 -0
- package/dist/clients/attach/overlays/auth.d.ts +13 -0
- package/dist/clients/attach/overlays/auth.js +245 -0
- package/dist/clients/attach/overlays/dialogs.d.ts +27 -0
- package/dist/clients/attach/overlays/dialogs.js +102 -0
- package/dist/clients/attach/overlays/file-review.d.ts +9 -0
- package/dist/clients/attach/overlays/file-review.js +181 -0
- package/dist/clients/attach/overlays/graph.d.ts +76 -0
- package/dist/clients/attach/overlays/graph.js +657 -0
- package/dist/clients/attach/overlays/mcp.d.ts +32 -0
- package/dist/clients/attach/overlays/mcp.js +310 -0
- package/dist/clients/attach/overlays/pickers.d.ts +59 -0
- package/dist/clients/attach/overlays/pickers.js +170 -0
- package/dist/clients/attach/render/chat-view.d.ts +185 -0
- package/dist/clients/attach/render/chat-view.js +946 -0
- package/dist/clients/attach/render/context-message.d.ts +14 -0
- package/dist/clients/attach/render/context-message.js +81 -0
- package/dist/clients/attach/render/crtr-output.d.ts +48 -0
- package/dist/clients/attach/render/crtr-output.js +694 -0
- package/dist/clients/attach/render/diagram.d.ts +14 -0
- package/dist/clients/attach/render/diagram.js +135 -0
- package/dist/clients/attach/render/edit-diff.d.ts +6 -0
- package/dist/clients/attach/render/edit-diff.js +263 -0
- package/dist/clients/attach/render/frozen-history.d.ts +38 -0
- package/dist/clients/attach/render/frozen-history.js +100 -0
- package/dist/clients/attach/render/markdown-headings.d.ts +11 -0
- package/dist/clients/attach/render/markdown-headings.js +82 -0
- package/dist/clients/attach/session/banner.d.ts +9 -0
- package/dist/clients/attach/session/banner.js +29 -0
- package/dist/clients/attach/session/bindings.d.ts +12 -0
- package/dist/clients/attach/session/bindings.js +52 -0
- package/dist/clients/attach/session/chrome-refresh.d.ts +34 -0
- package/dist/clients/attach/session/chrome-refresh.js +93 -0
- package/dist/clients/attach/session/connection.d.ts +44 -0
- package/dist/clients/attach/session/connection.js +178 -0
- package/dist/clients/attach/session/context.d.ts +54 -0
- package/dist/clients/attach/session/context.js +16 -0
- package/dist/clients/attach/session/editor-frame.d.ts +43 -0
- package/dist/clients/attach/session/editor-frame.js +136 -0
- package/dist/clients/attach/session/editor-inventory.d.ts +59 -0
- package/dist/clients/attach/session/editor-inventory.js +161 -0
- package/dist/clients/attach/session/focus-events.d.ts +4 -0
- package/dist/clients/attach/session/focus-events.js +12 -0
- package/dist/clients/attach/session/frames.d.ts +43 -0
- package/dist/clients/attach/session/frames.js +170 -0
- package/dist/clients/attach/session/identity.d.ts +16 -0
- package/dist/clients/attach/session/identity.js +25 -0
- package/dist/clients/attach/session/input-wiring.d.ts +31 -0
- package/dist/clients/attach/session/input-wiring.js +128 -0
- package/dist/clients/attach/session/keys.d.ts +22 -0
- package/dist/clients/attach/session/keys.js +60 -0
- package/dist/clients/attach/session/layout.d.ts +20 -0
- package/dist/clients/attach/session/layout.js +56 -0
- package/dist/clients/attach/session/mode.d.ts +25 -0
- package/dist/clients/attach/session/mode.js +97 -0
- package/dist/clients/attach/session/pane-tag.d.ts +15 -0
- package/dist/clients/attach/session/pane-tag.js +43 -0
- package/dist/clients/attach/session/pi-assets.d.ts +1 -0
- package/dist/clients/attach/session/pi-assets.js +24 -0
- package/dist/clients/attach/session/pi-oauth-flows.d.ts +1 -0
- package/dist/clients/attach/session/pi-oauth-flows.js +21 -0
- package/dist/clients/attach/session/reconnect.d.ts +37 -0
- package/dist/clients/attach/session/reconnect.js +127 -0
- package/dist/clients/attach/session/state-sync.d.ts +38 -0
- package/dist/clients/attach/session/state-sync.js +68 -0
- package/dist/clients/attach/session/teardown.d.ts +30 -0
- package/dist/clients/attach/session/teardown.js +96 -0
- package/dist/clients/attach/slash/dispatch.d.ts +124 -0
- package/dist/clients/attach/slash/dispatch.js +696 -0
- package/dist/clients/attach/slash/prompt-preview.d.ts +30 -0
- package/dist/clients/attach/slash/prompt-preview.js +121 -0
- package/dist/clients/attach/viewer.d.ts +8 -0
- package/dist/clients/attach/viewer.js +1559 -0
- package/dist/clients/attach/visible-paths.d.ts +9 -0
- package/dist/clients/attach/visible-paths.js +76 -0
- package/dist/clients/web/__tests__/push-engine.test.d.ts +1 -0
- package/dist/clients/web/__tests__/push-engine.test.js +302 -0
- package/dist/clients/web/__tests__/push-registry.test.d.ts +1 -0
- package/dist/clients/web/__tests__/push-registry.test.js +177 -0
- package/dist/clients/web/dev-server.d.ts +7 -0
- package/dist/clients/web/dev-server.js +60 -0
- package/dist/clients/web/events.d.ts +19 -0
- package/dist/clients/web/events.js +165 -0
- package/dist/clients/web/push-engine.d.ts +108 -0
- package/dist/clients/web/push-engine.js +256 -0
- package/dist/clients/web/push-registry.d.ts +89 -0
- package/dist/clients/web/push-registry.js +378 -0
- package/dist/clients/web/server.d.ts +19 -0
- package/dist/clients/web/server.js +693 -0
- package/dist/clients/web/web-client/shared/protocol.d.ts +322 -0
- package/dist/clients/web/web-client/shared/protocol.js +8 -0
- package/dist/clients/web/web-cmd.d.ts +2 -0
- package/dist/clients/web/web-cmd.js +141 -0
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.d.ts +1 -0
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +96 -0
- package/dist/commands/__tests__/canvas-config.test.d.ts +1 -0
- package/dist/commands/__tests__/canvas-config.test.js +107 -0
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +344 -0
- package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +1 -0
- package/dist/commands/__tests__/node-no-kickoff-flag.test.js +73 -0
- package/dist/commands/__tests__/node-worktree-flag.test.d.ts +1 -0
- package/dist/commands/__tests__/node-worktree-flag.test.js +49 -0
- package/dist/commands/__tests__/revive-now-gate.test.d.ts +1 -0
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/api-client.d.ts +100 -0
- package/dist/commands/api-client.js +377 -0
- package/dist/commands/attention.d.ts +2 -0
- package/dist/commands/attention.js +223 -0
- package/dist/commands/canvas-browse.d.ts +2 -0
- package/dist/commands/canvas-browse.js +95 -0
- package/dist/commands/canvas-config.d.ts +2 -0
- package/dist/commands/canvas-config.js +210 -0
- package/dist/commands/canvas-history/grep.d.ts +1 -0
- package/dist/commands/canvas-history/grep.js +104 -0
- package/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +48 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +118 -0
- package/dist/commands/canvas-history/shared.d.ts +27 -0
- package/dist/commands/canvas-history/shared.js +76 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +30 -0
- package/dist/commands/canvas-issue.d.ts +2 -0
- package/dist/commands/canvas-issue.js +149 -0
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +108 -0
- package/dist/commands/canvas-rebuild-index.d.ts +2 -0
- package/dist/commands/canvas-rebuild-index.js +46 -0
- package/dist/commands/canvas-roster.d.ts +2 -0
- package/dist/commands/canvas-roster.js +62 -0
- package/dist/commands/canvas-snapshot.d.ts +2 -0
- package/dist/commands/canvas-snapshot.js +34 -0
- package/dist/commands/canvas-use.d.ts +1 -0
- package/dist/commands/canvas-use.js +63 -0
- package/dist/commands/canvas.d.ts +3 -0
- package/dist/commands/canvas.js +50 -0
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +242 -0
- package/dist/commands/cron.d.ts +2 -0
- package/dist/commands/cron.js +524 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +80 -0
- package/dist/commands/human/prompts.d.ts +8 -0
- package/dist/commands/human/prompts.js +427 -0
- package/dist/commands/human/queue.d.ts +12 -0
- package/dist/commands/human/queue.js +531 -0
- package/dist/commands/human/shared.d.ts +80 -0
- package/dist/commands/human/shared.js +194 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +44 -0
- package/dist/commands/memory/delete.d.ts +1 -0
- package/dist/commands/memory/delete.js +77 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +192 -0
- package/dist/commands/memory/lint.d.ts +9 -0
- package/dist/commands/memory/lint.js +217 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +84 -0
- package/dist/commands/memory/origin.d.ts +1 -0
- package/dist/commands/memory/origin.js +100 -0
- package/dist/commands/memory/read.d.ts +1 -0
- package/dist/commands/memory/read.js +72 -0
- package/dist/commands/memory/shared.d.ts +54 -0
- package/dist/commands/memory/shared.js +225 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +142 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +26 -0
- package/dist/commands/node-context.d.ts +2 -0
- package/dist/commands/node-context.js +157 -0
- package/dist/commands/node-inspect-artifacts.d.ts +1 -0
- package/dist/commands/node-inspect-artifacts.js +109 -0
- package/dist/commands/node-lifecycle-revive.d.ts +12 -0
- package/dist/commands/node-lifecycle-revive.js +287 -0
- package/dist/commands/node-snapshot.d.ts +2 -0
- package/dist/commands/node-snapshot.js +37 -0
- package/dist/commands/node-transcript.d.ts +2 -0
- package/dist/commands/node-transcript.js +62 -0
- package/dist/commands/node-worktree.d.ts +2 -0
- package/dist/commands/node-worktree.js +102 -0
- package/dist/commands/node.d.ts +44 -0
- package/dist/commands/node.js +1943 -0
- package/dist/commands/pkg/cli-inspect.d.ts +17 -0
- package/dist/commands/pkg/cli-inspect.js +190 -0
- package/dist/commands/pkg/cli-manage.d.ts +3 -0
- package/dist/commands/pkg/cli-manage.js +206 -0
- package/dist/commands/pkg/cli.d.ts +1 -0
- package/dist/commands/pkg/cli.js +14 -0
- package/dist/commands/pkg/market-inspect.d.ts +2 -0
- package/dist/commands/pkg/market-inspect.js +141 -0
- package/dist/commands/pkg/market-manage.d.ts +3 -0
- package/dist/commands/pkg/market-manage.js +224 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +14 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +2 -0
- package/dist/commands/pkg/plugin-inspect.js +150 -0
- package/dist/commands/pkg/plugin-manage.d.ts +19 -0
- package/dist/commands/pkg/plugin-manage.js +398 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +14 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +24 -0
- package/dist/commands/profile/default.d.ts +2 -0
- package/dist/commands/profile/default.js +143 -0
- package/dist/commands/profile/delete.d.ts +1 -0
- package/dist/commands/profile/delete.js +39 -0
- package/dist/commands/profile/env.d.ts +2 -0
- package/dist/commands/profile/env.js +194 -0
- package/dist/commands/profile/list.d.ts +1 -0
- package/dist/commands/profile/list.js +35 -0
- package/dist/commands/profile/new.d.ts +1 -0
- package/dist/commands/profile/new.js +75 -0
- package/dist/commands/profile/project.d.ts +2 -0
- package/dist/commands/profile/project.js +97 -0
- package/dist/commands/profile/rename.d.ts +1 -0
- package/dist/commands/profile/rename.js +42 -0
- package/dist/commands/profile/show.d.ts +1 -0
- package/dist/commands/profile/show.js +50 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +37 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +166 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +174 -0
- package/dist/commands/search/answer.d.ts +1 -0
- package/dist/commands/search/answer.js +50 -0
- package/dist/commands/search/contents.d.ts +1 -0
- package/dist/commands/search/contents.js +96 -0
- package/dist/commands/search/exa.d.ts +53 -0
- package/dist/commands/search/exa.js +157 -0
- package/dist/commands/search/puremd.d.ts +11 -0
- package/dist/commands/search/puremd.js +53 -0
- package/dist/commands/search/web.d.ts +1 -0
- package/dist/commands/search/web.js +65 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +24 -0
- package/dist/commands/surface-edit.d.ts +2 -0
- package/dist/commands/surface-edit.js +159 -0
- package/dist/commands/surface-inspect.d.ts +2 -0
- package/dist/commands/surface-inspect.js +38 -0
- package/dist/commands/surface-node.d.ts +2 -0
- package/dist/commands/surface-node.js +16 -0
- package/dist/commands/surface-reload.d.ts +2 -0
- package/dist/commands/surface-reload.js +127 -0
- package/dist/commands/surface-tmux-spread.d.ts +2 -0
- package/dist/commands/surface-tmux-spread.js +220 -0
- package/dist/commands/surface.d.ts +2 -0
- package/dist/commands/surface.js +48 -0
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +95 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +5 -0
- package/dist/commands/sys/__tests__/helpers/setup-runtime.js +19 -0
- package/dist/commands/sys/__tests__/setup-core.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +291 -0
- package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/setup-front-door.test.js +137 -0
- package/dist/commands/sys/__tests__/setup-models.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/setup-models.test.js +205 -0
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +155 -0
- package/dist/commands/sys/__tests__/sync-deps.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/sync-deps.test.js +188 -0
- package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/sync-import.test.js +314 -0
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +292 -0
- package/dist/commands/sys/daemon.d.ts +2 -0
- package/dist/commands/sys/daemon.js +188 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +528 -0
- package/dist/commands/sys/feedback.d.ts +1 -0
- package/dist/commands/sys/feedback.js +174 -0
- package/dist/commands/sys/logs.d.ts +1 -0
- package/dist/commands/sys/logs.js +127 -0
- package/dist/commands/sys/prompt-review.d.ts +1 -0
- package/dist/commands/sys/prompt-review.js +291 -0
- package/dist/commands/sys/promptstudio.d.ts +2 -0
- package/dist/commands/sys/promptstudio.js +65 -0
- package/dist/commands/sys/setup-core.d.ts +138 -0
- package/dist/commands/sys/setup-core.js +582 -0
- package/dist/commands/sys/setup-wizard.d.ts +130 -0
- package/dist/commands/sys/setup-wizard.js +2310 -0
- package/dist/commands/sys/setup.d.ts +6 -0
- package/dist/commands/sys/setup.js +36 -0
- package/dist/commands/sys/shared.d.ts +51 -0
- package/dist/commands/sys/shared.js +81 -0
- package/dist/commands/sys/support/prepare.d.ts +13 -0
- package/dist/commands/sys/support/prepare.js +235 -0
- package/dist/commands/sys/support/submit.d.ts +19 -0
- package/dist/commands/sys/support/submit.js +525 -0
- package/dist/commands/sys/support.d.ts +2 -0
- package/dist/commands/sys/support.js +18 -0
- package/dist/commands/sys/sync-deps.d.ts +19 -0
- package/dist/commands/sys/sync-deps.js +508 -0
- package/dist/commands/sys/sync-project-guidance.d.ts +1 -0
- package/dist/commands/sys/sync-project-guidance.js +345 -0
- package/dist/commands/sys/sync-shared.d.ts +7 -0
- package/dist/commands/sys/sync-shared.js +49 -0
- package/dist/commands/sys/sync-skills.d.ts +1 -0
- package/dist/commands/sys/sync-skills.js +306 -0
- package/dist/commands/sys/sync.d.ts +2 -0
- package/dist/commands/sys/sync.js +18 -0
- package/dist/commands/sys/sysprompt.d.ts +1 -0
- package/dist/commands/sys/sysprompt.js +97 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +139 -0
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +30 -0
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +130 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +74 -0
- package/dist/commands/view-pick.d.ts +6 -0
- package/dist/commands/view-pick.js +129 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +214 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +31 -0
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +165 -0
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +60 -0
- package/dist/core/__tests__/boot-fault-attempt.test.d.ts +1 -0
- package/dist/core/__tests__/boot-fault-attempt.test.js +83 -0
- package/dist/core/__tests__/boot.test.d.ts +1 -0
- package/dist/core/__tests__/boot.test.js +275 -0
- package/dist/core/__tests__/branded-host-concurrency.test.d.ts +1 -0
- package/dist/core/__tests__/branded-host-concurrency.test.js +72 -0
- package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +118 -0
- package/dist/core/__tests__/broker-launch-candidates.test.d.ts +1 -0
- package/dist/core/__tests__/broker-launch-candidates.test.js +241 -0
- package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
- package/dist/core/__tests__/broker-preflight.test.js +95 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +732 -0
- package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
- package/dist/core/__tests__/broker-snapshot-history.test.js +213 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +502 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +255 -0
- package/dist/core/__tests__/canvas.test.d.ts +1 -0
- package/dist/core/__tests__/canvas.test.js +201 -0
- package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
- package/dist/core/__tests__/chat-view-reconnect.test.js +105 -0
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +161 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +254 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +325 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +448 -0
- package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
- package/dist/core/__tests__/connection-reconnect-resume.test.js +36 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +256 -0
- package/dist/core/__tests__/cron-broker-capacity.test.d.ts +1 -0
- package/dist/core/__tests__/cron-broker-capacity.test.js +100 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +374 -0
- package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-wedge.test.js +104 -0
- package/dist/core/__tests__/dead-node-policy-table.test.d.ts +1 -0
- package/dist/core/__tests__/dead-node-policy-table.test.js +103 -0
- package/dist/core/__tests__/deferred-no-wake.test.d.ts +1 -0
- package/dist/core/__tests__/deferred-no-wake.test.js +514 -0
- package/dist/core/__tests__/events-contract.test.d.ts +1 -0
- package/dist/core/__tests__/events-contract.test.js +76 -0
- package/dist/core/__tests__/fault-classifier.test.d.ts +1 -0
- package/dist/core/__tests__/fault-classifier.test.js +71 -0
- package/dist/core/__tests__/finalization-migration.test.d.ts +1 -0
- package/dist/core/__tests__/finalization-migration.test.js +38 -0
- package/dist/core/__tests__/fixtures/api-server-host.d.ts +1 -0
- package/dist/core/__tests__/fixtures/api-server-host.js +47 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +34 -0
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +8 -0
- package/dist/core/__tests__/fixtures/extra-extension.d.ts +1 -0
- package/dist/core/__tests__/fixtures/extra-extension.js +3 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +310 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +1210 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +311 -0
- package/dist/core/__tests__/fixtures/metadata-migration-race.d.ts +1 -0
- package/dist/core/__tests__/fixtures/metadata-migration-race.js +49 -0
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +1 -0
- package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +15 -0
- package/dist/core/__tests__/flagship-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +303 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +208 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +108 -0
- package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-attach-limits.test.js +147 -0
- package/dist/core/__tests__/full/broker-attach-stream.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-attach-stream.test.js +133 -0
- package/dist/core/__tests__/full/broker-crash-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +122 -0
- package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-deliver-ack.test.js +73 -0
- package/dist/core/__tests__/full/broker-dialogs.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +180 -0
- package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-display-snapshot.test.js +75 -0
- package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-dormant-wake.test.js +130 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-pane-resolution.test.js +155 -0
- package/dist/core/__tests__/full/broker-provider-retry.test.d.ts +1 -0
- package/dist/core/__tests__/full/broker-provider-retry.test.js +387 -0
- package/dist/core/__tests__/full/cascade-close.test.d.ts +1 -0
- package/dist/core/__tests__/full/cascade-close.test.js +240 -0
- package/dist/core/__tests__/full/consult-startup-failure.test.d.ts +1 -0
- package/dist/core/__tests__/full/consult-startup-failure.test.js +124 -0
- package/dist/core/__tests__/full/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/full/detach-focus.test.js +126 -0
- package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +1 -0
- package/dist/core/__tests__/full/idle-release-settlement.test.js +108 -0
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +1 -0
- package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +107 -0
- package/dist/core/__tests__/full/park-clock-release.test.d.ts +1 -0
- package/dist/core/__tests__/full/park-clock-release.test.js +56 -0
- package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +1 -0
- package/dist/core/__tests__/full/yield-message-transaction.test.js +70 -0
- package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
- package/dist/core/__tests__/helpers/broker-clients.js +182 -0
- package/dist/core/__tests__/helpers/fleet.d.ts +12 -0
- package/dist/core/__tests__/helpers/fleet.js +45 -0
- package/dist/core/__tests__/helpers/harness.d.ts +128 -0
- package/dist/core/__tests__/helpers/harness.js +867 -0
- package/dist/core/__tests__/helpers/viewer-panes.d.ts +12 -0
- package/dist/core/__tests__/helpers/viewer-panes.js +133 -0
- package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
- package/dist/core/__tests__/history-inbox.test.js +105 -0
- package/dist/core/__tests__/host-teardown-process-group.test.d.ts +1 -0
- package/dist/core/__tests__/host-teardown-process-group.test.js +476 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +71 -0
- package/dist/core/__tests__/human-deliver-e2e.test.d.ts +1 -0
- package/dist/core/__tests__/human-deliver-e2e.test.js +123 -0
- package/dist/core/__tests__/human-deliver.test.d.ts +1 -0
- package/dist/core/__tests__/human-deliver.test.js +399 -0
- package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +94 -0
- package/dist/core/__tests__/inline-memory-refs.test.d.ts +1 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +320 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +148 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +165 -0
- package/dist/core/__tests__/live-lifecycle-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/live-lifecycle-prompt.test.js +51 -0
- package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation-verbs.test.js +195 -0
- package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation.test.js +197 -0
- package/dist/core/__tests__/memory-index-render.test.d.ts +1 -0
- package/dist/core/__tests__/memory-index-render.test.js +60 -0
- package/dist/core/__tests__/memory-resolver-precedence.test.d.ts +1 -0
- package/dist/core/__tests__/memory-resolver-precedence.test.js +169 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +717 -0
- package/dist/core/__tests__/model-pin-durability.test.d.ts +1 -0
- package/dist/core/__tests__/model-pin-durability.test.js +124 -0
- package/dist/core/__tests__/on-read-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +67 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-dedup-resume.test.js +121 -0
- package/dist/core/__tests__/on-read-identity.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-identity.test.js +68 -0
- package/dist/core/__tests__/parse-argv-stdin-secret.test.d.ts +1 -0
- package/dist/core/__tests__/parse-argv-stdin-secret.test.js +26 -0
- package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
- package/dist/core/__tests__/passive-subscription.test.js +212 -0
- package/dist/core/__tests__/pid-identity-match.test.d.ts +1 -0
- package/dist/core/__tests__/pid-identity-match.test.js +145 -0
- package/dist/core/__tests__/pid-zombie.test.d.ts +1 -0
- package/dist/core/__tests__/pid-zombie.test.js +42 -0
- package/dist/core/__tests__/preview-registry-sync.test.d.ts +1 -0
- package/dist/core/__tests__/preview-registry-sync.test.js +65 -0
- package/dist/core/__tests__/prune-to-limit.test.d.ts +1 -0
- package/dist/core/__tests__/prune-to-limit.test.js +126 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +135 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +184 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/refresh-stall-recycle.test.js +101 -0
- package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch-root.test.js +114 -0
- package/dist/core/__tests__/remote-canvas-target.test.d.ts +1 -0
- package/dist/core/__tests__/remote-canvas-target.test.js +87 -0
- package/dist/core/__tests__/reset.test.d.ts +1 -0
- package/dist/core/__tests__/reset.test.js +69 -0
- package/dist/core/__tests__/respawn-throttle.test.d.ts +1 -0
- package/dist/core/__tests__/respawn-throttle.test.js +154 -0
- package/dist/core/__tests__/review-model-floor.test.d.ts +1 -0
- package/dist/core/__tests__/review-model-floor.test.js +104 -0
- package/dist/core/__tests__/revive-all.test.d.ts +1 -0
- package/dist/core/__tests__/revive-all.test.js +147 -0
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +575 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/secrets.test.d.ts +1 -0
- package/dist/core/__tests__/secrets.test.js +55 -0
- package/dist/core/__tests__/session-branch.test.d.ts +1 -0
- package/dist/core/__tests__/session-branch.test.js +61 -0
- package/dist/core/__tests__/session-cycles.test.d.ts +1 -0
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/session-model.test.d.ts +1 -0
- package/dist/core/__tests__/session-model.test.js +129 -0
- package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-no-kickoff.test.js +119 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +320 -0
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +59 -0
- package/dist/core/__tests__/stdin-body-no-discard.test.d.ts +1 -0
- package/dist/core/__tests__/stdin-body-no-discard.test.js +33 -0
- package/dist/core/__tests__/stdout-errors.test.d.ts +1 -0
- package/dist/core/__tests__/stdout-errors.test.js +16 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +41 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +157 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +154 -0
- package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
- package/dist/core/__tests__/subcommand-tier.test.js +99 -0
- package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/subscription-delivery.test.js +211 -0
- package/dist/core/__tests__/subscription-pool-credential-guard.test.d.ts +1 -0
- package/dist/core/__tests__/subscription-pool-credential-guard.test.js +94 -0
- package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +1 -0
- package/dist/core/__tests__/test-runner-cheap-sharding.test.js +97 -0
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +470 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +102 -0
- package/dist/core/__tests__/worktree.test.d.ts +1 -0
- package/dist/core/__tests__/worktree.test.js +876 -0
- package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
- package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
- package/dist/core/auth-file.d.ts +40 -0
- package/dist/core/auth-file.js +177 -0
- package/dist/core/auth-interaction.d.ts +19 -0
- package/dist/core/auth-interaction.js +94 -0
- package/dist/core/auto-update.d.ts +1 -0
- package/dist/core/auto-update.js +93 -0
- package/dist/core/bash-jobs.d.ts +55 -0
- package/dist/core/bash-jobs.js +150 -0
- package/dist/core/bootstrap.d.ts +5 -0
- package/dist/core/bootstrap.js +98 -0
- package/dist/core/broker-client/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/core/broker-client/__tests__/transport-relay.test.js +213 -0
- package/dist/core/broker-client/client.d.ts +97 -0
- package/dist/core/broker-client/client.js +189 -0
- package/dist/core/broker-client/index.d.ts +1 -0
- package/dist/core/broker-client/index.js +13 -0
- package/dist/core/broker-client/transport-relay.d.ts +51 -0
- package/dist/core/broker-client/transport-relay.js +169 -0
- package/dist/core/broker-client/transport-socket.d.ts +29 -0
- package/dist/core/broker-client/transport-socket.js +164 -0
- package/dist/core/broker-client/transport.d.ts +32 -0
- package/dist/core/broker-client/transport.js +26 -0
- package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/attention.test.js +97 -0
- package/dist/core/canvas/__tests__/human-work-outbox.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/human-work-outbox.test.js +123 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +372 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/remote-event-stream.test.js +146 -0
- package/dist/core/canvas/__tests__/render-remote.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/render-remote.test.js +241 -0
- package/dist/core/canvas/__tests__/source-resolve.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/source-resolve.test.js +77 -0
- package/dist/core/canvas/attention.d.ts +19 -0
- package/dist/core/canvas/attention.js +87 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/boot.d.ts +93 -0
- package/dist/core/canvas/boot.js +190 -0
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +290 -0
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +134 -0
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/render.test.js +111 -0
- package/dist/core/canvas/browse/app.d.ts +41 -0
- package/dist/core/canvas/browse/app.js +916 -0
- package/dist/core/canvas/browse/model.d.ts +169 -0
- package/dist/core/canvas/browse/model.js +485 -0
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/rebuild-coalescer.d.ts +7 -0
- package/dist/core/canvas/browse/rebuild-coalescer.js +61 -0
- package/dist/core/canvas/browse/render.d.ts +59 -0
- package/dist/core/canvas/browse/render.js +550 -0
- package/dist/core/canvas/canvas.d.ts +358 -0
- package/dist/core/canvas/canvas.js +897 -0
- package/dist/core/canvas/crons.d.ts +222 -0
- package/dist/core/canvas/crons.js +367 -0
- package/dist/core/canvas/daemon-owner.d.ts +67 -0
- package/dist/core/canvas/daemon-owner.js +141 -0
- package/dist/core/canvas/db.d.ts +30 -0
- package/dist/core/canvas/db.js +1167 -0
- package/dist/core/canvas/extensions.d.ts +5 -0
- package/dist/core/canvas/extensions.js +39 -0
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +490 -0
- package/dist/core/canvas/human-work-outbox.d.ts +73 -0
- package/dist/core/canvas/human-work-outbox.js +261 -0
- package/dist/core/canvas/index.d.ts +11 -0
- package/dist/core/canvas/index.js +13 -0
- package/dist/core/canvas/install-id.d.ts +9 -0
- package/dist/core/canvas/install-id.js +84 -0
- package/dist/core/canvas/labels.d.ts +28 -0
- package/dist/core/canvas/labels.js +38 -0
- package/dist/core/canvas/meta-file.d.ts +20 -0
- package/dist/core/canvas/meta-file.js +98 -0
- package/dist/core/canvas/nav-model.d.ts +55 -0
- package/dist/core/canvas/nav-model.js +284 -0
- package/dist/core/canvas/nav-render.d.ts +124 -0
- package/dist/core/canvas/nav-render.js +266 -0
- package/dist/core/canvas/paths.d.ts +81 -0
- package/dist/core/canvas/paths.js +164 -0
- package/dist/core/canvas/pid.d.ts +335 -0
- package/dist/core/canvas/pid.js +636 -0
- package/dist/core/canvas/remote-canvas-source.d.ts +62 -0
- package/dist/core/canvas/remote-canvas-source.js +231 -0
- package/dist/core/canvas/remote-event-stream.d.ts +24 -0
- package/dist/core/canvas/remote-event-stream.js +94 -0
- package/dist/core/canvas/render-source.d.ts +132 -0
- package/dist/core/canvas/render-source.js +457 -0
- package/dist/core/canvas/render.d.ts +47 -0
- package/dist/core/canvas/render.js +237 -0
- package/dist/core/canvas/source.d.ts +35 -0
- package/dist/core/canvas/source.js +44 -0
- package/dist/core/canvas/status-glyph.d.ts +42 -0
- package/dist/core/canvas/status-glyph.js +116 -0
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +302 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/clipboard-text.d.ts +3 -0
- package/dist/core/clipboard-text.js +39 -0
- package/dist/core/command-manifests/registry.d.ts +43 -0
- package/dist/core/command-manifests/registry.js +103 -0
- package/dist/core/command-manifests/schema.d.ts +87 -0
- package/dist/core/command-manifests/schema.js +849 -0
- package/dist/core/command-plugins/adapter.d.ts +22 -0
- package/dist/core/command-plugins/adapter.js +160 -0
- package/dist/core/command-plugins/compose.d.ts +11 -0
- package/dist/core/command-plugins/compose.js +88 -0
- package/dist/core/command-plugins/discovery.d.ts +74 -0
- package/dist/core/command-plugins/discovery.js +291 -0
- package/dist/core/command.d.ts +175 -0
- package/dist/core/command.js +636 -0
- package/dist/core/config.d.ts +125 -0
- package/dist/core/config.js +620 -0
- package/dist/core/configured-clis/cache.d.ts +16 -0
- package/dist/core/configured-clis/cache.js +57 -0
- package/dist/core/configured-clis/compose.d.ts +14 -0
- package/dist/core/configured-clis/compose.js +60 -0
- package/dist/core/configured-clis/discovery.d.ts +47 -0
- package/dist/core/configured-clis/discovery.js +173 -0
- package/dist/core/configured-clis/fetch.d.ts +51 -0
- package/dist/core/configured-clis/fetch.js +156 -0
- package/dist/core/configured-clis/invoker.d.ts +36 -0
- package/dist/core/configured-clis/invoker.js +547 -0
- package/dist/core/configured-clis/manifest.d.ts +24 -0
- package/dist/core/configured-clis/manifest.js +284 -0
- package/dist/core/configured-clis/registration.d.ts +40 -0
- package/dist/core/configured-clis/registration.js +201 -0
- package/dist/core/errors.d.ts +27 -0
- package/dist/core/errors.js +48 -0
- package/dist/core/events/emit.d.ts +4 -0
- package/dist/core/events/emit.js +17 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/index.d.ts +5 -0
- package/dist/core/events/index.js +4 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/read.d.ts +40 -0
- package/dist/core/events/read.js +639 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +18 -0
- package/dist/core/events/source.js +47 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/fault-classifier.d.ts +43 -0
- package/dist/core/fault-classifier.js +143 -0
- package/dist/core/feed/__tests__/coalesce-human.test.d.ts +1 -0
- package/dist/core/feed/__tests__/coalesce-human.test.js +36 -0
- package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
- package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
- package/dist/core/feed/__tests__/inbox-migrate.test.d.ts +1 -0
- package/dist/core/feed/__tests__/inbox-migrate.test.js +53 -0
- package/dist/core/feed/feed.d.ts +31 -0
- package/dist/core/feed/feed.js +159 -0
- package/dist/core/feed/inbox-migrate.d.ts +11 -0
- package/dist/core/feed/inbox-migrate.js +135 -0
- package/dist/core/feed/inbox.d.ts +84 -0
- package/dist/core/feed/inbox.js +407 -0
- package/dist/core/feed/passive.d.ts +14 -0
- package/dist/core/feed/passive.js +114 -0
- package/dist/core/frontmatter.d.ts +8 -0
- package/dist/core/frontmatter.js +35 -0
- package/dist/core/fs-utils.d.ts +21 -0
- package/dist/core/fs-utils.js +149 -0
- package/dist/core/git.d.ts +22 -0
- package/dist/core/git.js +72 -0
- package/dist/core/help.d.ts +223 -0
- package/dist/core/help.js +275 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
- package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +84 -0
- package/dist/core/host-exports/builtins.d.ts +50 -0
- package/dist/core/host-exports/builtins.js +68 -0
- package/dist/core/host-exports/export.d.ts +32 -0
- package/dist/core/host-exports/export.js +128 -0
- package/dist/core/inspector/core.d.ts +23 -0
- package/dist/core/inspector/core.js +148 -0
- package/dist/core/inspector/model.d.ts +54 -0
- package/dist/core/inspector/model.js +182 -0
- package/dist/core/inspector/text.d.ts +5 -0
- package/dist/core/inspector/text.js +32 -0
- package/dist/core/inspector/tui.d.ts +3 -0
- package/dist/core/inspector/tui.js +220 -0
- package/dist/core/io.d.ts +73 -0
- package/dist/core/io.js +254 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +38 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +233 -0
- package/dist/core/keybindings/background-bash.d.ts +13 -0
- package/dist/core/keybindings/background-bash.js +34 -0
- package/dist/core/keybindings/catalog.d.ts +15 -0
- package/dist/core/keybindings/catalog.js +285 -0
- package/dist/core/keybindings/inbox.d.ts +21 -0
- package/dist/core/keybindings/inbox.js +35 -0
- package/dist/core/keybindings/index.d.ts +7 -0
- package/dist/core/keybindings/index.js +6 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +105 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +22 -0
- package/dist/core/keybindings/resolve.js +333 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/locked-file.d.ts +24 -0
- package/dist/core/locked-file.js +91 -0
- package/dist/core/log.d.ts +3 -0
- package/dist/core/log.js +27 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/memory/inline-ref-grammar.d.ts +53 -0
- package/dist/core/memory/inline-ref-grammar.js +99 -0
- package/dist/core/memory/inline-ref-guidance.d.ts +8 -0
- package/dist/core/memory/inline-ref-guidance.js +35 -0
- package/dist/core/memory/inline-ref-inventory.d.ts +13 -0
- package/dist/core/memory/inline-ref-inventory.js +79 -0
- package/dist/core/memory-resolver.d.ts +106 -0
- package/dist/core/memory-resolver.js +422 -0
- package/dist/core/output.d.ts +35 -0
- package/dist/core/output.js +99 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/pool-credential-store.d.ts +34 -0
- package/dist/core/pool-credential-store.js +116 -0
- package/dist/core/predicate.d.ts +65 -0
- package/dist/core/predicate.js +191 -0
- package/dist/core/preview-registry.d.ts +25 -0
- package/dist/core/preview-registry.js +2841 -0
- package/dist/core/profiles/__tests__/env-store.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/env-store.test.js +97 -0
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/default-binding.d.ts +6 -0
- package/dist/core/profiles/default-binding.js +46 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +157 -0
- package/dist/core/profiles/manifest.d.ts +35 -0
- package/dist/core/profiles/manifest.js +371 -0
- package/dist/core/profiles/select.d.ts +42 -0
- package/dist/core/profiles/select.js +673 -0
- package/dist/core/profiles/state-block.d.ts +4 -0
- package/dist/core/profiles/state-block.js +16 -0
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +22 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +127 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +136 -0
- package/dist/core/runtime/__tests__/command-expansion.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/command-expansion.test.js +25 -0
- package/dist/core/runtime/__tests__/engine-route.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/engine-route.test.js +36 -0
- package/dist/core/runtime/__tests__/install-id-cache-scoping.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +41 -0
- package/dist/core/runtime/__tests__/node-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/node-env.test.js +73 -0
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +34 -0
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/auth-reload.d.ts +7 -0
- package/dist/core/runtime/auth-reload.js +67 -0
- package/dist/core/runtime/banner.d.ts +13 -0
- package/dist/core/runtime/banner.js +51 -0
- package/dist/core/runtime/bearings.d.ts +99 -0
- package/dist/core/runtime/bearings.js +429 -0
- package/dist/core/runtime/boot-root.d.ts +24 -0
- package/dist/core/runtime/boot-root.js +102 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +342 -0
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +81 -0
- package/dist/core/runtime/broker-protocol.d.ts +642 -0
- package/dist/core/runtime/broker-protocol.js +146 -0
- package/dist/core/runtime/broker-request.d.ts +37 -0
- package/dist/core/runtime/broker-request.js +85 -0
- package/dist/core/runtime/broker-sdk.d.ts +36 -0
- package/dist/core/runtime/broker-sdk.js +128 -0
- package/dist/core/runtime/broker.d.ts +245 -0
- package/dist/core/runtime/broker.js +3711 -0
- package/dist/core/runtime/busy.d.ts +32 -0
- package/dist/core/runtime/busy.js +107 -0
- package/dist/core/runtime/canvas-extensions.d.ts +20 -0
- package/dist/core/runtime/canvas-extensions.js +70 -0
- package/dist/core/runtime/close.d.ts +34 -0
- package/dist/core/runtime/close.js +223 -0
- package/dist/core/runtime/command-expansion.d.ts +19 -0
- package/dist/core/runtime/command-expansion.js +104 -0
- package/dist/core/runtime/connectivity.d.ts +12 -0
- package/dist/core/runtime/connectivity.js +73 -0
- package/dist/core/runtime/deliver-live.d.ts +26 -0
- package/dist/core/runtime/deliver-live.js +63 -0
- package/dist/core/runtime/fault-recovery-nudge.d.ts +3 -0
- package/dist/core/runtime/fault-recovery-nudge.js +3 -0
- package/dist/core/runtime/fault-recovery.d.ts +23 -0
- package/dist/core/runtime/fault-recovery.js +83 -0
- package/dist/core/runtime/fault.d.ts +64 -0
- package/dist/core/runtime/fault.js +260 -0
- package/dist/core/runtime/fleet.d.ts +57 -0
- package/dist/core/runtime/fleet.js +35 -0
- package/dist/core/runtime/front-door.d.ts +16 -0
- package/dist/core/runtime/front-door.js +186 -0
- package/dist/core/runtime/host.d.ts +43 -0
- package/dist/core/runtime/host.js +432 -0
- package/dist/core/runtime/interactive-deliver.d.ts +15 -0
- package/dist/core/runtime/interactive-deliver.js +41 -0
- package/dist/core/runtime/kickoff.d.ts +53 -0
- package/dist/core/runtime/kickoff.js +289 -0
- package/dist/core/runtime/launch-prompt.d.ts +8 -0
- package/dist/core/runtime/launch-prompt.js +13 -0
- package/dist/core/runtime/launch.d.ts +191 -0
- package/dist/core/runtime/launch.js +442 -0
- package/dist/core/runtime/lifecycle.d.ts +12 -0
- package/dist/core/runtime/lifecycle.js +121 -0
- package/dist/core/runtime/managed-provider-cooling.d.ts +38 -0
- package/dist/core/runtime/managed-provider-cooling.js +81 -0
- package/dist/core/runtime/memory.d.ts +3 -0
- package/dist/core/runtime/memory.js +18 -0
- package/dist/core/runtime/model-selection.d.ts +41 -0
- package/dist/core/runtime/model-selection.js +118 -0
- package/dist/core/runtime/model-swap.d.ts +27 -0
- package/dist/core/runtime/model-swap.js +70 -0
- package/dist/core/runtime/naming-persist.d.ts +11 -0
- package/dist/core/runtime/naming-persist.js +41 -0
- package/dist/core/runtime/naming.d.ts +26 -0
- package/dist/core/runtime/naming.js +174 -0
- package/dist/core/runtime/node-read.d.ts +36 -0
- package/dist/core/runtime/node-read.js +255 -0
- package/dist/core/runtime/nodes.d.ts +113 -0
- package/dist/core/runtime/nodes.js +330 -0
- package/dist/core/runtime/package-health.d.ts +49 -0
- package/dist/core/runtime/package-health.js +217 -0
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +132 -0
- package/dist/core/runtime/pi-cli.d.ts +8 -0
- package/dist/core/runtime/pi-cli.js +65 -0
- package/dist/core/runtime/pi-vendored.d.ts +26 -0
- package/dist/core/runtime/pi-vendored.js +77 -0
- package/dist/core/runtime/placement-tmux.d.ts +42 -0
- package/dist/core/runtime/placement-tmux.js +151 -0
- package/dist/core/runtime/placement.d.ts +97 -0
- package/dist/core/runtime/placement.js +394 -0
- package/dist/core/runtime/promote.d.ts +44 -0
- package/dist/core/runtime/promote.js +176 -0
- package/dist/core/runtime/recap.d.ts +8 -0
- package/dist/core/runtime/recap.js +136 -0
- package/dist/core/runtime/recycle.d.ts +14 -0
- package/dist/core/runtime/recycle.js +158 -0
- package/dist/core/runtime/reopen.d.ts +32 -0
- package/dist/core/runtime/reopen.js +119 -0
- package/dist/core/runtime/reset.d.ts +68 -0
- package/dist/core/runtime/reset.js +198 -0
- package/dist/core/runtime/resume-root.d.ts +6 -0
- package/dist/core/runtime/resume-root.js +292 -0
- package/dist/core/runtime/revive-all.d.ts +61 -0
- package/dist/core/runtime/revive-all.js +106 -0
- package/dist/core/runtime/revive.d.ts +58 -0
- package/dist/core/runtime/revive.js +357 -0
- package/dist/core/runtime/roadmap.d.ts +13 -0
- package/dist/core/runtime/roadmap.js +45 -0
- package/dist/core/runtime/session-branch.d.ts +13 -0
- package/dist/core/runtime/session-branch.js +46 -0
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/session-list-cache.d.ts +53 -0
- package/dist/core/runtime/session-list-cache.js +406 -0
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/situational-live.d.ts +9 -0
- package/dist/core/runtime/situational-live.js +40 -0
- package/dist/core/runtime/spawn-env.d.ts +31 -0
- package/dist/core/runtime/spawn-env.js +179 -0
- package/dist/core/runtime/spawn.d.ts +118 -0
- package/dist/core/runtime/spawn.js +473 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +77 -0
- package/dist/core/runtime/stop-signals.d.ts +14 -0
- package/dist/core/runtime/stop-signals.js +46 -0
- package/dist/core/runtime/stream-watchdog.d.ts +55 -0
- package/dist/core/runtime/stream-watchdog.js +140 -0
- package/dist/core/runtime/structured-output.d.ts +12 -0
- package/dist/core/runtime/structured-output.js +90 -0
- package/dist/core/runtime/surface-bg.d.ts +11 -0
- package/dist/core/runtime/surface-bg.js +68 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +247 -0
- package/dist/core/runtime/tmux.js +858 -0
- package/dist/core/runtime/warm-pool.d.ts +51 -0
- package/dist/core/runtime/warm-pool.js +373 -0
- package/dist/core/scope.d.ts +47 -0
- package/dist/core/scope.js +255 -0
- package/dist/core/secrets.d.ts +25 -0
- package/dist/core/secrets.js +55 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/core/self-update.js +105 -0
- package/dist/core/session-model/session-state.d.ts +51 -0
- package/dist/core/session-model/session-state.js +170 -0
- package/dist/core/spawn.d.ts +16 -0
- package/dist/core/spawn.js +30 -0
- package/dist/core/stdout-errors.d.ts +3 -0
- package/dist/core/stdout-errors.js +14 -0
- package/dist/core/subscription-state.d.ts +138 -0
- package/dist/core/subscription-state.js +918 -0
- package/dist/core/substrate/ceiling.d.ts +20 -0
- package/dist/core/substrate/ceiling.js +69 -0
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +11 -0
- package/dist/core/substrate/index.js +25 -0
- package/dist/core/substrate/injected-store.d.ts +10 -0
- package/dist/core/substrate/injected-store.js +55 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +404 -0
- package/dist/core/substrate/render-node.d.ts +11 -0
- package/dist/core/substrate/render-node.js +29 -0
- package/dist/core/substrate/render.d.ts +21 -0
- package/dist/core/substrate/render.js +547 -0
- package/dist/core/substrate/schema.d.ts +125 -0
- package/dist/core/substrate/schema.js +191 -0
- package/dist/core/substrate/session-cache.d.ts +25 -0
- package/dist/core/substrate/session-cache.js +101 -0
- package/dist/core/substrate/subject-fields.d.ts +45 -0
- package/dist/core/substrate/subject-fields.js +38 -0
- package/dist/core/substrate/subject.d.ts +12 -0
- package/dist/core/substrate/subject.js +47 -0
- package/dist/core/timing.d.ts +11 -0
- package/dist/core/timing.js +94 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +113 -0
- package/dist/core/tui/draw.d.ts +100 -0
- package/dist/core/tui/draw.js +359 -0
- package/dist/core/tui/host.d.ts +66 -0
- package/dist/core/tui/host.js +689 -0
- package/dist/core/tui/terminal.d.ts +23 -0
- package/dist/core/tui/terminal.js +101 -0
- package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
- package/dist/core/view/__tests__/transport-cache.test.js +62 -0
- package/dist/core/view/__tests__/transport-remote.test.d.ts +1 -0
- package/dist/core/view/__tests__/transport-remote.test.js +98 -0
- package/dist/core/view/bridge.d.ts +10 -0
- package/dist/core/view/bridge.js +31 -0
- package/dist/core/view/chrome.d.ts +9 -0
- package/dist/core/view/chrome.js +22 -0
- package/dist/core/view/contract.d.ts +205 -0
- package/dist/core/view/contract.js +23 -0
- package/dist/core/view/loader.d.ts +31 -0
- package/dist/core/view/loader.js +188 -0
- package/dist/core/view/remote-canvas-target.d.ts +4 -0
- package/dist/core/view/remote-canvas-target.js +30 -0
- package/dist/core/view/stream-local.d.ts +3 -0
- package/dist/core/view/stream-local.js +231 -0
- package/dist/core/view/transport-cache.d.ts +8 -0
- package/dist/core/view/transport-cache.js +38 -0
- package/dist/core/view/transport-local.d.ts +7 -0
- package/dist/core/view/transport-local.js +83 -0
- package/dist/core/view/transport-remote.d.ts +2 -0
- package/dist/core/view/transport-remote.js +58 -0
- package/dist/core/view/transport.d.ts +10 -0
- package/dist/core/view/transport.js +15 -0
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +307 -0
- package/dist/core/worktree.d.ts +111 -0
- package/dist/core/worktree.js +732 -0
- package/dist/daemon/__tests__/broker-ownership.test.d.ts +1 -0
- package/dist/daemon/__tests__/broker-ownership.test.js +25 -0
- package/dist/daemon/__tests__/daemon-ownership.test.d.ts +1 -0
- package/dist/daemon/__tests__/daemon-ownership.test.js +27 -0
- package/dist/daemon/api/__tests__/error-seam.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/error-seam.test.js +56 -0
- package/dist/daemon/api/__tests__/full/api-server.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/full/api-server.test.js +369 -0
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +220 -0
- package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/full/push-parity.test.js +199 -0
- package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/full/worktree-parity.test.js +160 -0
- package/dist/daemon/api/__tests__/inbox.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/inbox.test.js +578 -0
- package/dist/daemon/api/bridge.d.ts +21 -0
- package/dist/daemon/api/bridge.js +351 -0
- package/dist/daemon/api/handlers/attach.d.ts +2 -0
- package/dist/daemon/api/handlers/attach.js +56 -0
- package/dist/daemon/api/handlers/canvas.d.ts +2 -0
- package/dist/daemon/api/handlers/canvas.js +463 -0
- package/dist/daemon/api/handlers/crons.d.ts +2 -0
- package/dist/daemon/api/handlers/crons.js +354 -0
- package/dist/daemon/api/handlers/daemon.d.ts +2 -0
- package/dist/daemon/api/handlers/daemon.js +35 -0
- package/dist/daemon/api/handlers/files.d.ts +2 -0
- package/dist/daemon/api/handlers/files.js +64 -0
- package/dist/daemon/api/handlers/focus.d.ts +2 -0
- package/dist/daemon/api/handlers/focus.js +92 -0
- package/dist/daemon/api/handlers/health.d.ts +2 -0
- package/dist/daemon/api/handlers/health.js +65 -0
- package/dist/daemon/api/handlers/human.d.ts +12 -0
- package/dist/daemon/api/handlers/human.js +741 -0
- package/dist/daemon/api/handlers/inbox.d.ts +2 -0
- package/dist/daemon/api/handlers/inbox.js +423 -0
- package/dist/daemon/api/handlers/messages.d.ts +2 -0
- package/dist/daemon/api/handlers/messages.js +290 -0
- package/dist/daemon/api/handlers/modelauth.d.ts +2 -0
- package/dist/daemon/api/handlers/modelauth.js +116 -0
- package/dist/daemon/api/handlers/nodes.d.ts +2 -0
- package/dist/daemon/api/handlers/nodes.js +540 -0
- package/dist/daemon/api/handlers/profiles.d.ts +2 -0
- package/dist/daemon/api/handlers/profiles.js +69 -0
- package/dist/daemon/api/handlers/reports.d.ts +2 -0
- package/dist/daemon/api/handlers/reports.js +144 -0
- package/dist/daemon/api/handlers/subscriptions.d.ts +2 -0
- package/dist/daemon/api/handlers/subscriptions.js +53 -0
- package/dist/daemon/api/handlers/worktree.d.ts +2 -0
- package/dist/daemon/api/handlers/worktree.js +40 -0
- package/dist/daemon/api/map.d.ts +100 -0
- package/dist/daemon/api/map.js +360 -0
- package/dist/daemon/api/router.d.ts +56 -0
- package/dist/daemon/api/router.js +190 -0
- package/dist/daemon/api/server.d.ts +23 -0
- package/dist/daemon/api/server.js +258 -0
- package/dist/daemon/control.d.ts +21 -0
- package/dist/daemon/control.js +22 -0
- package/dist/daemon/cron-run.d.ts +64 -0
- package/dist/daemon/cron-run.js +944 -0
- package/dist/daemon/cron-sink.d.ts +26 -0
- package/dist/daemon/cron-sink.js +43 -0
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- package/dist/daemon/crtrd-cli.js +18 -0
- package/dist/daemon/crtrd.d.ts +89 -0
- package/dist/daemon/crtrd.js +1953 -0
- package/dist/daemon/fleet.d.ts +81 -0
- package/dist/daemon/fleet.js +405 -0
- package/dist/daemon/manage.d.ts +103 -0
- package/dist/daemon/manage.js +389 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/supervise-cadence.d.ts +1 -0
- package/dist/daemon/supervise-cadence.js +6 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +44 -0
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +31 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +182 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +467 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +8 -0
- package/dist/pi-extensions/canvas-bash-valve.js +297 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +91 -0
- package/dist/pi-extensions/canvas-context-intro.js +230 -0
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +151 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
- package/dist/pi-extensions/canvas-goal-capture.js +83 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +25 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +942 -0
- package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
- package/dist/pi-extensions/canvas-passive-context.js +134 -0
- package/dist/pi-extensions/canvas-recap.d.ts +41 -0
- package/dist/pi-extensions/canvas-recap.js +263 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +53 -0
- package/dist/pi-extensions/canvas-stophook.js +985 -0
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -0
- package/dist/pi-extensions/canvas-structured-output.js +155 -0
- package/dist/prompts/review.d.ts +2 -0
- package/dist/prompts/review.js +130 -0
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +172 -0
- package/dist/suppress-experimental-warnings.d.ts +1 -0
- package/dist/suppress-experimental-warnings.js +15 -0
- package/dist/types.d.ts +297 -0
- package/dist/types.js +153 -0
- package/dist/web/ViewChrome.d.ts +7 -0
- package/dist/web/ViewChrome.js +28 -0
- package/dist/web/ViewPane.d.ts +39 -0
- package/dist/web/ViewPane.js +48 -0
- package/dist/web/index.d.ts +8 -0
- package/dist/web/index.js +22 -0
- package/dist/web/runtime.d.ts +39 -0
- package/dist/web/runtime.js +232 -0
- package/dist/web/states.d.ts +24 -0
- package/dist/web/states.js +24 -0
- package/dist/web/transport-http.d.ts +5 -0
- package/dist/web/transport-http.js +34 -0
- package/dist/web/transport-stream.d.ts +3 -0
- package/dist/web/transport-stream.js +204 -0
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
- package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
- package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
- package/dist/web-client/assets/index-BpyZGBhI.js +79 -0
- package/dist/web-client/assets/index-CpEl9LTS.css +2 -0
- package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
- package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +17 -0
- package/dist/web-client/manifest.webmanifest +1 -0
- package/dist/web-client/sw.js +1 -0
- package/docs/compat/hearth-crtr-v1.md +187 -0
- package/docs/compat/hearth-crtr-v2.md +185 -0
- package/docs/compat/hearth-crtr-v3.md +184 -0
- package/docs/compat/hearth-crtr-v4.md +188 -0
- package/docs/compat/hearth-crtr-v5.md +175 -0
- package/docs/public-api.md +77 -0
- package/package.json +156 -0
- package/runtime.lock.json +12919 -0
- package/scripts/install-runtime.mjs +308 -0
- package/scripts/patch-pi-tree-root-ordering.mjs +57 -0
- package/scripts/postinstall.mjs +33 -0
- package/scripts/runtime-lock.mjs +26 -0
|
@@ -0,0 +1,3711 @@
|
|
|
1
|
+
// broker.ts — the headless node broker (design §4, §5; plan T4).
|
|
2
|
+
//
|
|
3
|
+
// One broker process per node — the SOLE host. It hosts ONE pi engine IN-PROCESS
|
|
4
|
+
// via the SDK (createAgentSession), is the SOLE writer of the node's session
|
|
5
|
+
// `.jsonl`, listens on `nodeDir(id)/view.sock`, fans the single engine event
|
|
6
|
+
// stream out to N viewers, serializes the drive commands of EVERY writable
|
|
7
|
+
// viewer through one frame loop, and fans blocking extension dialogs out to all
|
|
8
|
+
// of them (first answer wins). The engine has no terminal of its own; a
|
|
9
|
+
// tmux pane (or web tab) is only a viewer of this socket. It
|
|
10
|
+
// runs one turn-cycle and, when the engine settles (the stophook calls
|
|
11
|
+
// ctx.shutdown(), or the engine goes idle), disposes the engine and exits 0 —
|
|
12
|
+
// the daemon then routes revival per the intent the bound stophook set.
|
|
13
|
+
//
|
|
14
|
+
// Engine resolution flows through broker-sdk.ts (`loadBrokerEngine`) so the T11
|
|
15
|
+
// `CRTR_BROKER_ENGINE` test seam can swap a fake engine in. The resource loader
|
|
16
|
+
// + model registry are real SDK construction helpers (NOT part of the swappable
|
|
17
|
+
// engine seam — the fake engine receives the real, extension-loaded loader so
|
|
18
|
+
// the real canvas-stophook fires session_start).
|
|
19
|
+
import { createServer } from 'node:net';
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { execFile } from 'node:child_process';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { mark } from '../timing.js';
|
|
26
|
+
import { getAgentDir, initTheme, ModelRegistry, ModelRuntime, } from '@earendil-works/pi-coding-agent';
|
|
27
|
+
import { isContextOverflow } from '@earendil-works/pi-ai';
|
|
28
|
+
import { jobDir, nodeDir, sessionListCachePath, viewSocketPath } from '../canvas/paths.js';
|
|
29
|
+
import { bindBrokerEventSource } from '../events/source.js';
|
|
30
|
+
import { emitEvent } from '../events/emit.js';
|
|
31
|
+
import { operationIdContext } from '../events/operation-id.js';
|
|
32
|
+
import { findMissingPackagePaths } from './package-health.js';
|
|
33
|
+
import { SessionListCache } from './session-list-cache.js';
|
|
34
|
+
import { getNode, updateNode } from '../canvas/index.js';
|
|
35
|
+
import { buildRefInventory } from '../memory/inline-ref-inventory.js';
|
|
36
|
+
import { buildGuidance } from '../memory/inline-ref-guidance.js';
|
|
37
|
+
import { clearSessionCache } from '../substrate/session-cache.js';
|
|
38
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
39
|
+
import { CANVAS_EXTENSIONS, configuredLadderProviders, equivalentOtherProviderModel, isProviderPinnedModelToken, nextLadderModel, normalizeModel, piInvocationToSdkConfig, } from './launch.js';
|
|
40
|
+
import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
|
|
41
|
+
import { resolveBundledPiPackageDir } from './pi-cli.js';
|
|
42
|
+
import { classify } from '../fault-classifier.js';
|
|
43
|
+
import { PoolCredentialStore } from '../pool-credential-store.js';
|
|
44
|
+
import { clearFault, isModelNotFoundError, isProviderUnconfiguredError, readFault, recordFault, recordFaultForCurrentNode } from './fault.js';
|
|
45
|
+
import { clearCleanAbort, markBusy, markCleanAbort } from './busy.js';
|
|
46
|
+
import { extractCoolingDeadline, MANAGED_PROVIDER_ROTATION_POLICY_UI, } from './managed-provider-cooling.js';
|
|
47
|
+
import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './fault-recovery-nudge.js';
|
|
48
|
+
import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
|
|
49
|
+
import { probeOnline as probeOnlineReal } from './connectivity.js';
|
|
50
|
+
import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
|
|
51
|
+
import { BUILTIN_SLASH_COMMANDS, piSessionsRoot } from './pi-vendored.js';
|
|
52
|
+
import { StreamWatchdog, resolveStreamWatchdogMs, releaseKnownStreamWait, KNOWN_STREAM_WAIT_UI } from './stream-watchdog.js';
|
|
53
|
+
import { isDeterministicCommandExpansion } from './command-expansion.js';
|
|
54
|
+
import { clearPreferredModel, isManagedProvider, readRotationConfig, readSubscriptionPool, rememberPreferredModel, resolveProviderCandidates } from '../subscription-state.js';
|
|
55
|
+
import { encodeFrame, FrameDecoder, FrameOverflowError, BROKER_READ_CAPS, } from './broker-protocol.js';
|
|
56
|
+
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
57
|
+
/**
|
|
58
|
+
* pi 0.82 made `ModelRuntime` the canonical model/auth surface and demoted
|
|
59
|
+
* `ModelRegistry` to the synchronous facade over it (`find`/`getAll`/
|
|
60
|
+
* `getAvailable`/`hasConfiguredAuth` off the runtime's snapshot). The broker's
|
|
61
|
+
* model policy is synchronous throughout, so every read goes through this
|
|
62
|
+
* facade; it is a thin wrapper over the LIVE runtime, so constructing one per
|
|
63
|
+
* call reads current state rather than a captured view.
|
|
64
|
+
*/
|
|
65
|
+
function registryOf(services) {
|
|
66
|
+
return new ModelRegistry(services.modelRuntime);
|
|
67
|
+
}
|
|
68
|
+
/** The auth.json/models.json pair backing one agent dir's model runtime. */
|
|
69
|
+
function modelRuntimePaths(agentDir) {
|
|
70
|
+
return { authPath: join(agentDir, 'auth.json'), modelsPath: join(agentDir, 'models.json') };
|
|
71
|
+
}
|
|
72
|
+
function parseModelSpec(spec) {
|
|
73
|
+
const i = spec.lastIndexOf(':');
|
|
74
|
+
if (i <= 0)
|
|
75
|
+
return { modelSpec: spec };
|
|
76
|
+
const suffix = spec.slice(i + 1);
|
|
77
|
+
if (!THINKING_LEVELS.has(suffix))
|
|
78
|
+
return { modelSpec: spec };
|
|
79
|
+
return { modelSpec: spec.slice(0, i), thinkingLevel: suffix };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The pinned pi SDK never yields `session.model === undefined` for a model-less
|
|
83
|
+
* boot: Agent.createMutableAgentState() falls back to a hardcoded
|
|
84
|
+
* `{ provider:'unknown', id:'unknown', api:'unknown', … }` stub (DEFAULT_MODEL)
|
|
85
|
+
* whenever no model is supplied at construction, and `session.model` returns it
|
|
86
|
+
* verbatim. This mirrors the SDK's own (non-exported) interactive-mode.js
|
|
87
|
+
* `isUnknownModel()` so the broker recognizes that stub as "no model" wherever it
|
|
88
|
+
* guards the model-less launch path (kickoff deferral, reload_auth re-drive) and
|
|
89
|
+
* refuses to record/broadcast it as a real `unknown/unknown` model.
|
|
90
|
+
*/
|
|
91
|
+
export function isUnknownModel(model) {
|
|
92
|
+
return (model !== null &&
|
|
93
|
+
model !== undefined &&
|
|
94
|
+
model.provider === 'unknown' &&
|
|
95
|
+
model.id === 'unknown' &&
|
|
96
|
+
model.api === 'unknown');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Route a writable client's `prompt`/`follow_up` frame against the LIVE session
|
|
100
|
+
* state. The client picks its frame type off a possibly-STALE `isStreaming`
|
|
101
|
+
* snapshot, so the broker is authoritative and the client's choice is a HINT:
|
|
102
|
+
*
|
|
103
|
+
* - m-B (streaming-safe prompt): a `prompt` arriving mid-stream needs
|
|
104
|
+
* PromptOptions.streamingBehavior ("Required if streaming"; prompt() throws
|
|
105
|
+
* without it). "steer" mirrors pi interactive-mode's own
|
|
106
|
+
* Enter-while-streaming submit (`prompt(text, { streamingBehavior: "steer" })`).
|
|
107
|
+
* - m-C (idle-safe follow_up): pi's followUp() only enqueues into a RUNNING
|
|
108
|
+
* agent loop — on an idle session nothing ever drains the queue and the text
|
|
109
|
+
* is silently lost. An idle `follow_up` therefore runs as a prompt now.
|
|
110
|
+
*
|
|
111
|
+
* driveEngine calls this synchronously with `session.isStreaming`, so the
|
|
112
|
+
* routed state cannot go stale between the check and the engine call.
|
|
113
|
+
* C1: images ride as a field on PromptOptions (pi: `prompt(text, { images })`).
|
|
114
|
+
*/
|
|
115
|
+
export function resolveEngineRoute(frame, isStreaming) {
|
|
116
|
+
if (frame.type === 'follow_up' && isStreaming)
|
|
117
|
+
return { call: 'followUp' };
|
|
118
|
+
const options = {};
|
|
119
|
+
if (frame.images !== undefined)
|
|
120
|
+
options.images = frame.images;
|
|
121
|
+
if (isStreaming)
|
|
122
|
+
options.streamingBehavior = 'steer';
|
|
123
|
+
return { call: 'prompt', options };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* True iff `text` would be consumed by pi's OWN leading-name preflight before
|
|
127
|
+
* `session.prompt/steer/followUp` ever sees it as prose — an extension command
|
|
128
|
+
* (`session.extensionRunner.getCommand`), a `/skill:name` skill, or a `/name`
|
|
129
|
+
* prompt template (mirrors `_tryExecuteExtensionCommand` / `_expandSkillCommand`
|
|
130
|
+
* / `expandPromptTemplate` in the pinned SDK verbatim, including the exact
|
|
131
|
+
* name-parsing substrings). Inline memory-reference guidance must NEVER fire
|
|
132
|
+
* for these — the leading token routes to the engine's own command surface,
|
|
133
|
+
* not prose referencing a memory doc. Each lookup is independently wrapped so
|
|
134
|
+
* an engine missing one accessor (the fake, or a degraded session) degrades
|
|
135
|
+
* that check to "not a match" rather than aborting the whole classification —
|
|
136
|
+
* matching `buildCommandList`'s existing "no live extensionRunner" tolerance.
|
|
137
|
+
*/
|
|
138
|
+
export function isLeadingEngineCommand(text, session) {
|
|
139
|
+
if (text.startsWith('/')) {
|
|
140
|
+
const spaceIndex = text.indexOf(' ');
|
|
141
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
142
|
+
try {
|
|
143
|
+
if (session.extensionRunner?.getCommand(commandName))
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* engine without a live extensionRunner (e.g. the fake) — not a match */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (text.startsWith('/skill:')) {
|
|
151
|
+
const spaceIndex = text.indexOf(' ');
|
|
152
|
+
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
153
|
+
try {
|
|
154
|
+
if (session.resourceLoader?.getSkills().skills.some((s) => s.name === skillName))
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* engine without a live resourceLoader — not a match */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (text.startsWith('/')) {
|
|
162
|
+
const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
|
|
163
|
+
const templateName = match?.[1];
|
|
164
|
+
if (templateName !== undefined) {
|
|
165
|
+
try {
|
|
166
|
+
if (session.promptTemplates?.some((t) => t.name === templateName))
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* engine without a live promptTemplates getter — not a match */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Enumerate every leading-token STRING this session's engine would
|
|
178
|
+
* independently consume as a leading command/skill/template dispatch BEFORE
|
|
179
|
+
* inline memory-reference guidance ever applies — the read-op sibling of
|
|
180
|
+
* `isLeadingEngineCommand`'s point-lookup, gathered as a full enumeration for
|
|
181
|
+
* viewer-side suppression instead of a per-submission membership check. Same
|
|
182
|
+
* three sources, same shape of the leading match `isLeadingEngineCommand`
|
|
183
|
+
* checks (bare extension-command invocation names and prompt-template names,
|
|
184
|
+
* `skill:<name>` for skills), so `/${token}` round-trips through
|
|
185
|
+
* `isLeadingEngineCommand(text, session)` for every emitted token (locked by
|
|
186
|
+
* a test). Kept STRICTLY separate from `buildCommandList` (command
|
|
187
|
+
* completion) and the ref inventory (`RefMeta` rows) — this is neither.
|
|
188
|
+
*/
|
|
189
|
+
export function engineLeadingCommandTokens(session) {
|
|
190
|
+
const out = [];
|
|
191
|
+
try {
|
|
192
|
+
for (const c of session.extensionRunner?.getRegisteredCommands() ?? [])
|
|
193
|
+
out.push(c.invocationName);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* engine without a live extensionRunner (e.g. the fake) — none */
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
for (const s of session.resourceLoader?.getSkills().skills ?? [])
|
|
200
|
+
out.push(`skill:${s.name}`);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* engine without a live resourceLoader — none */
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
for (const t of session.promptTemplates ?? [])
|
|
207
|
+
out.push(t.name);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* engine without a live promptTemplates getter — none */
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Choose how the hidden `crtr-memory-refs` guidance custom message rides
|
|
216
|
+
* alongside a human frame's own engine call (design Flow B). Pure over the
|
|
217
|
+
* frame's routing outcome — never reads live session state itself, so a
|
|
218
|
+
* caller can decide the delivery mode from the exact same `route`
|
|
219
|
+
* `resolveEngineRoute` already computed for the human call.
|
|
220
|
+
*
|
|
221
|
+
* - `steer` frame -> 'steer' (always; no route call exists).
|
|
222
|
+
* - a TRUE mid-stream follow_up (`routeCall === 'followUp'`) -> 'followUp'.
|
|
223
|
+
* - every other `prompt`/`follow_up` routed as `routeCall === 'prompt'`:
|
|
224
|
+
* idle -> 'nextTurn' (the ONLY case guidance precedes the human call —
|
|
225
|
+
* pi's `prompt()` reads+clears its pending-nextTurn queue synchronously
|
|
226
|
+
* into the SAME turn as the new user message); streaming (m-B
|
|
227
|
+
* steer-while-streaming) -> 'steer', never 'nextTurn' (that would attach
|
|
228
|
+
* to a later, unrelated turn).
|
|
229
|
+
*/
|
|
230
|
+
export function chooseGuidanceDeliveryMode(frameType, routeCall, isStreaming) {
|
|
231
|
+
if (frameType === 'steer')
|
|
232
|
+
return 'steer';
|
|
233
|
+
if (routeCall === 'followUp')
|
|
234
|
+
return 'followUp';
|
|
235
|
+
return isStreaming ? 'steer' : 'nextTurn';
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Deliver a `nextTurn` guidance custom message ATOMICALLY around the idle-
|
|
239
|
+
* prompt call it's meant to ride alongside. Pi's `_pendingNextTurnMessages`
|
|
240
|
+
* queue (the only public seam for `deliverAs:'nextTurn'`) is drained ONLY deep
|
|
241
|
+
* inside `session.prompt()`, well after several await points — extension-
|
|
242
|
+
* command interception, the `input` extension transform, a streaming reroute,
|
|
243
|
+
* model/auth validation, pre-turn compaction — that can return or throw BEFORE
|
|
244
|
+
* the drain ever runs. `sendCustomMessage`'s nextTurn branch pushes
|
|
245
|
+
* synchronously and unconditionally, so any of those early exits strands the
|
|
246
|
+
* pushed guidance in the queue to attach itself to whichever LATER, unrelated
|
|
247
|
+
* prompt happens to reach the drain first.
|
|
248
|
+
*
|
|
249
|
+
* There is no public SDK seam to observe or cancel a pending nextTurn message
|
|
250
|
+
* (`_pendingNextTurnMessages` is TS-`private` only — a plain runtime field, not
|
|
251
|
+
* `#private`), so this reaches into that one field and removes EXACTLY the
|
|
252
|
+
* entry this call pushed, identified by object identity via a locally-
|
|
253
|
+
* generated marker riding the message's own `details` field (never surfaced to
|
|
254
|
+
* the model, and irrelevant if the message WAS consumed normally — `details`
|
|
255
|
+
* is just carried through like any other field once folded into a real turn).
|
|
256
|
+
* Runs UNCONDITIONALLY after the human call settles, success or failure/early-
|
|
257
|
+
* return alike — a "successful" `prompt()` promise can just as easily be an
|
|
258
|
+
* early return that skipped the drain (an intercepted extension command, an
|
|
259
|
+
* `input` transform, a streaming reroute) as a genuine consumed turn, so only
|
|
260
|
+
* checking `.catch` is not enough.
|
|
261
|
+
*/
|
|
262
|
+
export async function promptWithNextTurnGuidance(session, guidance, runPrompt) {
|
|
263
|
+
// Must survive Pi's structured-clone step when the queued custom message is
|
|
264
|
+
// folded into the turn. A Symbol reaches that step in `details` and aborts
|
|
265
|
+
// the real prompt with "could not be cloned" before the provider runs.
|
|
266
|
+
const marker = `crtr-memory-refs-pending:${randomUUID()}`;
|
|
267
|
+
let queued = false;
|
|
268
|
+
try {
|
|
269
|
+
await session.sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false, details: marker }, { deliverAs: 'nextTurn' });
|
|
270
|
+
queued = true;
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
emitEvent({
|
|
274
|
+
level: 'warn',
|
|
275
|
+
event: 'broker.memory_refs.guidance_send_failed',
|
|
276
|
+
error: err,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await runPrompt();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
if (queued)
|
|
284
|
+
clearOwnPendingNextTurnGuidance(session, marker);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Splice out exactly the pending-nextTurn entry `marker` identifies (object-
|
|
288
|
+
* identity match on `details`), if it is still present — a no-op (and safe)
|
|
289
|
+
* if `session.prompt()` already drained it normally, or if the session/field
|
|
290
|
+
* shape doesn't match (defensive against an engine without the field, e.g. a
|
|
291
|
+
* future SDK bump or a structurally-different test double). */
|
|
292
|
+
function clearOwnPendingNextTurnGuidance(session, marker) {
|
|
293
|
+
const pending = session._pendingNextTurnMessages;
|
|
294
|
+
if (!Array.isArray(pending))
|
|
295
|
+
return;
|
|
296
|
+
const idx = pending.findIndex((m) => m?.details === marker);
|
|
297
|
+
if (idx !== -1)
|
|
298
|
+
pending.splice(idx, 1);
|
|
299
|
+
}
|
|
300
|
+
/** True iff a parsed JSONL branch-entry line is a hidden (`display:false`)
|
|
301
|
+
* custom message — the general display boundary crossed if left in an
|
|
302
|
+
* exported branch (not just `crtr-memory-refs` guidance; any hidden custom
|
|
303
|
+
* content). Covers both persisted shapes a branch entry can take: a
|
|
304
|
+
* `type:'custom_message'` entry (display at the entry's own top level) and a
|
|
305
|
+
* `type:'message'` entry wrapping a `role:'custom'` message (display nested
|
|
306
|
+
* under `.message`). */
|
|
307
|
+
export function isHiddenJsonlBranchEntry(entry) {
|
|
308
|
+
if (entry === null || typeof entry !== 'object')
|
|
309
|
+
return false;
|
|
310
|
+
const e = entry;
|
|
311
|
+
if (e.type === 'custom_message' && e.display === false)
|
|
312
|
+
return true;
|
|
313
|
+
if (e.type === 'message' && e.message?.role === 'custom' && e.message.display === false)
|
|
314
|
+
return true;
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
/** Rewrite an already-exported JSONL file in place, dropping every hidden
|
|
318
|
+
* (`display:false`) branch entry while re-chaining `parentId` so the
|
|
319
|
+
* remaining entries stay a single valid linear branch — applying the general
|
|
320
|
+
* display:false boundary to `session.exportToJsonl`'s own output, which
|
|
321
|
+
* serializes every entry verbatim regardless of display. The header line
|
|
322
|
+
* (line 1) is copied through untouched. */
|
|
323
|
+
export function stripHiddenEntriesFromJsonlExport(filePath) {
|
|
324
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
325
|
+
const lines = raw.split('\n').filter((l) => l !== '');
|
|
326
|
+
if (lines.length === 0)
|
|
327
|
+
return;
|
|
328
|
+
const [headerLine, ...entryLines] = lines;
|
|
329
|
+
let prevId = null;
|
|
330
|
+
const kept = [];
|
|
331
|
+
for (const line of entryLines) {
|
|
332
|
+
const entry = JSON.parse(line);
|
|
333
|
+
if (isHiddenJsonlBranchEntry(entry))
|
|
334
|
+
continue;
|
|
335
|
+
kept.push(JSON.stringify({ ...entry, parentId: prevId }));
|
|
336
|
+
if (typeof entry.id === 'string')
|
|
337
|
+
prevId = entry.id;
|
|
338
|
+
}
|
|
339
|
+
writeFileSync(filePath, `${[headerLine, ...kept].join('\n')}\n`);
|
|
340
|
+
}
|
|
341
|
+
function formatModelSpec(model, thinkingLevel) {
|
|
342
|
+
if (model === null || model === undefined || isUnknownModel(model))
|
|
343
|
+
return null;
|
|
344
|
+
const suffix = thinkingLevel !== undefined && thinkingLevel !== '' && thinkingLevel !== 'off' ? `:${thinkingLevel}` : '';
|
|
345
|
+
return `${model.provider}/${model.id}${suffix}`;
|
|
346
|
+
}
|
|
347
|
+
/** The ask-time fork recipe requires an explicit thinking level: `:off` is a
|
|
348
|
+
* real captured choice, not an omitted default that a future runtime may infer
|
|
349
|
+
* differently. */
|
|
350
|
+
function formatExactModelSpec(model, thinkingLevel) {
|
|
351
|
+
const base = formatModelSpec(model, undefined);
|
|
352
|
+
return base === null ? null : `${base}:${thinkingLevel === undefined || thinkingLevel === '' ? 'off' : thinkingLevel}`;
|
|
353
|
+
}
|
|
354
|
+
function faultRetryProbe() {
|
|
355
|
+
const mode = process.env['CRTR_FAULT_RETRY_PROBE'];
|
|
356
|
+
if (mode === 'always')
|
|
357
|
+
return async () => true;
|
|
358
|
+
if (mode === 'never')
|
|
359
|
+
return async () => false;
|
|
360
|
+
return probeOnlineReal;
|
|
361
|
+
}
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Tunables (T3 backpressure / T4 dialog anti-deadlock)
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
/** Per-viewer outbound high-water mark (M1). A viewer whose unflushed backlog
|
|
366
|
+
* exceeds EITHER bound is DROPPED (socket destroyed) rather than allowed to
|
|
367
|
+
* apply indefinite backpressure to the shared engine. Modeled on pi's RPC
|
|
368
|
+
* output-guard: bound the queue, shed the slow viewer. */
|
|
369
|
+
const MAX_QUEUED_FRAMES = 1000;
|
|
370
|
+
const MAX_PENDING_BYTES = 32 * 1024 * 1024; // 32 MiB
|
|
371
|
+
/** F2 (attach typing-lag fix, 2026-06-09): coalesce window for relayed
|
|
372
|
+
* `message_update` frames. pi emits one per content delta and each carries the
|
|
373
|
+
* FULL accumulated assistant message, so a verbatim relay re-serializes (and
|
|
374
|
+
* every viewer re-parses + re-renders) the whole message per token — O(len²)
|
|
375
|
+
* bytes over a long reply, and measured ~3× the viewer's render CPU vs 75 ms
|
|
376
|
+
* coalescing. Only the LATEST pending update matters (each supersedes the
|
|
377
|
+
* last), so the relay keeps one and flushes it on this timer — or immediately,
|
|
378
|
+
* BEFORE any other event type, preserving ordering (an update never arrives
|
|
379
|
+
* after its message_end). */
|
|
380
|
+
const MESSAGE_UPDATE_COALESCE_MS = 75;
|
|
381
|
+
/** Broker-side default dialog timeout (C2 anti-deadlock, T4). When an extension
|
|
382
|
+
* dialog is forwarded to every writable client, the broker ALWAYS arms a timeout
|
|
383
|
+
* (this default, or a shorter per-dialog `opts.timeout` if the extension passed
|
|
384
|
+
* one) so unanswered dialogs — including ones whose writable viewers all detach
|
|
385
|
+
* — can never hang the agent turn forever. On fire it resolves to the SAFE default
|
|
386
|
+
* (deny for confirm; cancel/undefined for select/input/editor). */
|
|
387
|
+
const DEFAULT_DIALOG_TIMEOUT_MS = 120_000; // 120 s
|
|
388
|
+
/** Admission bound for `runReplacement` (one active head + at most one queued
|
|
389
|
+
* follow-up) and the mutation-through-rebind deadline each admitted head gets
|
|
390
|
+
* once it becomes the tail's head. The SDK has no cancellation primitive for
|
|
391
|
+
* newSession/switchSession/fork/bindExtensions, so an admitted replacement that
|
|
392
|
+
* neither settles nor completes within its deadline leaves runtime/shared state
|
|
393
|
+
* indeterminate — the only safe response is broker-terminal exit, never a retry
|
|
394
|
+
* and never letting a queued replacement start against that uncertain state. */
|
|
395
|
+
const MAX_ADMITTED_REPLACEMENTS = 2; // active + at most one queued
|
|
396
|
+
const REPLACEMENT_DEADLINE_MS = 30_000;
|
|
397
|
+
/** Locally branded sentinel `runReplacement`'s deadline race resolves to when
|
|
398
|
+
* the budget elapses before the runtime promise settles — never compared via
|
|
399
|
+
* string matching. */
|
|
400
|
+
const REPLACEMENT_DEADLINE_EXCEEDED = Symbol('replacement-deadline-exceeded');
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// M3 (scout mq5thyli): the active engine session, exposed so broker-cli's FATAL
|
|
403
|
+
// handlers (uncaughtException / unhandledRejection / the runBroker reject) can
|
|
404
|
+
// dispose it before process.exit. The bash tool spawns children `detached` (own
|
|
405
|
+
// pgid); only session.dispose() (→ abortBash / agent.abort → killProcessTree)
|
|
406
|
+
// reaps them. The graceful exit path already disposes; without this the crash
|
|
407
|
+
// path would `process.exit(1)` and ORPHAN every in-flight bash subprocess.
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
let activeSession = null;
|
|
410
|
+
/** Dispose the live engine session if one exists (idempotent). Called by
|
|
411
|
+
* broker-cli's fatal handlers before exit so a crash never orphans detached
|
|
412
|
+
* bash children. No-op before the session is built or after a clean dispose. */
|
|
413
|
+
export function disposeActiveSession() {
|
|
414
|
+
const s = activeSession;
|
|
415
|
+
activeSession = null;
|
|
416
|
+
if (s !== null) {
|
|
417
|
+
try {
|
|
418
|
+
s.dispose();
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* dispose must not block the fatal exit */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// runBroker — the broker process body (plan T4 steps 1–6)
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), startupTs = new Date().toISOString()) {
|
|
429
|
+
// A clean-abort certificate belongs only to the dead broker that wrote it.
|
|
430
|
+
// Fleet classification consumes it before launching this successor.
|
|
431
|
+
clearCleanAbort(nodeId);
|
|
432
|
+
// The canonical broker event source (job/log.jsonl) is bound before any other
|
|
433
|
+
// work in this function — no path/env/state setup may precede it, and the
|
|
434
|
+
// binder independently validates nodeId before constructing that path.
|
|
435
|
+
bindBrokerEventSource(nodeId);
|
|
436
|
+
// One low-volume, node-correlated boot trace. It deliberately carries only
|
|
437
|
+
// timing and runtime identity — never prompts, commands, tokens, or secrets.
|
|
438
|
+
// broker-cli captures startupAt/startupTs before dynamically loading this
|
|
439
|
+
// module, so module_loaded's elapsed value includes the cold broker import
|
|
440
|
+
// graph, and cli_entry carries the exact process-spawn wall-clock time.
|
|
441
|
+
operationIdContext.fresh(() => {
|
|
442
|
+
emitEvent({
|
|
443
|
+
level: 'info',
|
|
444
|
+
event: 'broker.startup.cli_entry',
|
|
445
|
+
ts: startupTs,
|
|
446
|
+
duration_ms: 0,
|
|
447
|
+
fields: { runtime_version: process.version, engine_version: null, model: null },
|
|
448
|
+
});
|
|
449
|
+
emitEvent({
|
|
450
|
+
level: 'info',
|
|
451
|
+
event: 'broker.startup.module_loaded',
|
|
452
|
+
duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000,
|
|
453
|
+
fields: { runtime_version: process.version, engine_version: null, model: null },
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
// Structured, duplicate-suppressed startup milestones reached only once the
|
|
457
|
+
// engine is streaming a turn. `engineVersion` is populated below once
|
|
458
|
+
// assertEngineVersion resolves it; the milestones fire well after that.
|
|
459
|
+
const markedStartupEvents = new Set();
|
|
460
|
+
let engineVersion = null;
|
|
461
|
+
const emitStartupMilestone = (event, model = null) => {
|
|
462
|
+
if (markedStartupEvents.has(event))
|
|
463
|
+
return;
|
|
464
|
+
markedStartupEvents.add(event);
|
|
465
|
+
// Under CRTR_TIMING the same milestone also lands on the cross-process
|
|
466
|
+
// front-door timeline (stderr → this broker's broker.log), so it can be
|
|
467
|
+
// read next to the cli/viewer markers instead of only in the job log.
|
|
468
|
+
mark(event, { duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000 });
|
|
469
|
+
emitEvent({
|
|
470
|
+
level: 'info',
|
|
471
|
+
event,
|
|
472
|
+
duration_ms: Number(process.hrtime.bigint() - startupAt) / 1_000_000,
|
|
473
|
+
fields: { runtime_version: process.version, engine_version: engineVersion, model },
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
// Defensive: any `crtr` child the engine/extensions spawn must inherit the
|
|
477
|
+
// front-door recursion guard (runtime/CLAUDE.md fork-bomb invariant). The host
|
|
478
|
+
// (T6) also sets this; we set it again so the broker is self-sufficient.
|
|
479
|
+
process.env[FRONT_DOOR_ENV] = '1';
|
|
480
|
+
// A successful in-turn crtr command stamps this process id into its stop
|
|
481
|
+
// signal, so a crash-revived broker cannot consume a prior broker's outcome.
|
|
482
|
+
process.env['CRTR_BROKER_PID'] = String(process.pid);
|
|
483
|
+
// The broker is PANELESS and detached, but a broker spawned by a tmux-resident
|
|
484
|
+
// parent inherits the parent's TMUX_PANE/TMUX in its env (the launcher spawns
|
|
485
|
+
// with {...process.env}). Left in place, every `crtr` command the engine runs
|
|
486
|
+
// (bash tool, goal-capture naming, a child spawn) would resolve `currentTmux()`
|
|
487
|
+
// to the PARENT's pane/session and act there — e.g. open viewers beside, or
|
|
488
|
+
// close, the parent's pane. Strip both so this broker's children see no tmux
|
|
489
|
+
// context and take their paneless path.
|
|
490
|
+
delete process.env['TMUX_PANE'];
|
|
491
|
+
delete process.env['TMUX'];
|
|
492
|
+
// 1. Version tripwire + load the launch recipe the host serialized.
|
|
493
|
+
const engine = await loadBrokerEngine();
|
|
494
|
+
assertEngineVersion(engine.VERSION);
|
|
495
|
+
engineVersion = engine.VERSION;
|
|
496
|
+
// M4 / M6 (scout mq5thyli) — DELIBERATELY SKIPPED on the 0.78.1 pin. Both
|
|
497
|
+
// `configureHttpDispatcher` (M4: proxy support + the undici decompression fix)
|
|
498
|
+
// and `runMigrations` (M6: config/auth migration for a box where the pi CLI
|
|
499
|
+
// never ran) are CLI-only helpers that the pinned SDK does NOT re-export: the
|
|
500
|
+
// package's `exports` map is `.`-only, neither symbol is in `dist/index.d.ts`,
|
|
501
|
+
// a deep import (`.../dist/core/http-dispatcher.js`) is blocked with
|
|
502
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED, and the only exports-map entries are `.` and
|
|
503
|
+
// `./hooks` (neither carries these symbols). Per the "verify export; skip-with-
|
|
504
|
+
// note if absent" directive we do not vendor or reach around the export wall.
|
|
505
|
+
// Revisit when the SDK pin bumps to a version that re-exports them publicly.
|
|
506
|
+
const dir = nodeDir(nodeId);
|
|
507
|
+
const launchFile = join(dir, 'broker-launch.json');
|
|
508
|
+
const inv = JSON.parse(readFileSync(launchFile, 'utf8'));
|
|
509
|
+
const cfg = piInvocationToSdkConfig(inv); // also merges inv.env
|
|
510
|
+
// 2–4. Build the engine session via the SERVICES path (C3) — see
|
|
511
|
+
// buildBrokerSession below. Register it so the FATAL exit path (M3) can
|
|
512
|
+
// dispose it and reap detached bash children.
|
|
513
|
+
// `session` + `services` are MUTABLE holders (T3 session-rebind): when the
|
|
514
|
+
// controller drives new_session/switch_session/fork, the AgentSessionRuntime
|
|
515
|
+
// tears down the old session and builds the next one, then runs our rebind
|
|
516
|
+
// callback which reassigns these. Every closure below (buildSnapshot,
|
|
517
|
+
// driveEngine, handleFrame, disposeAndExit) reads through them, so they all
|
|
518
|
+
// follow the live session automatically. `runtime` is undefined for an engine
|
|
519
|
+
// that does not expose the replacement API (the fake-engine fixture).
|
|
520
|
+
// eslint-disable-next-line prefer-const
|
|
521
|
+
let { session, services, resuming, runtime } = await buildBrokerSession(engine, cfg);
|
|
522
|
+
// M3: name the boot session as the process-fatal disposal target immediately —
|
|
523
|
+
// enqueueRebind reassigns this on every later replacement, but without this
|
|
524
|
+
// initial assignment `activeSession` stays null (and disposeActiveSession() a
|
|
525
|
+
// no-op) for every exit that happens before the FIRST session replacement,
|
|
526
|
+
// which is the common case.
|
|
527
|
+
activeSession = session;
|
|
528
|
+
const retryProbe = faultRetryProbe();
|
|
529
|
+
// Initialize pi's process-global theme. The pi binary does this at boot
|
|
530
|
+
// (main.js: initTheme(settingsManager.getTheme(), appMode === 'interactive')),
|
|
531
|
+
// but the SDK convenience path the broker uses does NOT. Without it, ANY
|
|
532
|
+
// extension hook that reads `ctx.ui.theme` throws "Theme not initialized" —
|
|
533
|
+
// and the extension runner SILENTLY swallows that throw and discards the
|
|
534
|
+
// hook's returned payload. The visible symptom was the OAuth billing adapter's
|
|
535
|
+
// before_provider_request handler throwing, so its injected Claude-Code billing
|
|
536
|
+
// header was dropped and subscription turns 400'd ("draws from extra usage").
|
|
537
|
+
// headless ⇒ no file watcher (false). Idempotent global; call once at boot.
|
|
538
|
+
// Always replace an inherited PI_PACKAGE_DIR: a viewer launched from a pnpm
|
|
539
|
+
// path may pass its `.pnpm` package copy down to the daemon/broker, but that
|
|
540
|
+
// copy can omit the JSON theme assets. The broker must use crouter's actual
|
|
541
|
+
// bundled package root, exactly like its incidental pi subprocesses.
|
|
542
|
+
try {
|
|
543
|
+
process.env['PI_PACKAGE_DIR'] = resolveBundledPiPackageDir();
|
|
544
|
+
initTheme(services.settingsManager.getTheme(), false);
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
emitEvent({
|
|
548
|
+
level: 'error',
|
|
549
|
+
event: 'broker.theme.initialization_failed',
|
|
550
|
+
error: err,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// Diagnose stale local-path pi packages without persisting the human-facing
|
|
554
|
+
// formatter's prose. The front door retains that actionable presentation.
|
|
555
|
+
try {
|
|
556
|
+
for (const missing of findMissingPackagePaths(getAgentDir())) {
|
|
557
|
+
emitEvent({
|
|
558
|
+
level: 'warn',
|
|
559
|
+
event: 'broker.startup.warning',
|
|
560
|
+
fields: {
|
|
561
|
+
stage: 'package_health',
|
|
562
|
+
source: missing.source,
|
|
563
|
+
resolved_path: missing.resolved,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
/* best-effort */
|
|
570
|
+
}
|
|
571
|
+
// N3: the node's display name is re-applied by the rebind lane below so it
|
|
572
|
+
// survives a session replacement (new_session/switch_session/fork), not only at
|
|
573
|
+
// boot.
|
|
574
|
+
// -------------------------------------------------------------------------
|
|
575
|
+
// Socket fan-out state
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
const sockPath = viewSocketPath(nodeId);
|
|
578
|
+
const attachPath = join(jobDir(nodeId), 'attach.json');
|
|
579
|
+
const clients = new Set();
|
|
580
|
+
const pendingDialogs = new Map();
|
|
581
|
+
// Fire-and-forget extension display state must survive a viewer detach. The
|
|
582
|
+
// extension may paint a widget (notably the inactivity brief) while nobody is
|
|
583
|
+
// attached; retain the latest keyed state so the next hello can replay it
|
|
584
|
+
// immediately after welcome instead of making "persistent" mean "only if you
|
|
585
|
+
// happened to be watching when the frame fired".
|
|
586
|
+
const displayStatuses = new Map();
|
|
587
|
+
const displayWidgets = new Map();
|
|
588
|
+
let displayTitle;
|
|
589
|
+
let disposed = false;
|
|
590
|
+
let agentRunActive = false;
|
|
591
|
+
let shutdownRequested = false;
|
|
592
|
+
let server;
|
|
593
|
+
// Every client whose `hello` claimed the writable role and whose transport is
|
|
594
|
+
// still usable. Liveness matters because on a unix socket a peer destroy()
|
|
595
|
+
// delivers EOF (readableEnded) promptly while the matching 'close' can lag
|
|
596
|
+
// arbitrarily behind undrainable pending writes — a departed peer must not be
|
|
597
|
+
// counted as an answerer a dialog fan-out waits on. Roles are FIXED per client,
|
|
598
|
+
// so this is a pure filter, never an arbitration.
|
|
599
|
+
const writableClients = () => {
|
|
600
|
+
const out = [];
|
|
601
|
+
for (const c of clients) {
|
|
602
|
+
if (!c.helloed || c.role !== 'controller')
|
|
603
|
+
continue;
|
|
604
|
+
if (c.socket.destroyed || c.socket.readableEnded || !c.socket.writable)
|
|
605
|
+
continue;
|
|
606
|
+
out.push(c);
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
};
|
|
610
|
+
// Persist viewer presence to job/attach.json when the set of helloed viewers
|
|
611
|
+
// changes (a hello is accepted, or a client drops/is shed) so out-of-process
|
|
612
|
+
// readers (the GRAPH view's attached-row tint) can see whether a human is
|
|
613
|
+
// watching this paneless node. Presence is a truthful COUNT and nothing more —
|
|
614
|
+
// there is no owner to record. Plain writeFileSync, matching telemetry.json's
|
|
615
|
+
// convention; best-effort — presence writing must never crash the broker.
|
|
616
|
+
// disposeAndExit unlinks the file, so a clean exit never leaves a stale claim
|
|
617
|
+
// (readers additionally trust it only while the node is 'active', fencing off
|
|
618
|
+
// a crash-orphaned file).
|
|
619
|
+
const persistAttachState = () => {
|
|
620
|
+
try {
|
|
621
|
+
let viewers = 0;
|
|
622
|
+
for (const c of clients)
|
|
623
|
+
if (c.helloed)
|
|
624
|
+
viewers += 1;
|
|
625
|
+
const dirPath = jobDir(nodeId);
|
|
626
|
+
if (!existsSync(dirPath))
|
|
627
|
+
mkdirSync(dirPath, { recursive: true });
|
|
628
|
+
writeFileSync(attachPath, JSON.stringify({ viewers, updated: new Date().toISOString() }, null, 2), 'utf8');
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
/* presence is best-effort; never crash the broker */
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
// Shed a client — destroy the socket + remove it — used by both the M1
|
|
635
|
+
// backpressure drop and the G7 frame-overflow drop. 'close' (→ drop) follows
|
|
636
|
+
// the destroy.
|
|
637
|
+
const dropClient = (client, reason) => {
|
|
638
|
+
if (!clients.has(client))
|
|
639
|
+
return; // already gone
|
|
640
|
+
// info, not debug: shedding a viewer (M1 backpressure / G7 frame overflow)
|
|
641
|
+
// must be visible in the canonical log without CRTR_LOG=debug — debug-level
|
|
642
|
+
// events are dropped by emitEvent outside that env, which silently hid
|
|
643
|
+
// every shed.
|
|
644
|
+
emitEvent({
|
|
645
|
+
level: 'info',
|
|
646
|
+
event: 'broker.runtime.diagnostic',
|
|
647
|
+
fields: { message: `dropping viewer ${client.id || '(pre-hello)'} — ${reason}` },
|
|
648
|
+
});
|
|
649
|
+
clients.delete(client);
|
|
650
|
+
persistAttachState(); // a viewer was shed
|
|
651
|
+
try {
|
|
652
|
+
client.socket.destroy();
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
/* ignore */
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// `bypassHwm` (F1): the catch-up `welcome` carries the full message history and
|
|
659
|
+
// can legitimately be tens of MiB — far larger than a steady-state live frame.
|
|
660
|
+
// It is still ACCOUNTED (so a genuinely stalled client that also floods on live
|
|
661
|
+
// events is still shed by the NEXT frame's check), but it must never be the
|
|
662
|
+
// REASON a healthy client is dropped before it has had a chance to drain it.
|
|
663
|
+
const sendFrame = (client, frame, bypassHwm = false) => {
|
|
664
|
+
let data;
|
|
665
|
+
try {
|
|
666
|
+
data = encodeFrame(frame);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return; // an unserializable frame must never crash the broker
|
|
670
|
+
}
|
|
671
|
+
const bytes = Buffer.byteLength(data);
|
|
672
|
+
// M1 high-water mark: a viewer that has fallen too far behind on draining its
|
|
673
|
+
// socket is shed rather than allowed to grow the broker's memory unbounded.
|
|
674
|
+
if (!bypassHwm &&
|
|
675
|
+
(client.queuedFrames + 1 > MAX_QUEUED_FRAMES ||
|
|
676
|
+
client.pendingBytes + bytes > MAX_PENDING_BYTES)) {
|
|
677
|
+
dropClient(client, `backpressure high-water mark exceeded (queued=${client.queuedFrames}, pending=${client.pendingBytes}B)`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
client.queuedFrames += 1;
|
|
681
|
+
client.pendingBytes += bytes;
|
|
682
|
+
try {
|
|
683
|
+
// The per-write completion callback fires when THIS chunk is flushed to the
|
|
684
|
+
// OS — a finer-grained drain signal than a socket-wide 'drain' event, and it
|
|
685
|
+
// keeps the accounting exact. write()'s boolean return is the coarse
|
|
686
|
+
// "buffer full" hint; the per-chunk callback is what we account on.
|
|
687
|
+
client.socket.write(data, () => {
|
|
688
|
+
client.queuedFrames -= 1;
|
|
689
|
+
client.pendingBytes -= bytes;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// Dead socket: undo this frame's accounting; 'close' → drop() cleans the rest.
|
|
694
|
+
client.queuedFrames -= 1;
|
|
695
|
+
client.pendingBytes -= bytes;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const broadcast = (frame) => {
|
|
699
|
+
for (const c of clients)
|
|
700
|
+
if (c.helloed)
|
|
701
|
+
sendFrame(c, frame);
|
|
702
|
+
};
|
|
703
|
+
/** Retain and broadcast extension UI frames. The three display METHODS of pi's
|
|
704
|
+
* `extension_ui_request` envelope are levels, so their last value is kept for
|
|
705
|
+
* the welcome snapshot; updating a keyed entry moves it to the end, matching
|
|
706
|
+
* pi's insertion-ordered widget behavior. Every other method (notify, the
|
|
707
|
+
* blocking dialogs) simply passes through. */
|
|
708
|
+
const broadcastUi = (frame) => {
|
|
709
|
+
if (frame.type === 'extension_ui_request') {
|
|
710
|
+
if (frame.method === 'setStatus') {
|
|
711
|
+
displayStatuses.delete(frame.statusKey);
|
|
712
|
+
if (frame.statusText !== undefined)
|
|
713
|
+
displayStatuses.set(frame.statusKey, frame.statusText);
|
|
714
|
+
}
|
|
715
|
+
else if (frame.method === 'setWidget') {
|
|
716
|
+
displayWidgets.delete(frame.widgetKey);
|
|
717
|
+
if (frame.widgetLines !== undefined) {
|
|
718
|
+
displayWidgets.set(frame.widgetKey, {
|
|
719
|
+
lines: [...frame.widgetLines],
|
|
720
|
+
placement: frame.widgetPlacement ?? 'aboveEditor',
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else if (frame.method === 'setTitle') {
|
|
725
|
+
displayTitle = frame.title;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
broadcast(frame);
|
|
729
|
+
};
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
// F2: message_update coalescing. `relayEvent` is the single entry point for
|
|
732
|
+
// engine events headed to viewers: a message_update is held (latest wins) and
|
|
733
|
+
// flushed on a short timer; ANY other event flushes the held update first,
|
|
734
|
+
// synchronously, so cross-type ordering is exactly the engine's. The timer is
|
|
735
|
+
// unref'd — it must never hold the broker process open past dispose.
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
let pendingUpdate = null;
|
|
738
|
+
let pendingUpdateTimer = null;
|
|
739
|
+
let faultRetryTimer = null;
|
|
740
|
+
let nextSubscriptionGeneration = 0;
|
|
741
|
+
let installedGeneration;
|
|
742
|
+
// Dead-provider-stream detector (see stream-watchdog.ts): poked while a turn
|
|
743
|
+
// is awaiting the provider (NOT while a tool executes — a silent long tool
|
|
744
|
+
// must not trip it), cleared at agent_end. Fires at most once per stall,
|
|
745
|
+
// aborting the session so the fault-retry machinery (armed at agent_settled)
|
|
746
|
+
// re-drives the turn instead of the broker wedging until the daemon's
|
|
747
|
+
// 20-minute hard kick.
|
|
748
|
+
const streamWatchdogMs = resolveStreamWatchdogMs();
|
|
749
|
+
const streamWatchdog = new StreamWatchdog((armedGeneration) => {
|
|
750
|
+
const generation = installedGeneration;
|
|
751
|
+
if (generation === undefined || armedGeneration !== generation.id)
|
|
752
|
+
return;
|
|
753
|
+
emitEvent({
|
|
754
|
+
level: 'debug',
|
|
755
|
+
event: 'broker.runtime.diagnostic',
|
|
756
|
+
fields: { message: `stream watchdog: no engine event for ${streamWatchdogMs}ms while streaming — aborting stalled provider stream` },
|
|
757
|
+
});
|
|
758
|
+
void generation.session.abort().catch((err) => {
|
|
759
|
+
emitEvent({
|
|
760
|
+
level: 'debug',
|
|
761
|
+
event: 'broker.runtime.diagnostic',
|
|
762
|
+
fields: { message: `stream watchdog abort failed: ${String(err)}` },
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}, streamWatchdogMs);
|
|
766
|
+
const flushPendingUpdate = () => {
|
|
767
|
+
if (pendingUpdateTimer !== null) {
|
|
768
|
+
clearTimeout(pendingUpdateTimer);
|
|
769
|
+
pendingUpdateTimer = null;
|
|
770
|
+
}
|
|
771
|
+
if (pendingUpdate !== null) {
|
|
772
|
+
const frame = pendingUpdate;
|
|
773
|
+
pendingUpdate = null;
|
|
774
|
+
broadcast(frame);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const clearFaultRetryTimer = () => {
|
|
778
|
+
if (faultRetryTimer === null)
|
|
779
|
+
return;
|
|
780
|
+
clearTimeout(faultRetryTimer);
|
|
781
|
+
faultRetryTimer = null;
|
|
782
|
+
};
|
|
783
|
+
const failedRetryProviders = new Set();
|
|
784
|
+
let providerFallbackInFlight = null;
|
|
785
|
+
// Models that have already produced a 404 not_found this session. Guards the
|
|
786
|
+
// not-found fallback below against an infinite swap→re-drive→404 loop when the
|
|
787
|
+
// fallback TARGET is itself unavailable (or equals the model that just failed).
|
|
788
|
+
const notFoundModels = new Set();
|
|
789
|
+
let notFoundFallbackInFlight = null;
|
|
790
|
+
// Tier 3 model-less boot: the first-prompt kickoff can't fire at listen time
|
|
791
|
+
// when the session booted with no model at all. Hold the prompt text here and
|
|
792
|
+
// fire it once a model actually becomes available (reload_auth re-drive,
|
|
793
|
+
// below) instead of the old unconditional call. For the common model-present
|
|
794
|
+
// path this fires immediately at listen time — byte-identical to the prior
|
|
795
|
+
// behavior.
|
|
796
|
+
let kickoffPending;
|
|
797
|
+
let kickoffInFlight = false;
|
|
798
|
+
// Guards the reload_auth re-drive (below) against overlapping /login frames
|
|
799
|
+
// re-running resolveLaunchModel + setModel + kickoff concurrently; the
|
|
800
|
+
// reload/refresh/ack/broadcast ahead of it stay idempotent and always run.
|
|
801
|
+
let reloadAuthInFlight = null;
|
|
802
|
+
// A fresh auth reload is also the explicit recovery trigger for a turn parked
|
|
803
|
+
// on a fatal provider-auth fault. Keep at most one rewind + prompt in flight
|
|
804
|
+
// when the daemon and an attached viewer both fan the same login update.
|
|
805
|
+
let authFaultRedriveInFlight = false;
|
|
806
|
+
// A model-less boot is EITHER a literal `undefined` model (the fake engine) OR
|
|
807
|
+
// the real SDK's unknown-stub (see isUnknownModel) — the real pinned engine
|
|
808
|
+
// NEVER yields `undefined`, so every model-less guard must accept both.
|
|
809
|
+
const sessionHasNoModel = () => session.model === undefined || isUnknownModel(session.model);
|
|
810
|
+
const drivePendingKickoff = () => {
|
|
811
|
+
if (kickoffPending === undefined || kickoffInFlight || sessionHasNoModel())
|
|
812
|
+
return;
|
|
813
|
+
const prompt = kickoffPending;
|
|
814
|
+
kickoffPending = undefined;
|
|
815
|
+
kickoffInFlight = true;
|
|
816
|
+
trackPromptDispatch(operationIdContext.fresh(() => session.prompt(prompt)
|
|
817
|
+
.catch((err) => {
|
|
818
|
+
emitEvent({
|
|
819
|
+
level: 'error',
|
|
820
|
+
event: 'broker.prompt.first_failed',
|
|
821
|
+
error: err,
|
|
822
|
+
});
|
|
823
|
+
})
|
|
824
|
+
.finally(() => {
|
|
825
|
+
kickoffInFlight = false;
|
|
826
|
+
})));
|
|
827
|
+
};
|
|
828
|
+
const maybeSwitchProviderForRetry = (event) => {
|
|
829
|
+
if (event.type !== 'auto_retry_start')
|
|
830
|
+
return;
|
|
831
|
+
const retry = event;
|
|
832
|
+
const kind = classify('pi→provider', retry.errorMessage ?? '').kind;
|
|
833
|
+
if (kind !== 'rate-limit' && kind !== 'connection')
|
|
834
|
+
return;
|
|
835
|
+
if (providerFallbackInFlight !== null)
|
|
836
|
+
return;
|
|
837
|
+
const generation = installedGeneration;
|
|
838
|
+
if (generation === undefined)
|
|
839
|
+
return;
|
|
840
|
+
const candidateSession = session;
|
|
841
|
+
const candidateServices = services;
|
|
842
|
+
// An exact-consult launch (cfg.modelExact) locks the session to the single
|
|
843
|
+
// captured model — never cross-provider-retry, not even the
|
|
844
|
+
// modelProviderPinned restore-toward-pin exception below. Unavailability
|
|
845
|
+
// must fail the turn, not substitute a different model.
|
|
846
|
+
if (cfg.modelExact === true)
|
|
847
|
+
return;
|
|
848
|
+
const currentSpec = formatModelSpec(session.model, session.thinkingLevel);
|
|
849
|
+
if (currentSpec === null)
|
|
850
|
+
return;
|
|
851
|
+
const next = equivalentOtherProviderModel(currentSpec, failedRetryProviders);
|
|
852
|
+
if (next === null)
|
|
853
|
+
return;
|
|
854
|
+
// An explicit provider pin forbids turn-time rotation AWAY to another
|
|
855
|
+
// provider — EXCEPT a switch back TOWARD the recorded restoration pin (a
|
|
856
|
+
// launch that fell back off a cooling pinned provider records the requested
|
|
857
|
+
// managed model). Restoring to the pinned provider honors the pin rather
|
|
858
|
+
// than violating it; every other rotation stays suppressed.
|
|
859
|
+
if (cfg.modelProviderPinned === true) {
|
|
860
|
+
const pin = readRotationConfig().preferredModel;
|
|
861
|
+
const targetSpec = parseModelSpec(next.model).modelSpec;
|
|
862
|
+
const targetProvider = targetSpec.slice(0, targetSpec.indexOf('/'));
|
|
863
|
+
const targetId = targetSpec.slice(targetSpec.indexOf('/') + 1);
|
|
864
|
+
if (pin?.providerId !== targetProvider || pin.modelId !== targetId)
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const fallbackPromise = (async () => {
|
|
868
|
+
const requestedSpec = parseModelSpec(next.model).modelSpec;
|
|
869
|
+
const slash = requestedSpec.indexOf('/');
|
|
870
|
+
const model = slash > 0
|
|
871
|
+
? registryOf(candidateServices).find(requestedSpec.slice(0, slash), requestedSpec.slice(slash + 1))
|
|
872
|
+
: undefined;
|
|
873
|
+
const requested = parseModelSpec(next.model);
|
|
874
|
+
const pool = isManagedProvider(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
|
|
875
|
+
? readSubscriptionPool(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
|
|
876
|
+
: [];
|
|
877
|
+
const resolved = resolveProviderCandidates([{
|
|
878
|
+
providerId: requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')),
|
|
879
|
+
modelId: requested.modelSpec.slice(requested.modelSpec.indexOf('/') + 1),
|
|
880
|
+
thinkingLevel: requested.thinkingLevel,
|
|
881
|
+
registered: model !== undefined,
|
|
882
|
+
authenticated: model !== undefined && (isManagedProvider(requested.modelSpec.slice(0, requested.modelSpec.indexOf('/')))
|
|
883
|
+
? pool.length > 0
|
|
884
|
+
: registryOf(candidateServices).hasConfiguredAuth(model)),
|
|
885
|
+
coolingUntil: pool.length > 0 && pool.every((entry) => (entry.rateLimitedUntil || 0) > Date.now())
|
|
886
|
+
? Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0))
|
|
887
|
+
: undefined,
|
|
888
|
+
}]);
|
|
889
|
+
if (resolved.candidates.length === 0 || model === undefined) {
|
|
890
|
+
emitEvent({
|
|
891
|
+
level: 'debug',
|
|
892
|
+
event: 'broker.runtime.diagnostic',
|
|
893
|
+
fields: { message: `provider fallback skipped: ${next.model} is ${resolved.rejected[0]?.reason ?? 'unavailable'}` },
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
// Snapshot the restoration intent before the asynchronous model change;
|
|
898
|
+
// this retry must not replace a launch-recorded requested pin.
|
|
899
|
+
const preferred = readRotationConfig().preferredModel;
|
|
900
|
+
await candidateSession.setModel(model);
|
|
901
|
+
if (installedGeneration !== generation || session !== candidateSession)
|
|
902
|
+
return;
|
|
903
|
+
if (requested.thinkingLevel !== undefined)
|
|
904
|
+
candidateSession.setThinkingLevel(requested.thinkingLevel);
|
|
905
|
+
const currentProvider = currentSpec.slice(0, currentSpec.indexOf('/'));
|
|
906
|
+
const currentModelId = parseModelSpec(currentSpec).modelSpec.slice(currentSpec.indexOf('/') + 1);
|
|
907
|
+
// A launch fallback already records its requested managed model as the
|
|
908
|
+
// restoration pin. Keep that intent through turn-time retries; reaching
|
|
909
|
+
// it makes the temporary pin fulfilled. A normal retry has no such pin,
|
|
910
|
+
// so retain the failed managed model for provider-rotation to restore.
|
|
911
|
+
const switchedProvider = requested.modelSpec.slice(0, requested.modelSpec.indexOf('/'));
|
|
912
|
+
const switchedModelId = requested.modelSpec.slice(switchedProvider.length + 1);
|
|
913
|
+
if (preferred?.providerId === switchedProvider && preferred.modelId === switchedModelId) {
|
|
914
|
+
clearPreferredModel();
|
|
915
|
+
}
|
|
916
|
+
else if (preferred === undefined && isManagedProvider(currentProvider)) {
|
|
917
|
+
rememberPreferredModel({ providerId: currentProvider, modelId: currentModelId });
|
|
918
|
+
}
|
|
919
|
+
failedRetryProviders.add(next.fromProvider);
|
|
920
|
+
broadcastModelChanged(false);
|
|
921
|
+
emitEvent({
|
|
922
|
+
level: 'debug',
|
|
923
|
+
event: 'broker.runtime.diagnostic',
|
|
924
|
+
fields: { message: `provider fallback after ${kind}: ${currentSpec} → ${formatModelSpec(candidateSession.model, candidateSession.thinkingLevel) ?? next.model}` },
|
|
925
|
+
});
|
|
926
|
+
})()
|
|
927
|
+
.catch((err) => {
|
|
928
|
+
emitEvent({
|
|
929
|
+
level: 'debug',
|
|
930
|
+
event: 'broker.runtime.diagnostic',
|
|
931
|
+
fields: { message: `provider fallback failed: ${err instanceof Error ? err.message : String(err)}` },
|
|
932
|
+
});
|
|
933
|
+
})
|
|
934
|
+
.finally(() => {
|
|
935
|
+
if (installedGeneration === generation)
|
|
936
|
+
providerFallbackInFlight = null;
|
|
937
|
+
});
|
|
938
|
+
providerFallbackInFlight = fallbackPromise;
|
|
939
|
+
};
|
|
940
|
+
// A 404 not_found is NOT one of pi's auto-retried kinds (it retries 429/5xx
|
|
941
|
+
// only) and never resolves on its own — the configured model id is wrong or
|
|
942
|
+
// decommissioned (e.g. the default `anthropic/ultra` pointing at a model the
|
|
943
|
+
// account can't reach). Left alone the node errors out, the stophook parks it
|
|
944
|
+
// as a stall, and the daemon force-recycles it onto the SAME bad model in a
|
|
945
|
+
// loop. Instead: fail the node over to the strong-anthropic ladder model and
|
|
946
|
+
// re-drive the turn so the work continues. broadcastModelChanged persists the
|
|
947
|
+
// swap into the durable launch recipe, so a later revive uses the good model
|
|
948
|
+
// too. Fires on the terminal-error message (stopReason 'error'), the signal a
|
|
949
|
+
// non-retried request surfaces.
|
|
950
|
+
//
|
|
951
|
+
// The SAME treatment covers a provider whose credential could not be resolved
|
|
952
|
+
// (`Provider is not configured: <id>`). That error is classified `other`, i.e.
|
|
953
|
+
// fatal, so without this the node parks and every wake re-fails on the same
|
|
954
|
+
// unusable provider — the only escape being a human running `crtr node config
|
|
955
|
+
// --model`. Here it substitutes an equivalent model on the OTHER provider (the
|
|
956
|
+
// same ladder cell `maybeSwitchProviderForRetry` uses), proves that target is
|
|
957
|
+
// actually authenticated before switching, and re-drives.
|
|
958
|
+
const maybeFallbackOnUnusableModel = (event) => {
|
|
959
|
+
if (event.type !== 'message_end')
|
|
960
|
+
return;
|
|
961
|
+
const msg = event.message;
|
|
962
|
+
if (msg === undefined || msg.role !== 'assistant' || msg.stopReason !== 'error')
|
|
963
|
+
return;
|
|
964
|
+
const unconfigured = isProviderUnconfiguredError(msg.errorMessage ?? '');
|
|
965
|
+
if (!unconfigured && !isModelNotFoundError(msg.errorMessage ?? ''))
|
|
966
|
+
return;
|
|
967
|
+
if (cfg.modelProviderPinned === true)
|
|
968
|
+
return;
|
|
969
|
+
// An exact-consult launch must fail closed on a 404 for its locked model,
|
|
970
|
+
// never auto-fail-over to the strong-anthropic ladder.
|
|
971
|
+
if (cfg.modelExact === true)
|
|
972
|
+
return;
|
|
973
|
+
if (notFoundFallbackInFlight !== null)
|
|
974
|
+
return;
|
|
975
|
+
const generation = installedGeneration;
|
|
976
|
+
if (generation === undefined)
|
|
977
|
+
return;
|
|
978
|
+
const candidateSession = session;
|
|
979
|
+
const candidateServices = services;
|
|
980
|
+
const currentSpec = formatModelSpec(session.model, session.thinkingLevel);
|
|
981
|
+
if (currentSpec === null)
|
|
982
|
+
return;
|
|
983
|
+
notFoundModels.add(currentSpec);
|
|
984
|
+
// The strong-anthropic ladder cell — "whatever the current strong anthropic
|
|
985
|
+
// provider model is" (config modelLadders.anthropic.strong), not hardcoded.
|
|
986
|
+
// An unusable PROVIDER (as opposed to a missing model id) prefers the
|
|
987
|
+
// same-strength cell on the other provider, so a node keeps the capability
|
|
988
|
+
// it was launched with instead of being promoted or demoted by the swap.
|
|
989
|
+
const equivalent = unconfigured ? equivalentOtherProviderModel(currentSpec, failedRetryProviders)?.model : undefined;
|
|
990
|
+
const target = normalizeModel(equivalent ?? 'anthropic/strong');
|
|
991
|
+
// Don't loop: the failed model already IS the target, or the target itself
|
|
992
|
+
// 404'd earlier this session. Leave it to the normal stall/recycle path.
|
|
993
|
+
if (target === currentSpec || notFoundModels.has(target)) {
|
|
994
|
+
emitEvent({
|
|
995
|
+
level: 'debug',
|
|
996
|
+
event: 'broker.runtime.diagnostic',
|
|
997
|
+
fields: { message: `not_found fallback skipped: ${currentSpec} has no healthy strong-anthropic target (${target})` },
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const fallbackPromise = (async () => {
|
|
1002
|
+
const targetSpec = parseModelSpec(target).modelSpec;
|
|
1003
|
+
const slash = targetSpec.indexOf('/');
|
|
1004
|
+
const model = slash > 0
|
|
1005
|
+
? registryOf(candidateServices).find(targetSpec.slice(0, slash), targetSpec.slice(slash + 1))
|
|
1006
|
+
: undefined;
|
|
1007
|
+
if (model === undefined) {
|
|
1008
|
+
emitEvent({
|
|
1009
|
+
level: 'debug',
|
|
1010
|
+
event: 'broker.runtime.diagnostic',
|
|
1011
|
+
fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback skipped: ${target} is not registered` },
|
|
1012
|
+
});
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
// Swapping onto a second provider that ALSO has no credential would just
|
|
1016
|
+
// repeat the fault under a different name, so prove this one resolves.
|
|
1017
|
+
if (unconfigured && !registryOf(candidateServices).hasConfiguredAuth(model)) {
|
|
1018
|
+
emitEvent({
|
|
1019
|
+
level: 'debug',
|
|
1020
|
+
event: 'broker.runtime.diagnostic',
|
|
1021
|
+
fields: { message: `unconfigured-provider fallback skipped: ${target} is not authenticated either` },
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const requested = parseModelSpec(target);
|
|
1026
|
+
await candidateSession.setModel(model);
|
|
1027
|
+
if (installedGeneration !== generation || session !== candidateSession)
|
|
1028
|
+
return;
|
|
1029
|
+
if (requested.thinkingLevel !== undefined)
|
|
1030
|
+
candidateSession.setThinkingLevel(requested.thinkingLevel);
|
|
1031
|
+
broadcastModelChanged();
|
|
1032
|
+
const resolved = formatModelSpec(candidateSession.model, candidateSession.thinkingLevel) ?? target;
|
|
1033
|
+
emitEvent({
|
|
1034
|
+
level: 'debug',
|
|
1035
|
+
event: 'broker.runtime.diagnostic',
|
|
1036
|
+
fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback: ${currentSpec} unavailable → ${resolved}; re-driving turn` },
|
|
1037
|
+
});
|
|
1038
|
+
// Re-drive: a 404 left the turn errored, so the model swap alone changes
|
|
1039
|
+
// nothing until the next prompt — re-issue so the now-healthy model picks
|
|
1040
|
+
// the task back up. We fire on message_end, BEFORE the errored turn has
|
|
1041
|
+
// fully settled, so wait for the engine to go idle first: prompt() THROWS
|
|
1042
|
+
// if called while streaming without a streamingBehavior, which would swap
|
|
1043
|
+
// the model but silently skip the continuation.
|
|
1044
|
+
await candidateSession.agent.waitForIdle();
|
|
1045
|
+
if (installedGeneration !== generation || session !== candidateSession)
|
|
1046
|
+
return;
|
|
1047
|
+
// The parked fatal describes a model this session no longer uses. Clearing
|
|
1048
|
+
// it after the errored turn has settled (waitForIdle is past agent_end, so
|
|
1049
|
+
// publishSettledProviderFault has already written it) keeps a recovered
|
|
1050
|
+
// node from reviving straight back into fault-recovery chrome.
|
|
1051
|
+
if (unconfigured)
|
|
1052
|
+
clearFault(nodeId, { link: 'pi→provider' });
|
|
1053
|
+
await operationIdContext.fresh(() => session.prompt(unconfigured
|
|
1054
|
+
? `The previous model (${currentSpec}) had no usable provider credential, so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`
|
|
1055
|
+
: `The previous model (${currentSpec}) was unavailable (provider 404 not_found), so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`));
|
|
1056
|
+
})()
|
|
1057
|
+
.catch((err) => {
|
|
1058
|
+
emitEvent({
|
|
1059
|
+
level: 'debug',
|
|
1060
|
+
event: 'broker.runtime.diagnostic',
|
|
1061
|
+
fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback failed: ${err instanceof Error ? err.message : String(err)}` },
|
|
1062
|
+
});
|
|
1063
|
+
})
|
|
1064
|
+
.finally(() => {
|
|
1065
|
+
if (installedGeneration === generation)
|
|
1066
|
+
notFoundFallbackInFlight = null;
|
|
1067
|
+
});
|
|
1068
|
+
notFoundFallbackInFlight = fallbackPromise;
|
|
1069
|
+
};
|
|
1070
|
+
const isActiveAutoFault = (fault) => {
|
|
1071
|
+
if (fault === null)
|
|
1072
|
+
return false;
|
|
1073
|
+
if (fault.retry.disposition !== 'auto')
|
|
1074
|
+
return false;
|
|
1075
|
+
if (fault.retry.by !== 'daemon')
|
|
1076
|
+
return false;
|
|
1077
|
+
return fault.kind === 'connection' || fault.kind === 'rate-limit' || fault.kind === 'overloaded';
|
|
1078
|
+
};
|
|
1079
|
+
const faultRetryDelayMs = (fault, now) => {
|
|
1080
|
+
if (fault.retry.nextAt !== undefined) {
|
|
1081
|
+
const ms = Date.parse(fault.retry.nextAt) - now;
|
|
1082
|
+
return Number.isFinite(ms) ? Math.max(0, ms) : null;
|
|
1083
|
+
}
|
|
1084
|
+
const plan = nextFaultRetry(fault, now);
|
|
1085
|
+
return plan?.delayMs ?? null;
|
|
1086
|
+
};
|
|
1087
|
+
// Node timers accept only a signed int32 delay. Long fault deadlines (for
|
|
1088
|
+
// example an invalid credential's remediation backoff) are chunked so they
|
|
1089
|
+
// cannot overflow into an immediate retry.
|
|
1090
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
1091
|
+
const clearAndArmFaultRetry = (delayMs, generation) => {
|
|
1092
|
+
if (installedGeneration !== generation)
|
|
1093
|
+
return;
|
|
1094
|
+
clearFaultRetryTimer();
|
|
1095
|
+
const chunkMs = Math.min(Math.max(0, delayMs), MAX_TIMEOUT_MS);
|
|
1096
|
+
const remainingMs = Math.max(0, delayMs - chunkMs);
|
|
1097
|
+
faultRetryTimer = setTimeout(() => {
|
|
1098
|
+
faultRetryTimer = null;
|
|
1099
|
+
if (installedGeneration !== generation)
|
|
1100
|
+
return;
|
|
1101
|
+
if (remainingMs > 0) {
|
|
1102
|
+
clearAndArmFaultRetry(remainingMs, generation);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
void runFaultRetry().catch((err) => {
|
|
1106
|
+
emitEvent({
|
|
1107
|
+
level: 'debug',
|
|
1108
|
+
event: 'broker.runtime.diagnostic',
|
|
1109
|
+
fields: { message: `fault retry fire failed: ${err instanceof Error ? err.message : String(err)}` },
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
}, chunkMs);
|
|
1113
|
+
if (typeof faultRetryTimer.unref === 'function')
|
|
1114
|
+
faultRetryTimer.unref();
|
|
1115
|
+
};
|
|
1116
|
+
const scheduleFaultRetry = () => {
|
|
1117
|
+
const generation = installedGeneration;
|
|
1118
|
+
if (generation === undefined || session.isStreaming)
|
|
1119
|
+
return;
|
|
1120
|
+
if (faultRetryTimer !== null)
|
|
1121
|
+
return;
|
|
1122
|
+
const fault = readFault(nodeId);
|
|
1123
|
+
if (!isActiveAutoFault(fault)) {
|
|
1124
|
+
clearFaultRetryTimer();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const delayMs = faultRetryDelayMs(fault, Date.now());
|
|
1128
|
+
if (delayMs === null)
|
|
1129
|
+
return;
|
|
1130
|
+
clearAndArmFaultRetry(delayMs, generation);
|
|
1131
|
+
};
|
|
1132
|
+
const publishSettledProviderFault = (generation) => {
|
|
1133
|
+
const agentEnd = generation.stagedProviderAgentEnd;
|
|
1134
|
+
const overflowFailure = generation.stagedOverflowFailure;
|
|
1135
|
+
generation.stagedProviderAgentEnd = null;
|
|
1136
|
+
generation.stagedOverflowFailure = null;
|
|
1137
|
+
const settledSession = generation.session;
|
|
1138
|
+
const messages = Array.isArray(agentEnd?.messages) ? agentEnd.messages : [];
|
|
1139
|
+
const last = [...messages].reverse().find((message) => typeof message === 'object' && message !== null && message.role === 'assistant');
|
|
1140
|
+
if (overflowFailure !== null) {
|
|
1141
|
+
// The settled terminal outcome supersedes any auto marker retained while
|
|
1142
|
+
// the broker re-drove this same turn; fault priority must not preserve a
|
|
1143
|
+
// now-obsolete retry plan over Pi's definitive recovery result.
|
|
1144
|
+
clearFault(nodeId, { link: 'pi→provider' });
|
|
1145
|
+
recordFault(nodeId, {
|
|
1146
|
+
link: 'pi→provider',
|
|
1147
|
+
op: 'context overflow recovery',
|
|
1148
|
+
kind: 'context-overflow',
|
|
1149
|
+
retry: { disposition: 'fatal' },
|
|
1150
|
+
message: overflowFailure.errorMessage,
|
|
1151
|
+
anchorEntryId: settledSession.sessionManager.getLeafId?.() ?? undefined,
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (last?.stopReason !== 'error')
|
|
1156
|
+
return;
|
|
1157
|
+
const raw = typeof last.errorMessage === 'string'
|
|
1158
|
+
? last.errorMessage
|
|
1159
|
+
: 'engine error (no errorMessage recorded)';
|
|
1160
|
+
const uncompactedOverflow = isContextOverflow(last, settledSession.model?.contextWindow);
|
|
1161
|
+
const classified = classify('pi→provider', last);
|
|
1162
|
+
const coolingDeadline = extractCoolingDeadline(last);
|
|
1163
|
+
if (uncompactedOverflow)
|
|
1164
|
+
clearFault(nodeId, { link: 'pi→provider' });
|
|
1165
|
+
recordFault(nodeId, {
|
|
1166
|
+
link: 'pi→provider',
|
|
1167
|
+
op: 'generation turn',
|
|
1168
|
+
kind: uncompactedOverflow ? 'context-overflow' : classified.kind,
|
|
1169
|
+
retry: uncompactedOverflow
|
|
1170
|
+
? { disposition: 'fatal' }
|
|
1171
|
+
: classified.disposition === 'auto'
|
|
1172
|
+
? { disposition: 'auto', by: 'daemon', ...(coolingDeadline !== undefined ? { nextAt: new Date(coolingDeadline).toISOString() } : {}) }
|
|
1173
|
+
: { disposition: classified.disposition },
|
|
1174
|
+
message: raw,
|
|
1175
|
+
anchorEntryId: settledSession.sessionManager.getLeafId?.() ?? undefined,
|
|
1176
|
+
});
|
|
1177
|
+
};
|
|
1178
|
+
const runFaultRetry = async () => {
|
|
1179
|
+
const generation = installedGeneration;
|
|
1180
|
+
if (generation === undefined)
|
|
1181
|
+
return;
|
|
1182
|
+
const candidateSession = generation.session;
|
|
1183
|
+
if (candidateSession.isStreaming)
|
|
1184
|
+
return;
|
|
1185
|
+
const initial = readFault(nodeId);
|
|
1186
|
+
if (!isActiveAutoFault(initial))
|
|
1187
|
+
return;
|
|
1188
|
+
let fault = initial;
|
|
1189
|
+
if (initial.kind === 'connection') {
|
|
1190
|
+
const online = await retryProbe();
|
|
1191
|
+
// The probe can take seconds. A turn may have started, or the marker may
|
|
1192
|
+
// have been cleared/replaced/recovered, in that window. Re-validate
|
|
1193
|
+
// against fresh state before re-arming or consuming an attempt —
|
|
1194
|
+
// otherwise a stale retry resurrects a recovered episode or double-drives
|
|
1195
|
+
// a now-streaming session.
|
|
1196
|
+
if (installedGeneration !== generation || session !== candidateSession || candidateSession.isStreaming)
|
|
1197
|
+
return;
|
|
1198
|
+
const fresh = readFault(nodeId);
|
|
1199
|
+
if (!isActiveAutoFault(fresh) || fresh.since !== initial.since)
|
|
1200
|
+
return;
|
|
1201
|
+
if (!online) {
|
|
1202
|
+
clearAndArmFaultRetry(policyFor(fresh.kind).baseDelayMs, generation);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
fault = fresh;
|
|
1206
|
+
}
|
|
1207
|
+
const now = Date.now();
|
|
1208
|
+
const attempt = fault.retry.attempt ?? 0;
|
|
1209
|
+
const policy = policyFor(fault.kind);
|
|
1210
|
+
if (attempt >= policy.max) {
|
|
1211
|
+
recordFault(nodeId, markFatalOnExhaust(fault));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const plan = nextFaultRetry(fault, now);
|
|
1215
|
+
if (plan === null) {
|
|
1216
|
+
recordFault(nodeId, markFatalOnExhaust(fault));
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const anchorEntryId = fault.anchorEntryId;
|
|
1220
|
+
if (anchorEntryId !== undefined) {
|
|
1221
|
+
try {
|
|
1222
|
+
const leafId = candidateSession.sessionManager.getLeafId?.() ?? null;
|
|
1223
|
+
if (leafId !== anchorEntryId) {
|
|
1224
|
+
await candidateSession.navigateTree(anchorEntryId);
|
|
1225
|
+
if (installedGeneration !== generation || session !== candidateSession)
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
catch (err) {
|
|
1230
|
+
emitEvent({
|
|
1231
|
+
level: 'debug',
|
|
1232
|
+
event: 'broker.runtime.diagnostic',
|
|
1233
|
+
fields: { message: `fault retry rewind skipped: ${err instanceof Error ? err.message : String(err)}` },
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (installedGeneration !== generation || session !== candidateSession)
|
|
1238
|
+
return;
|
|
1239
|
+
recordFault(nodeId, plan.fault);
|
|
1240
|
+
await operationIdContext.fresh(() => session.prompt(fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY));
|
|
1241
|
+
};
|
|
1242
|
+
// Waiters for "the engine ACCEPTED a prompt and a turn is now running". A
|
|
1243
|
+
// routed prompt's own promise settles only when the whole TURN ends, which is
|
|
1244
|
+
// minutes too late to ack a deliver; agent_start is the engine's own signal
|
|
1245
|
+
// that the call got past its model/auth gate. The deliver ack races these two.
|
|
1246
|
+
const turnAcceptedWaiters = new Set();
|
|
1247
|
+
const notifyTurnAccepted = () => {
|
|
1248
|
+
if (turnAcceptedWaiters.size === 0)
|
|
1249
|
+
return;
|
|
1250
|
+
const waiters = [...turnAcceptedWaiters];
|
|
1251
|
+
turnAcceptedWaiters.clear();
|
|
1252
|
+
for (const waiter of waiters)
|
|
1253
|
+
waiter();
|
|
1254
|
+
};
|
|
1255
|
+
const relayEvent = (event, generation) => {
|
|
1256
|
+
maybeSwitchProviderForRetry(event);
|
|
1257
|
+
maybeFallbackOnUnusableModel(event);
|
|
1258
|
+
const type = event.type;
|
|
1259
|
+
if (type === 'agent_start') {
|
|
1260
|
+
notifyTurnAccepted();
|
|
1261
|
+
emitStartupMilestone('broker.startup.agent_start', formatModelSpec(session.model, session.thinkingLevel));
|
|
1262
|
+
}
|
|
1263
|
+
if (type === 'message_update') {
|
|
1264
|
+
const message = event.message;
|
|
1265
|
+
if (message?.role === 'assistant')
|
|
1266
|
+
emitStartupMilestone('broker.startup.provider_first_delta', formatModelSpec(session.model, session.thinkingLevel));
|
|
1267
|
+
}
|
|
1268
|
+
if (type === 'message_end') {
|
|
1269
|
+
const message = event.message;
|
|
1270
|
+
if (message?.role === 'assistant')
|
|
1271
|
+
emitStartupMilestone('broker.startup.assistant_message_end', formatModelSpec(session.model, session.thinkingLevel));
|
|
1272
|
+
}
|
|
1273
|
+
if (type === 'agent_start' || type === 'turn_start')
|
|
1274
|
+
clearFaultRetryTimer();
|
|
1275
|
+
// The watchdog covers ONLY time awaiting the provider. `session.isStreaming`
|
|
1276
|
+
// stays true through tool execution, so a legitimately silent tool lasting
|
|
1277
|
+
// longer than the timeout would otherwise be aborted mid-run: disarm while
|
|
1278
|
+
// any tool executes and rearm once the last one ends. Multiple tools can run
|
|
1279
|
+
// concurrently, so track in-flight ids and only rearm when the set drains.
|
|
1280
|
+
if (type === 'agent_end') {
|
|
1281
|
+
// Clear the in-flight set too: an abnormal run can emit agent_end after a
|
|
1282
|
+
// tool_execution_start with no matching tool_execution_end. A leftover id
|
|
1283
|
+
// would keep executingTools non-empty forever, gating every future poke
|
|
1284
|
+
// (size === 0) and permanently disarming the watchdog until a rebind.
|
|
1285
|
+
generation.executingTools.clear();
|
|
1286
|
+
streamWatchdog.reset();
|
|
1287
|
+
}
|
|
1288
|
+
else if (type === 'tool_execution_start') {
|
|
1289
|
+
generation.executingTools.add(event.toolCallId ?? '');
|
|
1290
|
+
streamWatchdog.clear();
|
|
1291
|
+
}
|
|
1292
|
+
else if (type === 'tool_execution_end') {
|
|
1293
|
+
generation.executingTools.delete(event.toolCallId ?? '');
|
|
1294
|
+
if (generation.executingTools.size === 0 && generation.session.isStreaming)
|
|
1295
|
+
streamWatchdog.poke(generation.id);
|
|
1296
|
+
}
|
|
1297
|
+
else if (generation.session.isStreaming && generation.executingTools.size === 0) {
|
|
1298
|
+
streamWatchdog.poke(generation.id);
|
|
1299
|
+
}
|
|
1300
|
+
if (type === 'message_update') {
|
|
1301
|
+
pendingUpdate = event; // latest full-message update supersedes the held one
|
|
1302
|
+
if (pendingUpdateTimer === null) {
|
|
1303
|
+
pendingUpdateTimer = setTimeout(flushPendingUpdate, MESSAGE_UPDATE_COALESCE_MS);
|
|
1304
|
+
if (typeof pendingUpdateTimer.unref === 'function')
|
|
1305
|
+
pendingUpdateTimer.unref();
|
|
1306
|
+
}
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
// Any non-update event: flush the held update FIRST so a viewer never sees
|
|
1310
|
+
// (e.g.) message_end before the update it supersedes — then relay verbatim.
|
|
1311
|
+
flushPendingUpdate();
|
|
1312
|
+
broadcast(event);
|
|
1313
|
+
};
|
|
1314
|
+
// Persist a live model switch into the node's durable launch recipe so it
|
|
1315
|
+
// survives a yield/revive. pi's `/model` (→ set_model/cycle_model) only
|
|
1316
|
+
// mutates the in-memory engine; without this the node reverts to its
|
|
1317
|
+
// persona/spawn model on the next revive. We write the full `provider/id`
|
|
1318
|
+
// (which normalizeModel passes through unchanged) to BOTH model_override (so
|
|
1319
|
+
// polymorphs preserve it via buildLaunchSpec) and launch.model (the recipe
|
|
1320
|
+
// buildPiArgv replays on revive). Best-effort: a degenerate/ephemeral node
|
|
1321
|
+
// with no canvas row just skips persistence. CRTR_NODE_ID is set in the
|
|
1322
|
+
// broker's own env (merged by piInvocationToSdkConfig at boot).
|
|
1323
|
+
// `pinnedOverride`, when passed, is the durable-pin decision for THIS model
|
|
1324
|
+
// change — decided by the caller from the change's RAW pre-normalize token
|
|
1325
|
+
// (an explicit set_model). Omitted — an algorithmic model change (cycle_model,
|
|
1326
|
+
// cycle_ladder, a provider-fallback/not-found/cooling re-drive) — the existing
|
|
1327
|
+
// pin is carried through UNCHANGED via the `meta.launch` spread, exactly as
|
|
1328
|
+
// before: those changes never re-decide a pin the user didn't ask to change.
|
|
1329
|
+
//
|
|
1330
|
+
// `cfg.modelProviderPinned` is this broker's LIVE authoritative pin state —
|
|
1331
|
+
// every in-process policy read (the retry-fallback gate, the model-not-found
|
|
1332
|
+
// gate, and pinnedToFirst on a reload_auth re-drive) consults it directly, not
|
|
1333
|
+
// the boot-time launch recipe. Mutate it here, synchronously, BEFORE the
|
|
1334
|
+
// best-effort durable write below, so an explicit set_model takes effect for
|
|
1335
|
+
// the very next policy decision even when the canvas row is unreachable
|
|
1336
|
+
// (a degenerate/ephemeral node). Only ever reached from the success path of a
|
|
1337
|
+
// completed `set_model` — never before the engine call it decided from has
|
|
1338
|
+
// actually succeeded.
|
|
1339
|
+
const persistModelChoice = (pinnedOverride) => {
|
|
1340
|
+
if (pinnedOverride !== undefined)
|
|
1341
|
+
cfg.modelProviderPinned = pinnedOverride;
|
|
1342
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
1343
|
+
const spec = formatModelSpec(session.model, session.thinkingLevel);
|
|
1344
|
+
if (nodeId === undefined || nodeId === '' || spec === null)
|
|
1345
|
+
return;
|
|
1346
|
+
try {
|
|
1347
|
+
const meta = getNode(nodeId);
|
|
1348
|
+
if (meta === null)
|
|
1349
|
+
return;
|
|
1350
|
+
const launch = meta.launch !== undefined
|
|
1351
|
+
? { ...meta.launch, model: spec, ...(pinnedOverride !== undefined ? { modelProviderPinned: pinnedOverride } : {}) }
|
|
1352
|
+
: undefined;
|
|
1353
|
+
updateNode(nodeId, { model_override: spec, ...(launch !== undefined ? { launch } : {}) });
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
// Persistence is best-effort; the live switch already succeeded.
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
// pi emits no AgentSessionEvent for a model switch, so after a successful
|
|
1360
|
+
// set_model/cycle_model the broker announces it itself — otherwise the new
|
|
1361
|
+
// model reaches no viewer (the requester gets only a bare ack) and every
|
|
1362
|
+
// footer shows the stale model until the next unrelated event.
|
|
1363
|
+
const broadcastModelChanged = (persist = true, pinnedOverride) => {
|
|
1364
|
+
if (persist)
|
|
1365
|
+
persistModelChoice(pinnedOverride);
|
|
1366
|
+
broadcast({ type: 'model_changed', model: isUnknownModel(session.model) ? undefined : session.model, spec: formatModelSpec(session.model, session.thinkingLevel) });
|
|
1367
|
+
};
|
|
1368
|
+
const buildSnapshot = () => ({
|
|
1369
|
+
messages: snapshotMessages(session),
|
|
1370
|
+
stats: session.getSessionStats(),
|
|
1371
|
+
// pi's `get_state` (rpc-mode.js), field for field, from the same getters.
|
|
1372
|
+
// The ONE divergence: a model-less session (undefined OR the SDK
|
|
1373
|
+
// unknown-stub) reports no model, matching broadcastModelChanged — never a
|
|
1374
|
+
// fictitious `unknown` selection.
|
|
1375
|
+
state: {
|
|
1376
|
+
model: isUnknownModel(session.model) ? undefined : session.model,
|
|
1377
|
+
thinkingLevel: session.thinkingLevel,
|
|
1378
|
+
isStreaming: session.isStreaming,
|
|
1379
|
+
isCompacting: session.isCompacting,
|
|
1380
|
+
steeringMode: session.steeringMode,
|
|
1381
|
+
followUpMode: session.followUpMode,
|
|
1382
|
+
sessionFile: session.sessionFile,
|
|
1383
|
+
sessionId: session.sessionId,
|
|
1384
|
+
sessionName: session.sessionName,
|
|
1385
|
+
autoCompactionEnabled: session.autoCompactionEnabled,
|
|
1386
|
+
messageCount: session.messages.length,
|
|
1387
|
+
pendingMessageCount: session.pendingMessageCount,
|
|
1388
|
+
},
|
|
1389
|
+
// Broker-owned levels (see BrokerSnapshot.display). Copied out so a later
|
|
1390
|
+
// mutation of the retained maps cannot alias a frame already serialized.
|
|
1391
|
+
display: {
|
|
1392
|
+
statuses: Object.fromEntries(displayStatuses),
|
|
1393
|
+
widgets: Object.fromEntries([...displayWidgets].map(([k, w]) => [k, { lines: [...w.lines], placement: w.placement }])),
|
|
1394
|
+
title: displayTitle,
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
// Send a client its catch-up snapshot. welcome.pending_dialog carries a single
|
|
1398
|
+
// still-in-flight dialog to a WRITABLE client attaching mid-dialog; an observer
|
|
1399
|
+
// can never answer one, so observers get null. The pendingDialogs map is
|
|
1400
|
+
// insertion-ordered — the first entry is the canonical one carried here; any
|
|
1401
|
+
// extras are replayed explicitly by the caller (rare: concurrent dialogs).
|
|
1402
|
+
const sendWelcome = (client) => {
|
|
1403
|
+
const first = client.role === 'controller' ? pendingDialogs.values().next().value : undefined;
|
|
1404
|
+
sendFrame(client, {
|
|
1405
|
+
type: 'welcome',
|
|
1406
|
+
snapshot: buildSnapshot(),
|
|
1407
|
+
role: client.role,
|
|
1408
|
+
pending_dialog: first !== undefined ? first.request : null,
|
|
1409
|
+
agentDir: getAgentDir(),
|
|
1410
|
+
}, true);
|
|
1411
|
+
// No display_* replay: the retained extension state rides IN the snapshot
|
|
1412
|
+
// (`snapshot.display`), so catch-up is one atomic frame. A viewer attaching
|
|
1413
|
+
// after an idle brief was painted still sees it on first paint, and there is
|
|
1414
|
+
// no window between `welcome` and the replay where chrome renders blank.
|
|
1415
|
+
};
|
|
1416
|
+
// Attach-mid-dialog replay: a dialog raised before this client attached is
|
|
1417
|
+
// still pending (a peer detaching never cancels one — see makeBrokerUiContext),
|
|
1418
|
+
// so a writable client joining afterwards is handed every in-flight dialog and
|
|
1419
|
+
// becomes a full peer in the race to answer it. `welcome.pending_dialog` already
|
|
1420
|
+
// carried the FIRST entry, so this replays only the extras.
|
|
1421
|
+
const replayExtraPendingDialogsTo = (client) => {
|
|
1422
|
+
const pend = [...pendingDialogs.values()];
|
|
1423
|
+
for (let i = 1; i < pend.length; i++)
|
|
1424
|
+
sendFrame(client, pend[i].request);
|
|
1425
|
+
};
|
|
1426
|
+
// After a session-replacing op (new_session/switch_session/fork) the engine's
|
|
1427
|
+
// entire message history changed, so every attached viewer must rebuild from a
|
|
1428
|
+
// fresh snapshot of the NEW session.
|
|
1429
|
+
const reWelcomeAll = () => {
|
|
1430
|
+
for (const c of clients)
|
|
1431
|
+
if (c.helloed)
|
|
1432
|
+
sendWelcome(c);
|
|
1433
|
+
};
|
|
1434
|
+
// -------------------------------------------------------------------------
|
|
1435
|
+
// The single exit helper (plan §0): dispose the engine, close + unlink the
|
|
1436
|
+
// socket, exit 0. Idempotent — every trigger converges here.
|
|
1437
|
+
// -------------------------------------------------------------------------
|
|
1438
|
+
const disposeAndExit = (_reason, status = 0) => {
|
|
1439
|
+
if (disposed)
|
|
1440
|
+
return;
|
|
1441
|
+
disposed = true;
|
|
1442
|
+
streamWatchdog.reset();
|
|
1443
|
+
// disposeActiveSession atomically clears the process-global pointer before
|
|
1444
|
+
// disposing — whichever engine session (committed or an in-flight rebind
|
|
1445
|
+
// candidate) it currently names, matching broker-cli's fatal-handler primitive.
|
|
1446
|
+
disposeActiveSession();
|
|
1447
|
+
try {
|
|
1448
|
+
server?.close();
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
/* ignore */
|
|
1452
|
+
}
|
|
1453
|
+
// Finite and best-effort: destroy every currently tracked client socket so
|
|
1454
|
+
// teardown below can't race a viewer write, then drop the set. Do not wait
|
|
1455
|
+
// for 'close' events.
|
|
1456
|
+
for (const client of clients) {
|
|
1457
|
+
try {
|
|
1458
|
+
client.socket.destroy();
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
/* ignore */
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
clients.clear();
|
|
1465
|
+
try {
|
|
1466
|
+
if (existsSync(sockPath))
|
|
1467
|
+
unlinkSync(sockPath);
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
/* ignore */
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
// A clean exit never leaves a stale presence claim behind.
|
|
1474
|
+
if (existsSync(attachPath))
|
|
1475
|
+
unlinkSync(attachPath);
|
|
1476
|
+
}
|
|
1477
|
+
catch {
|
|
1478
|
+
/* ignore */
|
|
1479
|
+
}
|
|
1480
|
+
process.exit(status);
|
|
1481
|
+
};
|
|
1482
|
+
// A stophook can request shutdown while pi is still unwinding a completed
|
|
1483
|
+
// turn. Let that turn reach agent_settled first: inbox delivery commits its
|
|
1484
|
+
// durable cursor there, and only there. Explicit operator shutdown and OS
|
|
1485
|
+
// signals remain abrupt through disposeAndExit.
|
|
1486
|
+
const requestShutdown = () => {
|
|
1487
|
+
if (disposed || shutdownRequested)
|
|
1488
|
+
return;
|
|
1489
|
+
if (!agentRunActive) {
|
|
1490
|
+
disposeAndExit('shutdown-hook');
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
shutdownRequested = true;
|
|
1494
|
+
};
|
|
1495
|
+
// -------------------------------------------------------------------------
|
|
1496
|
+
// Extension-dialog routing (C2). makeBrokerUiContext owns the dialogPromise;
|
|
1497
|
+
// here we just give it the three broker-side hooks it needs: the live set of
|
|
1498
|
+
// writable clients (empty = nobody can answer → noOp fallback), how to forward a
|
|
1499
|
+
// dialog to one of them, and the pending-dialog registry whichever one answers
|
|
1500
|
+
// first settles through. The zero-writable path NEVER hangs and NEVER waits on a
|
|
1501
|
+
// per-dialog timeout (design §5.4's timeout premise is false — see
|
|
1502
|
+
// makeBrokerUiContext).
|
|
1503
|
+
// -------------------------------------------------------------------------
|
|
1504
|
+
const uiContext = makeBrokerUiContext({
|
|
1505
|
+
writable: writableClients,
|
|
1506
|
+
forward: (client, request) => sendFrame(client, request),
|
|
1507
|
+
pending: pendingDialogs,
|
|
1508
|
+
broadcast: broadcastUi,
|
|
1509
|
+
providerRotationPolicy: () => cfg.modelExact === true
|
|
1510
|
+
? { allowCrossProviderFallback: false, reason: 'model-exact' }
|
|
1511
|
+
: cfg.modelProviderPinned === true
|
|
1512
|
+
? { allowCrossProviderFallback: false, reason: 'provider-pinned' }
|
|
1513
|
+
: { allowCrossProviderFallback: true },
|
|
1514
|
+
// A deliberate managed-provider cooldown wait is known liveness, not a wedged
|
|
1515
|
+
// stream: pause the dead-stream watchdog for its duration. Only pause for a
|
|
1516
|
+
// finite future deadline, and resume ONLY while the same turn is still
|
|
1517
|
+
// awaiting the provider (streaming, no tool running) so a wait that outlived
|
|
1518
|
+
// its turn can't rearm a stale session; otherwise fully reset.
|
|
1519
|
+
onKnownStreamWait: (deadlineMs) => {
|
|
1520
|
+
if (!Number.isFinite(deadlineMs) || deadlineMs <= Date.now())
|
|
1521
|
+
return () => { };
|
|
1522
|
+
const generation = installedGeneration;
|
|
1523
|
+
if (generation === undefined)
|
|
1524
|
+
return () => { };
|
|
1525
|
+
const waitingSession = generation.session;
|
|
1526
|
+
streamWatchdog.pause();
|
|
1527
|
+
return () => {
|
|
1528
|
+
if (installedGeneration !== generation || session !== waitingSession)
|
|
1529
|
+
return;
|
|
1530
|
+
releaseKnownStreamWait(streamWatchdog, generation.id, waitingSession.isStreaming, generation.executingTools.size > 0);
|
|
1531
|
+
};
|
|
1532
|
+
},
|
|
1533
|
+
});
|
|
1534
|
+
// -------------------------------------------------------------------------
|
|
1535
|
+
// 3+4 (plan): bind the FULL canvas extensions (mode 'rpc') AND fan the single
|
|
1536
|
+
// engine event stream to all clients, VERBATIM — both wrapped in one
|
|
1537
|
+
// ticketed rebind lane so a session-replacing op (new_session/switch_session/fork)
|
|
1538
|
+
// can re-run them against the NEW session. This mirrors pi's rpc-mode callback:
|
|
1539
|
+
// the AgentSessionRuntime tears down the old
|
|
1540
|
+
// session, builds the next, then calls this back; we re-bind extensions and
|
|
1541
|
+
// re-subscribe so the canvas hooks + the fan-out follow the live session.
|
|
1542
|
+
//
|
|
1543
|
+
// bindExtensions fires session_start → the stophook records pi_session_id/file
|
|
1544
|
+
// + recordPid(pid); the shutdownHandler is the precise broker-exit trigger (the
|
|
1545
|
+
// stophook calls ctx.shutdown() only in its done / idle-release / refresh
|
|
1546
|
+
// branches, NOT on reprompt). A requested in-turn shutdown exits after the
|
|
1547
|
+
// subscriber observes agent_settled, while every other branch stays alive. The
|
|
1548
|
+
// relay must not infer shutdown from raw agent_end: that would override the
|
|
1549
|
+
// stophook's routing decision. shutdownHandler + the inbound `shutdown` frame
|
|
1550
|
+
// are the only exit triggers.
|
|
1551
|
+
//
|
|
1552
|
+
// m7: the subscriber body is wrapped in try/catch — a synchronous throw out of
|
|
1553
|
+
// a subscriber otherwise propagates as a run failure that would crash the broker.
|
|
1554
|
+
// -------------------------------------------------------------------------
|
|
1555
|
+
let unsubscribe;
|
|
1556
|
+
const removeInstalledSubscription = () => {
|
|
1557
|
+
const installed = unsubscribe;
|
|
1558
|
+
installed?.();
|
|
1559
|
+
unsubscribe = undefined;
|
|
1560
|
+
installedGeneration = undefined;
|
|
1561
|
+
};
|
|
1562
|
+
let rebindTail = Promise.resolve();
|
|
1563
|
+
let latestRebindRequest = 0;
|
|
1564
|
+
const captureSystemPrompt = (candidateSession) => {
|
|
1565
|
+
try {
|
|
1566
|
+
const dir = jobDir(nodeId);
|
|
1567
|
+
mkdirSync(dir, { recursive: true });
|
|
1568
|
+
writeFileSync(join(dir, 'system-prompt.md'), candidateSession.systemPrompt, 'utf8');
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
/* non-fatal: diagnostic only */
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
const executeSubscriptionCallback = (generation, event) => {
|
|
1575
|
+
try {
|
|
1576
|
+
if (event.type === 'agent_start')
|
|
1577
|
+
agentRunActive = true;
|
|
1578
|
+
if (event.type === 'agent_end') {
|
|
1579
|
+
generation.stagedProviderAgentEnd = event;
|
|
1580
|
+
// A continuation after failed compaction owns the final settled outcome.
|
|
1581
|
+
generation.stagedOverflowFailure = null;
|
|
1582
|
+
}
|
|
1583
|
+
if (event.type === 'compaction_end' &&
|
|
1584
|
+
event.reason === 'overflow' &&
|
|
1585
|
+
event.willRetry === false &&
|
|
1586
|
+
typeof event.errorMessage === 'string') {
|
|
1587
|
+
generation.stagedOverflowFailure = { errorMessage: event.errorMessage };
|
|
1588
|
+
}
|
|
1589
|
+
if (event.type === 'agent_settled') {
|
|
1590
|
+
agentRunActive = false;
|
|
1591
|
+
publishSettledProviderFault(generation);
|
|
1592
|
+
if (shutdownRequested) {
|
|
1593
|
+
disposeAndExit('shutdown-after-settle');
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
relayEvent(event, generation);
|
|
1598
|
+
if (event.type === 'agent_settled')
|
|
1599
|
+
scheduleFaultRetry();
|
|
1600
|
+
}
|
|
1601
|
+
catch (err) {
|
|
1602
|
+
emitEvent({
|
|
1603
|
+
level: 'debug',
|
|
1604
|
+
event: 'broker.runtime.diagnostic',
|
|
1605
|
+
fields: { message: `event relay threw: ${String(err)}` },
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
const enqueueRebind = (candidateSession, candidateServices) => {
|
|
1610
|
+
const ticket = ++latestRebindRequest;
|
|
1611
|
+
const task = rebindTail.then(async () => {
|
|
1612
|
+
if (ticket !== latestRebindRequest)
|
|
1613
|
+
return;
|
|
1614
|
+
installedGeneration?.executingTools.clear();
|
|
1615
|
+
removeInstalledSubscription();
|
|
1616
|
+
// This candidate is now the engine session a mid-rebind fatal cleanup must
|
|
1617
|
+
// dispose — set before the awaited bindExtensions so a timeout/fatal during
|
|
1618
|
+
// that await disposes the candidate rather than the stale committed session.
|
|
1619
|
+
// Shared session/services ownership still only commits below, post-await.
|
|
1620
|
+
activeSession = candidateSession;
|
|
1621
|
+
streamWatchdog.reset();
|
|
1622
|
+
agentRunActive = false;
|
|
1623
|
+
if (pendingUpdateTimer !== null) {
|
|
1624
|
+
clearTimeout(pendingUpdateTimer);
|
|
1625
|
+
pendingUpdateTimer = null;
|
|
1626
|
+
}
|
|
1627
|
+
pendingUpdate = null;
|
|
1628
|
+
clearFaultRetryTimer();
|
|
1629
|
+
providerFallbackInFlight = null;
|
|
1630
|
+
notFoundFallbackInFlight = null;
|
|
1631
|
+
reloadAuthInFlight = null;
|
|
1632
|
+
await candidateSession.bindExtensions({
|
|
1633
|
+
uiContext,
|
|
1634
|
+
// 'rpc' — NOT 'print'. Per pi docs/rpc.md:1078 this is exactly our shape:
|
|
1635
|
+
// a headless engine whose UI methods are functional through a remote client
|
|
1636
|
+
// (our uiContext relays them as `display_*` / `extension_ui_request` frames).
|
|
1637
|
+
// 'print' would tell every extension `ctx.mode === 'print'` — pi's own label
|
|
1638
|
+
// for "extensions run but can't prompt" — while we hand them a uiContext that
|
|
1639
|
+
// can. (`ctx.hasUI` is derived from uiContext identity, not from mode, so it
|
|
1640
|
+
// was already true; this fixes the mode label the extensions actually read.)
|
|
1641
|
+
// The `ctx.mode === 'tui'` guards in canvas-view/-resume/-commands stay dead.
|
|
1642
|
+
mode: 'rpc',
|
|
1643
|
+
shutdownHandler: requestShutdown,
|
|
1644
|
+
});
|
|
1645
|
+
if (ticket !== latestRebindRequest)
|
|
1646
|
+
return;
|
|
1647
|
+
removeInstalledSubscription();
|
|
1648
|
+
session = candidateSession;
|
|
1649
|
+
services = candidateServices;
|
|
1650
|
+
if (cfg.editorName !== undefined && cfg.editorName !== '') {
|
|
1651
|
+
try {
|
|
1652
|
+
candidateSession.setSessionName(cfg.editorName);
|
|
1653
|
+
}
|
|
1654
|
+
catch {
|
|
1655
|
+
/* non-fatal: naming is cosmetic */
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
const generation = {
|
|
1659
|
+
id: ++nextSubscriptionGeneration,
|
|
1660
|
+
session: candidateSession,
|
|
1661
|
+
fallback: { blocked: false },
|
|
1662
|
+
stagedProviderAgentEnd: null,
|
|
1663
|
+
stagedOverflowFailure: null,
|
|
1664
|
+
executingTools: new Set(),
|
|
1665
|
+
};
|
|
1666
|
+
unsubscribe = session.subscribe((event) => {
|
|
1667
|
+
if (installedGeneration !== generation)
|
|
1668
|
+
return;
|
|
1669
|
+
const ambientId = operationIdContext.current();
|
|
1670
|
+
if (ambientId !== undefined) {
|
|
1671
|
+
executeSubscriptionCallback(generation, event);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const fallback = generation.fallback;
|
|
1675
|
+
if (event.type === 'turn_start' && fallback.storedId !== undefined) {
|
|
1676
|
+
fallback.storedId = undefined;
|
|
1677
|
+
fallback.blocked = true;
|
|
1678
|
+
operationIdContext.fresh(() => {
|
|
1679
|
+
emitEvent({
|
|
1680
|
+
level: 'warn',
|
|
1681
|
+
event: 'broker.turn.fallback_overlap',
|
|
1682
|
+
fields: { generation: generation.id, engine_event: event.type },
|
|
1683
|
+
});
|
|
1684
|
+
executeSubscriptionCallback(generation, event);
|
|
1685
|
+
});
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (event.type === 'turn_start' && !fallback.blocked) {
|
|
1689
|
+
operationIdContext.fresh((operationId) => {
|
|
1690
|
+
fallback.storedId = operationId;
|
|
1691
|
+
executeSubscriptionCallback(generation, event);
|
|
1692
|
+
});
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const storedId = fallback.storedId;
|
|
1696
|
+
if (storedId !== undefined && !fallback.blocked) {
|
|
1697
|
+
operationIdContext.run(storedId, () => {
|
|
1698
|
+
executeSubscriptionCallback(generation, event);
|
|
1699
|
+
if (event.type === 'agent_settled' && fallback.storedId === storedId) {
|
|
1700
|
+
fallback.storedId = undefined;
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
executeSubscriptionCallback(generation, event);
|
|
1706
|
+
});
|
|
1707
|
+
installedGeneration = generation;
|
|
1708
|
+
captureSystemPrompt(candidateSession);
|
|
1709
|
+
});
|
|
1710
|
+
rebindTail = task.catch(() => undefined);
|
|
1711
|
+
return task;
|
|
1712
|
+
};
|
|
1713
|
+
// Register the rebind BEFORE the first bind so a replacement triggered during
|
|
1714
|
+
// startup is honored; then use the same ticketed lane for the initial install.
|
|
1715
|
+
runtime?.setRebindSession((candidateSession) => enqueueRebind(candidateSession, runtime.services));
|
|
1716
|
+
await enqueueRebind(session, services);
|
|
1717
|
+
// -------------------------------------------------------------------------
|
|
1718
|
+
// Drive the engine on behalf of a writable client.
|
|
1719
|
+
// -------------------------------------------------------------------------
|
|
1720
|
+
// ---------------------------------------------------------------------------
|
|
1721
|
+
// Inline memory-reference inventory (design: inline memory references) — a
|
|
1722
|
+
// single broker-level memo shared by BOTH the `list_memory_refs` read-op
|
|
1723
|
+
// reply and per-submission guidance resolution below, so a burst of reads +
|
|
1724
|
+
// submissions between reloads never re-walks the corpus per call. Cleared
|
|
1725
|
+
// (work item 3) only on a SUCCESSFUL `session.reload()` — a failed reload
|
|
1726
|
+
// leaves the prior memo intact, since nothing about the corpus is known to
|
|
1727
|
+
// have changed.
|
|
1728
|
+
// ---------------------------------------------------------------------------
|
|
1729
|
+
let memoizedRefInventory = null;
|
|
1730
|
+
const getRefInventory = () => {
|
|
1731
|
+
if (memoizedRefInventory === null)
|
|
1732
|
+
memoizedRefInventory = buildRefInventory();
|
|
1733
|
+
return memoizedRefInventory;
|
|
1734
|
+
};
|
|
1735
|
+
const buildListMemoryRefsData = (id) => ({
|
|
1736
|
+
type: 'data',
|
|
1737
|
+
id,
|
|
1738
|
+
kind: 'list_memory_refs',
|
|
1739
|
+
refs: getRefInventory().refs,
|
|
1740
|
+
engineLeadingCommandNames: engineLeadingCommandTokens(session),
|
|
1741
|
+
});
|
|
1742
|
+
/** Resolve the hidden guidance text for a human frame's verbatim text, or
|
|
1743
|
+
* `null` when no guidance should be sent (a leading engine command/skill/
|
|
1744
|
+
* template, no resolvable refs, or a degraded inventory/guidance build —
|
|
1745
|
+
* work item 6: any failure here degrades to "no guidance", logged once as a
|
|
1746
|
+
* structured warning, and NEVER blocks/alters the human delivery below). */
|
|
1747
|
+
const resolveFrameGuidance = (text) => {
|
|
1748
|
+
try {
|
|
1749
|
+
if (isLeadingEngineCommand(text, session))
|
|
1750
|
+
return null;
|
|
1751
|
+
return buildGuidance(text, getRefInventory().names);
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
emitEvent({
|
|
1755
|
+
level: 'warn',
|
|
1756
|
+
event: 'broker.memory_refs.guidance_resolve_failed',
|
|
1757
|
+
error: err,
|
|
1758
|
+
});
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
// Prompt dispatches whose promise has not yet settled. `session.prompt()`
|
|
1763
|
+
// does async setup (input transforms, model/auth validation, compaction)
|
|
1764
|
+
// BEFORE `_isAgentRunActive` flips, so for a short window after a routed
|
|
1765
|
+
// deliver the engine reads idle and `session.abort()` is a silent no-op — an
|
|
1766
|
+
// id-bearing abort racing that window would miss the turn entirely (verified
|
|
1767
|
+
// live). The counter lets it distinguish "genuinely idle" from "turn still
|
|
1768
|
+
// igniting" and wait for the run to become abortable.
|
|
1769
|
+
let pendingPromptDispatches = 0;
|
|
1770
|
+
const trackPromptDispatch = (p) => {
|
|
1771
|
+
pendingPromptDispatches++;
|
|
1772
|
+
const tracked = Promise.resolve(p);
|
|
1773
|
+
void tracked.finally(() => {
|
|
1774
|
+
pendingPromptDispatches--;
|
|
1775
|
+
});
|
|
1776
|
+
return tracked;
|
|
1777
|
+
};
|
|
1778
|
+
/**
|
|
1779
|
+
* Drive the engine for one client frame. For a prompt/follow_up frame this
|
|
1780
|
+
* returns a promise that settles with the engine call's OUTCOME — `undefined`
|
|
1781
|
+
* when the call succeeded, the rejection value when it did not — so a caller
|
|
1782
|
+
* that must report success (the `deliver` ack) can read it instead of assuming
|
|
1783
|
+
* the dispatch worked. Every other frame type keeps the fire-and-forget shape
|
|
1784
|
+
* and returns `undefined`.
|
|
1785
|
+
*/
|
|
1786
|
+
const driveEngine = (client, frame) => {
|
|
1787
|
+
// The engine call's failure, captured for the returned outcome. relayError
|
|
1788
|
+
// still relays the error frame; recording it is what lets the caller ack
|
|
1789
|
+
// from the outcome rather than from the fact that a call was made at all.
|
|
1790
|
+
let failure;
|
|
1791
|
+
const relayError = (err) => {
|
|
1792
|
+
failure = err;
|
|
1793
|
+
sendFrame(client, { type: 'error', code: 'engine_error', message: String(err) });
|
|
1794
|
+
};
|
|
1795
|
+
// Hidden `crtr-memory-refs` guidance rides ALONGSIDE the human frame's own
|
|
1796
|
+
// engine call, never rewriting `frame.text` — see resolveFrameGuidance /
|
|
1797
|
+
// chooseGuidanceDeliveryMode above. A failed SEND (as opposed to a failed
|
|
1798
|
+
// resolve, handled above) is likewise logged-and-swallowed: it must never
|
|
1799
|
+
// reach `relayError`, which is reserved for the human call's own outcome.
|
|
1800
|
+
const sendGuidance = (guidance, deliverAs) => session
|
|
1801
|
+
.sendCustomMessage({ customType: 'crtr-memory-refs', content: guidance, display: false }, { deliverAs })
|
|
1802
|
+
.catch((err) => {
|
|
1803
|
+
emitEvent({
|
|
1804
|
+
level: 'warn',
|
|
1805
|
+
event: 'broker.memory_refs.guidance_send_failed',
|
|
1806
|
+
error: err,
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
switch (frame.type) {
|
|
1810
|
+
case 'prompt':
|
|
1811
|
+
case 'follow_up': {
|
|
1812
|
+
// Settle on the engine call's outcome: `failure` is set by relayError
|
|
1813
|
+
// before the caught promise resolves, so reading it after the chain is
|
|
1814
|
+
// the call's verdict.
|
|
1815
|
+
const settle = (p) => p.then(() => failure);
|
|
1816
|
+
const route = resolveEngineRoute(frame, session.isStreaming);
|
|
1817
|
+
const guidance = resolveFrameGuidance(frame.text);
|
|
1818
|
+
if (guidance === null) {
|
|
1819
|
+
if (route.call === 'prompt') {
|
|
1820
|
+
// A routed prompt STARTS a turn — including an idle follow_up
|
|
1821
|
+
// escalated by m-C — so it gets a fresh operation id. A true
|
|
1822
|
+
// (mid-stream) followUp rides the current turn's operation, like
|
|
1823
|
+
// steer.
|
|
1824
|
+
return settle(trackPromptDispatch(operationIdContext.fresh(() => session.prompt(frame.text, route.options).catch(relayError))));
|
|
1825
|
+
}
|
|
1826
|
+
// C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
|
|
1827
|
+
return settle(session.followUp(frame.text, frame.images).catch(relayError));
|
|
1828
|
+
}
|
|
1829
|
+
const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, session.isStreaming);
|
|
1830
|
+
if (route.call === 'prompt') {
|
|
1831
|
+
if (deliverAs === 'nextTurn') {
|
|
1832
|
+
// The ONLY ordering where guidance precedes the human call — pi's
|
|
1833
|
+
// prompt() reads+clears its pending-nextTurn queue synchronously
|
|
1834
|
+
// at call time, injecting it into the SAME turn as the new user
|
|
1835
|
+
// message. promptWithNextTurnGuidance queues the guidance and then
|
|
1836
|
+
// ATOMICALLY clears its own pending entry if prompt() settles
|
|
1837
|
+
// without ever reaching that drain (an early throw/return leaves
|
|
1838
|
+
// nothing stranded to leak into a later, unrelated turn).
|
|
1839
|
+
return settle(trackPromptDispatch(promptWithNextTurnGuidance(session, guidance, () => operationIdContext.fresh(() => session.prompt(frame.text, route.options))).catch(relayError)));
|
|
1840
|
+
}
|
|
1841
|
+
return settle(trackPromptDispatch(operationIdContext.fresh(() => session.prompt(frame.text, route.options).catch(relayError).then(() => sendGuidance(guidance, deliverAs)))));
|
|
1842
|
+
}
|
|
1843
|
+
// deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
|
|
1844
|
+
// contract (a followUp routeCall never yields 'nextTurn').
|
|
1845
|
+
return settle(session.followUp(frame.text, frame.images).catch(relayError).then(() => sendGuidance(guidance, deliverAs)));
|
|
1846
|
+
}
|
|
1847
|
+
case 'steer': {
|
|
1848
|
+
const guidance = resolveFrameGuidance(frame.text);
|
|
1849
|
+
// C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
|
|
1850
|
+
if (guidance === null) {
|
|
1851
|
+
void session.steer(frame.text, frame.images).catch(relayError);
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
void session.steer(frame.text, frame.images).catch(relayError).then(() => sendGuidance(guidance, 'steer'));
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
case 'abort': {
|
|
1858
|
+
// esc/abort cancels whatever is live: a running `!` command (abortBash)
|
|
1859
|
+
// takes precedence over the agent turn, mirroring pi interactive mode.
|
|
1860
|
+
if (session.isBashRunning) {
|
|
1861
|
+
session.abortBash();
|
|
1862
|
+
if (frame.id !== undefined)
|
|
1863
|
+
ackTo(client, 'abort', true, 'bash', frame.id);
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
if (frame.id === undefined) {
|
|
1867
|
+
// A viewer's Esc — unchanged instant semantics, no ack.
|
|
1868
|
+
void session.abort().catch(relayError);
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
// An id-bearing abort (a one-shot daemon interrupt) must close the
|
|
1872
|
+
// pre-turn window: a routed deliver whose prompt() is still in async
|
|
1873
|
+
// setup reads as idle and abort() would silently miss it. If a prompt
|
|
1874
|
+
// dispatch is pending, wait (bounded) for the run to become abortable
|
|
1875
|
+
// — or for every dispatch to settle without one (an early return) —
|
|
1876
|
+
// then abort and ack with what was actually interrupted.
|
|
1877
|
+
void (async () => {
|
|
1878
|
+
try {
|
|
1879
|
+
if (!session.isStreaming && pendingPromptDispatches > 0) {
|
|
1880
|
+
const deadline = Date.now() + 5_000;
|
|
1881
|
+
while (Date.now() < deadline && !session.isStreaming && pendingPromptDispatches > 0) {
|
|
1882
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const detail = session.isStreaming ? 'turn' : 'idle';
|
|
1886
|
+
await session.abort();
|
|
1887
|
+
ackTo(client, 'abort', true, detail, frame.id);
|
|
1888
|
+
}
|
|
1889
|
+
catch (err) {
|
|
1890
|
+
engineErrorTo(client, frame.id)(err);
|
|
1891
|
+
}
|
|
1892
|
+
})();
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
case 'bash': {
|
|
1896
|
+
// `!`/`!!` bash: run via session.executeBash (records a bashExecution
|
|
1897
|
+
// context message, starts NO agent turn) and relay its lifecycle to ALL
|
|
1898
|
+
// viewers as bash_start/bash_output/bash_end so each renders the live
|
|
1899
|
+
// component. Serialized like pi's interactive mode — reject a second `!`
|
|
1900
|
+
// while one is already running rather than racing _bashAbortController.
|
|
1901
|
+
const command = frame.command;
|
|
1902
|
+
if (command.trim() === '')
|
|
1903
|
+
break;
|
|
1904
|
+
if (session.isBashRunning) {
|
|
1905
|
+
sendFrame(client, {
|
|
1906
|
+
type: 'error',
|
|
1907
|
+
code: 'bash_busy',
|
|
1908
|
+
message: 'A bash command is already running',
|
|
1909
|
+
});
|
|
1910
|
+
break;
|
|
1911
|
+
}
|
|
1912
|
+
const exclude = frame.excludeFromContext === true;
|
|
1913
|
+
// Order before any held assistant message_update, so a `!` run mid-stream
|
|
1914
|
+
// never renders ahead of the update it follows.
|
|
1915
|
+
flushPendingUpdate();
|
|
1916
|
+
broadcast({ type: 'bash_start', command, excludeFromContext: exclude });
|
|
1917
|
+
void session
|
|
1918
|
+
.executeBash(command, (chunk) => broadcast({ type: 'bash_output', chunk }), {
|
|
1919
|
+
excludeFromContext: exclude,
|
|
1920
|
+
})
|
|
1921
|
+
.then((result) => {
|
|
1922
|
+
broadcast({
|
|
1923
|
+
type: 'bash_end',
|
|
1924
|
+
exitCode: result.exitCode,
|
|
1925
|
+
cancelled: result.cancelled,
|
|
1926
|
+
truncated: result.truncated,
|
|
1927
|
+
fullOutputPath: result.fullOutputPath,
|
|
1928
|
+
});
|
|
1929
|
+
})
|
|
1930
|
+
.catch(relayError);
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
// -------------------------------------------------------------------------
|
|
1936
|
+
// Command-op helpers (T3, §2.3). The write guard is hoisted here (it was
|
|
1937
|
+
// inlined twice) and reused by all mutating ops; the ack/error replies
|
|
1938
|
+
// and a few resolvers keep the per-op cases one-liners.
|
|
1939
|
+
// -------------------------------------------------------------------------
|
|
1940
|
+
/** Reject a read-only client for a mutating op. The decision reads this
|
|
1941
|
+
* client's OWN fixed role — there is no shared writer slot, so any number of
|
|
1942
|
+
* writable clients pass concurrently. Returns true when rejected (the caller
|
|
1943
|
+
* should `break`). */
|
|
1944
|
+
const notWritable = (client, what, id) => {
|
|
1945
|
+
if (client.role === 'controller')
|
|
1946
|
+
return false;
|
|
1947
|
+
sendFrame(client, {
|
|
1948
|
+
type: 'error',
|
|
1949
|
+
code: 'read_only',
|
|
1950
|
+
message: `a read-only client may not ${what}`,
|
|
1951
|
+
// M1: echo a correlated request's id so its pending-by-id promise rejects
|
|
1952
|
+
// rather than hanging (e.g. a read-only `dequeue`).
|
|
1953
|
+
...(id !== undefined ? { id } : {}),
|
|
1954
|
+
});
|
|
1955
|
+
return true;
|
|
1956
|
+
};
|
|
1957
|
+
const ackTo = (client, op, ok = true, detail, id) => sendFrame(client, {
|
|
1958
|
+
type: 'ack',
|
|
1959
|
+
for: op,
|
|
1960
|
+
ok,
|
|
1961
|
+
...(detail !== undefined ? { detail } : {}),
|
|
1962
|
+
...(id !== undefined ? { id } : {}),
|
|
1963
|
+
});
|
|
1964
|
+
const engineErrorTo = (client, id) => (err) => sendFrame(client, {
|
|
1965
|
+
type: 'error',
|
|
1966
|
+
code: 'engine_error',
|
|
1967
|
+
message: String(err),
|
|
1968
|
+
// M1: a correlated read-op / dequeue failure echoes its id so the viewer's
|
|
1969
|
+
// pending-by-id promise rejects instead of hanging; uncorrelated callers
|
|
1970
|
+
// pass no id (unchanged behavior).
|
|
1971
|
+
...(id !== undefined ? { id } : {}),
|
|
1972
|
+
});
|
|
1973
|
+
/** Resolve a `provider/id` model spec against the LIVE services registry (which
|
|
1974
|
+
* carries any extension-registered providers). Pure: returns undefined on a
|
|
1975
|
+
* malformed spec or an unknown model. */
|
|
1976
|
+
const findModelSpec = (spec) => {
|
|
1977
|
+
const { modelSpec } = parseModelSpec(spec);
|
|
1978
|
+
const slash = modelSpec.indexOf('/');
|
|
1979
|
+
if (slash <= 0)
|
|
1980
|
+
return undefined;
|
|
1981
|
+
return registryOf(services).find(modelSpec.slice(0, slash), modelSpec.slice(slash + 1));
|
|
1982
|
+
};
|
|
1983
|
+
/** Resolve a model from a free-text query — `/model opus`, `/model fable`. Tries
|
|
1984
|
+
* the exact `provider/id` spec first; failing that, searches the registry the
|
|
1985
|
+
* way the picker's search box does (case-insensitive substring on the model id
|
|
1986
|
+
* and the `provider/id` key) and returns the BEST match, preferring authed/
|
|
1987
|
+
* available models. Returns undefined only when nothing matches. */
|
|
1988
|
+
const resolveModelQuery = (query) => {
|
|
1989
|
+
const exact = findModelSpec(query);
|
|
1990
|
+
if (exact)
|
|
1991
|
+
return exact;
|
|
1992
|
+
const q = query.trim().toLowerCase();
|
|
1993
|
+
if (q === '')
|
|
1994
|
+
return undefined;
|
|
1995
|
+
let all;
|
|
1996
|
+
try {
|
|
1997
|
+
all = registryOf(services).getAll();
|
|
1998
|
+
}
|
|
1999
|
+
catch {
|
|
2000
|
+
return undefined;
|
|
2001
|
+
}
|
|
2002
|
+
let availableKeys;
|
|
2003
|
+
try {
|
|
2004
|
+
availableKeys = new Set(registryOf(services).getAvailable().map(modelKey));
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
availableKeys = new Set();
|
|
2008
|
+
}
|
|
2009
|
+
// Rank: exact id (100) > exact provider/id (90) > id prefix (80) > id
|
|
2010
|
+
// substring (60) > provider/id substring (40); +5 if the model is available.
|
|
2011
|
+
const rank = (m) => {
|
|
2012
|
+
const id = m.id.toLowerCase();
|
|
2013
|
+
const key = modelKey(m).toLowerCase();
|
|
2014
|
+
let base = -1;
|
|
2015
|
+
if (id === q)
|
|
2016
|
+
base = 100;
|
|
2017
|
+
else if (key === q)
|
|
2018
|
+
base = 90;
|
|
2019
|
+
else if (id.startsWith(q))
|
|
2020
|
+
base = 80;
|
|
2021
|
+
else if (id.includes(q))
|
|
2022
|
+
base = 60;
|
|
2023
|
+
else if (key.includes(q))
|
|
2024
|
+
base = 40;
|
|
2025
|
+
else
|
|
2026
|
+
return -1;
|
|
2027
|
+
return base + (availableKeys.has(modelKey(m)) ? 5 : 0);
|
|
2028
|
+
};
|
|
2029
|
+
let best;
|
|
2030
|
+
let bestRank = 0;
|
|
2031
|
+
for (const m of all) {
|
|
2032
|
+
const r = rank(m);
|
|
2033
|
+
if (r > bestRank) {
|
|
2034
|
+
bestRank = r;
|
|
2035
|
+
best = m;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return best;
|
|
2039
|
+
};
|
|
2040
|
+
const buildCommandList = () => {
|
|
2041
|
+
const out = [];
|
|
2042
|
+
for (const b of BUILTIN_SLASH_COMMANDS) {
|
|
2043
|
+
out.push({ name: b.name, description: b.description, source: 'builtin' });
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
for (const c of session.extensionRunner.getRegisteredCommands()) {
|
|
2047
|
+
const expansion = c.expansion;
|
|
2048
|
+
out.push({
|
|
2049
|
+
name: c.invocationName,
|
|
2050
|
+
description: c.description ?? '',
|
|
2051
|
+
source: 'command',
|
|
2052
|
+
...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
catch {
|
|
2057
|
+
/* an engine without a live extensionRunner (e.g. the fake) — builtins only */
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
for (const t of session.promptTemplates) {
|
|
2061
|
+
out.push({ name: t.name, description: t.description, source: 'template', template: t.content });
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
catch {
|
|
2065
|
+
/* ignore */
|
|
2066
|
+
}
|
|
2067
|
+
return out;
|
|
2068
|
+
};
|
|
2069
|
+
// -------------------------------------------------------------------------
|
|
2070
|
+
// Read-op data builders (operator-view picker payloads, §5 Unit A). Each is a
|
|
2071
|
+
// PURE getter read against the live engine session — the data a native pi
|
|
2072
|
+
// picker's constructor needs, serialized for the viewer. Not role-gated
|
|
2073
|
+
// (read-only, like get_commands), so the web bridge's observer connection can
|
|
2074
|
+
// populate pickers too.
|
|
2075
|
+
// -------------------------------------------------------------------------
|
|
2076
|
+
// A model-less session's model is EITHER undefined OR the SDK unknown-stub;
|
|
2077
|
+
// both map to the WireModelRef no-model value (null), so list_models.current
|
|
2078
|
+
// and get_settings.settings.model never disclose a fictitious `unknown/unknown`.
|
|
2079
|
+
const toModelRef = (m) => m === undefined || isUnknownModel(m) ? null : { provider: m.provider, id: m.id };
|
|
2080
|
+
const modelKey = (m) => `${m.provider}/${m.id}`;
|
|
2081
|
+
const buildListModelsData = (id) => {
|
|
2082
|
+
const reg = registryOf(session);
|
|
2083
|
+
let availableIds = [];
|
|
2084
|
+
try {
|
|
2085
|
+
availableIds = reg.getAvailable().map(modelKey);
|
|
2086
|
+
}
|
|
2087
|
+
catch {
|
|
2088
|
+
/* getAvailable touches auth config; degrade to "none known available" */
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
type: 'data',
|
|
2092
|
+
id,
|
|
2093
|
+
kind: 'list_models',
|
|
2094
|
+
models: reg.getAll(),
|
|
2095
|
+
current: toModelRef(session.model),
|
|
2096
|
+
availableIds,
|
|
2097
|
+
scopedModels: session.scopedModels.map((s) => ({
|
|
2098
|
+
model: s.model,
|
|
2099
|
+
thinkingLevel: s.thinkingLevel,
|
|
2100
|
+
})),
|
|
2101
|
+
enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
|
|
2102
|
+
};
|
|
2103
|
+
};
|
|
2104
|
+
const buildScopedModelsData = (id) => ({
|
|
2105
|
+
type: 'data',
|
|
2106
|
+
id,
|
|
2107
|
+
kind: 'list_scoped_models',
|
|
2108
|
+
allModels: registryOf(session).getAll(),
|
|
2109
|
+
enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
|
|
2110
|
+
});
|
|
2111
|
+
const buildGetTreeData = (id) => {
|
|
2112
|
+
const sm = session.sessionManager;
|
|
2113
|
+
const forkPoints = session.getUserMessagesForForking().map((u) => ({
|
|
2114
|
+
id: u.entryId,
|
|
2115
|
+
text: u.text,
|
|
2116
|
+
timestamp: sm.getEntry(u.entryId)?.timestamp,
|
|
2117
|
+
}));
|
|
2118
|
+
return {
|
|
2119
|
+
type: 'data',
|
|
2120
|
+
id,
|
|
2121
|
+
kind: 'get_tree',
|
|
2122
|
+
tree: sm.getTree(),
|
|
2123
|
+
currentLeafId: sm.getLeafId(),
|
|
2124
|
+
forkPoints,
|
|
2125
|
+
};
|
|
2126
|
+
};
|
|
2127
|
+
const buildSettingsData = (id) => {
|
|
2128
|
+
const sm = session.settingsManager;
|
|
2129
|
+
// Theme names are enumerable broker-side via the resource loader. The theme
|
|
2130
|
+
// SUBMENU itself is viewer-local (theme is a viewer-only concern), but the
|
|
2131
|
+
// settings menu still shows currentTheme/availableThemes, so include them.
|
|
2132
|
+
let availableThemes = [];
|
|
2133
|
+
try {
|
|
2134
|
+
availableThemes = session.resourceLoader
|
|
2135
|
+
.getThemes()
|
|
2136
|
+
.themes.map((t) => t.name ?? '')
|
|
2137
|
+
.filter((n) => n !== '');
|
|
2138
|
+
}
|
|
2139
|
+
catch {
|
|
2140
|
+
/* loader without themes — viewer falls back to its local theme registry */
|
|
2141
|
+
}
|
|
2142
|
+
const settings = {
|
|
2143
|
+
autoCompact: session.autoCompactionEnabled,
|
|
2144
|
+
showImages: sm.getShowImages(),
|
|
2145
|
+
imageWidthCells: sm.getImageWidthCells(),
|
|
2146
|
+
autoResizeImages: sm.getImageAutoResize(),
|
|
2147
|
+
blockImages: sm.getBlockImages(),
|
|
2148
|
+
enableSkillCommands: sm.getEnableSkillCommands(),
|
|
2149
|
+
steeringMode: session.steeringMode,
|
|
2150
|
+
followUpMode: session.followUpMode,
|
|
2151
|
+
transport: sm.getTransport(),
|
|
2152
|
+
httpIdleTimeoutMs: sm.getHttpIdleTimeoutMs(),
|
|
2153
|
+
thinkingLevel: session.thinkingLevel,
|
|
2154
|
+
availableThinkingLevels: session.getAvailableThinkingLevels(),
|
|
2155
|
+
currentTheme: sm.getTheme() ?? '',
|
|
2156
|
+
// Light/dark appearance is a property of the terminal the VIEWER is attached
|
|
2157
|
+
// to; the headless broker has none, so emit a placeholder. The viewer owns
|
|
2158
|
+
// the theme submenu and resolves real terminal appearance locally.
|
|
2159
|
+
terminalTheme: 'dark',
|
|
2160
|
+
availableThemes,
|
|
2161
|
+
hideThinkingBlock: sm.getHideThinkingBlock(),
|
|
2162
|
+
collapseChangelog: sm.getCollapseChangelog(),
|
|
2163
|
+
enableInstallTelemetry: sm.getEnableInstallTelemetry(),
|
|
2164
|
+
doubleEscapeAction: sm.getDoubleEscapeAction(),
|
|
2165
|
+
treeFilterMode: sm.getTreeFilterMode(),
|
|
2166
|
+
showHardwareCursor: sm.getShowHardwareCursor(),
|
|
2167
|
+
editorPaddingX: sm.getEditorPaddingX(),
|
|
2168
|
+
autocompleteMaxVisible: sm.getAutocompleteMaxVisible(),
|
|
2169
|
+
quietStartup: sm.getQuietStartup(),
|
|
2170
|
+
clearOnShrink: sm.getClearOnShrink(),
|
|
2171
|
+
showTerminalProgress: sm.getShowTerminalProgress(),
|
|
2172
|
+
warnings: sm.getWarnings(),
|
|
2173
|
+
defaultProjectTrust: sm.getDefaultProjectTrust(),
|
|
2174
|
+
showCacheMissNotices: sm.getShowCacheMissNotices(),
|
|
2175
|
+
outputPad: sm.getOutputPad(),
|
|
2176
|
+
autoRetry: session.autoRetryEnabled,
|
|
2177
|
+
model: toModelRef(session.model),
|
|
2178
|
+
};
|
|
2179
|
+
return { type: 'data', id, kind: 'get_settings', settings };
|
|
2180
|
+
};
|
|
2181
|
+
// Cache per-session wire info keyed by (path, mtime, size) so reopening the
|
|
2182
|
+
// picker doesn't re-read the whole corpus (see session-list-cache.ts).
|
|
2183
|
+
const sessionListCache = new SessionListCache(sessionListCachePath());
|
|
2184
|
+
// Pre-warm the cwd scope (the picker's default tab) in the background so the
|
|
2185
|
+
// first /resume open on this fresh broker is already warm.
|
|
2186
|
+
sessionListCache.prewarm(session.sessionManager.getSessionDir());
|
|
2187
|
+
const buildSessionsData = async (id, scope) => {
|
|
2188
|
+
const sm = session.sessionManager;
|
|
2189
|
+
const sessionDir = sm.getSessionDir();
|
|
2190
|
+
const sessions = scope === 'all'
|
|
2191
|
+
? await sessionListCache.listAll(piSessionsRoot())
|
|
2192
|
+
: await sessionListCache.listDir(sessionDir);
|
|
2193
|
+
return {
|
|
2194
|
+
type: 'data',
|
|
2195
|
+
id,
|
|
2196
|
+
kind: 'list_sessions',
|
|
2197
|
+
scope,
|
|
2198
|
+
sessions,
|
|
2199
|
+
currentSessionFile: session.sessionFile,
|
|
2200
|
+
};
|
|
2201
|
+
};
|
|
2202
|
+
/** Runtime-mutating viewer replacements are serialized through their complete
|
|
2203
|
+
* rebind, re-welcome, and client settlement. Admission is bounded to at most
|
|
2204
|
+
* one active head plus one queued follow-up (`MAX_ADMITTED_REPLACEMENTS`); a
|
|
2205
|
+
* request over that cap is a definite, correlated rejection that never touches
|
|
2206
|
+
* the tail. Each admitted head gets one `REPLACEMENT_DEADLINE_MS` budget
|
|
2207
|
+
* covering the full runtime mutation through its awaited rebind. The SDK has
|
|
2208
|
+
* no cancellation primitive for these mutations, so a rejection or an expired
|
|
2209
|
+
* deadline after admission leaves runtime/shared state indeterminate — the
|
|
2210
|
+
* only truthful response is broker-terminal exit, never a retry, and the tail
|
|
2211
|
+
* never advances to the queued closure against that uncertain state. */
|
|
2212
|
+
let replacementTail = Promise.resolve();
|
|
2213
|
+
let admittedReplacements = 0;
|
|
2214
|
+
const runReplacement = (client, op, run) => {
|
|
2215
|
+
if (runtime === undefined) {
|
|
2216
|
+
sendFrame(client, {
|
|
2217
|
+
type: 'error',
|
|
2218
|
+
code: 'engine_error',
|
|
2219
|
+
message: 'session replacement unsupported at this engine pin',
|
|
2220
|
+
});
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if (admittedReplacements >= MAX_ADMITTED_REPLACEMENTS) {
|
|
2224
|
+
operationIdContext.fresh(() => {
|
|
2225
|
+
emitEvent({
|
|
2226
|
+
level: 'warn',
|
|
2227
|
+
event: 'broker.session_replacement.rejected',
|
|
2228
|
+
outcome: 'skipped',
|
|
2229
|
+
fields: {
|
|
2230
|
+
op,
|
|
2231
|
+
reason: 'capacity',
|
|
2232
|
+
admitted: MAX_ADMITTED_REPLACEMENTS,
|
|
2233
|
+
cap: MAX_ADMITTED_REPLACEMENTS,
|
|
2234
|
+
},
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
ackTo(client, op, false, `session replacement busy (${MAX_ADMITTED_REPLACEMENTS} active or queued)`);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
admittedReplacements++;
|
|
2241
|
+
const task = replacementTail.then(async () => {
|
|
2242
|
+
try {
|
|
2243
|
+
// The whole attempt — success, cancellation, rejection, and timeout —
|
|
2244
|
+
// executes inside one fresh operation scope so its outcome event (of
|
|
2245
|
+
// whichever kind fires) carries one coherent operation id.
|
|
2246
|
+
await operationIdContext.fresh(async () => {
|
|
2247
|
+
const startedAt = process.hrtime.bigint();
|
|
2248
|
+
let timer;
|
|
2249
|
+
try {
|
|
2250
|
+
const deadline = new Promise((resolve) => {
|
|
2251
|
+
timer = setTimeout(() => resolve(REPLACEMENT_DEADLINE_EXCEEDED), REPLACEMENT_DEADLINE_MS);
|
|
2252
|
+
});
|
|
2253
|
+
const outcome = await Promise.race([run(runtime), deadline]);
|
|
2254
|
+
if (outcome === REPLACEMENT_DEADLINE_EXCEEDED) {
|
|
2255
|
+
emitEvent({
|
|
2256
|
+
level: 'error',
|
|
2257
|
+
event: 'broker.session_replacement.timed_out',
|
|
2258
|
+
outcome: 'failed',
|
|
2259
|
+
duration_ms: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
|
|
2260
|
+
fields: { op, deadline_ms: REPLACEMENT_DEADLINE_MS, mutation_outcome: 'indeterminate' },
|
|
2261
|
+
});
|
|
2262
|
+
ackTo(client, op, false, 'session replacement timed out; broker restarting because session state is indeterminate');
|
|
2263
|
+
disposeAndExit('session-replacement-timeout', 1);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (!outcome.cancelled)
|
|
2267
|
+
reWelcomeAll();
|
|
2268
|
+
ackTo(client, op, !outcome.cancelled, outcome.cancelled ? outcome.detail ?? 'cancelled by extension' : undefined);
|
|
2269
|
+
}
|
|
2270
|
+
catch (err) {
|
|
2271
|
+
emitEvent({
|
|
2272
|
+
level: 'error',
|
|
2273
|
+
event: 'broker.session_replacement.failed',
|
|
2274
|
+
outcome: 'failed',
|
|
2275
|
+
error: err,
|
|
2276
|
+
fields: { op, mutation_outcome: 'indeterminate' },
|
|
2277
|
+
});
|
|
2278
|
+
ackTo(client, op, false, 'session replacement failed; broker restarting because session state is indeterminate');
|
|
2279
|
+
disposeAndExit('session-replacement-failed', 1);
|
|
2280
|
+
}
|
|
2281
|
+
finally {
|
|
2282
|
+
clearTimeout(timer);
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
finally {
|
|
2287
|
+
admittedReplacements--;
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
replacementTail = task.catch(() => undefined);
|
|
2291
|
+
};
|
|
2292
|
+
const handleFrame = (client, frame) => {
|
|
2293
|
+
switch (frame.type) {
|
|
2294
|
+
case 'hello': {
|
|
2295
|
+
// Admit at the REQUESTED role — unconditionally. `controller` is a
|
|
2296
|
+
// per-client write capability, not a singleton slot, so an arriving
|
|
2297
|
+
// writable client never demotes (or is demoted by) an already-attached
|
|
2298
|
+
// one: N terminal panes and browser tabs are all writable at once.
|
|
2299
|
+
//
|
|
2300
|
+
// A repeated hello on an already-helloed socket must not RE-role it: the
|
|
2301
|
+
// role is fixed for the socket's lifetime, so every downstream gate and
|
|
2302
|
+
// every already-fanned-out dialog keep the authority the first welcome
|
|
2303
|
+
// stated. A duplicate hello still re-sends the catch-up snapshot.
|
|
2304
|
+
const firstHello = !client.helloed;
|
|
2305
|
+
client.id = frame.client_id;
|
|
2306
|
+
if (firstHello) {
|
|
2307
|
+
client.helloed = true;
|
|
2308
|
+
client.role = frame.role === 'controller' ? 'controller' : 'observer';
|
|
2309
|
+
}
|
|
2310
|
+
sendWelcome(client);
|
|
2311
|
+
if (firstHello)
|
|
2312
|
+
persistAttachState(); // a helloed viewer arrived
|
|
2313
|
+
// welcome carried the FIRST pending dialog; replay any extras so a writable
|
|
2314
|
+
// client attaching mid-dialog can answer every in-flight dialog.
|
|
2315
|
+
if (client.role === 'controller')
|
|
2316
|
+
replayExtraPendingDialogsTo(client);
|
|
2317
|
+
break;
|
|
2318
|
+
}
|
|
2319
|
+
case 'prompt':
|
|
2320
|
+
case 'steer':
|
|
2321
|
+
case 'follow_up':
|
|
2322
|
+
case 'abort':
|
|
2323
|
+
case 'bash': {
|
|
2324
|
+
if (notWritable(client, 'drive the engine'))
|
|
2325
|
+
break;
|
|
2326
|
+
driveEngine(client, frame);
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
case 'deliver': {
|
|
2330
|
+
// Interactive delivery from a one-shot server client (crtrd's
|
|
2331
|
+
// `POST /messages` `delivery:'interactive'` path). Routed HERE,
|
|
2332
|
+
// broker-side, because only the broker authoritatively knows streaming
|
|
2333
|
+
// state: the synthesized `prompt` frame reuses resolveEngineRoute's m-B
|
|
2334
|
+
// routing (idle → prompt(); mid-stream → prompt({streamingBehavior:
|
|
2335
|
+
// 'steer'}), pi's own Enter-while-streaming submit) plus the memory-refs
|
|
2336
|
+
// guidance path — byte-identical behavior to a viewer's Enter. Acked
|
|
2337
|
+
// once ACCEPTED (correlated by the frame's id) so the daemon returns
|
|
2338
|
+
// deterministically; `detail` reports which route was taken.
|
|
2339
|
+
//
|
|
2340
|
+
// The ack reports the engine call's OUTCOME, never merely the fact that
|
|
2341
|
+
// a call was made: `session.prompt()` rejects outright when the model's
|
|
2342
|
+
// provider has no credential, and acking unconditionally reported
|
|
2343
|
+
// `delivered:true` for a message the engine had refused — the caller's
|
|
2344
|
+
// one-shot client then closed before the error frame arrived and the
|
|
2345
|
+
// message vanished with no trace. A prompt route therefore acks on the
|
|
2346
|
+
// race between acceptance (agent_start) and rejection; a steer route
|
|
2347
|
+
// joins a turn already running, so no new agent_start is coming and its
|
|
2348
|
+
// ack stays at routing time.
|
|
2349
|
+
if (notWritable(client, 'drive the engine', frame.id))
|
|
2350
|
+
break;
|
|
2351
|
+
const via = session.isStreaming ? 'steer' : 'prompt';
|
|
2352
|
+
let delivered = false;
|
|
2353
|
+
const ackAccepted = () => {
|
|
2354
|
+
if (delivered)
|
|
2355
|
+
return;
|
|
2356
|
+
delivered = true;
|
|
2357
|
+
ackTo(client, 'deliver', true, via, frame.id);
|
|
2358
|
+
};
|
|
2359
|
+
if (via === 'prompt')
|
|
2360
|
+
turnAcceptedWaiters.add(ackAccepted);
|
|
2361
|
+
const outcome = driveEngine(client, {
|
|
2362
|
+
type: 'prompt',
|
|
2363
|
+
text: frame.text,
|
|
2364
|
+
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
2365
|
+
});
|
|
2366
|
+
if (via === 'steer' || outcome === undefined) {
|
|
2367
|
+
turnAcceptedWaiters.delete(ackAccepted);
|
|
2368
|
+
ackAccepted();
|
|
2369
|
+
break;
|
|
2370
|
+
}
|
|
2371
|
+
void outcome.then((failure) => {
|
|
2372
|
+
turnAcceptedWaiters.delete(ackAccepted);
|
|
2373
|
+
if (failure === undefined) {
|
|
2374
|
+
ackAccepted();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
// The error frame already went to the client, but a one-shot deliver
|
|
2378
|
+
// client is gone by then — record it where it survives.
|
|
2379
|
+
emitEvent({ level: 'error', event: 'broker.prompt.deliver_failed', error: failure });
|
|
2380
|
+
if (delivered)
|
|
2381
|
+
return;
|
|
2382
|
+
delivered = true;
|
|
2383
|
+
ackTo(client, 'deliver', false, String(failure), frame.id);
|
|
2384
|
+
});
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
case 'extension_ui_response': {
|
|
2388
|
+
if (notWritable(client, 'answer dialogs'))
|
|
2389
|
+
break;
|
|
2390
|
+
// First response for this id wins: `resolve` settles the entry and deletes
|
|
2391
|
+
// it, so every later response from a peer that was also fanned this dialog
|
|
2392
|
+
// finds nothing here and is a silent no-op.
|
|
2393
|
+
pendingDialogs.get(frame.id)?.resolve(frame);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
// --- extended engine-command ops (T3, §1.2 floor set) ------------------
|
|
2397
|
+
case 'set_model': {
|
|
2398
|
+
if (notWritable(client, 'set the model'))
|
|
2399
|
+
break;
|
|
2400
|
+
const requested = parseModelSpec(frame.model);
|
|
2401
|
+
let model;
|
|
2402
|
+
let normalized;
|
|
2403
|
+
try {
|
|
2404
|
+
// Tier/alias specs (`opus`, `strong`, `anthropic/strong`, …) MUST resolve
|
|
2405
|
+
// through the config ladder before touching the registry: the fuzzy
|
|
2406
|
+
// registry search rank-ties `opus` between claude-3-opus-20240229 and
|
|
2407
|
+
// claude-opus-4-8, and the deprecated 2024 model wins on catalog order.
|
|
2408
|
+
// Concrete `provider/id` specs and genuine substrings (`fable`) pass
|
|
2409
|
+
// through normalizeModel unchanged and take the exact/search path below.
|
|
2410
|
+
normalized = parseModelSpec(normalizeModel(requested.modelSpec));
|
|
2411
|
+
model = resolveModelQuery(normalized.modelSpec);
|
|
2412
|
+
}
|
|
2413
|
+
catch (err) {
|
|
2414
|
+
// N2: registry.find should never throw on the real SDK, but a degenerate
|
|
2415
|
+
// engine must still get a reply rather than a silently-dropped frame.
|
|
2416
|
+
engineErrorTo(client)(err);
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
if (model === undefined) {
|
|
2420
|
+
sendFrame(client, {
|
|
2421
|
+
type: 'error',
|
|
2422
|
+
code: 'engine_error',
|
|
2423
|
+
message: `no model matching '${frame.model}' in the registry`,
|
|
2424
|
+
});
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
void session
|
|
2428
|
+
.setModel(model)
|
|
2429
|
+
.then(() => {
|
|
2430
|
+
// The caller's explicit `:thinking` wins; else the ladder cell's.
|
|
2431
|
+
const thinking = requested.thinkingLevel ?? normalized.thinkingLevel;
|
|
2432
|
+
if (thinking !== undefined)
|
|
2433
|
+
session.setThinkingLevel(thinking);
|
|
2434
|
+
ackTo(client, 'set_model');
|
|
2435
|
+
// frame.pinned, when the sender pre-normalized `frame.model` (e.g.
|
|
2436
|
+
// setModelLive), carries the RAW-token pin decision explicitly.
|
|
2437
|
+
// Otherwise frame.model itself IS the raw/explicit token (a
|
|
2438
|
+
// slash-command arg, or a concrete picker pick) and derives it.
|
|
2439
|
+
broadcastModelChanged(true, frame.pinned !== undefined ? frame.pinned : isProviderPinnedModelToken(frame.model));
|
|
2440
|
+
})
|
|
2441
|
+
.catch(engineErrorTo(client));
|
|
2442
|
+
break;
|
|
2443
|
+
}
|
|
2444
|
+
case 'deliver_custom_message': {
|
|
2445
|
+
if (notWritable(client, 'deliver a custom message'))
|
|
2446
|
+
break;
|
|
2447
|
+
// Never triggers a turn either way. `nextTurn` folds the message into
|
|
2448
|
+
// whatever turn comes next (situational context); omitting it pushes
|
|
2449
|
+
// straight onto the message list so it lands FIRST in a still-empty
|
|
2450
|
+
// session (a freshly-claimed spare's bearings). The inbox watcher's own
|
|
2451
|
+
// situational send uses `followUp` + triggerTurn because there its job
|
|
2452
|
+
// IS to wake the node.
|
|
2453
|
+
void session
|
|
2454
|
+
.sendCustomMessage({
|
|
2455
|
+
customType: frame.customType,
|
|
2456
|
+
content: frame.content,
|
|
2457
|
+
display: true,
|
|
2458
|
+
...(frame.details !== undefined ? { details: frame.details } : {}),
|
|
2459
|
+
}, ...(frame.deliverAs !== undefined ? [{ deliverAs: frame.deliverAs }] : []))
|
|
2460
|
+
.then(() => ackTo(client, 'deliver_custom_message'))
|
|
2461
|
+
.catch(engineErrorTo(client));
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
case 'cycle_model': {
|
|
2465
|
+
if (notWritable(client, 'cycle the model'))
|
|
2466
|
+
break;
|
|
2467
|
+
void session
|
|
2468
|
+
.cycleModel(frame.direction)
|
|
2469
|
+
.then(() => {
|
|
2470
|
+
ackTo(client, 'cycle_model');
|
|
2471
|
+
broadcastModelChanged();
|
|
2472
|
+
})
|
|
2473
|
+
.catch(engineErrorTo(client));
|
|
2474
|
+
break;
|
|
2475
|
+
}
|
|
2476
|
+
case 'cycle_ladder': {
|
|
2477
|
+
if (notWritable(client, 'cycle the model ladder'))
|
|
2478
|
+
break;
|
|
2479
|
+
// Resolve the next interleaved-ladder rung from the current model+thinking
|
|
2480
|
+
// spec, then reuse the set_model path (registry resolve + thinking apply).
|
|
2481
|
+
// The cycle only offers signed-in provider columns — same rule as the
|
|
2482
|
+
// setup wizard's Models panel: pool occupancy for a managed provider
|
|
2483
|
+
// (read live, so a login that landed mid-session counts immediately),
|
|
2484
|
+
// else the registry's auth status. When nobody is signed in the helper
|
|
2485
|
+
// returns every provider, so the cycle stays walkable pre-login.
|
|
2486
|
+
const offeredProviders = (() => {
|
|
2487
|
+
try {
|
|
2488
|
+
const registry = registryOf(services);
|
|
2489
|
+
return configuredLadderProviders((providerId) => (isManagedProvider(providerId) && readSubscriptionPool(providerId).length > 0)
|
|
2490
|
+
|| registry.getProviderAuthStatus(providerId).configured);
|
|
2491
|
+
}
|
|
2492
|
+
catch {
|
|
2493
|
+
return undefined; // auth state unreadable — cycle the full ladder
|
|
2494
|
+
}
|
|
2495
|
+
})();
|
|
2496
|
+
const currentSpec = formatModelSpec(session.model, session.thinkingLevel) ?? '';
|
|
2497
|
+
const nextSpec = parseModelSpec(nextLadderModel(currentSpec, frame.direction === 'backward' ? -1 : 1, offeredProviders));
|
|
2498
|
+
let ladderModel;
|
|
2499
|
+
try {
|
|
2500
|
+
ladderModel = resolveModelQuery(nextSpec.modelSpec);
|
|
2501
|
+
}
|
|
2502
|
+
catch (err) {
|
|
2503
|
+
engineErrorTo(client)(err);
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
if (ladderModel === undefined) {
|
|
2507
|
+
sendFrame(client, {
|
|
2508
|
+
type: 'error',
|
|
2509
|
+
code: 'engine_error',
|
|
2510
|
+
message: `ladder model '${nextSpec.modelSpec}' not in the registry`,
|
|
2511
|
+
});
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
void session
|
|
2515
|
+
.setModel(ladderModel)
|
|
2516
|
+
.then(() => {
|
|
2517
|
+
if (nextSpec.thinkingLevel !== undefined)
|
|
2518
|
+
session.setThinkingLevel(nextSpec.thinkingLevel);
|
|
2519
|
+
ackTo(client, 'cycle_ladder');
|
|
2520
|
+
broadcastModelChanged();
|
|
2521
|
+
})
|
|
2522
|
+
.catch(engineErrorTo(client));
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
case 'cycle_thinking': {
|
|
2526
|
+
if (notWritable(client, 'cycle the thinking level'))
|
|
2527
|
+
break;
|
|
2528
|
+
try {
|
|
2529
|
+
session.cycleThinkingLevel();
|
|
2530
|
+
ackTo(client, 'cycle_thinking');
|
|
2531
|
+
}
|
|
2532
|
+
catch (err) {
|
|
2533
|
+
engineErrorTo(client)(err);
|
|
2534
|
+
}
|
|
2535
|
+
break;
|
|
2536
|
+
}
|
|
2537
|
+
case 'dequeue': {
|
|
2538
|
+
if (notWritable(client, 'dequeue messages', frame.id))
|
|
2539
|
+
break;
|
|
2540
|
+
try {
|
|
2541
|
+
const { steering, followUp } = session.clearQueue();
|
|
2542
|
+
sendFrame(client, { type: 'data', id: frame.id, kind: 'dequeue', steering, followUp });
|
|
2543
|
+
}
|
|
2544
|
+
catch (err) {
|
|
2545
|
+
engineErrorTo(client, frame.id)(err);
|
|
2546
|
+
}
|
|
2547
|
+
break;
|
|
2548
|
+
}
|
|
2549
|
+
case 'set_thinking_level': {
|
|
2550
|
+
if (notWritable(client, 'set the thinking level'))
|
|
2551
|
+
break;
|
|
2552
|
+
try {
|
|
2553
|
+
session.setThinkingLevel(frame.level);
|
|
2554
|
+
ackTo(client, 'set_thinking_level');
|
|
2555
|
+
}
|
|
2556
|
+
catch (err) {
|
|
2557
|
+
engineErrorTo(client)(err);
|
|
2558
|
+
}
|
|
2559
|
+
break;
|
|
2560
|
+
}
|
|
2561
|
+
case 'set_auto_retry': {
|
|
2562
|
+
if (notWritable(client, 'set auto-retry'))
|
|
2563
|
+
break;
|
|
2564
|
+
try {
|
|
2565
|
+
session.setAutoRetryEnabled(frame.enabled);
|
|
2566
|
+
ackTo(client, 'set_auto_retry');
|
|
2567
|
+
}
|
|
2568
|
+
catch (err) {
|
|
2569
|
+
engineErrorTo(client)(err);
|
|
2570
|
+
}
|
|
2571
|
+
break;
|
|
2572
|
+
}
|
|
2573
|
+
case 'set_auto_compaction': {
|
|
2574
|
+
if (notWritable(client, 'set auto-compaction'))
|
|
2575
|
+
break;
|
|
2576
|
+
try {
|
|
2577
|
+
session.setAutoCompactionEnabled(frame.enabled);
|
|
2578
|
+
ackTo(client, 'set_auto_compaction');
|
|
2579
|
+
}
|
|
2580
|
+
catch (err) {
|
|
2581
|
+
engineErrorTo(client)(err);
|
|
2582
|
+
}
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
case 'compact': {
|
|
2586
|
+
if (notWritable(client, 'compact the session'))
|
|
2587
|
+
break;
|
|
2588
|
+
void session
|
|
2589
|
+
.compact(frame.instructions)
|
|
2590
|
+
.then(() => ackTo(client, 'compact'))
|
|
2591
|
+
.catch(engineErrorTo(client));
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
case 'set_session_name': {
|
|
2595
|
+
if (notWritable(client, 'rename the session'))
|
|
2596
|
+
break;
|
|
2597
|
+
try {
|
|
2598
|
+
session.setSessionName(frame.name);
|
|
2599
|
+
ackTo(client, 'set_session_name');
|
|
2600
|
+
}
|
|
2601
|
+
catch (err) {
|
|
2602
|
+
engineErrorTo(client)(err);
|
|
2603
|
+
}
|
|
2604
|
+
break;
|
|
2605
|
+
}
|
|
2606
|
+
case 'get_commands': {
|
|
2607
|
+
// OBSERVERS may call this: the merged command inventory is static
|
|
2608
|
+
// (extensions + templates + skills + builtins) and drives nothing in the
|
|
2609
|
+
// engine, so it is not a controller-gated op. The web bridge fetches it on
|
|
2610
|
+
// its observer-by-default upstream connection to populate the palette.
|
|
2611
|
+
// The merged command list rides in ack.detail as JSON (the foundation's
|
|
2612
|
+
// AckFrame.detail field) — the viewer (T6) JSON.parses it when for ===
|
|
2613
|
+
// 'get_commands'. Keeps every command op a uniform ack reply.
|
|
2614
|
+
try {
|
|
2615
|
+
ackTo(client, 'get_commands', true, JSON.stringify(buildCommandList()));
|
|
2616
|
+
}
|
|
2617
|
+
catch (err) {
|
|
2618
|
+
engineErrorTo(client)(err);
|
|
2619
|
+
}
|
|
2620
|
+
break;
|
|
2621
|
+
}
|
|
2622
|
+
// --- read-op picker data (§5 Unit A) ----------------------------------
|
|
2623
|
+
// NOT controller-gated: read-only, like get_commands. The web bridge's
|
|
2624
|
+
// observer connection populates pickers through these too.
|
|
2625
|
+
case 'list_models': {
|
|
2626
|
+
try {
|
|
2627
|
+
sendFrame(client, buildListModelsData(frame.id));
|
|
2628
|
+
}
|
|
2629
|
+
catch (err) {
|
|
2630
|
+
engineErrorTo(client, frame.id)(err);
|
|
2631
|
+
}
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
case 'list_scoped_models': {
|
|
2635
|
+
try {
|
|
2636
|
+
sendFrame(client, buildScopedModelsData(frame.id));
|
|
2637
|
+
}
|
|
2638
|
+
catch (err) {
|
|
2639
|
+
engineErrorTo(client, frame.id)(err);
|
|
2640
|
+
}
|
|
2641
|
+
break;
|
|
2642
|
+
}
|
|
2643
|
+
case 'list_memory_refs': {
|
|
2644
|
+
try {
|
|
2645
|
+
sendFrame(client, buildListMemoryRefsData(frame.id));
|
|
2646
|
+
}
|
|
2647
|
+
catch (err) {
|
|
2648
|
+
engineErrorTo(client, frame.id)(err);
|
|
2649
|
+
}
|
|
2650
|
+
break;
|
|
2651
|
+
}
|
|
2652
|
+
case 'get_tree': {
|
|
2653
|
+
try {
|
|
2654
|
+
sendFrame(client, buildGetTreeData(frame.id));
|
|
2655
|
+
}
|
|
2656
|
+
catch (err) {
|
|
2657
|
+
engineErrorTo(client, frame.id)(err);
|
|
2658
|
+
}
|
|
2659
|
+
break;
|
|
2660
|
+
}
|
|
2661
|
+
case 'get_settings': {
|
|
2662
|
+
try {
|
|
2663
|
+
sendFrame(client, buildSettingsData(frame.id));
|
|
2664
|
+
}
|
|
2665
|
+
catch (err) {
|
|
2666
|
+
engineErrorTo(client, frame.id)(err);
|
|
2667
|
+
}
|
|
2668
|
+
break;
|
|
2669
|
+
}
|
|
2670
|
+
case 'list_sessions': {
|
|
2671
|
+
// Session listing reads the session dir off disk — async; reply when it
|
|
2672
|
+
// resolves (or relay the error, correlated by id so the viewer doesn't hang).
|
|
2673
|
+
void buildSessionsData(frame.id, frame.scope ?? 'cwd')
|
|
2674
|
+
.then((d) => sendFrame(client, d))
|
|
2675
|
+
.catch(engineErrorTo(client, frame.id));
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
case 'get_human_fork_coordinates': {
|
|
2679
|
+
// NOT controller-gated: this is a pure observer read. Branch creation is
|
|
2680
|
+
// intentionally delegated to a detached SDK manager in the ask publisher;
|
|
2681
|
+
// calling createBranchedSession here corrupts this live manager's identity.
|
|
2682
|
+
try {
|
|
2683
|
+
const sm = session.sessionManager;
|
|
2684
|
+
const sourceFile = sm.getSessionFile();
|
|
2685
|
+
const leafId = sm.getLeafId();
|
|
2686
|
+
if (sourceFile === undefined || sourceFile === '' || leafId === null) {
|
|
2687
|
+
engineErrorTo(client, frame.id)(new Error('no persisted source session leaf'));
|
|
2688
|
+
break;
|
|
2689
|
+
}
|
|
2690
|
+
sendFrame(client, {
|
|
2691
|
+
type: 'data',
|
|
2692
|
+
id: frame.id,
|
|
2693
|
+
kind: 'human_fork_coordinates',
|
|
2694
|
+
sourceFile,
|
|
2695
|
+
sourceSessionId: sm.getSessionId(),
|
|
2696
|
+
leafId,
|
|
2697
|
+
modelSpec: formatExactModelSpec(session.model, session.thinkingLevel),
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
catch (err) {
|
|
2701
|
+
engineErrorTo(client, frame.id)(err);
|
|
2702
|
+
}
|
|
2703
|
+
break;
|
|
2704
|
+
}
|
|
2705
|
+
case 'navigate_tree': {
|
|
2706
|
+
if (notWritable(client, 'navigate the session tree'))
|
|
2707
|
+
break;
|
|
2708
|
+
// navigateTree rewinds IN-PLACE (same session file, new leaf) and emits no
|
|
2709
|
+
// relayed event, so every viewer must be re-snapshotted onto the rewound
|
|
2710
|
+
// transcript — same reWelcomeAll the session-replacing ops use. The ack's
|
|
2711
|
+
// detail carries the navigated-to user message's text (pi parity: the
|
|
2712
|
+
// interactive tree navigator restores it to the editor for re-editing).
|
|
2713
|
+
void session
|
|
2714
|
+
.navigateTree(frame.targetId, frame.options)
|
|
2715
|
+
.then((r) => {
|
|
2716
|
+
if (!r.cancelled)
|
|
2717
|
+
reWelcomeAll();
|
|
2718
|
+
ackTo(client, 'navigate_tree', !r.cancelled, r.editorText);
|
|
2719
|
+
})
|
|
2720
|
+
.catch(engineErrorTo(client));
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
case 'reload': {
|
|
2724
|
+
if (notWritable(client, 'reload'))
|
|
2725
|
+
break;
|
|
2726
|
+
// A SUCCESSFUL reload invalidates the memoized ref inventory (work item
|
|
2727
|
+
// 3) so the next read-op/submission re-walks the corpus, AND clears the
|
|
2728
|
+
// shared substrate parse cache buildRefInventory reads through —
|
|
2729
|
+
// otherwise a fresh buildRefInventory() call would still see the stale
|
|
2730
|
+
// cached docs. A FAILED reload (`.catch`) leaves both untouched: nothing
|
|
2731
|
+
// about the corpus is known to have changed.
|
|
2732
|
+
void session
|
|
2733
|
+
.reload()
|
|
2734
|
+
.then(() => {
|
|
2735
|
+
memoizedRefInventory = null;
|
|
2736
|
+
clearSessionCache();
|
|
2737
|
+
ackTo(client, 'reload');
|
|
2738
|
+
})
|
|
2739
|
+
.catch(engineErrorTo(client));
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
case 'export': {
|
|
2743
|
+
if (notWritable(client, 'export the session'))
|
|
2744
|
+
break;
|
|
2745
|
+
if (frame.format === 'jsonl') {
|
|
2746
|
+
try {
|
|
2747
|
+
const outPath = session.exportToJsonl(frame.path);
|
|
2748
|
+
stripHiddenEntriesFromJsonlExport(outPath);
|
|
2749
|
+
ackTo(client, 'export');
|
|
2750
|
+
}
|
|
2751
|
+
catch (err) {
|
|
2752
|
+
engineErrorTo(client)(err);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
void session
|
|
2757
|
+
.exportToHtml(frame.path)
|
|
2758
|
+
.then(() => ackTo(client, 'export'))
|
|
2759
|
+
.catch(engineErrorTo(client));
|
|
2760
|
+
}
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2763
|
+
case 'new_session': {
|
|
2764
|
+
if (notWritable(client, 'start a new session'))
|
|
2765
|
+
break;
|
|
2766
|
+
runReplacement(client, 'new_session', (rt) => rt.newSession());
|
|
2767
|
+
break;
|
|
2768
|
+
}
|
|
2769
|
+
case 'switch_session': {
|
|
2770
|
+
if (notWritable(client, 'switch sessions'))
|
|
2771
|
+
break;
|
|
2772
|
+
runReplacement(client, 'switch_session', (rt) => rt.switchSession(frame.path));
|
|
2773
|
+
break;
|
|
2774
|
+
}
|
|
2775
|
+
case 'fork': {
|
|
2776
|
+
if (notWritable(client, 'fork the session'))
|
|
2777
|
+
break;
|
|
2778
|
+
runReplacement(client, 'fork', (rt) => rt.fork(frame.entryId));
|
|
2779
|
+
break;
|
|
2780
|
+
}
|
|
2781
|
+
case 'clone': {
|
|
2782
|
+
if (notWritable(client, 'clone the session'))
|
|
2783
|
+
break;
|
|
2784
|
+
runReplacement(client, 'clone', async (rt) => {
|
|
2785
|
+
const sm = session.sessionManager;
|
|
2786
|
+
const leafId = sm.getLeafId();
|
|
2787
|
+
if (leafId === null) {
|
|
2788
|
+
return { cancelled: true, detail: 'no current leaf to clone from' };
|
|
2789
|
+
}
|
|
2790
|
+
const newPath = sm.createBranchedSession(leafId);
|
|
2791
|
+
if (newPath === undefined) {
|
|
2792
|
+
return { cancelled: true, detail: 'createBranchedSession returned no path' };
|
|
2793
|
+
}
|
|
2794
|
+
return rt.switchSession(newPath);
|
|
2795
|
+
});
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
case 'share': {
|
|
2799
|
+
if (notWritable(client, 'share the session'))
|
|
2800
|
+
break;
|
|
2801
|
+
const tmpPath = join(tmpdir(), `pi-share-${Date.now()}.html`);
|
|
2802
|
+
void session.exportToHtml(tmpPath)
|
|
2803
|
+
.then(() => new Promise((resolve, reject) => {
|
|
2804
|
+
execFile('gh', ['gist', 'create', '--secret', tmpPath], { timeout: 30_000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
2805
|
+
try {
|
|
2806
|
+
if (existsSync(tmpPath))
|
|
2807
|
+
unlinkSync(tmpPath);
|
|
2808
|
+
}
|
|
2809
|
+
catch { /* cleanup */ }
|
|
2810
|
+
if (err) {
|
|
2811
|
+
reject(err);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
resolve(stdout.trim());
|
|
2815
|
+
});
|
|
2816
|
+
}))
|
|
2817
|
+
.then((url) => {
|
|
2818
|
+
ackTo(client, 'share', true, url);
|
|
2819
|
+
})
|
|
2820
|
+
.catch((err) => {
|
|
2821
|
+
const msg = String(err?.message ?? err);
|
|
2822
|
+
ackTo(client, 'share', false, msg.includes('not found') || msg.includes('gh: command not found') || msg.includes('ENOENT')
|
|
2823
|
+
? '`gh` CLI not found — install GitHub CLI and authenticate with `gh auth login`'
|
|
2824
|
+
: `share failed: ${msg}`);
|
|
2825
|
+
});
|
|
2826
|
+
break;
|
|
2827
|
+
}
|
|
2828
|
+
case 'reload_auth': {
|
|
2829
|
+
// Open to any client, observers included: reload_auth is an idempotent
|
|
2830
|
+
// local re-read of the shared auth.json + a model-registry refresh. It
|
|
2831
|
+
// does NOT steer the conversation, so there is nothing to role-gate —
|
|
2832
|
+
// which is what lets the daemon's canvas-wide fan (one /login propagated
|
|
2833
|
+
// to every live broker) ride an observer connection.
|
|
2834
|
+
try {
|
|
2835
|
+
// pi 0.82: crouter's CredentialStore reads auth.json fresh under the
|
|
2836
|
+
// lock on every call, so there is no cached credential view left to
|
|
2837
|
+
// reload — only the runtime's models/availability snapshot has to be
|
|
2838
|
+
// recomputed, and that is asynchronous. Every consumer below awaits
|
|
2839
|
+
// the same promise so none of them reads a pre-login snapshot.
|
|
2840
|
+
const authRefreshed = registryOf(services).refresh().catch((err) => {
|
|
2841
|
+
emitEvent({ level: 'warn', event: 'broker.auth_reload.refresh_failed', error: err });
|
|
2842
|
+
});
|
|
2843
|
+
ackTo(client, 'reload_auth');
|
|
2844
|
+
void authRefreshed.then(() => broadcastModelChanged());
|
|
2845
|
+
// Tier 3 re-drive: a model-less boot (no viable candidate at launch)
|
|
2846
|
+
// stays live so the user can /login; once auth actually changes,
|
|
2847
|
+
// re-run the SAME launch policy against the now-refreshed services
|
|
2848
|
+
// and, if a model now resolves, adopt it and fire the deferred
|
|
2849
|
+
// kickoff. handleFrame is not async (broker.ts) so this runs as a
|
|
2850
|
+
// detached IIFE, the same async-in-sync-switch pattern used above
|
|
2851
|
+
// for provider/not-found fallback.
|
|
2852
|
+
const authGeneration = installedGeneration;
|
|
2853
|
+
if (sessionHasNoModel() && authGeneration !== undefined && reloadAuthInFlight === null) {
|
|
2854
|
+
const authSession = session;
|
|
2855
|
+
const authServices = services;
|
|
2856
|
+
reloadAuthInFlight = authGeneration;
|
|
2857
|
+
void (async () => {
|
|
2858
|
+
try {
|
|
2859
|
+
await authRefreshed;
|
|
2860
|
+
const resolved = await resolveLaunchModel(authServices, cfg);
|
|
2861
|
+
if (resolved !== undefined) {
|
|
2862
|
+
await authSession.setModel(resolved.model);
|
|
2863
|
+
if (installedGeneration !== authGeneration || session !== authSession)
|
|
2864
|
+
return;
|
|
2865
|
+
if (resolved.thinkingLevel !== undefined)
|
|
2866
|
+
authSession.setThinkingLevel(resolved.thinkingLevel);
|
|
2867
|
+
broadcastModelChanged();
|
|
2868
|
+
drivePendingKickoff();
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
catch (err) {
|
|
2872
|
+
emitEvent({
|
|
2873
|
+
level: 'error',
|
|
2874
|
+
event: 'broker.auth_reload.redrive_failed',
|
|
2875
|
+
error: err,
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
finally {
|
|
2879
|
+
if (reloadAuthInFlight === authGeneration)
|
|
2880
|
+
reloadAuthInFlight = null;
|
|
2881
|
+
}
|
|
2882
|
+
})();
|
|
2883
|
+
}
|
|
2884
|
+
const authFault = readFault(nodeId);
|
|
2885
|
+
const authFaultGeneration = installedGeneration;
|
|
2886
|
+
const authFaultSession = session;
|
|
2887
|
+
if (authFault?.link === 'pi→provider'
|
|
2888
|
+
&& authFault.kind === 'auth'
|
|
2889
|
+
&& !authFaultSession.isStreaming
|
|
2890
|
+
&& !authFaultRedriveInFlight) {
|
|
2891
|
+
authFaultRedriveInFlight = true;
|
|
2892
|
+
void (async () => {
|
|
2893
|
+
try {
|
|
2894
|
+
const anchorEntryId = authFault.anchorEntryId;
|
|
2895
|
+
if (anchorEntryId !== undefined) {
|
|
2896
|
+
try {
|
|
2897
|
+
const leafId = authFaultSession.sessionManager.getLeafId?.() ?? null;
|
|
2898
|
+
if (leafId !== anchorEntryId)
|
|
2899
|
+
await authFaultSession.navigateTree(anchorEntryId);
|
|
2900
|
+
if (installedGeneration !== authFaultGeneration || session !== authFaultSession)
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
catch (err) {
|
|
2904
|
+
emitEvent({ level: 'warn', event: 'broker.auth_fault.rewind_skipped', error: err });
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
const fresh = readFault(nodeId);
|
|
2908
|
+
if (installedGeneration !== authFaultGeneration
|
|
2909
|
+
|| session !== authFaultSession
|
|
2910
|
+
|| authFaultSession.isStreaming
|
|
2911
|
+
|| fresh?.link !== 'pi→provider'
|
|
2912
|
+
|| fresh.kind !== 'auth'
|
|
2913
|
+
|| fresh.since !== authFault.since)
|
|
2914
|
+
return;
|
|
2915
|
+
clearFault(nodeId, { link: 'pi→provider' });
|
|
2916
|
+
await authFaultSession.prompt(AUTH_FAULT_RECOVERY_BODY);
|
|
2917
|
+
}
|
|
2918
|
+
catch (err) {
|
|
2919
|
+
emitEvent({ level: 'error', event: 'broker.auth_fault.redrive_failed', error: err });
|
|
2920
|
+
}
|
|
2921
|
+
finally {
|
|
2922
|
+
authFaultRedriveInFlight = false;
|
|
2923
|
+
}
|
|
2924
|
+
})();
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
catch (err) {
|
|
2928
|
+
engineErrorTo(client)(err);
|
|
2929
|
+
}
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2932
|
+
case 'bye':
|
|
2933
|
+
client.socket.end();
|
|
2934
|
+
break;
|
|
2935
|
+
case 'shutdown':
|
|
2936
|
+
disposeAndExit('shutdown-frame');
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
// -------------------------------------------------------------------------
|
|
2941
|
+
// 4 (plan): the socket listener — unlink any stale socket first.
|
|
2942
|
+
// -------------------------------------------------------------------------
|
|
2943
|
+
try {
|
|
2944
|
+
if (existsSync(sockPath))
|
|
2945
|
+
unlinkSync(sockPath);
|
|
2946
|
+
}
|
|
2947
|
+
catch {
|
|
2948
|
+
/* a fresh listen below will surface a real problem */
|
|
2949
|
+
}
|
|
2950
|
+
server = createServer((socket) => {
|
|
2951
|
+
const client = {
|
|
2952
|
+
id: '',
|
|
2953
|
+
role: 'observer',
|
|
2954
|
+
socket,
|
|
2955
|
+
decoder: new FrameDecoder(BROKER_READ_CAPS),
|
|
2956
|
+
helloed: false,
|
|
2957
|
+
pendingBytes: 0,
|
|
2958
|
+
queuedFrames: 0,
|
|
2959
|
+
};
|
|
2960
|
+
clients.add(client);
|
|
2961
|
+
socket.on('data', (chunk) => {
|
|
2962
|
+
let frames;
|
|
2963
|
+
try {
|
|
2964
|
+
frames = client.decoder.push(chunk);
|
|
2965
|
+
}
|
|
2966
|
+
catch (err) {
|
|
2967
|
+
// G7: a client frame over the bounded decoder caps (FrameOverflowError) is
|
|
2968
|
+
// cap-and-dropped — best-effort error frame, then destroy the peer. The
|
|
2969
|
+
// broker survives. The try/catch is around push() itself (not just the
|
|
2970
|
+
// per-frame loop) because push() throws BEFORE returning any frames.
|
|
2971
|
+
if (err instanceof FrameOverflowError) {
|
|
2972
|
+
sendFrame(client, { type: 'error', code: 'frame_overflow', message: err.message });
|
|
2973
|
+
dropClient(client, `frame overflow: ${err.message}`);
|
|
2974
|
+
}
|
|
2975
|
+
else {
|
|
2976
|
+
dropClient(client, `decoder error: ${String(err)}`);
|
|
2977
|
+
}
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
for (const raw of frames) {
|
|
2981
|
+
try {
|
|
2982
|
+
handleFrame(client, raw);
|
|
2983
|
+
}
|
|
2984
|
+
catch {
|
|
2985
|
+
/* one bad frame never crashes the broker */
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2989
|
+
const drop = () => {
|
|
2990
|
+
clients.delete(client);
|
|
2991
|
+
persistAttachState(); // a viewer disconnected
|
|
2992
|
+
// A writable client detaching does NOT cancel in-flight dialogs — they stay
|
|
2993
|
+
// pending under the broker-side default timeout, so a peer that is still
|
|
2994
|
+
// attached (or one that attaches next and gets the replay) can still answer.
|
|
2995
|
+
// Only an answer, the timeout, or an abort resolves one.
|
|
2996
|
+
};
|
|
2997
|
+
socket.on('close', drop);
|
|
2998
|
+
socket.on('error', () => {
|
|
2999
|
+
/* close follows; drop there */
|
|
3000
|
+
});
|
|
3001
|
+
});
|
|
3002
|
+
server.on('error', (err) => {
|
|
3003
|
+
// A recursive error raised during/after disposeAndExit's own server.close()
|
|
3004
|
+
// (or any other exit already in flight) must not emit a second canonical
|
|
3005
|
+
// failure or dispose twice.
|
|
3006
|
+
if (disposed)
|
|
3007
|
+
return;
|
|
3008
|
+
emitEvent({ level: 'error', event: 'broker.socket.failed', error: err });
|
|
3009
|
+
disposeAndExit('socket-error', 1);
|
|
3010
|
+
});
|
|
3011
|
+
server.listen(sockPath, () => {
|
|
3012
|
+
// A launch prompt delivers only after the listener is accepting, so a
|
|
3013
|
+
// root-spawn readiness proof and prompt admission share one boundary. This
|
|
3014
|
+
// includes the one continuation carried by a clean runtime-abort resume.
|
|
3015
|
+
// A model-less tier-3 boot (session.model === undefined) leaves the prompt
|
|
3016
|
+
// pending in kickoffPending; drivePendingKickoff is a no-op until the
|
|
3017
|
+
// reload_auth re-drive resolves a model, at which point it fires. For the
|
|
3018
|
+
// common model-present path this fires immediately — unchanged behavior.
|
|
3019
|
+
if (cfg.firstPrompt !== undefined && cfg.firstPrompt !== '') {
|
|
3020
|
+
kickoffPending = cfg.firstPrompt;
|
|
3021
|
+
drivePendingKickoff();
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
// Initialize presence to the truthful zero state at boot — this also
|
|
3025
|
+
// overwrites a stale attach.json a crashed prior incarnation left behind.
|
|
3026
|
+
persistAttachState();
|
|
3027
|
+
// A daemon fleet bounce gives a busy Pi a short, bounded chance to persist
|
|
3028
|
+
// its aborted turn before teardown. `session.abort()` resolves only after Pi
|
|
3029
|
+
// reaches idle (and thus after its message_end persistence); only then may we
|
|
3030
|
+
// leave the durable clean-abort certificate that changes recovery to resume.
|
|
3031
|
+
// A timeout/rejection deliberately leaves no certificate, preserving dirty
|
|
3032
|
+
// crash → recovery-cycle behavior and the fleet's shared SIGKILL deadline.
|
|
3033
|
+
const ABORT_BEFORE_EXIT_MS = 2_500;
|
|
3034
|
+
let signalExitInFlight = false;
|
|
3035
|
+
const abortAndExit = async (reason) => {
|
|
3036
|
+
if (disposed || signalExitInFlight)
|
|
3037
|
+
return;
|
|
3038
|
+
signalExitInFlight = true;
|
|
3039
|
+
const busy = agentRunActive || session.isStreaming || pendingPromptDispatches > 0;
|
|
3040
|
+
if (busy) {
|
|
3041
|
+
const deadline = Date.now() + ABORT_BEFORE_EXIT_MS;
|
|
3042
|
+
// prompt() has a real async pre-turn window where abort() silently no-ops.
|
|
3043
|
+
// Wait only within the daemon's fixed teardown budget for it to become
|
|
3044
|
+
// abortable. A still-pending setup is deliberately marked dirty below so
|
|
3045
|
+
// recovery starts a cycle instead of silently losing the prompt on resume.
|
|
3046
|
+
while (pendingPromptDispatches > 0 && !session.isStreaming && Date.now() < deadline) {
|
|
3047
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
3048
|
+
}
|
|
3049
|
+
if (session.isStreaming) {
|
|
3050
|
+
const remaining = deadline - Date.now();
|
|
3051
|
+
if (remaining > 0) {
|
|
3052
|
+
let timer;
|
|
3053
|
+
try {
|
|
3054
|
+
const settled = await Promise.race([
|
|
3055
|
+
session.abort().then(() => true, () => false),
|
|
3056
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), remaining); }),
|
|
3057
|
+
]);
|
|
3058
|
+
if (settled)
|
|
3059
|
+
markCleanAbort(nodeId);
|
|
3060
|
+
}
|
|
3061
|
+
finally {
|
|
3062
|
+
if (timer !== undefined)
|
|
3063
|
+
clearTimeout(timer);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
else if (pendingPromptDispatches > 0 || agentRunActive) {
|
|
3068
|
+
// The engine never reached an abortable persisted turn in the budget.
|
|
3069
|
+
// Preserve the existing dirty-death recovery cycle rather than certifying
|
|
3070
|
+
// a session that may not contain the prompt it was preparing.
|
|
3071
|
+
markBusy(nodeId);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
disposeAndExit(reason);
|
|
3075
|
+
};
|
|
3076
|
+
process.on('SIGTERM', () => { void abortAndExit('SIGTERM'); });
|
|
3077
|
+
process.on('SIGINT', () => { void abortAndExit('SIGINT'); });
|
|
3078
|
+
}
|
|
3079
|
+
// ---------------------------------------------------------------------------
|
|
3080
|
+
// snapshotMessages — the catch-up snapshot's ordered message history.
|
|
3081
|
+
//
|
|
3082
|
+
// The broker is the SOLE writer of the node's session `.jsonl`, so the session
|
|
3083
|
+
// manager's persisted tree (`buildSessionContext`) is the canonical, complete,
|
|
3084
|
+
// ordered history — byte-identical to what a dormant reader (crouter-web's static
|
|
3085
|
+
// normalizer) reconstructs from the same file. We serve THAT, NOT the agent's
|
|
3086
|
+
// live `session.messages` (`agent.state.messages`), because pi's recovery paths
|
|
3087
|
+
// mutate the live array away from the persisted history: auto-retry, context-
|
|
3088
|
+
// overflow recovery, and compaction each SLICE the errored/superseded assistant
|
|
3089
|
+
// message out of `state.messages` while DELIBERATELY keeping it on disk ("keep in
|
|
3090
|
+
// session for history", agent-session.js). So `session.messages` can OMIT — or, via
|
|
3091
|
+
// branch/compaction reshaping, reorder — a turn the `.jsonl` still holds. Pre-
|
|
3092
|
+
// close that drift is invisible (the live stream and the live array agree); after
|
|
3093
|
+
// a revive the welcome snapshot built from the reloaded array would diverge from
|
|
3094
|
+
// the on-disk history a dormant viewer just saw. Reconstructing from the session
|
|
3095
|
+
// manager makes the live snapshot == the persisted history (single source of
|
|
3096
|
+
// truth); the relayed live stream then continues from there.
|
|
3097
|
+
//
|
|
3098
|
+
// `AgentSession.sessionManager` is a public readonly field of the real pi SDK; the
|
|
3099
|
+
// fake-engine test fixture mirrors the same `sessionManager.buildSessionContext()`
|
|
3100
|
+
// surface (plus, where present, the tree accessors `cycleAwareMessages` reads).
|
|
3101
|
+
// A refresh-yielded node keeps every prior cycle in the SAME `.jsonl` as sibling
|
|
3102
|
+
// branches (session-cycles.ts) — `cycleAwareMessages` replays them ahead of the
|
|
3103
|
+
// live cycle's context so the welcome snapshot renders one continuous
|
|
3104
|
+
// conversation instead of just the post-yield window; single-cycle sessions and
|
|
3105
|
+
// the fake-engine fixture (no tree accessors) fall through to plain
|
|
3106
|
+
// `buildSessionContext().messages` unchanged.
|
|
3107
|
+
export function snapshotMessages(session) {
|
|
3108
|
+
// Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
|
|
3109
|
+
// payload), narrower than `CycleSessionManagerLike`'s structural read of the
|
|
3110
|
+
// `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
|
|
3111
|
+
// are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
|
|
3112
|
+
return cycleAwareMessages(session.sessionManager);
|
|
3113
|
+
}
|
|
3114
|
+
// ---------------------------------------------------------------------------
|
|
3115
|
+
// resolveLaunchModel — the SOLE launch model-selection policy, module-scoped
|
|
3116
|
+
// so both buildForManager (initial boot, inside buildBrokerSession below) and
|
|
3117
|
+
// the reload_auth re-drive (inside runBroker, a DIFFERENT function scope) can
|
|
3118
|
+
// invoke the identical decision against a live `services`/`cfg` without
|
|
3119
|
+
// re-running session/service creation. Returns undefined when NO candidate is
|
|
3120
|
+
// viable at all (tier 3) — this is NOT a fault; the caller decides what to do
|
|
3121
|
+
// with it (construct a model-less session at boot, or leave a running session
|
|
3122
|
+
// alone on a failed re-drive attempt).
|
|
3123
|
+
// ---------------------------------------------------------------------------
|
|
3124
|
+
async function resolveLaunchModel(services, cfg) {
|
|
3125
|
+
// Resolve one viable model against the SERVICES registry, which includes
|
|
3126
|
+
// extension-provided providers. `resolveProviderCandidates` is the SOLE
|
|
3127
|
+
// launch selection path — the same viability policy governs turn-time retry
|
|
3128
|
+
// and rotation fallback — so launch selection can never diverge from
|
|
3129
|
+
// rotation. A requested model gets first refusal (its provider pinned when
|
|
3130
|
+
// the caller pinned it), then the authenticated registry order decides.
|
|
3131
|
+
const requested = cfg.model ? parseModelSpec(cfg.model) : undefined;
|
|
3132
|
+
const faultLaunchModel = (message) => {
|
|
3133
|
+
recordFaultForCurrentNode({
|
|
3134
|
+
link: 'crtr→pi',
|
|
3135
|
+
op: 'model resolution at launch',
|
|
3136
|
+
kind: 'model-not-found',
|
|
3137
|
+
retry: { disposition: 'fatal' },
|
|
3138
|
+
message,
|
|
3139
|
+
});
|
|
3140
|
+
throw new Error(`[broker] FATAL: ${message}`);
|
|
3141
|
+
};
|
|
3142
|
+
if (requested !== undefined && requested.modelSpec.indexOf('/') <= 0) {
|
|
3143
|
+
faultLaunchModel(`model '${cfg.model}' has no 'provider/id' form and cannot be resolved`);
|
|
3144
|
+
}
|
|
3145
|
+
const requestedProvider = requested?.modelSpec.slice(0, requested.modelSpec.indexOf('/'));
|
|
3146
|
+
const requestedId = requested?.modelSpec.slice(requested.modelSpec.indexOf('/') + 1);
|
|
3147
|
+
const registry = registryOf(services);
|
|
3148
|
+
// The requested model's SAME-STRENGTH cell on the OTHER provider — the one
|
|
3149
|
+
// substitution that preserves the caller's intent, since a persona asks for a
|
|
3150
|
+
// STRENGTH (`explore: openai/light`) and the ladder only picks the id. It is
|
|
3151
|
+
// candidate #2, ahead of raw registry order: without it, an unavailable
|
|
3152
|
+
// `openai/light` fell through to whatever the registry happened to list first
|
|
3153
|
+
// (`anthropic/claude-fable-5` — the most expensive model there is) for a scout
|
|
3154
|
+
// that asked for the cheapest. This is the SAME mapping turn-time rotation
|
|
3155
|
+
// already falls back through (`equivalentOtherProviderModel`), so launch
|
|
3156
|
+
// selection and rotation now degrade identically instead of divergently.
|
|
3157
|
+
const ladderFallback = cfg.modelExact === true || cfg.model === undefined
|
|
3158
|
+
? null
|
|
3159
|
+
: equivalentOtherProviderModel(cfg.model);
|
|
3160
|
+
const ladderCandidate = (() => {
|
|
3161
|
+
if (ladderFallback === null)
|
|
3162
|
+
return undefined;
|
|
3163
|
+
const spec = parseModelSpec(ladderFallback.model);
|
|
3164
|
+
const slash = spec.modelSpec.indexOf('/');
|
|
3165
|
+
if (slash <= 0)
|
|
3166
|
+
return undefined;
|
|
3167
|
+
const provider = spec.modelSpec.slice(0, slash);
|
|
3168
|
+
const id = spec.modelSpec.slice(slash + 1);
|
|
3169
|
+
if (provider === requestedProvider && id === requestedId)
|
|
3170
|
+
return undefined;
|
|
3171
|
+
return { provider, id, thinkingLevel: spec.thinkingLevel };
|
|
3172
|
+
})();
|
|
3173
|
+
const requestedModel = requestedProvider !== undefined && requestedId !== undefined
|
|
3174
|
+
? registry.find(requestedProvider, requestedId)
|
|
3175
|
+
: undefined;
|
|
3176
|
+
// Enumerate every REGISTERED model (not just the authenticated set) so the
|
|
3177
|
+
// resolver itself classifies each candidate's registered/authenticated/
|
|
3178
|
+
// cooling state and reports a per-candidate rejection reason. Requested
|
|
3179
|
+
// first, then the rest of the registry in order.
|
|
3180
|
+
// cfg.modelExact locks launch selection to ONLY the requested candidate —
|
|
3181
|
+
// the rest of the registry is never spread in, so no cross-model/cross-
|
|
3182
|
+
// provider substitution is even reachable below.
|
|
3183
|
+
const orderedModels = [
|
|
3184
|
+
...(requestedProvider !== undefined && requestedId !== undefined
|
|
3185
|
+
? [{ provider: requestedProvider, id: requestedId, model: requestedModel, thinkingLevel: requested?.thinkingLevel }]
|
|
3186
|
+
: []),
|
|
3187
|
+
...(ladderCandidate !== undefined
|
|
3188
|
+
? [{
|
|
3189
|
+
provider: ladderCandidate.provider,
|
|
3190
|
+
id: ladderCandidate.id,
|
|
3191
|
+
model: registry.find(ladderCandidate.provider, ladderCandidate.id),
|
|
3192
|
+
thinkingLevel: ladderCandidate.thinkingLevel,
|
|
3193
|
+
}]
|
|
3194
|
+
: []),
|
|
3195
|
+
...(cfg.modelExact === true ? [] : registry.getAll()
|
|
3196
|
+
.filter((candidate) => (candidate.provider !== requestedProvider || candidate.id !== requestedId)
|
|
3197
|
+
&& (ladderCandidate === undefined || candidate.provider !== ladderCandidate.provider || candidate.id !== ladderCandidate.id))
|
|
3198
|
+
.map((candidate) => ({
|
|
3199
|
+
provider: candidate.provider,
|
|
3200
|
+
id: candidate.id,
|
|
3201
|
+
model: candidate,
|
|
3202
|
+
thinkingLevel: undefined,
|
|
3203
|
+
}))),
|
|
3204
|
+
];
|
|
3205
|
+
// Tier 1/2 policy: a non-managed candidate is only automatic-fallback-eligible
|
|
3206
|
+
// when NO managed provider has an authenticated (pool-occupied) credential
|
|
3207
|
+
// ANYWHERE in the ordered set — regardless of that managed candidate's
|
|
3208
|
+
// current cooling state. A cooling-but-authenticated managed pool still
|
|
3209
|
+
// blocks automatic fallback to a non-managed provider (preserves 4be8be6's
|
|
3210
|
+
// intent: don't silently abandon a paid managed subscription for a
|
|
3211
|
+
// pay-per-token key just because it's rate-limited right now). Only when
|
|
3212
|
+
// there is NO managed-authenticated candidate at all does a registered +
|
|
3213
|
+
// authenticated non-managed model become an acceptable automatic launch
|
|
3214
|
+
// fallback (tier 2); only when even that is unavailable does launch fall all
|
|
3215
|
+
// the way through to a model-less boot (tier 3, below).
|
|
3216
|
+
const hasManagedAuthenticated = orderedModels.some((candidate) => {
|
|
3217
|
+
if (!isManagedProvider(candidate.provider))
|
|
3218
|
+
return false;
|
|
3219
|
+
if (candidate.model === undefined)
|
|
3220
|
+
return false; // not registered
|
|
3221
|
+
return readSubscriptionPool(candidate.provider).length > 0;
|
|
3222
|
+
});
|
|
3223
|
+
const selection = resolveProviderCandidates(orderedModels.map((candidate, index) => {
|
|
3224
|
+
// Managed providers authenticate off their subscription pool's accounts, not
|
|
3225
|
+
// static credentials; a fully-cooling pool marks the candidate cooling.
|
|
3226
|
+
const pool = isManagedProvider(candidate.provider) ? readSubscriptionPool(candidate.provider) : [];
|
|
3227
|
+
const explicit = index === 0 && requestedProvider !== undefined && requestedId !== undefined;
|
|
3228
|
+
return {
|
|
3229
|
+
providerId: candidate.provider,
|
|
3230
|
+
modelId: candidate.id,
|
|
3231
|
+
thinkingLevel: candidate.thinkingLevel,
|
|
3232
|
+
registered: candidate.model !== undefined,
|
|
3233
|
+
// Non-managed models are valid automatic fallback when explicitly
|
|
3234
|
+
// requested, when the candidate IS a managed provider itself, or (tier
|
|
3235
|
+
// 2) when no managed-authenticated candidate exists anywhere in this
|
|
3236
|
+
// launch's candidate set.
|
|
3237
|
+
automaticFallback: explicit || isManagedProvider(candidate.provider) || !hasManagedAuthenticated,
|
|
3238
|
+
authenticated: candidate.model !== undefined && (isManagedProvider(candidate.provider)
|
|
3239
|
+
? pool.length > 0
|
|
3240
|
+
: registry.hasConfiguredAuth(candidate.model)),
|
|
3241
|
+
coolingUntil: pool.length > 0 && pool.every((entry) => (entry.rateLimitedUntil || 0) > Date.now())
|
|
3242
|
+
? Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0))
|
|
3243
|
+
: undefined,
|
|
3244
|
+
};
|
|
3245
|
+
}), Date.now(),
|
|
3246
|
+
// An explicit provider pin forbids cross-provider fallback for an
|
|
3247
|
+
// unregistered/unauthenticated request; only a transient managed cooldown
|
|
3248
|
+
// on the pinned provider still boots a viable alternative.
|
|
3249
|
+
{ pinnedToFirst: cfg.modelProviderPinned === true && requested !== undefined });
|
|
3250
|
+
const selected = selection.candidates[0];
|
|
3251
|
+
if (selected === undefined) {
|
|
3252
|
+
const rejectionText = selection.rejected.length > 0
|
|
3253
|
+
? selection.rejected.map(({ candidate, reason }) => `'${candidate.providerId}/${candidate.modelId}' ${reason === 'non-managed-fallback' ? 'non-managed provider is not an automatic fallback' : reason}`).join('; ')
|
|
3254
|
+
: 'no candidates were supplied';
|
|
3255
|
+
// An exact-consult launch must FAIL rather than fall through to a
|
|
3256
|
+
// model-less session — an unavailable captured model faults the launch
|
|
3257
|
+
// (crash), never a silent degrade/substitution.
|
|
3258
|
+
if (cfg.modelExact === true) {
|
|
3259
|
+
return faultLaunchModel(`exact-consult model '${cfg.model}' is unavailable — refusing to substitute (${rejectionText})`);
|
|
3260
|
+
}
|
|
3261
|
+
// Tier 3: no viable candidate at all (genuine zero-auth, OR every managed
|
|
3262
|
+
// pool is cooling AND no non-managed provider is authenticated). This is
|
|
3263
|
+
// NOT a fault — the caller constructs a model-less session so the
|
|
3264
|
+
// socket/listener still comes up and the user can /login; reload_auth
|
|
3265
|
+
// re-invokes this same helper to re-drive once auth changes.
|
|
3266
|
+
emitEvent({
|
|
3267
|
+
level: 'warn',
|
|
3268
|
+
event: 'broker.model.none_viable',
|
|
3269
|
+
fields: {
|
|
3270
|
+
rejected_candidates: selection.rejected.slice(0, 16).map(({ candidate, reason }) => ({
|
|
3271
|
+
provider: candidate.providerId,
|
|
3272
|
+
model: candidate.modelId,
|
|
3273
|
+
reason,
|
|
3274
|
+
})),
|
|
3275
|
+
rejected_candidates_omitted: Math.max(0, selection.rejected.length - 16),
|
|
3276
|
+
},
|
|
3277
|
+
});
|
|
3278
|
+
return undefined;
|
|
3279
|
+
}
|
|
3280
|
+
const model = registry.find(selected.providerId, selected.modelId);
|
|
3281
|
+
if (model === undefined) {
|
|
3282
|
+
return faultLaunchModel(`selected model '${selected.providerId}/${selected.modelId}' disappeared during launch`);
|
|
3283
|
+
}
|
|
3284
|
+
const thinkingLevel = selected.thinkingLevel;
|
|
3285
|
+
// A launch that does not get the model it asked for is a SUBSTITUTION, and it
|
|
3286
|
+
// used to be entirely silent: the session simply came up on another model and
|
|
3287
|
+
// the only trace was the pi `model_change` entry in the .jsonl. One line, only
|
|
3288
|
+
// on the rare path, carrying the per-candidate rejection reasons that decided
|
|
3289
|
+
// it — so "why is this node on the wrong model" is answerable from the log.
|
|
3290
|
+
if (requestedProvider !== undefined &&
|
|
3291
|
+
requestedId !== undefined &&
|
|
3292
|
+
(selected.providerId !== requestedProvider || selected.modelId !== requestedId)) {
|
|
3293
|
+
emitEvent({
|
|
3294
|
+
level: 'warn',
|
|
3295
|
+
event: 'broker.model.launch_fallback',
|
|
3296
|
+
fields: {
|
|
3297
|
+
requested: `${requestedProvider}/${requestedId}`,
|
|
3298
|
+
selected: `${selected.providerId}/${selected.modelId}`,
|
|
3299
|
+
ladder_equivalent: ladderFallback === null ? null : ladderFallback.model,
|
|
3300
|
+
rejected_candidates: selection.rejected.slice(0, 8).map(({ candidate, reason }) => ({
|
|
3301
|
+
provider: candidate.providerId,
|
|
3302
|
+
model: candidate.modelId,
|
|
3303
|
+
reason,
|
|
3304
|
+
})),
|
|
3305
|
+
},
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
if (requestedProvider !== undefined &&
|
|
3309
|
+
requestedId !== undefined &&
|
|
3310
|
+
isManagedProvider(requestedProvider) &&
|
|
3311
|
+
requestedModel !== undefined &&
|
|
3312
|
+
(selected.providerId !== requestedProvider || selected.modelId !== requestedId)) {
|
|
3313
|
+
// This launch fallback is temporary: retain the requested managed pin for
|
|
3314
|
+
// provider-rotation's before_agent_start restoration once it is ready.
|
|
3315
|
+
rememberPreferredModel({ providerId: requestedProvider, modelId: requestedId });
|
|
3316
|
+
}
|
|
3317
|
+
return { model, thinkingLevel };
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Wrap a live ModelRuntime so a model-less `createAgentSessionFromServices`
|
|
3321
|
+
* call can't have the SDK's own `findInitialModel` silently pick a model
|
|
3322
|
+
* behind crouter's launch policy. Both `getModel()` (step 3: settingsManager's
|
|
3323
|
+
* durable `defaultProvider`/`defaultModel` — persisted into a GLOBAL
|
|
3324
|
+
* settings.json by every `session.setModel()` call the broker itself already
|
|
3325
|
+
* makes at broker.ts:609/675/1606/1650, so this is a live bypass on any
|
|
3326
|
+
* machine that has ever picked a model, not a theoretical corner case) and
|
|
3327
|
+
* `getAvailable()` (step 4: registry default) must both be intercepted —
|
|
3328
|
+
* wrapping only `getAvailable()` leaves `getModel()`'s step-3 path (and the
|
|
3329
|
+
* resume path's model restore, sdk.js:88) wide open. Everything else
|
|
3330
|
+
* (`getModels`, `hasConfiguredAuth`, `refresh`, …) passes through untouched via
|
|
3331
|
+
* `Reflect.get`. Used ONLY
|
|
3332
|
+
* for the one model-less construction call; the `services` returned to every
|
|
3333
|
+
* other caller (reload_auth, hasConfiguredAuth checks, …) keeps the REAL
|
|
3334
|
+
* unwrapped registry.
|
|
3335
|
+
*
|
|
3336
|
+
* CONSTRUCTION-ONLY MASK: the SDK stores `options.services.modelRuntime` as the
|
|
3337
|
+
* session's private `_modelRuntime` for its ENTIRE lifetime and never replaces
|
|
3338
|
+
* it (agent-session-services.js:104-119, sdk.js:253). A permanent mask would
|
|
3339
|
+
* leave list_models (getAvailable→[]), cycle_model, and provider-rotation
|
|
3340
|
+
* fallback (getModel()→undefined) broken forever for any model-less-booted node,
|
|
3341
|
+
* even after a later /login selects a model. Model selection can only happen
|
|
3342
|
+
* DURING construction, so this proxy self-heals: it masks while `masked`, then
|
|
3343
|
+
* `reveal()` (called immediately after createAgentSessionFromServices returns)
|
|
3344
|
+
* flips the SAME retained proxy to a transparent passthrough forever after.
|
|
3345
|
+
*/
|
|
3346
|
+
function modelLessRegistry(real) {
|
|
3347
|
+
let masked = true;
|
|
3348
|
+
const runtime = new Proxy(real, {
|
|
3349
|
+
get(target, prop, receiver) {
|
|
3350
|
+
// `getAvailable` is async on ModelRuntime; findInitialModel awaits it.
|
|
3351
|
+
if (masked && prop === 'getAvailable')
|
|
3352
|
+
return async () => [];
|
|
3353
|
+
if (masked && prop === 'getModel')
|
|
3354
|
+
return () => undefined;
|
|
3355
|
+
return Reflect.get(target, prop, receiver);
|
|
3356
|
+
},
|
|
3357
|
+
});
|
|
3358
|
+
return { runtime, reveal: () => { masked = false; } };
|
|
3359
|
+
}
|
|
3360
|
+
// ---------------------------------------------------------------------------
|
|
3361
|
+
// buildBrokerSession (plan T4 steps 2–4) — turn the launch recipe into a live
|
|
3362
|
+
// engine session via the pi SDK SERVICES path. Exported so the C3/C4 real-SDK
|
|
3363
|
+
// regression tests can drive the EXACT production wiring (not the mock).
|
|
3364
|
+
// ---------------------------------------------------------------------------
|
|
3365
|
+
export async function buildBrokerSession(engine, cfg) {
|
|
3366
|
+
// 2. Build cwd-bound runtime services via the SERVICES path (C3) — NOT plain
|
|
3367
|
+
// createAgentSession. createAgentSessionServices builds + reloads the
|
|
3368
|
+
// resource loader (the `-e` canvas extensions + --append-system-prompt) and
|
|
3369
|
+
// REGISTERS extension-provided model providers into the ModelRegistry (it is
|
|
3370
|
+
// also where extension flag values would be applied — the broker recipe
|
|
3371
|
+
// carries none today). Plain createAgentSession does NEITHER, so a node whose
|
|
3372
|
+
// model comes from a custom-provider extension would get NO model. Mirrors pi
|
|
3373
|
+
// main.js (createAgentSessionServices → …FromServices).
|
|
3374
|
+
//
|
|
3375
|
+
// C4 note: the pinned SDK (0.78.1) has NO project-trust concept at all —
|
|
3376
|
+
// project context files (AGENTS.md/CLAUDE.md) load UNCONDITIONALLY (gated
|
|
3377
|
+
// only by `noContextFiles`, left default-false), so the headless "trust
|
|
3378
|
+
// resolves false → context silently dropped" gap cannot occur here. WHEN the
|
|
3379
|
+
// SDK pin bumps to 0.79.0+ (which adds project-trust), pass an explicit
|
|
3380
|
+
// `settingsManager: SettingsManager.create(cfg.cwd, getAgentDir(), { projectTrusted: true })`
|
|
3381
|
+
// so headless trust defaults TRUSTED — and do NOT re-introduce the CLI's
|
|
3382
|
+
// resolveProjectTrust, which returns false with no TTY.
|
|
3383
|
+
//
|
|
3384
|
+
// T3 session-rebind: services + model + session creation is factored into
|
|
3385
|
+
// `buildForManager` so it can serve BOTH the initial boot AND the
|
|
3386
|
+
// AgentSessionRuntime factory it is reused as below (new_session/
|
|
3387
|
+
// switch_session/fork rebuild the session by re-invoking it for a new
|
|
3388
|
+
// SessionManager). Mirrors pi main.js's `createRuntime`.
|
|
3389
|
+
const agentDir = getAgentDir();
|
|
3390
|
+
const buildForManager = async (o) => {
|
|
3391
|
+
// pi 0.82 owns credentials through an app-supplied CredentialStore. Hand it
|
|
3392
|
+
// crouter's, which serves managed providers straight from the subscription
|
|
3393
|
+
// pool and everything else from auth.json — both read fresh under their lock
|
|
3394
|
+
// on every call, because the rotation extension, `/provider-sub`, and the
|
|
3395
|
+
// daemon's login fan all write out of band mid-session and pi's own default
|
|
3396
|
+
// store would serve a cached view with no reload seam.
|
|
3397
|
+
const { authPath, modelsPath } = modelRuntimePaths(o.agentDir);
|
|
3398
|
+
const modelRuntime = await ModelRuntime.create({
|
|
3399
|
+
credentials: new PoolCredentialStore(authPath),
|
|
3400
|
+
modelsPath,
|
|
3401
|
+
});
|
|
3402
|
+
const services = await engine.createAgentSessionServices({
|
|
3403
|
+
cwd: o.cwd,
|
|
3404
|
+
agentDir: o.agentDir,
|
|
3405
|
+
modelRuntime,
|
|
3406
|
+
resourceLoaderOptions: {
|
|
3407
|
+
additionalExtensionPaths: cfg.extensionPaths,
|
|
3408
|
+
appendSystemPrompt: cfg.appendSystemPromptPath !== undefined ? [cfg.appendSystemPromptPath] : undefined,
|
|
3409
|
+
// Suppress pi's <project_context> AGENTS.md/CLAUDE.md injection in the
|
|
3410
|
+
// SYSTEM PROMPT unconditionally — crouter reads project guidance through
|
|
3411
|
+
// the document substrate instead (a migrated AGENTS.md doc rides the
|
|
3412
|
+
// <memory kind="knowledge"> block in the first-message bearings; see
|
|
3413
|
+
// bearings.ts and `crtr sys sync project-guidance`), never pi's own file-based loader.
|
|
3414
|
+
noContextFiles: true,
|
|
3415
|
+
},
|
|
3416
|
+
});
|
|
3417
|
+
const extensionResult = services.resourceLoader.getExtensions();
|
|
3418
|
+
// buildBrokerSession also supports standalone SDK consumers (for example a
|
|
3419
|
+
// custom-provider extension test) that are not canvas nodes. Production
|
|
3420
|
+
// canvas invocations always carry the full mandatory set after preflight;
|
|
3421
|
+
// once any canvas hook is requested, require every one to load.
|
|
3422
|
+
if (cfg.extensionPaths.some((path) => CANVAS_EXTENSIONS.includes(path))) {
|
|
3423
|
+
const loadedPaths = new Set(extensionResult.extensions.map((extension) => extension.path));
|
|
3424
|
+
const missingHooks = CANVAS_EXTENSIONS.filter((path) => !loadedPaths.has(path));
|
|
3425
|
+
if (missingHooks.length > 0) {
|
|
3426
|
+
const errors = extensionResult.errors
|
|
3427
|
+
.filter((error) => CANVAS_EXTENSIONS.includes(error.path))
|
|
3428
|
+
.map((error) => `${error.path}: ${error.error}`)
|
|
3429
|
+
.join('; ');
|
|
3430
|
+
throw new Error(`[broker] FATAL: mandatory canvas extensions failed to load: ${missingHooks.join(', ')}${errors === '' ? '' : ` (${errors})`}`);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
// 3. Resolve one viable model — resolveLaunchModel (module-scoped above) is
|
|
3434
|
+
// the SOLE launch selection policy, shared with the reload_auth re-drive so
|
|
3435
|
+
// a later /login re-runs the identical decision against this same `services`.
|
|
3436
|
+
const resolved = await resolveLaunchModel(services, cfg);
|
|
3437
|
+
// Tier 3: no viable candidate at all. Do NOT fault — construct a live
|
|
3438
|
+
// model-less session so the socket/listener still comes up and the user can
|
|
3439
|
+
// /login; suppress the SDK's own findInitialModel auto-selection
|
|
3440
|
+
// (modelLessRegistry) so a stale global settings.json default or an
|
|
3441
|
+
// available-but-policy-rejected model can't sneak in behind crouter's back.
|
|
3442
|
+
const model = resolved?.model;
|
|
3443
|
+
const thinkingLevel = resolved?.thinkingLevel;
|
|
3444
|
+
const modelLess = resolved === undefined ? modelLessRegistry(services.modelRuntime) : undefined;
|
|
3445
|
+
const constructionServices = modelLess !== undefined
|
|
3446
|
+
? { ...services, modelRuntime: modelLess.runtime }
|
|
3447
|
+
: services;
|
|
3448
|
+
const created = await engine.createAgentSessionFromServices({
|
|
3449
|
+
services: constructionServices,
|
|
3450
|
+
sessionManager: o.sessionManager,
|
|
3451
|
+
model,
|
|
3452
|
+
thinkingLevel,
|
|
3453
|
+
tools: cfg.tools,
|
|
3454
|
+
sessionStartEvent: o.sessionStartEvent,
|
|
3455
|
+
});
|
|
3456
|
+
// CONSTRUCTION-ONLY MASK — see modelLessRegistry: reveal now that model
|
|
3457
|
+
// selection (construction-time only) is done, so a later /login re-drive's
|
|
3458
|
+
// list_models/cycle_model/provider-rotation see the real registry.
|
|
3459
|
+
modelLess?.reveal();
|
|
3460
|
+
return { ...created, services };
|
|
3461
|
+
};
|
|
3462
|
+
// 4. Select the SessionManager: FORK (spawn-time --fork), OPEN (resume), or
|
|
3463
|
+
// CREATE (fresh). The broker is the sole writer of the resulting .jsonl.
|
|
3464
|
+
const resumePath = cfg.resumeSessionPath;
|
|
3465
|
+
const forking = cfg.forkFrom !== undefined && cfg.forkFrom !== '';
|
|
3466
|
+
if (!forking &&
|
|
3467
|
+
(resumePath === undefined || resumePath === '') &&
|
|
3468
|
+
cfg.resumeSessionId !== undefined &&
|
|
3469
|
+
cfg.resumeSessionId !== '') {
|
|
3470
|
+
// SessionManager.open() takes a FILE path — it only path-normalizes; it does
|
|
3471
|
+
// NOT resolve a bare uuid to its .jsonl the way pi's CLI does (via
|
|
3472
|
+
// SessionManager.list). A bare-id-only resume would open a nonexistent
|
|
3473
|
+
// <cwd>/<uuid> and silently start an EMPTY session. Fail loud: revive always
|
|
3474
|
+
// passes the .jsonl path, so a bare id means an old node that never captured
|
|
3475
|
+
// pi_session_file — mis-resuming it silently is worse than a crash.
|
|
3476
|
+
throw new Error(`[broker] resume requires a session .jsonl PATH; got only a bare id ` +
|
|
3477
|
+
`'${cfg.resumeSessionId}' (pi_session_file was never captured for this node)`);
|
|
3478
|
+
}
|
|
3479
|
+
// A fork is a fresh spawn (resuming=false) so its kickoff firstPrompt fires: a
|
|
3480
|
+
// `node new --fork-from <id>` gives the new node the source's full history AND a
|
|
3481
|
+
// new task to start on. An ordinary resume supplies no prompt, but a
|
|
3482
|
+
// broker-certified runtime-abort resume deliberately carries a continuation.
|
|
3483
|
+
//
|
|
3484
|
+
// A refresh-yield CYCLES instead: `cfg.newCycle` (the `--crtr-new-cycle` token)
|
|
3485
|
+
// marks a revive that reuses this SAME `.jsonl` rather than resuming it — the
|
|
3486
|
+
// session manager opens the file, then resets the leaf and roots a `crtr-cycle`
|
|
3487
|
+
// marker entry (data: `{cycle, fromLeaf}`) as the new cycle's ROOT before the
|
|
3488
|
+
// session is built. Custom entries never enter LLM context, so the freshly
|
|
3489
|
+
// rooted branch is exactly as clean as a brand-new session; `cycleAwareMessages`
|
|
3490
|
+
// (session-cycles.ts) is the display-side counterpart that replays every prior
|
|
3491
|
+
// cycle for snapshot rendering. cycling ⇒ resuming=false, so the kickoff
|
|
3492
|
+
// firstPrompt fires below just like a true fresh launch.
|
|
3493
|
+
const hasSessionPath = !forking && resumePath !== undefined && resumePath !== '';
|
|
3494
|
+
const cycling = hasSessionPath && cfg.newCycle === true;
|
|
3495
|
+
const resuming = hasSessionPath && !cycling;
|
|
3496
|
+
let sessionManager;
|
|
3497
|
+
if (forking) {
|
|
3498
|
+
// Spawn-time fork via pi's REAL fork seam, SessionManager.forkFrom — the exact
|
|
3499
|
+
// method pi's own `--fork` CLI flag uses (main.js forkSessionOrExit). It writes
|
|
3500
|
+
// a NEW session file with a NEW id and a `parentSession` header pointing at the
|
|
3501
|
+
// source, then copies the source's history into it. NOT a naïve `.jsonl` copy
|
|
3502
|
+
// (which would collide session ids and drop the fork metadata), and NOT
|
|
3503
|
+
// runtime.fork() (which forks an entry WITHIN an already-loaded session — there
|
|
3504
|
+
// is no current session to fork at boot). The caller (spawn.ts resolveForkSource)
|
|
3505
|
+
// resolves cfg.forkFrom to an absolute source .jsonl before launch, so a bad ref
|
|
3506
|
+
// fails loudly here (forkFrom throws on an empty/invalid/header-less source).
|
|
3507
|
+
sessionManager = engine.SessionManager.forkFrom(cfg.forkFrom, cfg.cwd);
|
|
3508
|
+
}
|
|
3509
|
+
else if (resuming || cycling) {
|
|
3510
|
+
sessionManager = engine.SessionManager.open(resumePath);
|
|
3511
|
+
if (cycling) {
|
|
3512
|
+
const fromLeaf = sessionManager.getLeafId();
|
|
3513
|
+
sessionManager.resetLeaf();
|
|
3514
|
+
sessionManager.appendCustomEntry(CRTR_CYCLE_CUSTOM_TYPE, {
|
|
3515
|
+
cycle: Number(process.env['CRTR_CYCLES']),
|
|
3516
|
+
fromLeaf,
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
sessionManager = engine.SessionManager.create(cfg.cwd);
|
|
3522
|
+
}
|
|
3523
|
+
// When the engine exposes the replacement API (real SDK), wrap the builder in an
|
|
3524
|
+
// AgentSessionRuntime so new_session/switch_session/fork work; the runtime
|
|
3525
|
+
// re-invokes `buildForManager` for each new SessionManager. The fake-engine
|
|
3526
|
+
// fixture omits it — fall back to a single direct build (those three ops then
|
|
3527
|
+
// reply error{engine_error}).
|
|
3528
|
+
if (engine.createAgentSessionRuntime !== undefined) {
|
|
3529
|
+
const factory = async (o) => {
|
|
3530
|
+
const r = await buildForManager({
|
|
3531
|
+
cwd: o.cwd,
|
|
3532
|
+
agentDir: o.agentDir,
|
|
3533
|
+
sessionManager: o.sessionManager,
|
|
3534
|
+
sessionStartEvent: o.sessionStartEvent,
|
|
3535
|
+
});
|
|
3536
|
+
return { ...r, diagnostics: r.services.diagnostics ?? [] };
|
|
3537
|
+
};
|
|
3538
|
+
const runtime = await engine.createAgentSessionRuntime(factory, {
|
|
3539
|
+
cwd: cfg.cwd,
|
|
3540
|
+
agentDir,
|
|
3541
|
+
sessionManager,
|
|
3542
|
+
});
|
|
3543
|
+
return { session: runtime.session, services: runtime.services, resuming, runtime };
|
|
3544
|
+
}
|
|
3545
|
+
const r = await buildForManager({ cwd: cfg.cwd, agentDir, sessionManager });
|
|
3546
|
+
return { session: r.session, services: r.services, resuming, runtime: undefined };
|
|
3547
|
+
}
|
|
3548
|
+
export function makeBrokerUiContext(deps) {
|
|
3549
|
+
// The dialog router. C2 (scout mq5thyli): the design §5.4 premise — that an
|
|
3550
|
+
// unattended dialog auto-resolves on its OWN timeout — is FALSE. `timeout` is
|
|
3551
|
+
// OPTIONAL on dialog opts, editor() takes none, and almost no real extension
|
|
3552
|
+
// passes one (permission-gate / confirm-destructive / plan-mode / subagent all
|
|
3553
|
+
// omit it). A timeout-reliant unattended node therefore deadlocks the agent
|
|
3554
|
+
// turn FOREVER. So with NO WRITABLE client attached we fall back to the SDK's
|
|
3555
|
+
// noOp UI behavior — resolve to the default (deny / cancel / undefined)
|
|
3556
|
+
// IMMEDIATELY, never arming a timer, never waiting. With one or more, the dialog
|
|
3557
|
+
// is fanned out to all of them under a broker-side timeout+abort, so viewers
|
|
3558
|
+
// that attach but never answer cannot hang the turn either.
|
|
3559
|
+
const dialogPromise = (defaultValue, request, parse, opts) => {
|
|
3560
|
+
if (opts?.signal?.aborted)
|
|
3561
|
+
return Promise.resolve(defaultValue);
|
|
3562
|
+
// Snapshot the writable set ONCE, at raise time — the fan-out targets and the
|
|
3563
|
+
// zero-answerer decision must agree.
|
|
3564
|
+
const targets = deps.writable();
|
|
3565
|
+
// C2 (Wave-0, KEEP): nobody can answer at raise time → noOp, resolved at once.
|
|
3566
|
+
// No timer, no wait, no deadlock.
|
|
3567
|
+
if (targets.length === 0)
|
|
3568
|
+
return Promise.resolve(defaultValue);
|
|
3569
|
+
// At least one writable client: register the dialog FIRST (so a client that
|
|
3570
|
+
// attaches mid-dialog gets it replayed and joins the race), fan it out to every
|
|
3571
|
+
// target, and ALWAYS arm a broker-side timeout (C2 anti-deadlock): viewers that
|
|
3572
|
+
// never answer — or all detach — can never hang the turn. Honor a shorter
|
|
3573
|
+
// per-dialog timeout if the extension passed one; otherwise the broker default.
|
|
3574
|
+
// On fire it resolves to the SAFE default (deny/cancel/undefined). A detach
|
|
3575
|
+
// does NOT cancel this — only an answer, the timeout, or an abort.
|
|
3576
|
+
//
|
|
3577
|
+
// EVERY settlement path runs the same `cleanup` (drop the entry, so a later
|
|
3578
|
+
// answer for this id is a no-op) and then broadcasts `extension_ui_dismiss`
|
|
3579
|
+
// keyed to this id, so all peers close their copy of exactly this dialog and
|
|
3580
|
+
// nothing else.
|
|
3581
|
+
return new Promise((resolve) => {
|
|
3582
|
+
let timer;
|
|
3583
|
+
const cleanup = () => {
|
|
3584
|
+
if (timer !== undefined)
|
|
3585
|
+
clearTimeout(timer);
|
|
3586
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
3587
|
+
deps.pending.delete(request.id);
|
|
3588
|
+
};
|
|
3589
|
+
const onAbort = () => {
|
|
3590
|
+
cleanup();
|
|
3591
|
+
// The extension aborted this request out-of-band (e.g. an OAuth loopback
|
|
3592
|
+
// callback won the race against a still-open manual-paste dialog). Tell
|
|
3593
|
+
// every peer to tear down THIS overlay by id — an unanswered dialog whose
|
|
3594
|
+
// broker entry we just dropped would otherwise linger onscreen with nothing
|
|
3595
|
+
// left to answer it. Keyed to request.id so only the abandoned dialog is
|
|
3596
|
+
// dismissed, never an unrelated one.
|
|
3597
|
+
deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
|
|
3598
|
+
resolve(defaultValue);
|
|
3599
|
+
};
|
|
3600
|
+
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
3601
|
+
// N1: honor the extension's EXPLICIT per-dialog timeout if it passed one
|
|
3602
|
+
// (longer or shorter than the broker default is fine — both are bounded);
|
|
3603
|
+
// otherwise fall back to the broker default. The broker never RELIES on the
|
|
3604
|
+
// extension having passed one (C2).
|
|
3605
|
+
const ms = opts?.timeout !== undefined ? opts.timeout : DEFAULT_DIALOG_TIMEOUT_MS;
|
|
3606
|
+
timer = setTimeout(() => {
|
|
3607
|
+
cleanup();
|
|
3608
|
+
// Same correlated teardown as onAbort: the broker resolved this dialog on
|
|
3609
|
+
// its own timeout, so every peer's overlay (none of which has an
|
|
3610
|
+
// independent timer) must be dismissed by id or it lingers forever.
|
|
3611
|
+
deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
|
|
3612
|
+
resolve(defaultValue);
|
|
3613
|
+
}, ms);
|
|
3614
|
+
if (typeof timer.unref === 'function')
|
|
3615
|
+
timer.unref();
|
|
3616
|
+
deps.pending.set(request.id, {
|
|
3617
|
+
request,
|
|
3618
|
+
resolve: (r) => {
|
|
3619
|
+
cleanup();
|
|
3620
|
+
// The first answer settled it; tell every OTHER client that was fanned
|
|
3621
|
+
// this dialog (and the answerer, for whom it is an idempotent no-op) to
|
|
3622
|
+
// close its copy. Without this, peers would sit on an overlay whose
|
|
3623
|
+
// answer can never arrive.
|
|
3624
|
+
deps.broadcast({ type: 'extension_ui_dismiss', id: request.id });
|
|
3625
|
+
resolve(parse(r));
|
|
3626
|
+
},
|
|
3627
|
+
});
|
|
3628
|
+
for (const target of targets)
|
|
3629
|
+
deps.forward(target, request);
|
|
3630
|
+
});
|
|
3631
|
+
};
|
|
3632
|
+
const noop = () => { };
|
|
3633
|
+
const ctx = {
|
|
3634
|
+
[MANAGED_PROVIDER_ROTATION_POLICY_UI]: deps.providerRotationPolicy ?? (() => ({ allowCrossProviderFallback: true })),
|
|
3635
|
+
// The broker-owned known-stream-wait control (see BrokerDialogDeps): a
|
|
3636
|
+
// deliberate provider cooldown is KNOWN liveness, so the provider extension
|
|
3637
|
+
// reaches this through the stable symbol to pause the dead-stream watchdog for
|
|
3638
|
+
// the wait's duration. A no-op when the host supplied none.
|
|
3639
|
+
[KNOWN_STREAM_WAIT_UI]: deps.onKnownStreamWait ?? ((_deadlineMs) => () => { }),
|
|
3640
|
+
// pi's ExtensionUIContext exposes `theme`; package extensions (e.g. the OAuth
|
|
3641
|
+
// billing adapter's status rendering) read it from ANY lifecycle hook. The
|
|
3642
|
+
// SDK's noOpUIContext returns the process-global theme proxy here; we mirror
|
|
3643
|
+
// that by reading the same global symbol (populated by initTheme() at broker
|
|
3644
|
+
// boot — see runBroker). Without this property `ctx.ui.theme` is undefined and
|
|
3645
|
+
// the hook throws, which the runner silently swallows while dropping the hook's
|
|
3646
|
+
// returned payload (the OAuth 400 root cause).
|
|
3647
|
+
get theme() {
|
|
3648
|
+
const t = globalThis[Symbol.for('@earendil-works/pi-coding-agent:theme')];
|
|
3649
|
+
if (t === undefined || t === null) {
|
|
3650
|
+
throw new Error('Theme not initialized. Call initTheme() first.');
|
|
3651
|
+
}
|
|
3652
|
+
return t;
|
|
3653
|
+
},
|
|
3654
|
+
select: (title, options, opts) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'select', title, options, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined), opts),
|
|
3655
|
+
confirm: (title, message, opts) => dialogPromise(false, { type: 'extension_ui_request', id: randomUUID(), method: 'confirm', title, message, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? false : 'confirmed' in r ? r.confirmed : false), opts),
|
|
3656
|
+
input: (title, placeholder, opts) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'input', title, placeholder, timeout: opts?.timeout }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined), opts),
|
|
3657
|
+
editor: (title, prefill) => dialogPromise(undefined, { type: 'extension_ui_request', id: randomUUID(), method: 'editor', title, prefill }, (r) => ('cancelled' in r && r.cancelled ? undefined : 'value' in r ? r.value : undefined)),
|
|
3658
|
+
// Non-blocking extension-UI relay: broadcast these to all viewers instead of
|
|
3659
|
+
// dropping them, in pi's OWN `extension_ui_request` envelope (rpc-mode.js
|
|
3660
|
+
// emits these three identically). The viewer owns the slots that render them,
|
|
3661
|
+
// and the broker retains their last value for the welcome snapshot. The other
|
|
3662
|
+
// non-blocking methods stay inert.
|
|
3663
|
+
setStatus: (key, text) => deps.broadcast({
|
|
3664
|
+
type: 'extension_ui_request',
|
|
3665
|
+
id: randomUUID(),
|
|
3666
|
+
method: 'setStatus',
|
|
3667
|
+
statusKey: key,
|
|
3668
|
+
statusText: text,
|
|
3669
|
+
}),
|
|
3670
|
+
setWidget: (key, content, options) => {
|
|
3671
|
+
// setWidget's component-factory overload cannot be serialized over the
|
|
3672
|
+
// socket (R1 caveat) — drop it and relay only the string[] form, exactly as
|
|
3673
|
+
// pi's own RPC setWidget carries only `widgetLines`.
|
|
3674
|
+
if (typeof content === 'function')
|
|
3675
|
+
return;
|
|
3676
|
+
deps.broadcast({
|
|
3677
|
+
type: 'extension_ui_request',
|
|
3678
|
+
id: randomUUID(),
|
|
3679
|
+
method: 'setWidget',
|
|
3680
|
+
widgetKey: key,
|
|
3681
|
+
widgetLines: content,
|
|
3682
|
+
widgetPlacement: options?.placement,
|
|
3683
|
+
});
|
|
3684
|
+
},
|
|
3685
|
+
setTitle: (title) => deps.broadcast({ type: 'extension_ui_request', id: randomUUID(), method: 'setTitle', title }),
|
|
3686
|
+
notify: (message, type) => {
|
|
3687
|
+
const notifyType = type === 'warn' ? 'warning' : type;
|
|
3688
|
+
deps.broadcast({ type: 'extension_ui_request', id: randomUUID(), method: 'notify', message, notifyType });
|
|
3689
|
+
},
|
|
3690
|
+
onTerminalInput: () => noop,
|
|
3691
|
+
setWorkingMessage: noop,
|
|
3692
|
+
setWorkingVisible: noop,
|
|
3693
|
+
setWorkingIndicator: noop,
|
|
3694
|
+
setHiddenThinkingLabel: noop,
|
|
3695
|
+
setFooter: noop,
|
|
3696
|
+
setHeader: noop,
|
|
3697
|
+
custom: () => Promise.resolve(undefined),
|
|
3698
|
+
pasteToEditor: noop,
|
|
3699
|
+
setEditorText: noop,
|
|
3700
|
+
getEditorText: () => '',
|
|
3701
|
+
addAutocompleteProvider: noop,
|
|
3702
|
+
setEditorComponent: noop,
|
|
3703
|
+
getEditorComponent: () => undefined,
|
|
3704
|
+
getAllThemes: () => [],
|
|
3705
|
+
getTheme: () => undefined,
|
|
3706
|
+
setTheme: noop,
|
|
3707
|
+
getToolsExpanded: () => false,
|
|
3708
|
+
setToolsExpanded: noop,
|
|
3709
|
+
};
|
|
3710
|
+
return ctx;
|
|
3711
|
+
}
|