@selesai/code 0.5.28 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/credential-print.d.ts +23 -0
- package/dist/cli/credential-print.d.ts.map +1 -0
- package/dist/cli/credential-print.js +117 -0
- package/dist/cli/credential-print.js.map +1 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +3 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +12 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +20 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +11 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +10 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +1 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/llama/provider.d.ts.map +1 -1
- package/dist/core/llama/provider.js +8 -3
- package/dist/core/llama/provider.js.map +1 -1
- package/dist/core/model-config.d.ts +30 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +6 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +20 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +5 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +13 -6
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +24 -11
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +66 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +19 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/core/tools/bash.d.ts +2 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +34 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +3 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
- package/dist/extensions/pi-intercom/README.md +102 -39
- package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
- package/dist/extensions/pi-intercom/broker/client.ts +315 -12
- package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
- package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
- package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
- package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
- package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
- package/dist/extensions/pi-intercom/config.test.ts +93 -0
- package/dist/extensions/pi-intercom/config.ts +55 -6
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
- package/dist/extensions/pi-intercom/cwd.ts +31 -0
- package/dist/extensions/pi-intercom/extension-api.ts +44 -0
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
- package/dist/extensions/pi-intercom/format-context.ts +32 -0
- package/dist/extensions/pi-intercom/index.ts +742 -145
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
- package/dist/extensions/pi-intercom/package.json +15 -5
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
- package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
- package/dist/extensions/pi-intercom/types.ts +94 -4
- package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
- package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
- package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
- package/dist/extensions/pi-subagents/CHANGELOG.md +88 -0
- package/dist/extensions/pi-subagents/LICENSE +21 -0
- package/dist/extensions/pi-subagents/README.md +158 -69
- package/dist/extensions/pi-subagents/agents/architect.md +5 -4
- package/dist/extensions/pi-subagents/agents/builder.md +6 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +3 -3
- package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -988
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +281 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
- package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +86 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +56 -5
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
- package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +214 -60
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
- package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +22 -1
- package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
- package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +196 -12
- package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
- package/dist/extensions/pi-subagents/src/tui/render.ts +55 -34
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +113 -8
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +62 -22
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +8 -6
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +199 -17
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +17 -12
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +39 -2
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +46 -2
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +43 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +10 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +16 -46
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/external-editor.d.ts +12 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -0
- package/dist/modes/interactive/external-editor.js +37 -0
- package/dist/modes/interactive/external-editor.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +72 -64
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +14 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +14 -5
- package/docs/environment-variables.md +86 -0
- package/docs/extensions.md +15 -5
- package/docs/index.md +1 -0
- package/docs/models.md +9 -2
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/docs/providers.md +21 -2
- package/docs/rpc.md +23 -6
- package/docs/session-format.md +2 -0
- package/docs/settings.md +1 -1
- package/docs/usage.md +0 -13
- package/examples/extensions/custom-compaction.ts +5 -2
- package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/handoff.ts +11 -3
- package/examples/extensions/message-renderer.ts +3 -3
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/summarize.ts +5 -2
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +3 -1
- package/package.json +16 -7
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const MAX_STATE_BYTES = 64 * 1024;
|
|
17
|
+
|
|
18
|
+
export interface StateEnvelope {
|
|
19
|
+
formatVersion: 1;
|
|
20
|
+
namespace: string;
|
|
21
|
+
revision: number;
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
payloadSha256: string;
|
|
24
|
+
payload: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StateCommitResult {
|
|
28
|
+
committed: boolean;
|
|
29
|
+
revision: number;
|
|
30
|
+
reason?: string;
|
|
31
|
+
payload?: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function serializePayload(payload: unknown): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const json = JSON.stringify(payload);
|
|
37
|
+
if (json === undefined || Buffer.byteLength(json, "utf8") > MAX_STATE_BYTES) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return json;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function payloadHash(payloadJson: string): string {
|
|
47
|
+
return createHash("sha256").update(payloadJson).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ExtensionStateManager {
|
|
51
|
+
private readonly states = new Map<string, { revision: number; payload: unknown }>();
|
|
52
|
+
private readonly stateDir: string;
|
|
53
|
+
|
|
54
|
+
constructor(runtimeDir: string) {
|
|
55
|
+
this.stateDir = join(runtimeDir, "extension-state");
|
|
56
|
+
mkdirSync(this.stateDir, { recursive: true, mode: 0o700 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private statePath(namespace: string): string {
|
|
60
|
+
const hash = createHash("sha256").update(namespace).digest("hex");
|
|
61
|
+
return join(this.stateDir, `${hash}.json`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private backupPath(namespace: string): string {
|
|
65
|
+
return `${this.statePath(namespace)}.bak`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private readEnvelope(filePath: string, namespace: string): StateEnvelope | null {
|
|
69
|
+
if (!existsSync(filePath)) return null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = readFileSync(filePath, "utf8");
|
|
73
|
+
const value: unknown = JSON.parse(content);
|
|
74
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
75
|
+
|
|
76
|
+
const envelope = value as Record<string, unknown>;
|
|
77
|
+
if (
|
|
78
|
+
envelope.formatVersion !== 1
|
|
79
|
+
|| envelope.namespace !== namespace
|
|
80
|
+
|| !Number.isSafeInteger(envelope.revision)
|
|
81
|
+
|| (envelope.revision as number) < 0
|
|
82
|
+
|| typeof envelope.updatedAt !== "number"
|
|
83
|
+
|| typeof envelope.payloadSha256 !== "string"
|
|
84
|
+
) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payloadJson = serializePayload(envelope.payload);
|
|
89
|
+
if (payloadJson === null || payloadHash(payloadJson) !== envelope.payloadSha256) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return envelope as unknown as StateEnvelope;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
loadState(namespace: string): { revision: number; payload: unknown } | null {
|
|
100
|
+
const cached = this.states.get(namespace);
|
|
101
|
+
if (cached) return cached;
|
|
102
|
+
|
|
103
|
+
const envelope = this.readEnvelope(this.statePath(namespace), namespace)
|
|
104
|
+
?? this.readEnvelope(this.backupPath(namespace), namespace);
|
|
105
|
+
if (!envelope) return null;
|
|
106
|
+
|
|
107
|
+
const state = { revision: envelope.revision, payload: envelope.payload };
|
|
108
|
+
this.states.set(namespace, state);
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
commitState(namespace: string, expectedRevision: number, payload: unknown): StateCommitResult {
|
|
113
|
+
const payloadJson = serializePayload(payload);
|
|
114
|
+
const current = this.loadState(namespace);
|
|
115
|
+
const currentRevision = current?.revision ?? 0;
|
|
116
|
+
|
|
117
|
+
if (payloadJson === null) {
|
|
118
|
+
return {
|
|
119
|
+
committed: false,
|
|
120
|
+
revision: currentRevision,
|
|
121
|
+
reason: "Invalid extension state or payload exceeds 64 KiB limit",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
|
125
|
+
return { committed: false, revision: currentRevision, reason: "Invalid expected revision" };
|
|
126
|
+
}
|
|
127
|
+
if (expectedRevision !== currentRevision) {
|
|
128
|
+
return {
|
|
129
|
+
committed: false,
|
|
130
|
+
revision: currentRevision,
|
|
131
|
+
reason: "Revision mismatch",
|
|
132
|
+
...(current ? { payload: current.payload } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const envelope: StateEnvelope = {
|
|
137
|
+
formatVersion: 1,
|
|
138
|
+
namespace,
|
|
139
|
+
revision: currentRevision + 1,
|
|
140
|
+
updatedAt: Date.now(),
|
|
141
|
+
payloadSha256: payloadHash(payloadJson),
|
|
142
|
+
payload,
|
|
143
|
+
};
|
|
144
|
+
const statePath = this.statePath(namespace);
|
|
145
|
+
const backupPath = this.backupPath(namespace);
|
|
146
|
+
const tempPath = `${statePath}.tmp.${process.pid}.${randomUUID()}`;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
writeFileSync(tempPath, JSON.stringify(envelope), { mode: 0o600 });
|
|
150
|
+
const file = openSync(tempPath, "r");
|
|
151
|
+
try {
|
|
152
|
+
fsyncSync(file);
|
|
153
|
+
} finally {
|
|
154
|
+
closeSync(file);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.readEnvelope(statePath, namespace)) {
|
|
158
|
+
copyFileSync(statePath, backupPath);
|
|
159
|
+
}
|
|
160
|
+
renameSync(tempPath, statePath);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const directory = openSync(dirname(statePath), "r");
|
|
164
|
+
try {
|
|
165
|
+
fsyncSync(directory);
|
|
166
|
+
} finally {
|
|
167
|
+
closeSync(directory);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Directory fsync is unavailable on some platforms.
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const state = { revision: envelope.revision, payload };
|
|
174
|
+
this.states.set(namespace, state);
|
|
175
|
+
return { committed: true, revision: envelope.revision };
|
|
176
|
+
} catch {
|
|
177
|
+
return { committed: false, revision: currentRevision, reason: "Failed to persist extension state" };
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(tempPath, { force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getCurrentRevision(namespace: string): number {
|
|
184
|
+
return this.loadState(namespace)?.revision ?? 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import type { BrokerMessage, SessionRegistration } from "../types.ts";
|
|
9
|
+
import { IntercomClient } from "./client.ts";
|
|
10
|
+
import { ExtensionStateManager } from "./extension-state.ts";
|
|
11
|
+
import { getTsxCliPath } from "./spawn.ts";
|
|
12
|
+
|
|
13
|
+
const repoDir = process.cwd();
|
|
14
|
+
|
|
15
|
+
function registration(name: string, startedAt: number, ownerEligible?: boolean): SessionRegistration {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
cwd: "/test",
|
|
19
|
+
model: "test-model",
|
|
20
|
+
pid: process.pid,
|
|
21
|
+
startedAt,
|
|
22
|
+
lastActivity: Date.now(),
|
|
23
|
+
...(ownerEligible === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { extensions: [{ namespace: "test/v1", ownerEligible }] }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function waitFor(
|
|
30
|
+
messages: BrokerMessage[],
|
|
31
|
+
predicate: (message: BrokerMessage) => boolean,
|
|
32
|
+
timeoutMs = 3000,
|
|
33
|
+
): Promise<BrokerMessage> {
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
const message = messages.find(predicate);
|
|
37
|
+
if (message) return message;
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
39
|
+
}
|
|
40
|
+
throw new Error("Timed out waiting for broker message");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function startBroker(agentDir: string): Promise<ChildProcessWithoutNullStreams> {
|
|
44
|
+
const broker = spawn(
|
|
45
|
+
process.execPath,
|
|
46
|
+
[getTsxCliPath(repoDir), path.join(repoDir, "broker", "broker.ts")],
|
|
47
|
+
{
|
|
48
|
+
cwd: repoDir,
|
|
49
|
+
env: { ...process.env, SELESAI_CODING_AGENT_DIR: agentDir },
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
54
|
+
const timeout = setTimeout(() => reject(new Error("Broker startup timed out")), 10_000);
|
|
55
|
+
broker.stdout.on("data", (chunk: Buffer) => {
|
|
56
|
+
if (chunk.toString().includes("Intercom broker started")) {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
broker.once("exit", (code, signal) => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
reject(new Error(`Broker exited before startup (${code ?? signal})`));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
await ready;
|
|
67
|
+
return broker;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function stopBroker(broker: ChildProcessWithoutNullStreams): Promise<void> {
|
|
71
|
+
if (broker.exitCode !== null) return;
|
|
72
|
+
broker.kill("SIGTERM");
|
|
73
|
+
await once(broker, "exit");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("extension bus negotiates, routes, elects an owner, and persists state", { concurrency: false, timeout: 30_000 }, async () => {
|
|
77
|
+
const agentDir = mkdtempSync(path.join(tmpdir(), "pi-intercom-extension-"));
|
|
78
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
79
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
80
|
+
const broker = await startBroker(agentDir);
|
|
81
|
+
const clients: IntercomClient[] = [];
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const invalidNamespace = new IntercomClient();
|
|
85
|
+
await assert.rejects(
|
|
86
|
+
invalidNamespace.connect({
|
|
87
|
+
...registration("invalid", Date.now()),
|
|
88
|
+
extensions: [{ namespace: "Invalid Namespace", ownerEligible: true }],
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const tooManyExtensions = new IntercomClient();
|
|
93
|
+
await assert.rejects(
|
|
94
|
+
tooManyExtensions.connect({
|
|
95
|
+
...registration("too-many", Date.now()),
|
|
96
|
+
extensions: Array.from({ length: 33 }, (_, index) => ({
|
|
97
|
+
namespace: `test-${index}/v1`,
|
|
98
|
+
ownerEligible: true,
|
|
99
|
+
})),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const owner = new IntercomClient();
|
|
104
|
+
const peer = new IntercomClient();
|
|
105
|
+
const legacy = new IntercomClient();
|
|
106
|
+
const late = new IntercomClient();
|
|
107
|
+
const peerOnlyA = new IntercomClient();
|
|
108
|
+
const peerOnlyB = new IntercomClient();
|
|
109
|
+
clients.push(owner, peer, legacy, late, peerOnlyA, peerOnlyB);
|
|
110
|
+
|
|
111
|
+
const ownerMessages: BrokerMessage[] = [];
|
|
112
|
+
const peerMessages: BrokerMessage[] = [];
|
|
113
|
+
const legacyMessages: BrokerMessage[] = [];
|
|
114
|
+
const lateMessages: BrokerMessage[] = [];
|
|
115
|
+
const peerOnlyBMessages: BrokerMessage[] = [];
|
|
116
|
+
const ownerErrors: Error[] = [];
|
|
117
|
+
const legacyErrors: Error[] = [];
|
|
118
|
+
owner.onBrokerMessage((message) => ownerMessages.push(message));
|
|
119
|
+
peer.onBrokerMessage((message) => peerMessages.push(message));
|
|
120
|
+
legacy.onBrokerMessage((message) => legacyMessages.push(message));
|
|
121
|
+
late.onBrokerMessage((message) => lateMessages.push(message));
|
|
122
|
+
peerOnlyB.onBrokerMessage((message) => peerOnlyBMessages.push(message));
|
|
123
|
+
owner.on("error", (error) => ownerErrors.push(error));
|
|
124
|
+
peer.on("error", () => {});
|
|
125
|
+
legacy.on("error", (error) => legacyErrors.push(error));
|
|
126
|
+
late.on("error", () => {});
|
|
127
|
+
peerOnlyA.on("error", () => {});
|
|
128
|
+
peerOnlyB.on("error", () => {});
|
|
129
|
+
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
await owner.connect(registration("owner", now - 1000, true), "owner-id");
|
|
132
|
+
|
|
133
|
+
const invalidReplacement = new IntercomClient();
|
|
134
|
+
await assert.rejects(invalidReplacement.connect({
|
|
135
|
+
...registration("invalid-replacement", now),
|
|
136
|
+
extensions: [{ namespace: "Invalid Namespace", ownerEligible: true }],
|
|
137
|
+
}, "owner-id"));
|
|
138
|
+
assert.equal((await owner.listSessions()).some((session) => session.id === "owner-id"), true);
|
|
139
|
+
await invalidReplacement.disconnect().catch(() => undefined);
|
|
140
|
+
|
|
141
|
+
await peer.connect(registration("peer", now, false), "peer-id");
|
|
142
|
+
await legacy.connect(registration("legacy", now), "legacy-id");
|
|
143
|
+
await late.connect(registration("late", 0), "late-id");
|
|
144
|
+
await peerOnlyA.connect({
|
|
145
|
+
...registration("peer-only-a", now + 2),
|
|
146
|
+
extensions: [{ namespace: "peer-only/v1", ownerEligible: false }],
|
|
147
|
+
}, "peer-only-a-id");
|
|
148
|
+
await peerOnlyB.connect({
|
|
149
|
+
...registration("peer-only-b", now + 3),
|
|
150
|
+
extensions: [{ namespace: "peer-only/v1", ownerEligible: false }],
|
|
151
|
+
}, "peer-only-b-id");
|
|
152
|
+
|
|
153
|
+
assert.equal(owner.supportsFeature("extension-bus-v1"), true);
|
|
154
|
+
assert.equal(legacy.supportsFeature("extension-bus-v1"), true, "new broker advertises support even to clients without capabilities");
|
|
155
|
+
|
|
156
|
+
const ownerEvent = await waitFor(ownerMessages, (message) => message.type === "extension_owner");
|
|
157
|
+
assert.equal(ownerEvent.type, "extension_owner");
|
|
158
|
+
assert.equal(ownerEvent.ownerId, "owner-id");
|
|
159
|
+
assert.ok(ownerEvent.ownerEpoch);
|
|
160
|
+
const peerOwnerEvent = await waitFor(peerMessages, (message) => message.type === "extension_owner");
|
|
161
|
+
assert.equal(peerOwnerEvent.type, "extension_owner");
|
|
162
|
+
assert.equal(peerOwnerEvent.ownerId, "owner-id");
|
|
163
|
+
|
|
164
|
+
late.updateExtensionCapabilities([{ namespace: "test/v1", ownerEligible: true }]);
|
|
165
|
+
const lateOwnerEvent = await waitFor(lateMessages, (message) => message.type === "extension_owner");
|
|
166
|
+
assert.equal(lateOwnerEvent.type, "extension_owner");
|
|
167
|
+
assert.equal(lateOwnerEvent.ownerId, "owner-id", "backdated client must not seize namespace ownership");
|
|
168
|
+
late.updateExtensionCapabilities([{ namespace: "test/v1", ownerEligible: false }]);
|
|
169
|
+
|
|
170
|
+
legacy.updateExtensionCapabilities([{ namespace: "other/v1", ownerEligible: false }]);
|
|
171
|
+
legacy.sendExtensionMessage({
|
|
172
|
+
type: "extension_publish",
|
|
173
|
+
namespace: "test/v1",
|
|
174
|
+
audience: "capable",
|
|
175
|
+
payload: { unauthorized: true },
|
|
176
|
+
});
|
|
177
|
+
const unauthorizedDeadline = Date.now() + 3000;
|
|
178
|
+
while (Date.now() < unauthorizedDeadline && legacyErrors.length === 0) {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
180
|
+
}
|
|
181
|
+
assert.match(legacyErrors[0]?.message ?? "", /does not have capability for this namespace/);
|
|
182
|
+
legacy.sendExtensionMessage({
|
|
183
|
+
type: "extension_state_commit",
|
|
184
|
+
namespace: "test/v1",
|
|
185
|
+
ownerEpoch: "unauthorized",
|
|
186
|
+
expectedRevision: 0,
|
|
187
|
+
payload: {},
|
|
188
|
+
});
|
|
189
|
+
const unauthorizedState = await waitFor(
|
|
190
|
+
legacyMessages,
|
|
191
|
+
(message) => message.type === "extension_state_result",
|
|
192
|
+
);
|
|
193
|
+
assert.equal(unauthorizedState.type, "extension_state_result");
|
|
194
|
+
assert.equal(unauthorizedState.committed, false);
|
|
195
|
+
assert.match(unauthorizedState.reason ?? "", /does not have capability for this namespace/);
|
|
196
|
+
|
|
197
|
+
owner.sendExtensionMessage({
|
|
198
|
+
type: "extension_publish",
|
|
199
|
+
namespace: "test/v1",
|
|
200
|
+
audience: "capable",
|
|
201
|
+
payload: "x".repeat(16 * 1024),
|
|
202
|
+
});
|
|
203
|
+
const oversizedDeadline = Date.now() + 3000;
|
|
204
|
+
while (Date.now() < oversizedDeadline && !ownerErrors.some((error) => /16 KiB/.test(error.message))) {
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
206
|
+
}
|
|
207
|
+
assert.equal(ownerErrors.some((error) => /16 KiB/.test(error.message)), true);
|
|
208
|
+
ownerErrors.length = 0;
|
|
209
|
+
|
|
210
|
+
peerOnlyA.sendExtensionMessage({
|
|
211
|
+
type: "extension_publish",
|
|
212
|
+
namespace: "peer-only/v1",
|
|
213
|
+
audience: "capable",
|
|
214
|
+
payload: { kind: "peer-broadcast" },
|
|
215
|
+
});
|
|
216
|
+
const peerBroadcast = await waitFor(peerOnlyBMessages, (message) => message.type === "extension_message");
|
|
217
|
+
assert.equal(peerBroadcast.type, "extension_message");
|
|
218
|
+
assert.equal(peerBroadcast.ownerId, undefined);
|
|
219
|
+
assert.equal(peerBroadcast.ownerEpoch, undefined);
|
|
220
|
+
assert.deepEqual(peerBroadcast.payload, { kind: "peer-broadcast" });
|
|
221
|
+
|
|
222
|
+
peer.sendExtensionMessage({
|
|
223
|
+
type: "extension_publish",
|
|
224
|
+
namespace: "test/v1",
|
|
225
|
+
audience: "owner",
|
|
226
|
+
payload: { kind: "owner-target" },
|
|
227
|
+
});
|
|
228
|
+
const ownerTarget = await waitFor(
|
|
229
|
+
ownerMessages,
|
|
230
|
+
(message) => message.type === "extension_message"
|
|
231
|
+
&& (message.payload as { kind?: string }).kind === "owner-target",
|
|
232
|
+
);
|
|
233
|
+
assert.equal(ownerTarget.type, "extension_message");
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
235
|
+
assert.equal(
|
|
236
|
+
peerMessages.some((message) => message.type === "extension_message"
|
|
237
|
+
&& (message.payload as { kind?: string }).kind === "owner-target"),
|
|
238
|
+
false,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
owner.sendExtensionMessage({
|
|
242
|
+
type: "extension_publish",
|
|
243
|
+
namespace: "test/v1",
|
|
244
|
+
audience: "capable",
|
|
245
|
+
ownerOnly: true,
|
|
246
|
+
ownerEpoch: ownerEvent.ownerEpoch,
|
|
247
|
+
payload: { kind: "assignment" },
|
|
248
|
+
});
|
|
249
|
+
const received = await waitFor(peerMessages, (message) => message.type === "extension_message");
|
|
250
|
+
assert.equal(received.type, "extension_message");
|
|
251
|
+
assert.deepEqual(received.payload, { kind: "assignment" });
|
|
252
|
+
const lateReceived = await waitFor(lateMessages, (message) => message.type === "extension_message");
|
|
253
|
+
assert.equal(lateReceived.type, "extension_message");
|
|
254
|
+
assert.deepEqual(lateReceived.payload, { kind: "assignment" });
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
256
|
+
assert.equal(legacyMessages.some((message) => message.type === "extension_message"), false);
|
|
257
|
+
|
|
258
|
+
owner.sendExtensionMessage({
|
|
259
|
+
type: "extension_state_commit",
|
|
260
|
+
namespace: "test/v1",
|
|
261
|
+
ownerEpoch: ownerEvent.ownerEpoch!,
|
|
262
|
+
expectedRevision: 0,
|
|
263
|
+
payload: { groups: ["alpha"] },
|
|
264
|
+
});
|
|
265
|
+
const committed = await waitFor(ownerMessages, (message) => message.type === "extension_state_result");
|
|
266
|
+
assert.equal(committed.type, "extension_state_result");
|
|
267
|
+
assert.equal(committed.committed, true);
|
|
268
|
+
assert.equal(committed.revision, 1);
|
|
269
|
+
const state = await waitFor(peerMessages, (message) => message.type === "extension_state");
|
|
270
|
+
assert.equal(state.type, "extension_state");
|
|
271
|
+
assert.deepEqual(state.payload, { groups: ["alpha"] });
|
|
272
|
+
|
|
273
|
+
owner.sendExtensionMessage({
|
|
274
|
+
type: "extension_publish",
|
|
275
|
+
namespace: "test/v1",
|
|
276
|
+
audience: "capable",
|
|
277
|
+
ownerOnly: true,
|
|
278
|
+
payload: {},
|
|
279
|
+
});
|
|
280
|
+
const missingEpochDeadline = Date.now() + 3000;
|
|
281
|
+
while (Date.now() < missingEpochDeadline && !ownerErrors.some((error) => /ownerEpoch required/.test(error.message))) {
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
283
|
+
}
|
|
284
|
+
assert.equal(ownerErrors.some((error) => /ownerEpoch required/.test(error.message)), true);
|
|
285
|
+
ownerErrors.length = 0;
|
|
286
|
+
|
|
287
|
+
owner.sendExtensionMessage({
|
|
288
|
+
type: "extension_publish",
|
|
289
|
+
namespace: "test/v1",
|
|
290
|
+
audience: "capable",
|
|
291
|
+
ownerOnly: true,
|
|
292
|
+
ownerEpoch: "stale",
|
|
293
|
+
payload: {},
|
|
294
|
+
});
|
|
295
|
+
await waitFor(
|
|
296
|
+
ownerMessages,
|
|
297
|
+
(message) => message.type === "error" && message.error === "Owner validation failed",
|
|
298
|
+
).catch(async () => {
|
|
299
|
+
const deadline = Date.now() + 3000;
|
|
300
|
+
while (Date.now() < deadline && ownerErrors.length === 0) {
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
302
|
+
}
|
|
303
|
+
assert.match(ownerErrors[0]?.message ?? "", /Owner validation failed/);
|
|
304
|
+
return { type: "error", error: ownerErrors[0]!.message } as BrokerMessage;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await owner.disconnect();
|
|
308
|
+
const noOwner = await waitFor(
|
|
309
|
+
peerMessages,
|
|
310
|
+
(message) => message.type === "extension_owner" && message.ownerId === undefined,
|
|
311
|
+
);
|
|
312
|
+
assert.equal(noOwner.type, "extension_owner");
|
|
313
|
+
|
|
314
|
+
const replacement = new IntercomClient();
|
|
315
|
+
clients.push(replacement);
|
|
316
|
+
const replacementMessages: BrokerMessage[] = [];
|
|
317
|
+
replacement.onBrokerMessage((message) => replacementMessages.push(message));
|
|
318
|
+
replacement.on("error", () => {});
|
|
319
|
+
await replacement.connect(registration("replacement", now + 1, true), "owner-id");
|
|
320
|
+
const replacementOwner = await waitFor(replacementMessages, (message) => message.type === "extension_owner");
|
|
321
|
+
assert.equal(replacementOwner.type, "extension_owner");
|
|
322
|
+
assert.notEqual(replacementOwner.ownerEpoch, ownerEvent.ownerEpoch);
|
|
323
|
+
const restored = await waitFor(replacementMessages, (message) => message.type === "extension_state");
|
|
324
|
+
assert.equal(restored.type, "extension_state");
|
|
325
|
+
assert.equal(restored.revision, 1);
|
|
326
|
+
} finally {
|
|
327
|
+
await Promise.all(clients.map((client) => client.disconnect().catch(() => undefined)));
|
|
328
|
+
await stopBroker(broker);
|
|
329
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
330
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
331
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("extension state compare-and-swap rejects stale and invalid revisions", () => {
|
|
336
|
+
const runtimeDir = mkdtempSync(path.join(tmpdir(), "pi-intercom-state-cas-"));
|
|
337
|
+
try {
|
|
338
|
+
const manager = new ExtensionStateManager(runtimeDir);
|
|
339
|
+
assert.deepEqual(manager.commitState("test/v1", 0, { version: 1 }), { committed: true, revision: 1 });
|
|
340
|
+
assert.deepEqual(manager.commitState("test/v1", 0, { version: 2 }), {
|
|
341
|
+
committed: false,
|
|
342
|
+
revision: 1,
|
|
343
|
+
reason: "Revision mismatch",
|
|
344
|
+
payload: { version: 1 },
|
|
345
|
+
});
|
|
346
|
+
assert.deepEqual(manager.commitState("test/v1", -1, { version: 2 }), {
|
|
347
|
+
committed: false,
|
|
348
|
+
revision: 1,
|
|
349
|
+
reason: "Invalid expected revision",
|
|
350
|
+
});
|
|
351
|
+
assert.deepEqual(manager.commitState("test/v1", 1, { version: 2 }), { committed: true, revision: 2 });
|
|
352
|
+
assert.deepEqual(manager.commitState("test/v1", 2, "x".repeat(64 * 1024)), {
|
|
353
|
+
committed: false,
|
|
354
|
+
revision: 2,
|
|
355
|
+
reason: "Invalid extension state or payload exceeds 64 KiB limit",
|
|
356
|
+
});
|
|
357
|
+
const circular: { self?: unknown } = {};
|
|
358
|
+
circular.self = circular;
|
|
359
|
+
assert.deepEqual(manager.commitState("test/v1", 2, circular), {
|
|
360
|
+
committed: false,
|
|
361
|
+
revision: 2,
|
|
362
|
+
reason: "Invalid extension state or payload exceeds 64 KiB limit",
|
|
363
|
+
});
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("extension state falls back to a valid backup", () => {
|
|
370
|
+
const runtimeDir = mkdtempSync(path.join(tmpdir(), "pi-intercom-state-"));
|
|
371
|
+
try {
|
|
372
|
+
const manager = new ExtensionStateManager(runtimeDir);
|
|
373
|
+
assert.equal(manager.commitState("test/v1", 0, { version: 1 }).committed, true);
|
|
374
|
+
assert.equal(manager.commitState("test/v1", 1, { version: 2 }).committed, true);
|
|
375
|
+
|
|
376
|
+
const stateDir = path.join(runtimeDir, "extension-state");
|
|
377
|
+
const files = readdirSync(stateDir);
|
|
378
|
+
const primary = files.find((file) => file.endsWith(".json"));
|
|
379
|
+
assert.ok(primary);
|
|
380
|
+
writeFileSync(path.join(stateDir, primary!), "corrupt", "utf8");
|
|
381
|
+
|
|
382
|
+
const recovered = new ExtensionStateManager(runtimeDir).loadState("test/v1");
|
|
383
|
+
assert.deepEqual(recovered, { revision: 1, payload: { version: 1 } });
|
|
384
|
+
} finally {
|
|
385
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createMessageReader, writeMessage } from "./framing.ts";
|
|
4
|
+
|
|
5
|
+
function framePayload(payload: Buffer): Buffer {
|
|
6
|
+
const header = Buffer.alloc(4);
|
|
7
|
+
header.writeUInt32BE(payload.length, 0);
|
|
8
|
+
return Buffer.concat([header, payload]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("createMessageReader handles normal fragmented frames", () => {
|
|
12
|
+
const messages: unknown[] = [];
|
|
13
|
+
const errors: Error[] = [];
|
|
14
|
+
const reader = createMessageReader(
|
|
15
|
+
(message) => messages.push(message),
|
|
16
|
+
(error) => errors.push(error),
|
|
17
|
+
64,
|
|
18
|
+
);
|
|
19
|
+
const frameA = framePayload(Buffer.from(JSON.stringify({ type: "one" }), "utf-8"));
|
|
20
|
+
const frameB = framePayload(Buffer.from(JSON.stringify({ type: "two" }), "utf-8"));
|
|
21
|
+
const combined = Buffer.concat([frameA, frameB]);
|
|
22
|
+
|
|
23
|
+
reader(combined.subarray(0, 2));
|
|
24
|
+
reader(combined.subarray(2, 7));
|
|
25
|
+
reader(combined.subarray(7));
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(messages, [{ type: "one" }, { type: "two" }]);
|
|
28
|
+
assert.deepEqual(errors, []);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("createMessageReader reassembles a fragmented frame after a header-boundary fast-path frame", () => {
|
|
32
|
+
const messages: unknown[] = [];
|
|
33
|
+
const errors: Error[] = [];
|
|
34
|
+
const reader = createMessageReader(
|
|
35
|
+
(message) => messages.push(message),
|
|
36
|
+
(error) => errors.push(error),
|
|
37
|
+
64,
|
|
38
|
+
);
|
|
39
|
+
const frameA = framePayload(Buffer.from(JSON.stringify({ a: 1 }), "utf-8"));
|
|
40
|
+
const frameB = framePayload(Buffer.from(JSON.stringify({ bb: "1234567890" }), "utf-8"));
|
|
41
|
+
|
|
42
|
+
reader(frameA.subarray(0, 4)); // chunk ends exactly at the end of frame A's header
|
|
43
|
+
reader(frameA.subarray(4)); // frame A's payload arrives whole (zero-copy fast path)
|
|
44
|
+
reader(frameB.subarray(0, 6)); // frame B header + partial payload (buffered path)
|
|
45
|
+
reader(frameB.subarray(6));
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(messages, [{ a: 1 }, { bb: "1234567890" }]);
|
|
48
|
+
assert.deepEqual(errors, []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("createMessageReader rejects an oversized declared frame", () => {
|
|
52
|
+
const messages: unknown[] = [];
|
|
53
|
+
const errors: Error[] = [];
|
|
54
|
+
const reader = createMessageReader(
|
|
55
|
+
(message) => messages.push(message),
|
|
56
|
+
(error) => errors.push(error),
|
|
57
|
+
8,
|
|
58
|
+
);
|
|
59
|
+
const oversizedFrame = framePayload(Buffer.from(JSON.stringify({ text: "too large" }), "utf-8"));
|
|
60
|
+
|
|
61
|
+
reader(oversizedFrame);
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(messages, []);
|
|
64
|
+
assert.equal(errors.length, 1);
|
|
65
|
+
assert.match(errors[0].message, /Intercom frame length \d+ exceeds maximum 8 bytes/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("createMessageReader rejects an oversized frame before retaining same-chunk payload bytes", () => {
|
|
69
|
+
const messages: unknown[] = [];
|
|
70
|
+
const errors: Error[] = [];
|
|
71
|
+
const reader = createMessageReader(
|
|
72
|
+
(message) => messages.push(message),
|
|
73
|
+
(error) => errors.push(error),
|
|
74
|
+
8,
|
|
75
|
+
);
|
|
76
|
+
const header = Buffer.alloc(4);
|
|
77
|
+
header.writeUInt32BE(9, 0);
|
|
78
|
+
|
|
79
|
+
reader(Buffer.concat([header, Buffer.alloc(1024 * 1024)]));
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(messages, []);
|
|
82
|
+
assert.equal(errors.length, 1);
|
|
83
|
+
assert.equal(errors[0].message, "Intercom frame length 9 exceeds maximum 8 bytes");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("createMessageReader rejects a partial oversized frame before buffering the payload", () => {
|
|
87
|
+
const messages: unknown[] = [];
|
|
88
|
+
const errors: Error[] = [];
|
|
89
|
+
const reader = createMessageReader(
|
|
90
|
+
(message) => messages.push(message),
|
|
91
|
+
(error) => errors.push(error),
|
|
92
|
+
8,
|
|
93
|
+
);
|
|
94
|
+
const header = Buffer.alloc(4);
|
|
95
|
+
header.writeUInt32BE(9, 0);
|
|
96
|
+
|
|
97
|
+
reader(header);
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(messages, []);
|
|
100
|
+
assert.equal(errors.length, 1);
|
|
101
|
+
assert.equal(errors[0].message, "Intercom frame length 9 exceeds maximum 8 bytes");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("writeMessage emits frames accepted by createMessageReader", () => {
|
|
105
|
+
const chunks: Buffer[] = [];
|
|
106
|
+
const socket = { write: (chunk: Buffer) => chunks.push(chunk) };
|
|
107
|
+
const messages: unknown[] = [];
|
|
108
|
+
const reader = createMessageReader((message) => messages.push(message), assert.fail, 64);
|
|
109
|
+
|
|
110
|
+
writeMessage(socket as never, { ok: true });
|
|
111
|
+
reader(Buffer.concat(chunks));
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(messages, [{ ok: true }]);
|
|
114
|
+
});
|