@phuetz/code-buddy 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +56 -0
- package/README.md +73 -16
- package/codebuddy-runtime.json +5 -5
- package/dist/agent/codebuddy-agent.d.ts +18 -0
- package/dist/agent/codebuddy-agent.js +45 -8
- package/dist/agent/execution/agent-executor.d.ts +0 -9
- package/dist/agent/execution/agent-executor.js +165 -12
- package/dist/agent/execution/context-pipeline.d.ts +9 -0
- package/dist/agent/execution/context-pipeline.js +64 -2
- package/dist/agent/execution/tool-loop-guard.d.ts +76 -0
- package/dist/agent/execution/tool-loop-guard.js +165 -0
- package/dist/agent/execution/tool-selection-strategy.js +19 -2
- package/dist/agent/facades/session-facade.d.ts +0 -6
- package/dist/agent/facades/session-facade.js +0 -45
- package/dist/agent/hermes-claw-migrate.d.ts +0 -21
- package/dist/agent/hermes-claw-migrate.js +9 -4
- package/dist/agent/hermes-protocol-gateways.js +8 -1
- package/dist/agent/repo-profiler.js +17 -4
- package/dist/agent/self-improvement/authored-tool-runtime.js +10 -2
- package/dist/agent/self-improvement/evolution/ast-novelty.d.ts +5 -0
- package/dist/agent/self-improvement/evolution/ast-novelty.js +58 -23
- package/dist/agent/self-improvement/evolution/code-variant-store.d.ts +3 -0
- package/dist/agent/self-improvement/evolution/evolution-engine.js +2 -0
- package/dist/agent/self-improvement/evolution/protected-paths.js +6 -0
- package/dist/agent/self-improvement/evolution/variant-fitness.d.ts +13 -2
- package/dist/agent/self-improvement/evolution/variant-fitness.js +184 -58
- package/dist/agent/self-improvement/evolution/worktree-scorer.d.ts +2 -0
- package/dist/agent/self-improvement/evolution/worktree-scorer.js +15 -0
- package/dist/agent/self-improvement/evolutionary-archive.d.ts +2 -1
- package/dist/agent/self-improvement/evolutionary-archive.js +34 -5
- package/dist/agent/self-improvement/proposal-store.d.ts +1 -0
- package/dist/agent/self-improvement/proposal-store.js +79 -17
- package/dist/agent/self-improvement/skill-apply-journal.d.ts +44 -0
- package/dist/agent/self-improvement/skill-apply-journal.js +178 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.d.ts +3 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.js +19 -0
- package/dist/agent/self-improvement/skill-behavior-gate.d.ts +34 -0
- package/dist/agent/self-improvement/skill-behavior-gate.js +96 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +22 -0
- package/dist/agent/self-improvement/skill-engine.js +385 -13
- package/dist/agent/self-improvement/skill-gate.d.ts +2 -0
- package/dist/agent/self-improvement/skill-gate.js +17 -2
- package/dist/agent/self-improvement/skill-mutator.d.ts +6 -1
- package/dist/agent/self-improvement/skill-mutator.js +51 -15
- package/dist/agent/self-improvement/skill-types.d.ts +2 -1
- package/dist/agent/self-improvement/types.d.ts +11 -0
- package/dist/agent/tool-handler.d.ts +1 -1
- package/dist/agent/tool-handler.js +28 -9
- package/dist/auth/profile-manager.js +1 -1
- package/dist/channels/companion-channel-turn.d.ts +53 -4
- package/dist/channels/companion-channel-turn.js +218 -16
- package/dist/channels/slash-parity.d.ts +4 -6
- package/dist/channels/slash-parity.js +9 -44
- package/dist/cli/session-commands.d.ts +9 -0
- package/dist/cli/session-commands.js +45 -5
- package/dist/cli/session-picker.d.ts +43 -0
- package/dist/cli/session-picker.js +128 -0
- package/dist/codebuddy/a2a-call-tool-defs.d.ts +26 -0
- package/dist/codebuddy/a2a-call-tool-defs.js +12 -0
- package/dist/codebuddy/fleet-tool-defs.d.ts +1 -0
- package/dist/codebuddy/fleet-tool-defs.js +24 -0
- package/dist/codebuddy/providers/chatgpt-headers.d.ts +18 -0
- package/dist/codebuddy/providers/chatgpt-headers.js +37 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +2 -1
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +10 -23
- package/dist/codebuddy/providers/provider-openai-compat.js +1 -1
- package/dist/codebuddy/ragchat-tool-defs.d.ts +31 -0
- package/dist/codebuddy/ragchat-tool-defs.js +19 -0
- package/dist/codebuddy/resource-catalog-tool-defs.d.ts +28 -0
- package/dist/codebuddy/resource-catalog-tool-defs.js +18 -0
- package/dist/codebuddy/tool-definitions/agent-tools.js +2 -2
- package/dist/codebuddy/tool-definitions/code-exec-tools.js +5 -1
- package/dist/codebuddy/tool-definitions/cron-tools.js +6 -0
- package/dist/codebuddy/tool-definitions/self-describe-tools.js +6 -1
- package/dist/codebuddy/tools.d.ts +0 -6
- package/dist/codebuddy/tools.js +25 -3
- package/dist/commands/cli/daemon-commands.js +3 -1
- package/dist/commands/cli/evolve-command.js +21 -5
- package/dist/commands/cli/fleet-collaboration-commands.d.ts +2 -0
- package/dist/commands/cli/fleet-collaboration-commands.js +57 -0
- package/dist/commands/cli/fleet-commands.js +29 -0
- package/dist/commands/cli/fleet-mission-commands.d.ts +3 -0
- package/dist/commands/cli/fleet-mission-commands.js +72 -0
- package/dist/commands/cli/fleet-rooms-commands.d.ts +10 -0
- package/dist/commands/cli/fleet-rooms-commands.js +289 -0
- package/dist/commands/cli/tools-commands.js +2 -1
- package/dist/commands/cli/triage-command.d.ts +8 -0
- package/dist/commands/cli/triage-command.js +72 -0
- package/dist/commands/cli/utility-commands.js +25 -1
- package/dist/commands/client-dispatcher.d.ts +7 -0
- package/dist/commands/client-dispatcher.js +73 -21
- package/dist/commands/cron-cli/index.d.ts +3 -1
- package/dist/commands/cron-cli/index.js +25 -0
- package/dist/commands/device-auth.d.ts +10 -0
- package/dist/commands/device-auth.js +91 -0
- package/dist/commands/enhanced-command-handler.d.ts +9 -0
- package/dist/commands/enhanced-command-handler.js +28 -6
- package/dist/commands/handlers/agent-handlers.d.ts +1 -1
- package/dist/commands/handlers/agent-handlers.js +4 -4
- package/dist/commands/handlers/channel-handlers.js +53 -2
- package/dist/commands/handlers/export-handlers.d.ts +3 -2
- package/dist/commands/handlers/export-handlers.js +55 -28
- package/dist/commands/handlers/extra-handlers.js +5 -1
- package/dist/commands/handlers/grill-me-handler.js +26 -18
- package/dist/commands/handlers/memory-handlers.d.ts +6 -2
- package/dist/commands/handlers/memory-handlers.js +35 -15
- package/dist/commands/handlers/missing-handlers.d.ts +2 -2
- package/dist/commands/handlers/missing-handlers.js +4 -4
- package/dist/commands/handlers/resources-handler.d.ts +12 -0
- package/dist/commands/handlers/resources-handler.js +52 -0
- package/dist/commands/handlers/starter-handlers.d.ts +1 -1
- package/dist/commands/handlers/starter-handlers.js +32 -2
- package/dist/commands/handlers/switch-handler.d.ts +1 -1
- package/dist/commands/handlers/switch-handler.js +15 -0
- package/dist/commands/handlers/test-handlers.js +4 -86
- package/dist/commands/handlers/ui-handlers.js +11 -5
- package/dist/commands/login-chatgpt.d.ts +8 -0
- package/dist/commands/login-chatgpt.js +17 -0
- package/dist/commands/mcp-import.d.ts +11 -0
- package/dist/commands/mcp-import.js +67 -0
- package/dist/commands/mcp-invoke.d.ts +2 -0
- package/dist/commands/mcp-invoke.js +68 -0
- package/dist/commands/mcp.js +4 -0
- package/dist/commands/pipeline.d.ts +7 -1
- package/dist/commands/pipeline.js +104 -7
- package/dist/commands/resources.d.ts +2 -0
- package/dist/commands/resources.js +64 -0
- package/dist/commands/skills-cli/index.js +35 -12
- package/dist/commands/slash/builtin-commands.js +16 -3
- package/dist/commands/slash/index.d.ts +3 -1
- package/dist/commands/slash/index.js +2 -0
- package/dist/commands/slash/surfaces.d.ts +53 -0
- package/dist/commands/slash/surfaces.js +177 -0
- package/dist/commands/slash/types.d.ts +17 -0
- package/dist/commands/try.d.ts +1 -1
- package/dist/commands/try.js +1 -1
- package/dist/companion/companion-identity.d.ts +43 -0
- package/dist/companion/companion-identity.js +136 -0
- package/dist/companion/companion-toolset.d.ts +66 -0
- package/dist/companion/companion-toolset.js +368 -0
- package/dist/companion/companion-turn.d.ts +10 -2
- package/dist/companion/companion-turn.js +38 -1
- package/dist/config/code-exec-policy.d.ts +25 -0
- package/dist/config/code-exec-policy.js +39 -0
- package/dist/config/model-tools.d.ts +9 -0
- package/dist/config/model-tools.js +4 -0
- package/dist/context/context-manager-v2.js +2 -2
- package/dist/context/lm-resizer-compressor.js +3 -2
- package/dist/context/lm-resizer-diagnostics.d.ts +9 -0
- package/dist/context/lm-resizer-diagnostics.js +30 -0
- package/dist/context/tool-output-masking.js +3 -4
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.js +53 -3
- package/dist/daemon/cron-agent-bridge.d.ts +6 -0
- package/dist/daemon/cron-agent-bridge.js +31 -4
- package/dist/doctor/index.d.ts +11 -2
- package/dist/doctor/index.js +84 -21
- package/dist/doctor/integrations.d.ts +179 -0
- package/dist/doctor/integrations.js +227 -0
- package/dist/doctor/triage.d.ts +108 -0
- package/dist/doctor/triage.js +304 -0
- package/dist/fleet/collaboration.d.ts +44 -0
- package/dist/fleet/collaboration.js +170 -0
- package/dist/fleet/fleet-listener.d.ts +1 -0
- package/dist/fleet/fleet-listener.js +87 -46
- package/dist/fleet/fleet-registry.d.ts +2 -0
- package/dist/fleet/resource-catalog.d.ts +245 -0
- package/dist/fleet/resource-catalog.js +211 -0
- package/dist/fleet/rooms/room-access.d.ts +138 -0
- package/dist/fleet/rooms/room-access.js +196 -0
- package/dist/fleet/rooms/room-client.d.ts +158 -0
- package/dist/fleet/rooms/room-client.js +685 -0
- package/dist/fleet/rooms/room-event.d.ts +139 -0
- package/dist/fleet/rooms/room-event.js +330 -0
- package/dist/fleet/rooms/room-filter.d.ts +48 -0
- package/dist/fleet/rooms/room-filter.js +152 -0
- package/dist/fleet/rooms/room-hub.d.ts +154 -0
- package/dist/fleet/rooms/room-hub.js +363 -0
- package/dist/fleet/rooms/room-identity.d.ts +26 -0
- package/dist/fleet/rooms/room-identity.js +82 -0
- package/dist/fleet/rooms/room-lock.d.ts +34 -0
- package/dist/fleet/rooms/room-lock.js +180 -0
- package/dist/fleet/rooms/room-observations.d.ts +7 -0
- package/dist/fleet/rooms/room-observations.js +61 -0
- package/dist/fleet/rooms/room-server.d.ts +28 -0
- package/dist/fleet/rooms/room-server.js +75 -0
- package/dist/fleet/rooms/room-store.d.ts +119 -0
- package/dist/fleet/rooms/room-store.js +448 -0
- package/dist/fleet/rooms/room-ws-bridge.d.ts +27 -0
- package/dist/fleet/rooms/room-ws-bridge.js +137 -0
- package/dist/fleet/sensory-mission-bridge.d.ts +42 -0
- package/dist/fleet/sensory-mission-bridge.js +136 -0
- package/dist/git/worktree-sessions.js +1 -0
- package/dist/harness/contract.d.ts +4 -4
- package/dist/harness/fleet-supervisor.d.ts +16 -0
- package/dist/harness/fleet-supervisor.js +64 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.js +4 -0
- package/dist/harness/mission-runner.d.ts +3 -0
- package/dist/harness/mission-runner.js +53 -0
- package/dist/harness/mission-store.d.ts +93 -0
- package/dist/harness/mission-store.js +297 -0
- package/dist/harness/tool-harness.d.ts +56 -0
- package/dist/harness/tool-harness.js +153 -0
- package/dist/hooks/use-enhanced-input.js +48 -4
- package/dist/hooks/use-input-handler.d.ts +1 -0
- package/dist/hooks/use-input-handler.js +82 -22
- package/dist/hooks/use-input-history.js +1 -0
- package/dist/identity/lisa-introspection.js +5 -2
- package/dist/identity/operational-self-model.d.ts +2 -0
- package/dist/identity/operational-self-model.js +4 -0
- package/dist/index.js +52 -6
- package/dist/mcp/client.js +6 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +121 -8
- package/dist/mcp/import-normalize.d.ts +12 -0
- package/dist/mcp/import-normalize.js +114 -0
- package/dist/mcp/transports.d.ts +10 -5
- package/dist/mcp/transports.js +61 -99
- package/dist/mcp/types.d.ts +5 -0
- package/dist/media/comfyui-recipe-contract.d.ts +7 -7
- package/dist/memory/facts-memory.d.ts +11 -1
- package/dist/memory/facts-memory.js +47 -8
- package/dist/memory/memory-candidate-queue.d.ts +2 -2
- package/dist/memory/memory-candidate-queue.js +6 -6
- package/dist/memory/persistent-memory.d.ts +13 -8
- package/dist/memory/persistent-memory.js +77 -30
- package/dist/nodes/device-node.js +5 -9
- package/dist/observability/mobile-supervision-gateway-contract.js +20 -7
- package/dist/observability/run-event-writer.d.ts +21 -0
- package/dist/observability/run-event-writer.js +81 -0
- package/dist/observability/run-store.d.ts +5 -0
- package/dist/observability/run-store.js +18 -13
- package/dist/observability/run-trajectory.js +1 -1
- package/dist/optimization/prompt-cache.d.ts +19 -2
- package/dist/optimization/prompt-cache.js +41 -6
- package/dist/orchestration/orchestrator.d.ts +21 -0
- package/dist/orchestration/orchestrator.js +122 -15
- package/dist/persistence/session-content.d.ts +8 -0
- package/dist/persistence/session-content.js +76 -0
- package/dist/persistence/session-handoff.d.ts +64 -0
- package/dist/persistence/session-handoff.js +80 -0
- package/dist/persistence/session-history.d.ts +4 -0
- package/dist/persistence/session-history.js +38 -0
- package/dist/persistence/session-store.d.ts +9 -0
- package/dist/persistence/session-store.js +48 -20
- package/dist/personas/persona-manager.js +17 -1
- package/dist/plugins/code-explorer/CodeExplorerManager.d.ts +10 -4
- package/dist/plugins/code-explorer/CodeExplorerManager.js +69 -16
- package/dist/plugins/plugin-manager.js +10 -1
- package/dist/prompts/prompt-manager.js +11 -4
- package/dist/prompts/system-base.js +6 -2
- package/dist/protocols/a2a/codebuddy-executor.d.ts +11 -2
- package/dist/protocols/a2a/codebuddy-executor.js +9 -8
- package/dist/protocols/a2a/index.d.ts +1 -1
- package/dist/protocols/a2a/jsonrpc-v1.d.ts +41 -0
- package/dist/protocols/a2a/jsonrpc-v1.js +145 -0
- package/dist/protocols/a2a/peer-config.d.ts +20 -0
- package/dist/protocols/a2a/peer-config.js +55 -0
- package/dist/providers/codex-oauth.js +30 -6
- package/dist/providers/model-provider-compat.js +2 -1
- package/dist/sandbox/os-sandbox.d.ts +1 -1
- package/dist/scheduler/cron-scheduler.d.ts +15 -2
- package/dist/scheduler/cron-scheduler.js +35 -4
- package/dist/scheduler/job-notepad.d.ts +69 -0
- package/dist/scheduler/job-notepad.js +339 -0
- package/dist/security/audit-logger.d.ts +1 -1
- package/dist/security/compute-confinement.d.ts +4 -0
- package/dist/security/compute-confinement.js +89 -0
- package/dist/security/session-encryption.d.ts +4 -0
- package/dist/security/session-encryption.js +31 -0
- package/dist/sensory/voice-loop.d.ts +17 -0
- package/dist/sensory/voice-loop.js +72 -15
- package/dist/server/auth/device-session-context.d.ts +9 -0
- package/dist/server/auth/device-session-context.js +13 -0
- package/dist/server/auth/device-store.d.ts +89 -0
- package/dist/server/auth/device-store.js +261 -0
- package/dist/server/auth/device-token.d.ts +3 -0
- package/dist/server/auth/device-token.js +6 -0
- package/dist/server/auth/jwt.js +4 -0
- package/dist/server/index.d.ts +0 -10
- package/dist/server/index.js +28 -0
- package/dist/server/middleware/auth.js +3 -2
- package/dist/server/routes/a2a-jsonrpc.d.ts +9 -0
- package/dist/server/routes/a2a-jsonrpc.js +66 -0
- package/dist/server/routes/a2a-protocol.d.ts +12 -0
- package/dist/server/routes/a2a-protocol.js +1 -1
- package/dist/server/routes/device-auth.d.ts +4 -0
- package/dist/server/routes/device-auth.js +40 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/websocket/handler.d.ts +13 -0
- package/dist/server/websocket/handler.js +102 -17
- package/dist/services/prompt-builder.d.ts +2 -0
- package/dist/services/prompt-builder.js +13 -5
- package/dist/services/runtime-settings-context.d.ts +73 -0
- package/dist/services/runtime-settings-context.js +69 -0
- package/dist/skills/executor.js +2 -0
- package/dist/skills/hub.js +16 -14
- package/dist/skills/local-inventory.d.ts +21 -0
- package/dist/skills/local-inventory.js +72 -0
- package/dist/skills/skill-loader.js +2 -2
- package/dist/skills/skill-usage-store.d.ts +49 -0
- package/dist/skills/skill-usage-store.js +121 -0
- package/dist/testing/ai-integration-tests.js +8 -5
- package/dist/themes/theme-manager.d.ts +12 -0
- package/dist/themes/theme-manager.js +29 -0
- package/dist/themes/theme-schema.d.ts +24 -24
- package/dist/tools/a2a-call-tool.d.ts +34 -0
- package/dist/tools/a2a-call-tool.js +75 -0
- package/dist/tools/bash/streaming-executor.d.ts +1 -0
- package/dist/tools/bash/streaming-executor.js +78 -90
- package/dist/tools/code-exec-preflight-runner.d.ts +41 -0
- package/dist/tools/code-exec-preflight-runner.js +254 -0
- package/dist/tools/code-exec-preflight.d.ts +44 -0
- package/dist/tools/code-exec-preflight.js +501 -0
- package/dist/tools/code-exec-tool.d.ts +48 -1
- package/dist/tools/code-exec-tool.js +191 -47
- package/dist/tools/comment-watcher.js +29 -12
- package/dist/tools/core-code-inspection.d.ts +56 -0
- package/dist/tools/core-code-inspection.js +86 -0
- package/dist/tools/create-skill-tool.d.ts +2 -3
- package/dist/tools/create-skill-tool.js +16 -75
- package/dist/tools/cronjob-tool.d.ts +1 -0
- package/dist/tools/cronjob-tool.js +1 -0
- package/dist/tools/env-doctor-tool.d.ts +2 -0
- package/dist/tools/env-doctor-tool.js +4 -3
- package/dist/tools/execute-code-rpc-invoker.d.ts +1 -1
- package/dist/tools/execute-code-rpc-invoker.js +35 -19
- package/dist/tools/execute-code-runner.d.ts +2 -0
- package/dist/tools/execute-code-runner.js +52 -17
- package/dist/tools/fleet-room-tool.d.ts +104 -0
- package/dist/tools/fleet-room-tool.js +124 -0
- package/dist/tools/integration-tool-hints.d.ts +4 -0
- package/dist/tools/integration-tool-hints.js +19 -0
- package/dist/tools/list-peers-tool.d.ts +3 -0
- package/dist/tools/list-peers-tool.js +11 -4
- package/dist/tools/media-generation-tool.d.ts +16 -1
- package/dist/tools/media-generation-tool.js +341 -9
- package/dist/tools/merge-conflict-tool.js +53 -25
- package/dist/tools/metadata.d.ts +1 -5
- package/dist/tools/metadata.js +21 -28
- package/dist/tools/ragchat-tool.d.ts +39 -0
- package/dist/tools/ragchat-tool.js +105 -0
- package/dist/tools/registry/fleet-tools.d.ts +9 -0
- package/dist/tools/registry/fleet-tools.js +22 -1
- package/dist/tools/registry/knowledge-tools.d.ts +2 -2
- package/dist/tools/registry/knowledge-tools.js +2 -2
- package/dist/tools/registry/lessons-tools.d.ts +7 -0
- package/dist/tools/registry/lessons-tools.js +43 -3
- package/dist/tools/registry/memory-tools.d.ts +1 -1
- package/dist/tools/registry/memory-tools.js +8 -8
- package/dist/tools/registry/research-tools.d.ts +1 -0
- package/dist/tools/registry/research-tools.js +4 -13
- package/dist/tools/registry/self-describe-tools.d.ts +1 -1
- package/dist/tools/registry/self-describe-tools.js +43 -30
- package/dist/tools/resource-catalog-tool.d.ts +38 -0
- package/dist/tools/resource-catalog-tool.js +46 -0
- package/dist/tools/skills-inspection-tool.js +30 -1
- package/dist/tools/stream-tool-output.d.ts +3 -0
- package/dist/tools/stream-tool-output.js +33 -0
- package/dist/tools/tool-call-scheduler.d.ts +10 -0
- package/dist/tools/tool-call-scheduler.js +58 -0
- package/dist/tools/tool-effect.d.ts +5 -0
- package/dist/tools/tool-effect.js +26 -0
- package/dist/tools/tool-search.d.ts +12 -11
- package/dist/tools/tool-search.js +67 -26
- package/dist/tools/types.d.ts +6 -0
- package/dist/ui/components/ChatHistory.d.ts +1 -1
- package/dist/ui/components/ChatHistory.js +2 -2
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +42 -88
- package/dist/ui/components/ChatInterface.d.ts +4 -2
- package/dist/ui/components/ChatInterface.js +27 -63
- package/dist/ui/components/CommandSuggestions.d.ts +0 -1
- package/dist/ui/components/CommandSuggestions.js +4 -6
- package/dist/ui/components/FileAutocomplete.js +1 -1
- package/dist/ui/components/KeyboardHelp.js +8 -6
- package/dist/ui/components/ModelSelection.js +6 -5
- package/dist/ui/components/StatusBar.d.ts +2 -1
- package/dist/ui/components/StatusBar.js +8 -7
- package/dist/ui/context/theme-context.js +3 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/utils/markdown-renderer.js +4 -13
- package/dist/utils/atomic-write.d.ts +22 -0
- package/dist/utils/atomic-write.js +29 -0
- package/dist/utils/bounded-output.d.ts +16 -0
- package/dist/utils/bounded-output.js +63 -0
- package/dist/utils/device-store-file.d.ts +4 -0
- package/dist/utils/device-store-file.js +40 -0
- package/dist/utils/export-manager.d.ts +9 -0
- package/dist/utils/export-manager.js +19 -0
- package/dist/utils/first-use-hints.d.ts +25 -0
- package/dist/utils/first-use-hints.js +58 -0
- package/dist/utils/history-manager.js +1 -1
- package/dist/utils/update-notifier.js +16 -18
- package/dist/wizard/environment-detection.d.ts +5 -1
- package/dist/wizard/environment-detection.js +49 -7
- package/dist/wizard/onboarding.d.ts +5 -1
- package/dist/wizard/onboarding.js +90 -30
- package/dist/wizard/provider-onboarding.d.ts +2 -0
- package/dist/wizard/provider-onboarding.js +55 -17
- package/package.json +7 -3
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — durable append-only message ledger.
|
|
3
|
+
*
|
|
4
|
+
* Buzz stores events in Postgres and replays by `created_at` (`since`), which
|
|
5
|
+
* trusts every author's clock (`crates/buzz-acp/src/relay.rs::channel_since`).
|
|
6
|
+
* A fleet mixes machines whose clocks drift, so the hub also stamps each
|
|
7
|
+
* accepted event with a strictly increasing `seq`: reconnection resumes from
|
|
8
|
+
* the last `seq` a member saw, and a message signed with a late clock cannot
|
|
9
|
+
* fall behind the cursor. `storeId` names the ledger generation so a cursor
|
|
10
|
+
* from a wiped ledger is never trusted (same role as the cognition bus epoch).
|
|
11
|
+
*
|
|
12
|
+
* Guarantees, each covered by tests:
|
|
13
|
+
* - single writer: an exclusive directory lock ({@link RoomDirectoryLock}),
|
|
14
|
+
* re-checked with the ledger inode and size before every write or compaction;
|
|
15
|
+
* - durability: an append is `fsync`ed before `append()` returns, so the hub
|
|
16
|
+
* only acknowledges what survives a crash;
|
|
17
|
+
* - crash tail: bytes after the last newline (torn OR complete-but-unterminated)
|
|
18
|
+
* are copied to `ledger.jsonl.tail-*` and truncated BEFORE `seq` is reused;
|
|
19
|
+
* - strict order and integrity: every complete record must be valid, authentic
|
|
20
|
+
* and strictly increasing or the store fails closed;
|
|
21
|
+
* - compaction: eviction watermarks are persisted before the ledger rewrite,
|
|
22
|
+
* so a crash in between can only over-report a gap, never hide one;
|
|
23
|
+
* - duplicate detection: ids are remembered for the retained window only;
|
|
24
|
+
* - immutability: stored records are deep-frozen copies of the input.
|
|
25
|
+
*
|
|
26
|
+
* @module fleet/rooms/room-store
|
|
27
|
+
*/
|
|
28
|
+
import { randomUUID } from 'crypto';
|
|
29
|
+
import fs from 'fs';
|
|
30
|
+
import path from 'path';
|
|
31
|
+
import { writeFileAtomicSync, writeJsonAtomicSync } from '../../utils/atomic-write.js';
|
|
32
|
+
import { getCodeBuddyPath } from '../../utils/codebuddy-home.js';
|
|
33
|
+
import { logger } from '../../utils/logger.js';
|
|
34
|
+
import { checkRoomEventShape, freezeRoomEvent, isValidRoomId, ROOM_MESSAGE_KIND, roomOf, verifyRoomEvent, } from './room-event.js';
|
|
35
|
+
import { DEFAULT_ROOM_HISTORY_LIMIT, MAX_ROOM_HISTORY_LIMIT, roomFilterMatches, roomFiltersMatch, roomsReferenced, } from './room-filter.js';
|
|
36
|
+
import { RoomDirectoryLock } from './room-lock.js';
|
|
37
|
+
export const DEFAULT_MAX_EVENTS_PER_ROOM = 2_000;
|
|
38
|
+
export const DEFAULT_MAX_ROOMS = 1_024;
|
|
39
|
+
export const DEFAULT_MAX_LEDGER_BYTES = 128 * 1024 * 1024;
|
|
40
|
+
const DEFAULT_COMPACT_AFTER_EVICTIONS = 500;
|
|
41
|
+
const LEDGER_FILE = 'ledger.jsonl';
|
|
42
|
+
const META_FILE = 'meta.json';
|
|
43
|
+
const RECORD_FIELDS = new Set(['v', 'seq', 'receivedAt', 'event']);
|
|
44
|
+
export class RoomStoreError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
constructor(message, code) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.name = 'RoomStoreError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function positiveInteger(name, value, fallback) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return fallback;
|
|
55
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
56
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function isRoomMeta(value) {
|
|
61
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
62
|
+
return false;
|
|
63
|
+
const meta = value;
|
|
64
|
+
return meta.version === 1 &&
|
|
65
|
+
typeof meta.storeId === 'string' && /^[0-9a-f-]{36}$/.test(meta.storeId) &&
|
|
66
|
+
!!meta.evicted && typeof meta.evicted === 'object' && !Array.isArray(meta.evicted) &&
|
|
67
|
+
Object.entries(meta.evicted).every(([room, seq]) => isValidRoomId(room) && Number.isSafeInteger(seq) && seq >= 0);
|
|
68
|
+
}
|
|
69
|
+
function freezeRecord(seq, receivedAt, event) {
|
|
70
|
+
return Object.freeze({ seq, receivedAt, event });
|
|
71
|
+
}
|
|
72
|
+
export function defaultRoomStoreDirectory() {
|
|
73
|
+
return process.env.CODEBUDDY_FLEET_ROOMS_DIR || getCodeBuddyPath('fleet', 'rooms');
|
|
74
|
+
}
|
|
75
|
+
export class RoomStore {
|
|
76
|
+
directory;
|
|
77
|
+
storeId;
|
|
78
|
+
ledgerPath;
|
|
79
|
+
metaPath;
|
|
80
|
+
maxEventsPerRoom;
|
|
81
|
+
maxRooms;
|
|
82
|
+
maxLedgerBytes;
|
|
83
|
+
compactAfterEvictions;
|
|
84
|
+
lock;
|
|
85
|
+
byRoom = new Map();
|
|
86
|
+
byId = new Map();
|
|
87
|
+
evicted = {};
|
|
88
|
+
fd;
|
|
89
|
+
expectedSize = 0;
|
|
90
|
+
lastSeq = 0;
|
|
91
|
+
evictionsSinceCompaction = 0;
|
|
92
|
+
closed = false;
|
|
93
|
+
failure;
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.maxEventsPerRoom = positiveInteger('maxEventsPerRoom', options.maxEventsPerRoom, DEFAULT_MAX_EVENTS_PER_ROOM);
|
|
96
|
+
this.maxRooms = positiveInteger('maxRooms', options.maxRooms, DEFAULT_MAX_ROOMS);
|
|
97
|
+
this.maxLedgerBytes = positiveInteger('maxLedgerBytes', options.maxLedgerBytes, DEFAULT_MAX_LEDGER_BYTES);
|
|
98
|
+
this.compactAfterEvictions = positiveInteger('compactAfterEvictions', options.compactAfterEvictions, DEFAULT_COMPACT_AFTER_EVICTIONS);
|
|
99
|
+
this.directory = options.directory ?? defaultRoomStoreDirectory();
|
|
100
|
+
this.ledgerPath = path.join(this.directory, LEDGER_FILE);
|
|
101
|
+
this.metaPath = path.join(this.directory, META_FILE);
|
|
102
|
+
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
103
|
+
this.lock = RoomDirectoryLock.acquire(this.directory);
|
|
104
|
+
try {
|
|
105
|
+
const meta = this.readMeta();
|
|
106
|
+
this.storeId = meta.storeId;
|
|
107
|
+
for (const [room, seq] of Object.entries(meta.evicted))
|
|
108
|
+
this.evicted[room] = seq;
|
|
109
|
+
this.lastSeq = Math.max(0, ...Object.values(this.evicted));
|
|
110
|
+
if (!fs.existsSync(this.ledgerPath)) {
|
|
111
|
+
// Creating through the atomic helper fsyncs both the empty file and its
|
|
112
|
+
// directory entry. A plain open('a') can disappear after a power loss.
|
|
113
|
+
writeFileAtomicSync(this.ledgerPath, '', { mode: 0o600 });
|
|
114
|
+
}
|
|
115
|
+
this.load();
|
|
116
|
+
this.fd = fs.openSync(this.ledgerPath, 'a', 0o600);
|
|
117
|
+
this.expectedSize = fs.fstatSync(this.fd).size;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (this.fd !== undefined)
|
|
121
|
+
fs.closeSync(this.fd);
|
|
122
|
+
this.lock.release();
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
get latestSeq() {
|
|
127
|
+
return this.lastSeq;
|
|
128
|
+
}
|
|
129
|
+
readMeta() {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(fs.readFileSync(this.metaPath, 'utf8'));
|
|
132
|
+
if (isRoomMeta(parsed)) {
|
|
133
|
+
if (fs.existsSync(this.ledgerPath))
|
|
134
|
+
return parsed;
|
|
135
|
+
logger.warn('[fleet-rooms] ledger missing for valid meta.json; starting a new ledger generation');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
logger.warn('[fleet-rooms] invalid meta.json; starting a new ledger generation');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error.code !== 'ENOENT') {
|
|
143
|
+
logger.warn('[fleet-rooms] unreadable meta.json; starting a new ledger generation');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const meta = { version: 1, storeId: randomUUID(), evicted: {} };
|
|
147
|
+
// Records written under an unknown generation are kept aside, never
|
|
148
|
+
// served under the new storeId and never deleted.
|
|
149
|
+
if (fs.existsSync(this.ledgerPath)) {
|
|
150
|
+
const orphan = `${this.ledgerPath}.orphan-${Date.now()}`;
|
|
151
|
+
fs.renameSync(this.ledgerPath, orphan);
|
|
152
|
+
logger.warn('[fleet-rooms] ledger without valid meta moved aside', { orphan });
|
|
153
|
+
}
|
|
154
|
+
writeJsonAtomicSync(this.metaPath, meta);
|
|
155
|
+
return meta;
|
|
156
|
+
}
|
|
157
|
+
load() {
|
|
158
|
+
let size;
|
|
159
|
+
try {
|
|
160
|
+
size = fs.statSync(this.ledgerPath).size;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error.code === 'ENOENT')
|
|
164
|
+
return;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
if (size > this.maxLedgerBytes) {
|
|
168
|
+
throw new RoomStoreError(`room ledger is ${size} bytes, above maxLedgerBytes ${this.maxLedgerBytes}; refusing to load it`, 'FULL');
|
|
169
|
+
}
|
|
170
|
+
if (size === 0)
|
|
171
|
+
return;
|
|
172
|
+
const buffer = fs.readFileSync(this.ledgerPath);
|
|
173
|
+
const keep = buffer.lastIndexOf(0x0a) + 1;
|
|
174
|
+
if (keep < buffer.length)
|
|
175
|
+
this.quarantineTail(buffer.subarray(keep), keep);
|
|
176
|
+
const lines = buffer.subarray(0, keep).toString('utf8').split('\n');
|
|
177
|
+
lines.pop();
|
|
178
|
+
let previousSeq = 0;
|
|
179
|
+
for (const [index, line] of lines.entries()) {
|
|
180
|
+
let record;
|
|
181
|
+
try {
|
|
182
|
+
record = this.parseRecord(line);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error instanceof RoomStoreError && error.code === 'CORRUPT') {
|
|
186
|
+
throw new RoomStoreError(`room ledger line ${index + 1} is corrupt: ${error.message}`, 'CORRUPT');
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
if (record.seq <= previousSeq) {
|
|
191
|
+
throw new RoomStoreError(`room ledger line ${index + 1} does not have an increasing seq`, 'CORRUPT');
|
|
192
|
+
}
|
|
193
|
+
previousSeq = record.seq;
|
|
194
|
+
const room = roomOf(record.event);
|
|
195
|
+
// The watermark is committed before compaction replaces the ledger. If a
|
|
196
|
+
// crash leaves the old ledger behind, never resurrect records it retired.
|
|
197
|
+
if (record.seq <= (this.evicted[room] ?? 0))
|
|
198
|
+
continue;
|
|
199
|
+
if (this.byId.has(record.event.id)) {
|
|
200
|
+
throw new RoomStoreError(`room ledger line ${index + 1} duplicates a retained event id`, 'CORRUPT');
|
|
201
|
+
}
|
|
202
|
+
this.insert(record);
|
|
203
|
+
}
|
|
204
|
+
this.lastSeq = Math.max(this.lastSeq, previousSeq);
|
|
205
|
+
}
|
|
206
|
+
/** Move the bytes after the last newline aside and cut them from the ledger. */
|
|
207
|
+
quarantineTail(tail, keep) {
|
|
208
|
+
const tailPath = `${this.ledgerPath}.tail-${Date.now()}-${process.pid}`;
|
|
209
|
+
fs.writeFileSync(tailPath, tail, { mode: 0o600, flag: 'wx' });
|
|
210
|
+
const fd = fs.openSync(this.ledgerPath, 'r+');
|
|
211
|
+
try {
|
|
212
|
+
fs.ftruncateSync(fd, keep);
|
|
213
|
+
fs.fsyncSync(fd);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
fs.closeSync(fd);
|
|
217
|
+
}
|
|
218
|
+
logger.warn('[fleet-rooms] unterminated ledger tail quarantined', { tailPath, bytes: tail.length });
|
|
219
|
+
}
|
|
220
|
+
parseRecord(line) {
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(line);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new RoomStoreError('record is not valid JSON', 'CORRUPT');
|
|
227
|
+
}
|
|
228
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
229
|
+
throw new RoomStoreError('record must be an object', 'CORRUPT');
|
|
230
|
+
}
|
|
231
|
+
const value = parsed;
|
|
232
|
+
if (Object.keys(value).some((key) => !RECORD_FIELDS.has(key))) {
|
|
233
|
+
throw new RoomStoreError('record has an unknown field', 'CORRUPT');
|
|
234
|
+
}
|
|
235
|
+
if (value.v !== 1 || !Number.isSafeInteger(value.seq) || value.seq <= 0 ||
|
|
236
|
+
!Number.isSafeInteger(value.receivedAt) || value.receivedAt < 0) {
|
|
237
|
+
throw new RoomStoreError('record envelope is invalid', 'CORRUPT');
|
|
238
|
+
}
|
|
239
|
+
const shape = checkRoomEventShape(value.event);
|
|
240
|
+
if (!shape.ok || shape.event.kind !== ROOM_MESSAGE_KIND || !roomOf(shape.event)) {
|
|
241
|
+
throw new RoomStoreError('record event shape is invalid', 'CORRUPT');
|
|
242
|
+
}
|
|
243
|
+
const verified = verifyRoomEvent(shape.event);
|
|
244
|
+
if (!verified.ok) {
|
|
245
|
+
throw new RoomStoreError(`record event is not authentic: ${verified.reason}`, 'CORRUPT');
|
|
246
|
+
}
|
|
247
|
+
return freezeRecord(value.seq, value.receivedAt, freezeRoomEvent(shape.event));
|
|
248
|
+
}
|
|
249
|
+
insert(record) {
|
|
250
|
+
const room = roomOf(record.event);
|
|
251
|
+
let records = this.byRoom.get(room);
|
|
252
|
+
if (!records) {
|
|
253
|
+
if (this.byRoom.size >= this.maxRooms) {
|
|
254
|
+
throw new RoomStoreError(`room ledger already holds maxRooms ${this.maxRooms}`, 'FULL');
|
|
255
|
+
}
|
|
256
|
+
records = [];
|
|
257
|
+
this.byRoom.set(room, records);
|
|
258
|
+
}
|
|
259
|
+
records.push(record);
|
|
260
|
+
this.byId.set(record.event.id, record);
|
|
261
|
+
this.lastSeq = Math.max(this.lastSeq, record.seq);
|
|
262
|
+
while (records.length > this.maxEventsPerRoom) {
|
|
263
|
+
const oldest = records.shift();
|
|
264
|
+
if (!oldest)
|
|
265
|
+
break;
|
|
266
|
+
this.byId.delete(oldest.event.id);
|
|
267
|
+
this.evicted[room] = Math.max(this.evicted[room] ?? 0, oldest.seq);
|
|
268
|
+
this.evictionsSinceCompaction++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
assertWritable() {
|
|
272
|
+
if (this.closed)
|
|
273
|
+
throw new RoomStoreError('room store is closed', 'CLOSED');
|
|
274
|
+
if (this.failure)
|
|
275
|
+
throw new RoomStoreError(`room store stopped after a failure: ${this.failure.message}`, 'CONFLICT');
|
|
276
|
+
}
|
|
277
|
+
/** Stop at the first sign that another process touched the ledger. */
|
|
278
|
+
assertExclusive() {
|
|
279
|
+
const fail = (reason) => {
|
|
280
|
+
this.failure = new Error(reason);
|
|
281
|
+
throw new RoomStoreError(reason, 'CONFLICT');
|
|
282
|
+
};
|
|
283
|
+
if (!this.lock.isHeld())
|
|
284
|
+
fail('room ledger lock is no longer held by this process');
|
|
285
|
+
let onDisk;
|
|
286
|
+
try {
|
|
287
|
+
onDisk = fs.statSync(this.ledgerPath);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return fail('room ledger disappeared');
|
|
291
|
+
}
|
|
292
|
+
const mine = fs.fstatSync(this.fd);
|
|
293
|
+
if (onDisk.ino !== mine.ino || onDisk.dev !== mine.dev || mine.size !== this.expectedSize) {
|
|
294
|
+
fail('room ledger was modified by another writer');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
get(id) {
|
|
298
|
+
return this.byId.get(id);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Durably persist an authentic room message. Idempotency is guaranteed for
|
|
302
|
+
* ids still in the retained window; an evicted id may be stored again. The
|
|
303
|
+
* input is copied and the returned record is deep-frozen.
|
|
304
|
+
*/
|
|
305
|
+
append(input, receivedAt = Date.now()) {
|
|
306
|
+
this.assertWritable();
|
|
307
|
+
const shape = checkRoomEventShape(input);
|
|
308
|
+
if (!shape.ok)
|
|
309
|
+
throw new RoomStoreError(shape.reason, 'INVALID');
|
|
310
|
+
const room = roomOf(shape.event);
|
|
311
|
+
if (shape.event.kind !== ROOM_MESSAGE_KIND || !room) {
|
|
312
|
+
throw new RoomStoreError('only room messages with exactly one valid h tag can be stored', 'INVALID');
|
|
313
|
+
}
|
|
314
|
+
const verified = verifyRoomEvent(shape.event);
|
|
315
|
+
if (!verified.ok) {
|
|
316
|
+
throw new RoomStoreError(`room event is not authentic: ${verified.reason}`, 'INVALID');
|
|
317
|
+
}
|
|
318
|
+
if (!Number.isSafeInteger(receivedAt) || receivedAt < 0) {
|
|
319
|
+
throw new RoomStoreError('receivedAt must be a non-negative integer', 'INVALID');
|
|
320
|
+
}
|
|
321
|
+
const existing = this.byId.get(shape.event.id);
|
|
322
|
+
if (existing)
|
|
323
|
+
return { status: 'duplicate', record: existing };
|
|
324
|
+
if (!this.byRoom.has(room) && this.byRoom.size >= this.maxRooms) {
|
|
325
|
+
throw new RoomStoreError(`room ledger already holds maxRooms ${this.maxRooms}`, 'FULL');
|
|
326
|
+
}
|
|
327
|
+
if (this.lastSeq >= Number.MAX_SAFE_INTEGER) {
|
|
328
|
+
throw new RoomStoreError('room ledger sequence space is exhausted', 'FULL');
|
|
329
|
+
}
|
|
330
|
+
const event = freezeRoomEvent(shape.event);
|
|
331
|
+
const seq = this.lastSeq + 1;
|
|
332
|
+
const bytes = Buffer.from(`${JSON.stringify({ v: 1, seq, receivedAt, event })}\n`, 'utf8');
|
|
333
|
+
if (this.expectedSize + bytes.length > this.maxLedgerBytes) {
|
|
334
|
+
this.compact();
|
|
335
|
+
if (this.expectedSize + bytes.length > this.maxLedgerBytes) {
|
|
336
|
+
throw new RoomStoreError('room ledger is full', 'FULL');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.assertExclusive();
|
|
340
|
+
try {
|
|
341
|
+
let offset = 0;
|
|
342
|
+
while (offset < bytes.length) {
|
|
343
|
+
offset += fs.writeSync(this.fd, bytes, offset, bytes.length - offset);
|
|
344
|
+
}
|
|
345
|
+
fs.fsyncSync(this.fd);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
// The ledger may end with a partial line: stop writing; the next open
|
|
349
|
+
// quarantines that tail before any seq is reused.
|
|
350
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
351
|
+
throw new RoomStoreError(`room ledger write failed: ${this.failure.message}`, 'CONFLICT');
|
|
352
|
+
}
|
|
353
|
+
this.expectedSize += bytes.length;
|
|
354
|
+
const record = freezeRecord(seq, receivedAt, event);
|
|
355
|
+
this.insert(record);
|
|
356
|
+
if (this.evictionsSinceCompaction >= this.compactAfterEvictions) {
|
|
357
|
+
try {
|
|
358
|
+
this.compact();
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
// The append itself was fsynced before maintenance began. Acknowledge
|
|
362
|
+
// that durable record, but stop later writes because compaction may
|
|
363
|
+
// have replaced or closed the active ledger.
|
|
364
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
365
|
+
logger.error('[fleet-rooms] post-append compaction failed; store is now read-only', {
|
|
366
|
+
error: this.failure.message,
|
|
367
|
+
seq: record.seq,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { status: 'stored', record };
|
|
372
|
+
}
|
|
373
|
+
/** Rewrite the ledger with retained records; watermarks are persisted first. */
|
|
374
|
+
compact() {
|
|
375
|
+
this.assertWritable();
|
|
376
|
+
this.assertExclusive();
|
|
377
|
+
const retained = [...this.byRoom.values()].flat().sort((a, b) => a.seq - b.seq);
|
|
378
|
+
writeJsonAtomicSync(this.metaPath, { version: 1, storeId: this.storeId, evicted: { ...this.evicted } });
|
|
379
|
+
const body = retained
|
|
380
|
+
.map((record) => JSON.stringify({ v: 1, seq: record.seq, receivedAt: record.receivedAt, event: record.event }))
|
|
381
|
+
.join('\n');
|
|
382
|
+
fs.closeSync(this.fd);
|
|
383
|
+
this.fd = undefined;
|
|
384
|
+
try {
|
|
385
|
+
writeFileAtomicSync(this.ledgerPath, retained.length > 0 ? `${body}\n` : '');
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
this.fd = fs.openSync(this.ledgerPath, 'a', 0o600);
|
|
389
|
+
this.expectedSize = fs.fstatSync(this.fd).size;
|
|
390
|
+
}
|
|
391
|
+
this.evictionsSinceCompaction = 0;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* True when records of these rooms with `seq > afterSeq` were evicted, i.e.
|
|
395
|
+
* a replay from `afterSeq` cannot be complete.
|
|
396
|
+
*/
|
|
397
|
+
hasGapAfter(rooms, afterSeq) {
|
|
398
|
+
return rooms.some((room) => (this.evicted[room] ?? 0) > afterSeq);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Replay (`afterSeq` set): every match after the cursor, oldest first, capped.
|
|
402
|
+
* History (no cursor): the newest `limit` matches of each filter, NIP-01
|
|
403
|
+
* style, returned oldest first.
|
|
404
|
+
*/
|
|
405
|
+
query(filters, options = {}) {
|
|
406
|
+
const candidates = roomsReferenced(filters).flatMap((room) => this.byRoom.get(room) ?? []);
|
|
407
|
+
if (options.afterSeq !== undefined) {
|
|
408
|
+
if (!Number.isSafeInteger(options.afterSeq) || options.afterSeq < 0) {
|
|
409
|
+
throw new RangeError('afterSeq must be a non-negative safe integer');
|
|
410
|
+
}
|
|
411
|
+
const cap = Math.min(MAX_ROOM_HISTORY_LIMIT, positiveInteger('maxRecords', options.maxRecords, MAX_ROOM_HISTORY_LIMIT));
|
|
412
|
+
const cursor = options.afterSeq;
|
|
413
|
+
const matches = candidates
|
|
414
|
+
.filter((record) => record.seq > cursor && roomFiltersMatch(filters, record.event))
|
|
415
|
+
.sort((a, b) => a.seq - b.seq);
|
|
416
|
+
return { records: matches.slice(0, cap), truncated: matches.length > cap };
|
|
417
|
+
}
|
|
418
|
+
const selected = new Map();
|
|
419
|
+
for (const filter of filters) {
|
|
420
|
+
const limit = filter.limit ?? DEFAULT_ROOM_HISTORY_LIMIT;
|
|
421
|
+
if (limit === 0)
|
|
422
|
+
continue;
|
|
423
|
+
const newestFirst = candidates
|
|
424
|
+
.filter((record) => roomFilterMatches(filter, record.event))
|
|
425
|
+
.sort((a, b) => b.event.created_at - a.event.created_at || b.seq - a.seq);
|
|
426
|
+
for (const record of newestFirst.slice(0, limit))
|
|
427
|
+
selected.set(record.seq, record);
|
|
428
|
+
}
|
|
429
|
+
return { records: [...selected.values()].sort((a, b) => a.seq - b.seq), truncated: false };
|
|
430
|
+
}
|
|
431
|
+
/** Close the append descriptor and release the directory lock. Idempotent. */
|
|
432
|
+
close() {
|
|
433
|
+
if (this.closed)
|
|
434
|
+
return;
|
|
435
|
+
this.closed = true;
|
|
436
|
+
if (this.fd !== undefined) {
|
|
437
|
+
try {
|
|
438
|
+
fs.closeSync(this.fd);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// already closed
|
|
442
|
+
}
|
|
443
|
+
this.fd = undefined;
|
|
444
|
+
}
|
|
445
|
+
this.lock.release();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
//# sourceMappingURL=room-store.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — wiring on the existing `/ws` endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Uses `registerWebSocketExtension` (no access to the socket or connection
|
|
5
|
+
* state) like the cognition bus. Transport gate: an authenticated principal
|
|
6
|
+
* holding `fleet:listen` (or `admin`), never an anonymous remote client. Room
|
|
7
|
+
* access itself is decided by the member key proof and the room policy.
|
|
8
|
+
*
|
|
9
|
+
* Client → hub (`{ type, id?, payload }`):
|
|
10
|
+
* - `fleet.rooms.hello` → `fleet.rooms.challenge { challenge, expiresAt }`
|
|
11
|
+
* - `fleet.rooms.auth { event }` → `fleet.rooms.auth { ok, pubkey?, name?, rooms?, message? }`
|
|
12
|
+
* - `fleet.rooms.publish { event }` → `fleet.rooms.ok { id, accepted, message, seq?, storeId }`
|
|
13
|
+
* - `fleet.rooms.subscribe { subId, filters, afterSeq?, storeId? }` →
|
|
14
|
+
* `fleet.rooms.event*`, then `fleet.rooms.eose` (or `fleet.rooms.closed`)
|
|
15
|
+
* - `fleet.rooms.close { subId }` → `fleet.rooms.unsubscribed { subId, closed }`
|
|
16
|
+
*
|
|
17
|
+
* Replies carry the request `id` as `requestId`; stream frames carry `subId`.
|
|
18
|
+
*
|
|
19
|
+
* @module fleet/rooms/room-ws-bridge
|
|
20
|
+
*/
|
|
21
|
+
import type { RoomHub } from './room-hub.js';
|
|
22
|
+
export interface FleetRoomsBridgeOptions {
|
|
23
|
+
/** Close a subscription instead of queueing once the socket holds this many bytes. */
|
|
24
|
+
maxBufferedBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const FLEET_ROOMS_MESSAGE_TYPES: readonly ["fleet.rooms.hello", "fleet.rooms.auth", "fleet.rooms.publish", "fleet.rooms.subscribe", "fleet.rooms.close"];
|
|
27
|
+
export declare function wireFleetRoomsBridge(hub: RoomHub, options?: FleetRoomsBridgeOptions): () => void;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — wiring on the existing `/ws` endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Uses `registerWebSocketExtension` (no access to the socket or connection
|
|
5
|
+
* state) like the cognition bus. Transport gate: an authenticated principal
|
|
6
|
+
* holding `fleet:listen` (or `admin`), never an anonymous remote client. Room
|
|
7
|
+
* access itself is decided by the member key proof and the room policy.
|
|
8
|
+
*
|
|
9
|
+
* Client → hub (`{ type, id?, payload }`):
|
|
10
|
+
* - `fleet.rooms.hello` → `fleet.rooms.challenge { challenge, expiresAt }`
|
|
11
|
+
* - `fleet.rooms.auth { event }` → `fleet.rooms.auth { ok, pubkey?, name?, rooms?, message? }`
|
|
12
|
+
* - `fleet.rooms.publish { event }` → `fleet.rooms.ok { id, accepted, message, seq?, storeId }`
|
|
13
|
+
* - `fleet.rooms.subscribe { subId, filters, afterSeq?, storeId? }` →
|
|
14
|
+
* `fleet.rooms.event*`, then `fleet.rooms.eose` (or `fleet.rooms.closed`)
|
|
15
|
+
* - `fleet.rooms.close { subId }` → `fleet.rooms.unsubscribed { subId, closed }`
|
|
16
|
+
*
|
|
17
|
+
* Replies carry the request `id` as `requestId`; stream frames carry `subId`.
|
|
18
|
+
*
|
|
19
|
+
* @module fleet/rooms/room-ws-bridge
|
|
20
|
+
*/
|
|
21
|
+
import { SERVER_CONFIG } from '../../config/constants.js';
|
|
22
|
+
import { registerWebSocketExtension, } from '../../server/websocket/handler.js';
|
|
23
|
+
import { logger } from '../../utils/logger.js';
|
|
24
|
+
export const FLEET_ROOMS_MESSAGE_TYPES = [
|
|
25
|
+
'fleet.rooms.hello',
|
|
26
|
+
'fleet.rooms.auth',
|
|
27
|
+
'fleet.rooms.publish',
|
|
28
|
+
'fleet.rooms.subscribe',
|
|
29
|
+
'fleet.rooms.close',
|
|
30
|
+
];
|
|
31
|
+
function requestIdOf(envelope) {
|
|
32
|
+
return envelope.requestId ?? envelope.id;
|
|
33
|
+
}
|
|
34
|
+
function reply(context, envelope, type, payload) {
|
|
35
|
+
const requestId = requestIdOf(envelope);
|
|
36
|
+
context.send({ type, ...(requestId ? { requestId } : {}), payload, timestamp: new Date().toISOString() });
|
|
37
|
+
}
|
|
38
|
+
function transportAllowed(context) {
|
|
39
|
+
const { principal } = context;
|
|
40
|
+
if (principal.anonymousRemote)
|
|
41
|
+
return false;
|
|
42
|
+
return principal.scopes.includes('fleet:listen') || principal.scopes.includes('admin');
|
|
43
|
+
}
|
|
44
|
+
function eventOf(payload) {
|
|
45
|
+
return payload && typeof payload === 'object' ? payload.event : undefined;
|
|
46
|
+
}
|
|
47
|
+
export function wireFleetRoomsBridge(hub, options = {}) {
|
|
48
|
+
const maxBufferedBytes = options.maxBufferedBytes ?? SERVER_CONFIG.WS_BROADCAST_BUFFER_LIMIT;
|
|
49
|
+
if (!Number.isFinite(maxBufferedBytes) || maxBufferedBytes < 0) {
|
|
50
|
+
throw new RangeError('maxBufferedBytes must be a finite number >= 0');
|
|
51
|
+
}
|
|
52
|
+
const sessions = new Map();
|
|
53
|
+
const unregister = [];
|
|
54
|
+
let wired = true;
|
|
55
|
+
const sessionFor = (context) => {
|
|
56
|
+
const existing = sessions.get(context.connectionId);
|
|
57
|
+
if (existing && !existing.session.isDisposed)
|
|
58
|
+
return existing.session;
|
|
59
|
+
const session = hub.open({
|
|
60
|
+
connectionId: context.connectionId,
|
|
61
|
+
principalId: context.principal.id,
|
|
62
|
+
send: (frame) => context.send({ ...frame, timestamp: new Date().toISOString() }),
|
|
63
|
+
isBackpressured: () => context.isBackpressured(maxBufferedBytes),
|
|
64
|
+
});
|
|
65
|
+
const deregister = context.onClose(() => {
|
|
66
|
+
session.dispose();
|
|
67
|
+
sessions.delete(context.connectionId);
|
|
68
|
+
});
|
|
69
|
+
sessions.set(context.connectionId, { session, deregister });
|
|
70
|
+
return session;
|
|
71
|
+
};
|
|
72
|
+
const register = (type, handle) => {
|
|
73
|
+
unregister.push(registerWebSocketExtension({
|
|
74
|
+
type,
|
|
75
|
+
handle(context, payload, envelope) {
|
|
76
|
+
if (!wired)
|
|
77
|
+
return;
|
|
78
|
+
if (!transportAllowed(context)) {
|
|
79
|
+
reply(context, envelope, 'fleet.rooms.error', {
|
|
80
|
+
code: 'FORBIDDEN',
|
|
81
|
+
message: 'restricted: fleet rooms require an authenticated connection with fleet:listen',
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
handle(sessionFor(context), context, payload, envelope);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logger.error('[fleet-rooms] bridge failure', {
|
|
90
|
+
type,
|
|
91
|
+
connectionId: context.connectionId,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
reply(context, envelope, 'fleet.rooms.error', { code: 'INTERNAL', message: 'error: fleet rooms internal error' });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
register('fleet.rooms.hello', (session, context, _payload, envelope) => {
|
|
101
|
+
reply(context, envelope, 'fleet.rooms.challenge', session.hello());
|
|
102
|
+
});
|
|
103
|
+
register('fleet.rooms.auth', (session, context, payload, envelope) => {
|
|
104
|
+
reply(context, envelope, 'fleet.rooms.auth', session.authenticate(eventOf(payload)));
|
|
105
|
+
});
|
|
106
|
+
register('fleet.rooms.publish', (session, context, payload, envelope) => {
|
|
107
|
+
reply(context, envelope, 'fleet.rooms.ok', session.publish(eventOf(payload)));
|
|
108
|
+
});
|
|
109
|
+
register('fleet.rooms.subscribe', (session, context, payload, envelope) => {
|
|
110
|
+
const result = session.subscribe(payload);
|
|
111
|
+
if (!result.ok)
|
|
112
|
+
reply(context, envelope, 'fleet.rooms.error', { code: 'INVALID', message: result.message });
|
|
113
|
+
});
|
|
114
|
+
register('fleet.rooms.close', (session, context, payload, envelope) => {
|
|
115
|
+
const subId = payload && typeof payload === 'object' ? payload.subId : undefined;
|
|
116
|
+
reply(context, envelope, 'fleet.rooms.unsubscribed', session.unsubscribe(subId));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
for (const undo of unregister.splice(0))
|
|
121
|
+
undo();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return () => {
|
|
125
|
+
if (!wired)
|
|
126
|
+
return;
|
|
127
|
+
wired = false;
|
|
128
|
+
for (const undo of unregister.splice(0))
|
|
129
|
+
undo();
|
|
130
|
+
for (const { session, deregister } of sessions.values()) {
|
|
131
|
+
deregister();
|
|
132
|
+
session.dispose();
|
|
133
|
+
}
|
|
134
|
+
sessions.clear();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=room-ws-bridge.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BaseEvent } from '../events/types.js';
|
|
2
|
+
export interface SensoryMissionBus {
|
|
3
|
+
on(type: string, listener: (event: BaseEvent) => unknown): string;
|
|
4
|
+
off(id: string): unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface MissionObservation {
|
|
7
|
+
id: string;
|
|
8
|
+
missionId: string;
|
|
9
|
+
type: 'observation';
|
|
10
|
+
source: 'buddy-sense' | 'system-vitals';
|
|
11
|
+
kind: 'heartbeat' | 'resource_threshold' | 'disk_low' | 'fleet_saturated';
|
|
12
|
+
receivedAt: number;
|
|
13
|
+
/** Receipt time is authoritative; producer clocks may differ. */
|
|
14
|
+
observedAt?: number;
|
|
15
|
+
values: Record<string, number | boolean>;
|
|
16
|
+
}
|
|
17
|
+
export interface SensoryMissionBridgeOptions {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
missionId?: string;
|
|
20
|
+
bus: SensoryMissionBus;
|
|
21
|
+
/** Resolve only once the journal has durably accepted the observation.
|
|
22
|
+
* Use observation.id as an idempotency key; errors retry at most twice.
|
|
23
|
+
* The signal is aborted at teardown. No downstream action dispatch here.
|
|
24
|
+
*/
|
|
25
|
+
publish(observation: MissionObservation, signal: AbortSignal): Promise<void>;
|
|
26
|
+
intervalMs?: number;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}
|
|
29
|
+
/** Adapt a local signed-room publisher without importing the hub or its keys.
|
|
30
|
+
* The injected publisher must retain the same signing key across retries, build
|
|
31
|
+
* a kind-9 message with these exact content/createdAt values, and resolve only
|
|
32
|
+
* on durable acceptance (including an already-stored duplicate).
|
|
33
|
+
* JSON includes observation.id, so equal samples from distinct observations
|
|
34
|
+
* cannot collide; stable receivedAt keeps the signed event id stable on retry.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createMissionObservationRoomPublisher(room: string, publish: (room: string, content: string, createdAt: number, signal: AbortSignal) => Promise<void>): SensoryMissionBridgeOptions['publish'];
|
|
37
|
+
/** Disabled or unconfigured means zero subscriptions, timers, and publications.
|
|
38
|
+
* Exactly four possible keys bound queued status snapshots and dedupe memory.
|
|
39
|
+
* Replacing a queued snapshot coalesces bursts; this is status, not an audit log
|
|
40
|
+
* of every physical transition. Incoming remote messages must use a separate bus.
|
|
41
|
+
*/
|
|
42
|
+
export declare function wireSensoryMissionBridge(options: SensoryMissionBridgeOptions): () => void;
|