@gajae-code/coding-agent 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -2
- package/dist/types/append-only-mode.d.ts +1 -1
- package/dist/types/cli/args.d.ts +4 -5
- package/dist/types/cli/daemon-cli.d.ts +12 -1
- package/dist/types/cli/notify-cli.d.ts +21 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +39 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/notify.d.ts +28 -1
- package/dist/types/commands/sdk.d.ts +33 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
- package/dist/types/config/model-profile-activation.d.ts +35 -15
- package/dist/types/config/model-profiles.d.ts +11 -5
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +39 -6
- package/dist/types/config/model-selector-value.d.ts +17 -0
- package/dist/types/config/models-config-schema.d.ts +10 -6
- package/dist/types/config/settings-schema.d.ts +133 -6
- package/dist/types/config/settings.d.ts +63 -35
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
- package/dist/types/coordinator-mcp/server.d.ts +6 -49
- package/dist/types/daemon/builtin.d.ts +6 -5
- package/dist/types/daemon/control-types.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +12 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
- package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +10 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +141 -16
- package/dist/types/extensibility/hooks/types.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
- package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
- package/dist/types/harness-control-plane/operate.d.ts +5 -5
- package/dist/types/harness-control-plane/owner.d.ts +18 -4
- package/dist/types/harness-control-plane/receipts.d.ts +0 -1
- package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
- package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
- package/dist/types/harness-control-plane/types.d.ts +3 -4
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/main.d.ts +32 -38
- package/dist/types/modes/acp/acp-agent.d.ts +224 -53
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -15
- package/dist/types/modes/acp/permission-mode.d.ts +4 -0
- package/dist/types/modes/acp/startup-options.d.ts +6 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -4
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
- package/dist/types/modes/index.d.ts +0 -4
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/runtime-init.d.ts +2 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +169 -30
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/runtime-mcp/client.d.ts +2 -0
- package/dist/types/runtime-mcp/config.d.ts +5 -1
- package/dist/types/runtime-mcp/manager.d.ts +8 -1
- package/dist/types/runtime-mcp/types.d.ts +4 -0
- package/dist/types/sdk/acp/adapter.d.ts +67 -0
- package/dist/types/sdk/acp/index.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +53 -0
- package/dist/types/sdk/broker/discovery.d.ts +30 -0
- package/dist/types/sdk/broker/ensure.d.ts +33 -0
- package/dist/types/sdk/broker/identity.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +8 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
- package/dist/types/sdk/broker/runtime.d.ts +33 -0
- package/dist/types/sdk/broker/session-index.d.ts +57 -0
- package/dist/types/sdk/broker/state-version.d.ts +8 -0
- package/dist/types/sdk/broker/transport.d.ts +9 -0
- package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
- package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
- package/dist/types/sdk/bus/config.d.ts +274 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +45 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
- package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
- package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
- package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
- package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
- package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
- package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
- package/dist/types/sdk/bus/session-control.d.ts +83 -0
- package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
- package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
- package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
- package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +55 -17
- package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
- package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/session-cli.d.ts +19 -0
- package/dist/types/sdk/client/client.d.ts +1 -0
- package/dist/types/sdk/client/discovery.d.ts +27 -0
- package/dist/types/sdk/client/index.d.ts +2 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
- package/dist/types/sdk/host/control/index.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +65 -0
- package/dist/types/sdk/host/events.d.ts +35 -0
- package/dist/types/sdk/host/host.d.ts +35 -0
- package/dist/types/sdk/host/index.d.ts +4 -0
- package/dist/types/sdk/host/query/cursor.d.ts +62 -0
- package/dist/types/sdk/host/query/handlers.d.ts +70 -0
- package/dist/types/sdk/host/query/index.d.ts +3 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
- package/dist/types/sdk/host/types.d.ts +32 -0
- package/dist/types/sdk/index.d.ts +6 -0
- package/dist/types/sdk/lifecycle-session.d.ts +14 -0
- package/dist/types/sdk/mcp/index.d.ts +1 -0
- package/dist/types/sdk/mcp/server.d.ts +37 -0
- package/dist/types/sdk/models.d.ts +44 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
- package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
- package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
- package/dist/types/sdk/startup-capability.d.ts +50 -0
- package/dist/types/session/agent-session.d.ts +114 -21
- package/dist/types/session/default-model-selection.d.ts +25 -0
- package/dist/types/session/fallback-chain-controller.d.ts +52 -0
- package/dist/types/session/session-manager.d.ts +58 -1
- package/dist/types/setup/credential-auto-import.d.ts +36 -6
- package/dist/types/tools/ask-answer-registry.d.ts +1 -1
- package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
- package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
- package/dist/types/tools/browser/launch.d.ts +22 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
- package/dist/types/tools/browser/profile-posture.d.ts +37 -0
- package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
- package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
- package/dist/types/tools/browser/registry.d.ts +19 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +8 -8
- package/package.json +34 -13
- package/scripts/build-sdk-package-smoke.ts +94 -0
- package/scripts/capture-notifications-settings-showcase.ts +435 -0
- package/scripts/compile-args.ts +4 -2
- package/scripts/g005-daemon-smoke.ts +4 -5
- package/scripts/g011-daemon-path-smoke.ts +5 -5
- package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
- package/scripts/generate-sdk-operation-inventory.ts +867 -0
- package/scripts/generate-telegram-baseline-manifest.ts +128 -0
- package/scripts/manifest-schema.ts +124 -0
- package/scripts/run-test-manifest.ts +153 -0
- package/scripts/run-threat-model-check.ts +157 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
- package/scripts/verify-gjc-sdk-rename.ts +90 -0
- package/src/append-only-mode.ts +1 -1
- package/src/cli/args.ts +43 -12
- package/src/cli/auth-gateway-cli.ts +2 -2
- package/src/cli/config-cli.ts +2 -1
- package/src/cli/daemon-cli.ts +63 -14
- package/src/cli/list-models.ts +1 -1
- package/src/cli/notify-cli.ts +288 -286
- package/src/cli.ts +21 -2
- package/src/commands/daemon.ts +50 -6
- package/src/commands/harness.ts +93 -36
- package/src/commands/launch.ts +5 -2
- package/src/commands/mcp-serve.ts +19 -12
- package/src/commands/notify.ts +36 -3
- package/src/commands/sdk.ts +426 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/config/atomic-yaml-patch.ts +423 -0
- package/src/config/model-profile-activation.ts +240 -89
- package/src/config/model-profiles.ts +18 -15
- package/src/config/model-registry.ts +51 -26
- package/src/config/model-resolver.ts +138 -51
- package/src/config/model-selector-value.ts +50 -0
- package/src/config/models-config-schema.ts +14 -21
- package/src/config/settings-schema.ts +128 -10
- package/src/config/settings.ts +1120 -158
- package/src/coordinator-mcp/model-preset.ts +74 -20
- package/src/coordinator-mcp/server.ts +1597 -3052
- package/src/daemon/builtin.ts +8 -5
- package/src/daemon/control-types.ts +1 -1
- package/src/defaults/gjc/skills/team/SKILL.md +4 -4
- package/src/discovery/mcp-json.ts +327 -20
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/compact-handler.ts +3 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -2
- package/src/extensibility/extensions/loader.ts +69 -5
- package/src/extensibility/extensions/runner.ts +112 -4
- package/src/extensibility/extensions/types.ts +176 -17
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +170 -86
- package/src/gjc-runtime/psmux-detect.ts +82 -4
- package/src/gjc-runtime/session-state-sidecar.ts +41 -31
- package/src/gjc-runtime/team-runtime.ts +54 -35
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
- package/src/gjc-runtime/tmux-sessions.ts +19 -13
- package/src/gjc-runtime/ultragoal-guard.ts +53 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +155 -3
- package/src/gjc-runtime/windows-powershell-command.ts +58 -0
- package/src/harness-control-plane/operate.ts +9 -9
- package/src/harness-control-plane/owner.ts +283 -39
- package/src/harness-control-plane/receipts.ts +0 -1
- package/src/harness-control-plane/sdk-transport.ts +395 -0
- package/src/harness-control-plane/session-transport.ts +94 -0
- package/src/harness-control-plane/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +34 -35
- package/src/lsp/client.ts +42 -6
- package/src/lsp/index.ts +2 -2
- package/src/main.ts +299 -204
- package/src/modes/DESIGN.md +258 -0
- package/src/modes/acp/acp-agent.ts +1228 -2612
- package/src/modes/acp/acp-client-bridge.ts +2 -20
- package/src/modes/acp/acp-event-mapper.ts +18 -2
- package/src/modes/acp/acp-mode.ts +8 -36
- package/src/modes/acp/permission-mode.ts +25 -0
- package/src/modes/acp/startup-options.ts +6 -0
- package/src/modes/components/agent-dashboard.ts +37 -17
- package/src/modes/components/model-selector.ts +93 -35
- package/src/modes/components/notifications-settings-editor.ts +1379 -0
- package/src/modes/components/settings-selector.ts +60 -11
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/event-controller.ts +11 -16
- package/src/modes/controllers/extension-ui-controller.ts +348 -4
- package/src/modes/controllers/selector-controller.ts +674 -36
- package/src/modes/index.ts +0 -22
- package/src/modes/interactive-mode.ts +59 -20
- package/src/modes/runtime-init.ts +298 -6
- package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
- package/src/modes/shared/agent-wire/event-contract.ts +7 -8
- package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
- package/src/modes/shared/agent-wire/event-observation.ts +16 -7
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1254 -87
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
- package/src/modes/types.ts +5 -0
- package/src/runtime-mcp/client.ts +94 -19
- package/src/runtime-mcp/config.ts +27 -15
- package/src/runtime-mcp/manager.ts +343 -105
- package/src/runtime-mcp/oauth-flow.ts +42 -12
- package/src/runtime-mcp/transports/http.ts +49 -19
- package/src/runtime-mcp/transports/stdio.ts +47 -28
- package/src/runtime-mcp/types.ts +10 -0
- package/src/sdk/acp/adapter.ts +418 -0
- package/src/sdk/acp/index.ts +2 -0
- package/src/sdk/broker/broker.ts +642 -0
- package/src/sdk/broker/discovery.ts +94 -0
- package/src/sdk/broker/ensure.ts +249 -0
- package/src/sdk/broker/identity.ts +42 -0
- package/src/sdk/broker/index.ts +8 -0
- package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
- package/src/sdk/broker/internal-source.bunfig.toml +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +286 -0
- package/src/sdk/broker/lifecycle.ts +2157 -0
- package/src/sdk/broker/process-incarnation.ts +159 -0
- package/src/sdk/broker/runtime.ts +164 -0
- package/src/sdk/broker/session-index.ts +318 -0
- package/src/sdk/broker/state-version.ts +23 -0
- package/src/sdk/broker/transport.ts +125 -0
- package/src/sdk/bus/chat-command-policy.ts +107 -0
- package/src/sdk/bus/chat-daemon-cli.ts +214 -0
- package/src/sdk/bus/chat-daemon-control.ts +713 -0
- package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
- package/src/sdk/bus/chat-effect-journal.ts +353 -0
- package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
- package/src/sdk/bus/config.ts +433 -0
- package/src/sdk/bus/conversation-store.ts +391 -0
- package/src/sdk/bus/discord-conversation.ts +85 -0
- package/src/sdk/bus/discord-daemon.ts +2275 -0
- package/src/sdk/bus/discord-live-provider.ts +521 -0
- package/src/sdk/bus/discord-provider.ts +56 -0
- package/src/{notifications → sdk/bus}/html-format.ts +2 -2
- package/src/sdk/bus/index.ts +3943 -0
- package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
- package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
- package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
- package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
- package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
- package/src/sdk/bus/natives-augment.d.ts +38 -0
- package/src/sdk/bus/notification-orchestration.ts +535 -0
- package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
- package/src/sdk/bus/session-control.ts +333 -0
- package/src/sdk/bus/slack-conversation.ts +74 -0
- package/src/sdk/bus/slack-daemon.ts +2144 -0
- package/src/sdk/bus/slack-live-provider.ts +380 -0
- package/src/sdk/bus/slack-provider.ts +76 -0
- package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
- package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
- package/src/{notifications → sdk/bus}/telegram-daemon.ts +502 -54
- package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
- package/src/sdk/bus/telegram-setup.ts +663 -0
- package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/session-cli.ts +275 -0
- package/src/sdk/client/client.ts +1 -0
- package/src/sdk/client/discovery.ts +108 -0
- package/src/sdk/client/index.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +367 -0
- package/src/sdk/host/control/index.ts +10 -0
- package/src/sdk/host/control/operations.ts +63 -0
- package/src/sdk/host/events.ts +91 -0
- package/src/sdk/host/host.ts +296 -0
- package/src/sdk/host/index.ts +4 -0
- package/src/sdk/host/query/cursor.ts +189 -0
- package/src/sdk/host/query/handlers.ts +533 -0
- package/src/sdk/host/query/index.ts +3 -0
- package/src/sdk/host/query/revision-store.ts +1126 -0
- package/src/sdk/host/reverse-leases.ts +325 -0
- package/src/sdk/host/types.ts +29 -0
- package/src/sdk/index.ts +13 -0
- package/src/sdk/lifecycle-session.ts +39 -0
- package/src/sdk/mcp/index.ts +1 -0
- package/src/sdk/mcp/server.ts +332 -0
- package/src/sdk/models.ts +168 -0
- package/src/sdk/protocol/adapter-validation.ts +48 -0
- package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
- package/src/sdk/protocol/operation-registry.ts +312 -0
- package/src/{sdk.ts → sdk/session.ts} +339 -212
- package/src/sdk/startup-capability.ts +189 -0
- package/src/session/agent-session.ts +2453 -894
- package/src/session/default-model-selection.ts +93 -0
- package/src/session/fallback-chain-controller.ts +136 -0
- package/src/session/session-manager.ts +438 -11
- package/src/session/session-storage.ts +8 -1
- package/src/setup/credential-auto-import.ts +290 -35
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/executor.ts +62 -13
- package/src/tools/ask-answer-registry.ts +1 -1
- package/src/tools/ask.ts +13 -8
- package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
- package/src/tools/browser/benchmark/detector-report.ts +157 -0
- package/src/tools/browser/benchmark/run-suite.ts +39 -0
- package/src/tools/browser/launch.ts +124 -19
- package/src/tools/browser/profile-discovery.ts +63 -0
- package/src/tools/browser/profile-posture.ts +59 -0
- package/src/tools/browser/profile-reuse.ts +102 -0
- package/src/tools/browser/profile-warmup.ts +109 -0
- package/src/tools/browser/registry.ts +87 -11
- package/src/tools/browser/tab-protocol.ts +1 -0
- package/src/tools/browser/tab-supervisor.ts +1 -0
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +6 -0
- package/src/tools/index.ts +8 -8
- package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
- package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
- package/src/tools/telegram-send.ts +2 -2
- package/vendor/insane-search/MANIFEST.json +2 -1
- package/vendor/insane-search/engine/bias_check.py +4 -1
- package/vendor/insane-search/engine/phase0.py +26 -1
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
- package/dist/types/modes/bridge/auth.d.ts +0 -12
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
- package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
- package/dist/types/modes/bridge/event-stream.d.ts +0 -8
- package/dist/types/modes/rpc/host-tools.d.ts +0 -1
- package/dist/types/modes/rpc/host-uris.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
- package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
- package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
- package/dist/types/notifications/config.d.ts +0 -132
- package/src/harness-control-plane/rpc-adapter.ts +0 -290
- package/src/modes/bridge/auth.ts +0 -41
- package/src/modes/bridge/bridge-client-bridge.ts +0 -47
- package/src/modes/bridge/bridge-mode.ts +0 -675
- package/src/modes/bridge/bridge-ui-context.ts +0 -200
- package/src/modes/bridge/event-stream.ts +0 -70
- package/src/modes/rpc/host-tools.ts +0 -1
- package/src/modes/rpc/host-uris.ts +0 -1
- package/src/modes/rpc/rpc-client.ts +0 -1166
- package/src/modes/rpc/rpc-mode.ts +0 -1029
- package/src/modes/rpc/rpc-socket-security.ts +0 -115
- package/src/modes/rpc/rpc-types.ts +0 -637
- package/src/modes/shared/agent-wire/command-contract.ts +0 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
- package/src/modes/shared/agent-wire/command-validation.ts +0 -181
- package/src/modes/shared/agent-wire/handshake.ts +0 -157
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
- package/src/modes/shared/agent-wire/protocol.ts +0 -46
- package/src/modes/shared/agent-wire/responses.ts +0 -17
- package/src/modes/shared/agent-wire/scopes.ts +0 -94
- package/src/modes/shared/agent-wire/session-registry.ts +0 -109
- package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
- package/src/modes/shared/agent-wire/ui-result.ts +0 -48
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
- package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
- package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
- package/src/notifications/config.ts +0 -219
- package/src/notifications/index.ts +0 -1835
- package/src/notifications/telegram-daemon-cli.ts +0 -243
- package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
- /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
- /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
- /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
- /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
- /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
- /package/src/{notifications → sdk/bus}/engine.ts +0 -0
- /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
- /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
- /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
- /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
- /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
- /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
- /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, open, readFile, rename, rm, unlink } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const MAX_REVISIONS_PER_RESOURCE = 8;
|
|
7
|
+
export const MAX_PINNED_REVISIONS = 128;
|
|
8
|
+
export const MAX_MEMORY_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
export const SNAPSHOT_TTL_MS = 15 * 60 * 1000;
|
|
10
|
+
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
const STRING_INDEX_BYTES = 64 * 1024;
|
|
12
|
+
const STRING_READ_BYTES = 512 * 1024;
|
|
13
|
+
|
|
14
|
+
export class RevisionStoreError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly code: "resource_gone" | "snapshot_capacity_exceeded",
|
|
17
|
+
message: string = code,
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface StringCheckpoint {
|
|
24
|
+
decodedOffset: number;
|
|
25
|
+
serializedOffset: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EscapedStringIndex {
|
|
29
|
+
byteLength: number;
|
|
30
|
+
checkpoints: StringCheckpoint[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface IndexedField {
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
plainStringBytes?: number;
|
|
37
|
+
isString?: boolean;
|
|
38
|
+
stringIndex?: EscapedStringIndex;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RevisionIndex {
|
|
42
|
+
items?: {
|
|
43
|
+
start: number;
|
|
44
|
+
end: number;
|
|
45
|
+
entryId?: string;
|
|
46
|
+
fields?: Record<string, IndexedField>;
|
|
47
|
+
}[];
|
|
48
|
+
fields?: Record<string, IndexedField>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Revision {
|
|
52
|
+
id: string;
|
|
53
|
+
hash: string;
|
|
54
|
+
bytes: number;
|
|
55
|
+
payload?: Buffer;
|
|
56
|
+
manifest?: string;
|
|
57
|
+
chunks?: string[];
|
|
58
|
+
chunkLengths?: number[];
|
|
59
|
+
index?: RevisionIndex;
|
|
60
|
+
pins: Set<string>;
|
|
61
|
+
lastAccessed: number;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SerializedRevision {
|
|
66
|
+
hash: string;
|
|
67
|
+
bytes: number;
|
|
68
|
+
payload?: Buffer;
|
|
69
|
+
manifest: string;
|
|
70
|
+
chunks: string[];
|
|
71
|
+
chunkLengths: number[];
|
|
72
|
+
index: RevisionIndex;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class ChunkJsonParser {
|
|
76
|
+
#chunkIndex = 0;
|
|
77
|
+
#offset = 0;
|
|
78
|
+
#text = "";
|
|
79
|
+
readonly #decoder = new TextDecoder();
|
|
80
|
+
|
|
81
|
+
constructor(
|
|
82
|
+
private readonly directory: string,
|
|
83
|
+
private readonly chunks: readonly string[],
|
|
84
|
+
private readonly lengths: readonly number[],
|
|
85
|
+
private readonly recordBufferedBytes: (bytes: number) => void,
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async parse(): Promise<unknown> {
|
|
89
|
+
const value = await this.#value();
|
|
90
|
+
await this.#whitespace();
|
|
91
|
+
if ((await this.#peek()) !== undefined) throw new SyntaxError("unexpected data after JSON value");
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async #fill(): Promise<boolean> {
|
|
96
|
+
while (this.#offset >= this.#text.length && this.#chunkIndex < this.chunks.length) {
|
|
97
|
+
const chunk = this.chunks[this.#chunkIndex]!;
|
|
98
|
+
const data = await readFile(join(this.directory, "objects", chunk));
|
|
99
|
+
const expectedLength = this.lengths[this.#chunkIndex];
|
|
100
|
+
if (expectedLength !== undefined && data.length !== expectedLength)
|
|
101
|
+
throw new SyntaxError("snapshot chunk length does not match manifest");
|
|
102
|
+
if (createHash("sha256").update(data).digest("hex") !== chunk)
|
|
103
|
+
throw new SyntaxError("snapshot chunk hash does not match manifest");
|
|
104
|
+
this.#chunkIndex++;
|
|
105
|
+
this.#text = this.#decoder.decode(data, { stream: this.#chunkIndex < this.chunks.length });
|
|
106
|
+
this.#offset = 0;
|
|
107
|
+
this.recordBufferedBytes(data.length + Buffer.byteLength(this.#text));
|
|
108
|
+
}
|
|
109
|
+
return this.#offset < this.#text.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async #peek(): Promise<string | undefined> {
|
|
113
|
+
return (await this.#fill()) ? this.#text[this.#offset] : undefined;
|
|
114
|
+
}
|
|
115
|
+
async #take(): Promise<string> {
|
|
116
|
+
const character = await this.#peek();
|
|
117
|
+
if (character === undefined) throw new SyntaxError("unexpected end of JSON snapshot");
|
|
118
|
+
this.#offset++;
|
|
119
|
+
return character;
|
|
120
|
+
}
|
|
121
|
+
async #expect(expected: string): Promise<void> {
|
|
122
|
+
if ((await this.#take()) !== expected) throw new SyntaxError(`expected ${expected} in JSON snapshot`);
|
|
123
|
+
}
|
|
124
|
+
async #whitespace(): Promise<void> {
|
|
125
|
+
while (/\s/.test((await this.#peek()) ?? "")) this.#offset++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #value(): Promise<unknown> {
|
|
129
|
+
await this.#whitespace();
|
|
130
|
+
switch (await this.#peek()) {
|
|
131
|
+
case "{":
|
|
132
|
+
return this.#object();
|
|
133
|
+
case "[":
|
|
134
|
+
return this.#array();
|
|
135
|
+
case '"':
|
|
136
|
+
return this.#string();
|
|
137
|
+
case "t":
|
|
138
|
+
await this.#literal("true");
|
|
139
|
+
return true;
|
|
140
|
+
case "f":
|
|
141
|
+
await this.#literal("false");
|
|
142
|
+
return false;
|
|
143
|
+
case "n":
|
|
144
|
+
await this.#literal("null");
|
|
145
|
+
return null;
|
|
146
|
+
default:
|
|
147
|
+
return this.#number();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async #object(): Promise<Record<string, unknown>> {
|
|
152
|
+
const result: Record<string, unknown> = {};
|
|
153
|
+
await this.#expect("{");
|
|
154
|
+
await this.#whitespace();
|
|
155
|
+
if ((await this.#peek()) === "}") {
|
|
156
|
+
this.#offset++;
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
while (true) {
|
|
160
|
+
if ((await this.#peek()) !== '"') throw new SyntaxError("expected object key in JSON snapshot");
|
|
161
|
+
const key = await this.#string();
|
|
162
|
+
await this.#whitespace();
|
|
163
|
+
await this.#expect(":");
|
|
164
|
+
Object.defineProperty(result, key, {
|
|
165
|
+
configurable: true,
|
|
166
|
+
enumerable: true,
|
|
167
|
+
value: await this.#value(),
|
|
168
|
+
writable: true,
|
|
169
|
+
});
|
|
170
|
+
await this.#whitespace();
|
|
171
|
+
const separator = await this.#take();
|
|
172
|
+
if (separator === "}") return result;
|
|
173
|
+
if (separator !== ",") throw new SyntaxError("expected object separator in JSON snapshot");
|
|
174
|
+
await this.#whitespace();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async #array(): Promise<unknown[]> {
|
|
179
|
+
const result: unknown[] = [];
|
|
180
|
+
await this.#expect("[");
|
|
181
|
+
await this.#whitespace();
|
|
182
|
+
if ((await this.#peek()) === "]") {
|
|
183
|
+
this.#offset++;
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
while (true) {
|
|
187
|
+
result.push(await this.#value());
|
|
188
|
+
await this.#whitespace();
|
|
189
|
+
const separator = await this.#take();
|
|
190
|
+
if (separator === "]") return result;
|
|
191
|
+
if (separator !== ",") throw new SyntaxError("expected array separator in JSON snapshot");
|
|
192
|
+
await this.#whitespace();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #string(): Promise<string> {
|
|
197
|
+
await this.#expect('"');
|
|
198
|
+
const parts: string[] = [];
|
|
199
|
+
let output = "";
|
|
200
|
+
const append = (value: string): void => {
|
|
201
|
+
output += value;
|
|
202
|
+
if (output.length >= 64 * 1024) {
|
|
203
|
+
parts.push(output);
|
|
204
|
+
output = "";
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
while (true) {
|
|
208
|
+
if (!(await this.#fill())) throw new SyntaxError("unexpected end of JSON snapshot");
|
|
209
|
+
const start = this.#offset;
|
|
210
|
+
while (this.#offset < this.#text.length) {
|
|
211
|
+
const character = this.#text[this.#offset]!;
|
|
212
|
+
if (character === '"' || character === "\\" || character < " ") break;
|
|
213
|
+
this.#offset++;
|
|
214
|
+
}
|
|
215
|
+
if (this.#offset > start) {
|
|
216
|
+
append(this.#text.slice(start, this.#offset));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const character = await this.#take();
|
|
220
|
+
if (character === '"') return parts.length === 0 ? output : `${parts.join("")}${output}`;
|
|
221
|
+
if (character < " ") throw new SyntaxError("unescaped control character in JSON snapshot");
|
|
222
|
+
switch (await this.#take()) {
|
|
223
|
+
case '"':
|
|
224
|
+
append('"');
|
|
225
|
+
break;
|
|
226
|
+
case "\\":
|
|
227
|
+
append("\\");
|
|
228
|
+
break;
|
|
229
|
+
case "/":
|
|
230
|
+
append("/");
|
|
231
|
+
break;
|
|
232
|
+
case "b":
|
|
233
|
+
append("\b");
|
|
234
|
+
break;
|
|
235
|
+
case "f":
|
|
236
|
+
append("\f");
|
|
237
|
+
break;
|
|
238
|
+
case "n":
|
|
239
|
+
append("\n");
|
|
240
|
+
break;
|
|
241
|
+
case "r":
|
|
242
|
+
append("\r");
|
|
243
|
+
break;
|
|
244
|
+
case "t":
|
|
245
|
+
append("\t");
|
|
246
|
+
break;
|
|
247
|
+
case "u": {
|
|
248
|
+
let hex = "";
|
|
249
|
+
for (let index = 0; index < 4; index++) hex += await this.#take();
|
|
250
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new SyntaxError("invalid Unicode escape in JSON snapshot");
|
|
251
|
+
append(String.fromCharCode(Number.parseInt(hex, 16)));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
throw new SyntaxError("invalid string escape in JSON snapshot");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async #literal(expected: string): Promise<void> {
|
|
261
|
+
for (const character of expected) await this.#expect(character);
|
|
262
|
+
}
|
|
263
|
+
async #number(): Promise<number> {
|
|
264
|
+
let text = "";
|
|
265
|
+
while (true) {
|
|
266
|
+
const character = await this.#peek();
|
|
267
|
+
if (character === undefined || /[\s,}\]]/.test(character)) break;
|
|
268
|
+
text += await this.#take();
|
|
269
|
+
}
|
|
270
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(text))
|
|
271
|
+
throw new SyntaxError("invalid number in JSON snapshot");
|
|
272
|
+
return Number(text);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Per-session MVCC snapshots. Payloads hold canonical JSON UTF-8. */
|
|
277
|
+
export class RevisionStore {
|
|
278
|
+
readonly #resources = new Map<string, Revision[]>();
|
|
279
|
+
readonly #pinIndex = new Map<string, Revision>();
|
|
280
|
+
#memoryBytes = 0;
|
|
281
|
+
#directory?: string;
|
|
282
|
+
#chunkRefs = new Map<string, number>();
|
|
283
|
+
#manifestRefs = new Map<string, number>();
|
|
284
|
+
#peakBufferedBytes = 0;
|
|
285
|
+
#peakReadBufferedBytes = 0;
|
|
286
|
+
readonly #onReadRange?: (start: number, end: number) => void;
|
|
287
|
+
|
|
288
|
+
constructor(
|
|
289
|
+
readonly sessionId: string,
|
|
290
|
+
private readonly now: () => number = Date.now,
|
|
291
|
+
options?: { storageDir?: string; onReadRange?: (start: number, end: number) => void },
|
|
292
|
+
) {
|
|
293
|
+
this.#directory = options?.storageDir ? join(options.storageDir, "sdk", "snapshots", sessionId) : undefined;
|
|
294
|
+
this.#onReadRange = options?.onReadRange;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async createRevision(resourceKind: string, resourceId: string, payload: unknown): Promise<string> {
|
|
298
|
+
if (payload === undefined) throw new RevisionStoreError("resource_gone", "snapshot payload is unavailable");
|
|
299
|
+
const serialised = await this.#serialise(payload);
|
|
300
|
+
const key = `${resourceKind}:${resourceId}`;
|
|
301
|
+
const revisions = this.#resources.get(key) ?? [];
|
|
302
|
+
const previous = revisions.length === 0 ? undefined : revisions[revisions.length - 1];
|
|
303
|
+
if (previous?.hash === serialised.hash) {
|
|
304
|
+
await this.#discardUnreferenced(serialised.chunks, serialised.manifest);
|
|
305
|
+
return previous.id;
|
|
306
|
+
}
|
|
307
|
+
const revision: Revision = {
|
|
308
|
+
id: String(previous ? Number(previous.id) + 1 : 1),
|
|
309
|
+
hash: serialised.hash,
|
|
310
|
+
bytes: serialised.bytes,
|
|
311
|
+
payload: serialised.payload,
|
|
312
|
+
manifest: serialised.manifest,
|
|
313
|
+
chunks: serialised.chunks,
|
|
314
|
+
chunkLengths: serialised.chunkLengths,
|
|
315
|
+
index: serialised.index,
|
|
316
|
+
pins: new Set(),
|
|
317
|
+
lastAccessed: this.now(),
|
|
318
|
+
createdAt: this.now(),
|
|
319
|
+
};
|
|
320
|
+
revisions.push(revision);
|
|
321
|
+
this.#resources.set(key, revisions);
|
|
322
|
+
this.#retainSpill(revision);
|
|
323
|
+
if (revision.payload) this.#memoryBytes += revision.bytes;
|
|
324
|
+
await this.#enforceMemory();
|
|
325
|
+
while (revisions.length > MAX_REVISIONS_PER_RESOURCE) {
|
|
326
|
+
const candidate = revisions.find(item => item.pins.size === 0);
|
|
327
|
+
if (!candidate) break;
|
|
328
|
+
revisions.splice(revisions.indexOf(candidate), 1);
|
|
329
|
+
this.#drop(candidate);
|
|
330
|
+
}
|
|
331
|
+
return revision.id;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async readRevision(resourceKind: string, resourceId: string, id: string): Promise<unknown> {
|
|
335
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
336
|
+
if (!revision) return undefined;
|
|
337
|
+
revision.lastAccessed = this.now();
|
|
338
|
+
if (revision.payload !== undefined) return JSON.parse(revision.payload.toString("utf8"));
|
|
339
|
+
return revision.chunks === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: this.#parseChunks(revision.chunks, revision.chunkLengths ?? []);
|
|
342
|
+
}
|
|
343
|
+
async revisionByteLength(resourceKind: string, resourceId: string, id: string): Promise<number | undefined> {
|
|
344
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
345
|
+
if (!revision) return undefined;
|
|
346
|
+
revision.lastAccessed = this.now();
|
|
347
|
+
return revision.bytes;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async readPage(
|
|
351
|
+
resourceKind: string,
|
|
352
|
+
resourceId: string,
|
|
353
|
+
id: string,
|
|
354
|
+
offset: number,
|
|
355
|
+
targetBytes: number,
|
|
356
|
+
): Promise<{ items: unknown[]; complete: boolean } | undefined> {
|
|
357
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
358
|
+
if (!revision) return undefined;
|
|
359
|
+
revision.lastAccessed = this.now();
|
|
360
|
+
if (!revision.index?.items) return undefined;
|
|
361
|
+
const items: unknown[] = [];
|
|
362
|
+
for (const range of revision.index.items.slice(offset)) {
|
|
363
|
+
// The manifest records the canonical item length, so reject an oversized
|
|
364
|
+
// item before reading or parsing its complete range.
|
|
365
|
+
if (range.end - range.start > targetBytes) break;
|
|
366
|
+
const value = JSON.parse(await this.#readRange(revision, range));
|
|
367
|
+
if (Buffer.byteLength(JSON.stringify([...items, value])) > targetBytes && items.length) break;
|
|
368
|
+
if (Buffer.byteLength(JSON.stringify([...items, value])) > 1024 * 1024) break;
|
|
369
|
+
items.push(value);
|
|
370
|
+
}
|
|
371
|
+
return { items, complete: offset + items.length >= revision.index.items.length };
|
|
372
|
+
}
|
|
373
|
+
/** Returns canonical root JSON in a UTF-8-safe bounded range without parsing it. */
|
|
374
|
+
async readRootRange(
|
|
375
|
+
resourceKind: string,
|
|
376
|
+
resourceId: string,
|
|
377
|
+
id: string,
|
|
378
|
+
offset: number,
|
|
379
|
+
length: number,
|
|
380
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
381
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
382
|
+
if (!revision || offset < 0 || length <= 0) return undefined;
|
|
383
|
+
revision.lastAccessed = this.now();
|
|
384
|
+
const requestedStart = Math.min(offset, revision.bytes);
|
|
385
|
+
const source = await this.#readBytes(
|
|
386
|
+
revision,
|
|
387
|
+
requestedStart,
|
|
388
|
+
Math.min(revision.bytes, requestedStart + length + 7),
|
|
389
|
+
);
|
|
390
|
+
const skipped = utf8ContinuationPrefixLength(source);
|
|
391
|
+
const start = requestedStart + skipped;
|
|
392
|
+
const body = source.subarray(skipped);
|
|
393
|
+
const end = utf8BoundaryAtOrBefore(body, Math.min(body.length, length));
|
|
394
|
+
if (end === 0 && start < revision.bytes) return undefined;
|
|
395
|
+
return { body: body.subarray(0, end).toString("utf8"), complete: start + end === revision.bytes, offset: start };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async readStringRange(
|
|
399
|
+
resourceKind: string,
|
|
400
|
+
resourceId: string,
|
|
401
|
+
id: string,
|
|
402
|
+
field: string,
|
|
403
|
+
offset: number,
|
|
404
|
+
length: number,
|
|
405
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
406
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
407
|
+
if (!revision) return undefined;
|
|
408
|
+
revision.lastAccessed = this.now();
|
|
409
|
+
const range = revision.index?.fields?.[field];
|
|
410
|
+
return range ? this.#readStringRange(revision, range, offset, length) : undefined;
|
|
411
|
+
}
|
|
412
|
+
async readIndexedStringRange(
|
|
413
|
+
resourceKind: string,
|
|
414
|
+
resourceId: string,
|
|
415
|
+
id: string,
|
|
416
|
+
itemId: string,
|
|
417
|
+
field: string,
|
|
418
|
+
offset: number,
|
|
419
|
+
length: number,
|
|
420
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
421
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
422
|
+
if (!revision) return undefined;
|
|
423
|
+
revision.lastAccessed = this.now();
|
|
424
|
+
const item = revision.index?.items?.find(candidate => candidate.entryId === itemId);
|
|
425
|
+
const range = item?.fields?.[field];
|
|
426
|
+
return range ? this.#readStringRange(revision, range, offset, length) : undefined;
|
|
427
|
+
}
|
|
428
|
+
async describeIndexedItem(
|
|
429
|
+
resourceKind: string,
|
|
430
|
+
resourceId: string,
|
|
431
|
+
id: string,
|
|
432
|
+
offset: number,
|
|
433
|
+
): Promise<{ itemId?: string; fields: string[] } | undefined> {
|
|
434
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
435
|
+
const item = revision?.index?.items?.[offset];
|
|
436
|
+
if (!item) return undefined;
|
|
437
|
+
return {
|
|
438
|
+
itemId: item.entryId,
|
|
439
|
+
fields: Object.entries(item.fields ?? {})
|
|
440
|
+
.filter(([, range]) => range.isString)
|
|
441
|
+
.map(([field]) => field),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async readTranscriptBodyRange(
|
|
445
|
+
resourceId: string,
|
|
446
|
+
id: string,
|
|
447
|
+
entryId: string,
|
|
448
|
+
offset: number,
|
|
449
|
+
length: number,
|
|
450
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
451
|
+
return (
|
|
452
|
+
(await this.readIndexedStringRange("transcript", resourceId, id, entryId, "body", offset, length)) ??
|
|
453
|
+
(await this.readIndexedStringRange("transcript", resourceId, id, entryId, "content", offset, length))
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async pin(cursorId: string, resourceKind: string, resourceId: string, id: string): Promise<void> {
|
|
458
|
+
if (this.#pinIndex.has(cursorId)) return;
|
|
459
|
+
if (this.#pinIndex.size >= MAX_PINNED_REVISIONS) throw new RevisionStoreError("snapshot_capacity_exceeded");
|
|
460
|
+
const revision = this.#resources.get(`${resourceKind}:${resourceId}`)?.find(item => item.id === id);
|
|
461
|
+
if (!revision) throw new RevisionStoreError("snapshot_capacity_exceeded", "snapshot is unavailable");
|
|
462
|
+
revision.pins.add(cursorId);
|
|
463
|
+
this.#pinIndex.set(cursorId, revision);
|
|
464
|
+
await this.#enforceMemory();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
unpin(cursorId: string): void {
|
|
468
|
+
const revision = this.#pinIndex.get(cursorId);
|
|
469
|
+
if (!revision) return;
|
|
470
|
+
revision.pins.delete(cursorId);
|
|
471
|
+
this.#pinIndex.delete(cursorId);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
sweep(): void {
|
|
475
|
+
const cutoff = this.now() - SNAPSHOT_TTL_MS;
|
|
476
|
+
for (const [key, revisions] of this.#resources) {
|
|
477
|
+
for (const revision of [...revisions]) {
|
|
478
|
+
if (revision.pins.size === 0 && revision.lastAccessed < cutoff) {
|
|
479
|
+
revisions.splice(revisions.indexOf(revision), 1);
|
|
480
|
+
this.#drop(revision);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (revisions.length === 0) this.#resources.delete(key);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async close(): Promise<void> {
|
|
488
|
+
this.#resources.clear();
|
|
489
|
+
this.#pinIndex.clear();
|
|
490
|
+
this.#memoryBytes = 0;
|
|
491
|
+
this.#chunkRefs.clear();
|
|
492
|
+
this.#manifestRefs.clear();
|
|
493
|
+
if (this.#directory) await rm(this.#directory, { recursive: true, force: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
get pinnedCount(): number {
|
|
497
|
+
return this.#pinIndex.size;
|
|
498
|
+
}
|
|
499
|
+
get memoryBytes(): number {
|
|
500
|
+
return this.#memoryBytes;
|
|
501
|
+
}
|
|
502
|
+
/** Test-only accounting for the bounded serializer's staging buffer. */
|
|
503
|
+
get peakBufferedBytes(): number {
|
|
504
|
+
return this.#peakBufferedBytes;
|
|
505
|
+
}
|
|
506
|
+
/** Test-only accounting for a spilled reader's raw chunk and decoded-text buffers. */
|
|
507
|
+
get peakReadBufferedBytes(): number {
|
|
508
|
+
return this.#peakReadBufferedBytes;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async #enforceMemory(): Promise<void> {
|
|
512
|
+
while (this.#memoryBytes > MAX_MEMORY_BYTES) {
|
|
513
|
+
const candidates = [...this.#resources.values()]
|
|
514
|
+
.flat()
|
|
515
|
+
.filter(item => item.payload)
|
|
516
|
+
.sort((a, b) => a.pins.size - b.pins.size || a.lastAccessed - b.lastAccessed);
|
|
517
|
+
const candidate = candidates[0];
|
|
518
|
+
if (!candidate) break;
|
|
519
|
+
candidate.payload = undefined;
|
|
520
|
+
this.#memoryBytes -= candidate.bytes;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async #serialise(value: unknown): Promise<SerializedRevision> {
|
|
525
|
+
const directory = await this.#spillDirectory();
|
|
526
|
+
const chunks: string[] = [];
|
|
527
|
+
const chunkLengths: number[] = [];
|
|
528
|
+
const hash = createHash("sha256");
|
|
529
|
+
let bytes = 0;
|
|
530
|
+
let buffer = "";
|
|
531
|
+
let bufferBytes = 0;
|
|
532
|
+
const flush = async () => {
|
|
533
|
+
if (buffer.length === 0) return;
|
|
534
|
+
const data = Buffer.from(buffer);
|
|
535
|
+
buffer = "";
|
|
536
|
+
bufferBytes = 0;
|
|
537
|
+
const chunkHash = createHash("sha256").update(data).digest("hex");
|
|
538
|
+
const file = join(directory, "objects", chunkHash);
|
|
539
|
+
await this.#writeAtomic(file, data);
|
|
540
|
+
chunks.push(chunkHash);
|
|
541
|
+
chunkLengths.push(data.length);
|
|
542
|
+
};
|
|
543
|
+
const append = async (text: string) => {
|
|
544
|
+
for (let offset = 0; offset < text.length; ) {
|
|
545
|
+
let remaining = CHUNK_BYTES - bufferBytes;
|
|
546
|
+
if (remaining === 0) {
|
|
547
|
+
await flush();
|
|
548
|
+
remaining = CHUNK_BYTES;
|
|
549
|
+
}
|
|
550
|
+
const end = utf8ChunkEnd(text, offset, remaining);
|
|
551
|
+
|
|
552
|
+
if (end === offset) {
|
|
553
|
+
await flush();
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const part = text.slice(offset, end);
|
|
557
|
+
buffer += part;
|
|
558
|
+
hash.update(part);
|
|
559
|
+
const partBytes = Buffer.byteLength(part);
|
|
560
|
+
bytes += partBytes;
|
|
561
|
+
bufferBytes += partBytes;
|
|
562
|
+
this.#peakBufferedBytes = Math.max(this.#peakBufferedBytes, bufferBytes);
|
|
563
|
+
offset = end;
|
|
564
|
+
if (bufferBytes === CHUNK_BYTES) await flush();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const index: RevisionIndex = {};
|
|
568
|
+
const root =
|
|
569
|
+
value && typeof value === "object" && typeof (value as { toJSON?: unknown }).toJSON === "function"
|
|
570
|
+
? (value as { toJSON: (key: string) => unknown }).toJSON("")
|
|
571
|
+
: value;
|
|
572
|
+
if (Array.isArray(root)) {
|
|
573
|
+
index.items = [];
|
|
574
|
+
await append("[");
|
|
575
|
+
for (let position = 0; position < root.length; position++) {
|
|
576
|
+
if (position) await append(",");
|
|
577
|
+
const start = bytes;
|
|
578
|
+
const item = await this.#encodeIndexedItem(root[position], append, () => bytes);
|
|
579
|
+
index.items.push({ start, end: bytes, ...item });
|
|
580
|
+
}
|
|
581
|
+
await append("]");
|
|
582
|
+
} else if (root && typeof root === "object") {
|
|
583
|
+
index.fields = {};
|
|
584
|
+
await append("{");
|
|
585
|
+
let first = true;
|
|
586
|
+
for (const [key, child] of Object.entries(root as Record<string, unknown>)) {
|
|
587
|
+
if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
|
|
588
|
+
if (!first) await append(",");
|
|
589
|
+
first = false;
|
|
590
|
+
await this.#encodeString(key, append);
|
|
591
|
+
await append(":");
|
|
592
|
+
const start = bytes;
|
|
593
|
+
const field = await this.#encodeIndexedValue(child, append);
|
|
594
|
+
index.fields[key] = { start, end: bytes, ...field };
|
|
595
|
+
}
|
|
596
|
+
await append("}");
|
|
597
|
+
} else await this.#encode(root, append, false);
|
|
598
|
+
await flush();
|
|
599
|
+
const revisionHash = hash.digest("hex");
|
|
600
|
+
const manifest = join(directory, "manifests", `${revisionHash}.json`);
|
|
601
|
+
const manifestChunks = chunks.map((hash, index) => ({ hash, length: chunkLengths[index]! }));
|
|
602
|
+
await this.#writeAtomic(manifest, Buffer.from(JSON.stringify({ chunks: manifestChunks, index })));
|
|
603
|
+
return {
|
|
604
|
+
hash: revisionHash,
|
|
605
|
+
bytes,
|
|
606
|
+
payload: bytes <= MAX_MEMORY_BYTES ? await this.#readChunks(chunks, chunkLengths) : undefined,
|
|
607
|
+
manifest,
|
|
608
|
+
chunks,
|
|
609
|
+
chunkLengths,
|
|
610
|
+
index,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async #encode(value: unknown, append: (text: string) => Promise<void>, inArray: boolean): Promise<void> {
|
|
615
|
+
if (value === null) return append("null");
|
|
616
|
+
if (typeof value === "string") {
|
|
617
|
+
await this.#encodeString(value, append);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (typeof value === "number") return append(Number.isFinite(value) ? String(value) : "null");
|
|
621
|
+
if (typeof value === "boolean") return append(value ? "true" : "false");
|
|
622
|
+
if (typeof value === "bigint") throw new TypeError("Do not know how to serialize a BigInt");
|
|
623
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol")
|
|
624
|
+
return append(inArray ? "null" : "null");
|
|
625
|
+
if (typeof value === "object" && typeof (value as { toJSON?: unknown }).toJSON === "function")
|
|
626
|
+
return this.#encode((value as { toJSON: (key: string) => unknown }).toJSON(""), append, inArray);
|
|
627
|
+
if (Array.isArray(value)) {
|
|
628
|
+
await append("[");
|
|
629
|
+
for (let index = 0; index < value.length; index++) {
|
|
630
|
+
if (index) await append(",");
|
|
631
|
+
await this.#encode(value[index], append, true);
|
|
632
|
+
}
|
|
633
|
+
return append("]");
|
|
634
|
+
}
|
|
635
|
+
if (typeof value === "object") {
|
|
636
|
+
await append("{");
|
|
637
|
+
let first = true;
|
|
638
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
639
|
+
if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
|
|
640
|
+
if (!first) await append(",");
|
|
641
|
+
first = false;
|
|
642
|
+
await this.#encodeString(key, append);
|
|
643
|
+
await append(":");
|
|
644
|
+
await this.#encode(child, append, false);
|
|
645
|
+
}
|
|
646
|
+
return append("}");
|
|
647
|
+
}
|
|
648
|
+
return append("null");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async #encodeString(
|
|
652
|
+
value: string,
|
|
653
|
+
append: (text: string) => Promise<void>,
|
|
654
|
+
indexEscapedString = false,
|
|
655
|
+
): Promise<EscapedStringIndex | undefined> {
|
|
656
|
+
await append('"');
|
|
657
|
+
let output = "";
|
|
658
|
+
let serializedOffset = 1;
|
|
659
|
+
const stringIndex = indexEscapedString
|
|
660
|
+
? { byteLength: 0, checkpoints: [{ decodedOffset: 0, serializedOffset }] }
|
|
661
|
+
: undefined;
|
|
662
|
+
for (let index = 0; index < value.length; index++) {
|
|
663
|
+
const code = value.charCodeAt(index);
|
|
664
|
+
let encoded: string;
|
|
665
|
+
let decoded: string;
|
|
666
|
+
if (code === 0x22) {
|
|
667
|
+
encoded = '\\"';
|
|
668
|
+
decoded = '"';
|
|
669
|
+
} else if (code === 0x5c) {
|
|
670
|
+
encoded = "\\\\";
|
|
671
|
+
decoded = "\\";
|
|
672
|
+
} else if (code === 0x08) {
|
|
673
|
+
encoded = "\\b";
|
|
674
|
+
decoded = "\b";
|
|
675
|
+
} else if (code === 0x0c) {
|
|
676
|
+
encoded = "\\f";
|
|
677
|
+
decoded = "\f";
|
|
678
|
+
} else if (code === 0x0a) {
|
|
679
|
+
encoded = "\\n";
|
|
680
|
+
decoded = "\n";
|
|
681
|
+
} else if (code === 0x0d) {
|
|
682
|
+
encoded = "\\r";
|
|
683
|
+
decoded = "\r";
|
|
684
|
+
} else if (code === 0x09) {
|
|
685
|
+
encoded = "\\t";
|
|
686
|
+
decoded = "\t";
|
|
687
|
+
} else if (
|
|
688
|
+
code >= 0xd800 &&
|
|
689
|
+
code <= 0xdbff &&
|
|
690
|
+
index + 1 < value.length &&
|
|
691
|
+
value.charCodeAt(index + 1) >= 0xdc00 &&
|
|
692
|
+
value.charCodeAt(index + 1) <= 0xdfff
|
|
693
|
+
) {
|
|
694
|
+
decoded = value.slice(index, index + 2);
|
|
695
|
+
encoded = decoded;
|
|
696
|
+
index++;
|
|
697
|
+
} else if (code < 0x20 || (code >= 0xd800 && code <= 0xdfff)) {
|
|
698
|
+
encoded = `\\u${code.toString(16).padStart(4, "0")}`;
|
|
699
|
+
decoded = value[index]!;
|
|
700
|
+
} else {
|
|
701
|
+
encoded = value[index]!;
|
|
702
|
+
decoded = encoded;
|
|
703
|
+
}
|
|
704
|
+
output += encoded;
|
|
705
|
+
if (stringIndex) {
|
|
706
|
+
serializedOffset += Buffer.byteLength(encoded);
|
|
707
|
+
stringIndex.byteLength += Buffer.byteLength(decoded);
|
|
708
|
+
const last = stringIndex.checkpoints[stringIndex.checkpoints.length - 1]!;
|
|
709
|
+
if (stringIndex.byteLength - last.decodedOffset >= STRING_INDEX_BYTES)
|
|
710
|
+
stringIndex.checkpoints.push({
|
|
711
|
+
decodedOffset: stringIndex.byteLength,
|
|
712
|
+
serializedOffset,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (output.length >= 64 * 1024) {
|
|
716
|
+
await append(output);
|
|
717
|
+
output = "";
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (output) await append(output);
|
|
721
|
+
await append('"');
|
|
722
|
+
if (stringIndex) {
|
|
723
|
+
const last = stringIndex.checkpoints[stringIndex.checkpoints.length - 1]!;
|
|
724
|
+
if (last.decodedOffset !== stringIndex.byteLength)
|
|
725
|
+
stringIndex.checkpoints.push({
|
|
726
|
+
decodedOffset: stringIndex.byteLength,
|
|
727
|
+
serializedOffset,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return stringIndex;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async #encodeIndexedValue(
|
|
734
|
+
value: unknown,
|
|
735
|
+
append: (text: string) => Promise<void>,
|
|
736
|
+
): Promise<Omit<IndexedField, "start" | "end">> {
|
|
737
|
+
if (typeof value !== "string") {
|
|
738
|
+
await this.#encode(value, append, false);
|
|
739
|
+
return {};
|
|
740
|
+
}
|
|
741
|
+
if (isPlainJsonString(value)) {
|
|
742
|
+
await this.#encodeString(value, append);
|
|
743
|
+
return { plainStringBytes: Buffer.byteLength(value), isString: true };
|
|
744
|
+
}
|
|
745
|
+
const stringIndex = await this.#encodeString(value, append, true);
|
|
746
|
+
if (!stringIndex) throw new SyntaxError("escaped string index is unavailable");
|
|
747
|
+
return { isString: true, stringIndex };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async #readStringRange(
|
|
751
|
+
revision: Revision,
|
|
752
|
+
range: IndexedField,
|
|
753
|
+
offset: number,
|
|
754
|
+
length: number,
|
|
755
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
756
|
+
if (offset < 0 || length <= 0 || !range.isString) return undefined;
|
|
757
|
+
if (range.plainStringBytes !== undefined) {
|
|
758
|
+
const requestedStart = Math.min(offset, range.plainStringBytes);
|
|
759
|
+
const source = await this.#readBytes(
|
|
760
|
+
revision,
|
|
761
|
+
range.start + 1 + requestedStart,
|
|
762
|
+
range.start + 1 + Math.min(range.plainStringBytes, requestedStart + length + 7),
|
|
763
|
+
);
|
|
764
|
+
const skipped = utf8ContinuationPrefixLength(source);
|
|
765
|
+
const start = requestedStart + skipped;
|
|
766
|
+
const body = source.subarray(skipped);
|
|
767
|
+
const end = utf8BoundaryAtOrBefore(body, Math.min(body.length, length));
|
|
768
|
+
if (end === 0 && start < range.plainStringBytes) return undefined;
|
|
769
|
+
return {
|
|
770
|
+
body: body.subarray(0, end).toString("utf8"),
|
|
771
|
+
complete: start + end === range.plainStringBytes,
|
|
772
|
+
offset: start,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (!range.stringIndex) throw new SyntaxError("escaped string index is unavailable");
|
|
776
|
+
return this.#readEscapedStringRange(revision, range, offset, length);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async #readEscapedStringRange(
|
|
780
|
+
revision: Revision,
|
|
781
|
+
range: IndexedField,
|
|
782
|
+
offset: number,
|
|
783
|
+
length: number,
|
|
784
|
+
): Promise<{ body: string; complete: boolean; offset: number } | undefined> {
|
|
785
|
+
const stringIndex = range.stringIndex;
|
|
786
|
+
if (!stringIndex) throw new SyntaxError("escaped string index is unavailable");
|
|
787
|
+
const openingQuote = await this.#readBytes(revision, range.start, range.start + 1);
|
|
788
|
+
if (openingQuote.length !== 1 || openingQuote[0] !== 0x22)
|
|
789
|
+
throw new SyntaxError("expected string in JSON snapshot");
|
|
790
|
+
const requestedOffset = Math.min(offset, stringIndex.byteLength);
|
|
791
|
+
const checkpoint = stringCheckpointAtOrBefore(stringIndex.checkpoints, requestedOffset);
|
|
792
|
+
let position = range.start + checkpoint.serializedOffset;
|
|
793
|
+
let source: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
794
|
+
let sourceStart = position;
|
|
795
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
796
|
+
const fill = async (needed = 1): Promise<boolean> => {
|
|
797
|
+
if (position + needed > range.end) return false;
|
|
798
|
+
if (position >= sourceStart && position + needed <= sourceStart + source.length) return true;
|
|
799
|
+
sourceStart = position;
|
|
800
|
+
source = await this.#readBytes(
|
|
801
|
+
revision,
|
|
802
|
+
position,
|
|
803
|
+
Math.min(range.end, position + Math.max(STRING_READ_BYTES, needed)),
|
|
804
|
+
);
|
|
805
|
+
return source.length >= needed;
|
|
806
|
+
};
|
|
807
|
+
const take = async (): Promise<number> => {
|
|
808
|
+
if (!(await fill())) throw new SyntaxError("unexpected end of JSON string");
|
|
809
|
+
return source[position++ - sourceStart]!;
|
|
810
|
+
};
|
|
811
|
+
const readUnicodeEscape = async (): Promise<number> => {
|
|
812
|
+
let hex = "";
|
|
813
|
+
for (let index = 0; index < 4; index++) hex += String.fromCharCode(await take());
|
|
814
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new SyntaxError("invalid Unicode escape in JSON snapshot");
|
|
815
|
+
return Number.parseInt(hex, 16);
|
|
816
|
+
};
|
|
817
|
+
const hasEscapedLowSurrogate = async (): Promise<boolean> => {
|
|
818
|
+
if (!(await fill(6))) return false;
|
|
819
|
+
const start = position - sourceStart;
|
|
820
|
+
if (source[start] !== 0x5c || source[start + 1] !== 0x75) return false;
|
|
821
|
+
const hex = source.subarray(start + 2, start + 6).toString("ascii");
|
|
822
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return false;
|
|
823
|
+
const code = Number.parseInt(hex, 16);
|
|
824
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
825
|
+
};
|
|
826
|
+
const readUnit = async (): Promise<string | undefined> => {
|
|
827
|
+
if (!(await fill())) throw new SyntaxError("unexpected end of JSON string");
|
|
828
|
+
const start = position - sourceStart;
|
|
829
|
+
const first = source[start]!;
|
|
830
|
+
if (first === 0x22) {
|
|
831
|
+
position++;
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
if (first === 0x5c) {
|
|
835
|
+
position++;
|
|
836
|
+
switch (await take()) {
|
|
837
|
+
case 0x22:
|
|
838
|
+
return '"';
|
|
839
|
+
case 0x5c:
|
|
840
|
+
return "\\";
|
|
841
|
+
case 0x2f:
|
|
842
|
+
return "/";
|
|
843
|
+
case 0x62:
|
|
844
|
+
return "\b";
|
|
845
|
+
case 0x66:
|
|
846
|
+
return "\f";
|
|
847
|
+
case 0x6e:
|
|
848
|
+
return "\n";
|
|
849
|
+
case 0x72:
|
|
850
|
+
return "\r";
|
|
851
|
+
case 0x74:
|
|
852
|
+
return "\t";
|
|
853
|
+
case 0x75: {
|
|
854
|
+
const code = await readUnicodeEscape();
|
|
855
|
+
let decoded = String.fromCharCode(code);
|
|
856
|
+
if (code >= 0xd800 && code <= 0xdbff && (await hasEscapedLowSurrogate())) {
|
|
857
|
+
position += 2;
|
|
858
|
+
decoded += String.fromCharCode(await readUnicodeEscape());
|
|
859
|
+
}
|
|
860
|
+
return decoded;
|
|
861
|
+
}
|
|
862
|
+
default:
|
|
863
|
+
throw new SyntaxError("invalid string escape in JSON snapshot");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (first < 0x20) throw new SyntaxError("unescaped control character in JSON snapshot");
|
|
867
|
+
if (first < 0x80) {
|
|
868
|
+
position++;
|
|
869
|
+
return String.fromCharCode(first);
|
|
870
|
+
}
|
|
871
|
+
const width =
|
|
872
|
+
first >= 0xc2 && first <= 0xdf
|
|
873
|
+
? 2
|
|
874
|
+
: first >= 0xe0 && first <= 0xef
|
|
875
|
+
? 3
|
|
876
|
+
: first >= 0xf0 && first <= 0xf4
|
|
877
|
+
? 4
|
|
878
|
+
: 0;
|
|
879
|
+
if (width === 0 || !(await fill(width))) throw new SyntaxError("invalid UTF-8 in JSON string");
|
|
880
|
+
const bytes = source.subarray(position - sourceStart, position - sourceStart + width);
|
|
881
|
+
if (bytes.subarray(1).some(byte => (byte & 0xc0) !== 0x80))
|
|
882
|
+
throw new SyntaxError("invalid UTF-8 in JSON string");
|
|
883
|
+
position += width;
|
|
884
|
+
try {
|
|
885
|
+
return utf8Decoder.decode(bytes);
|
|
886
|
+
} catch {
|
|
887
|
+
throw new SyntaxError("invalid UTF-8 in JSON string");
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
let decodedOffset = checkpoint.decodedOffset;
|
|
891
|
+
let responseOffset: number | undefined;
|
|
892
|
+
let bodyBytes = 0;
|
|
893
|
+
const parts: string[] = [];
|
|
894
|
+
let body = "";
|
|
895
|
+
const append = (value: string): void => {
|
|
896
|
+
body += value;
|
|
897
|
+
if (body.length >= 64 * 1024) {
|
|
898
|
+
parts.push(body);
|
|
899
|
+
body = "";
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
const result = (complete: boolean) => ({
|
|
903
|
+
body: parts.length === 0 ? body : `${parts.join("")}${body}`,
|
|
904
|
+
complete,
|
|
905
|
+
offset: responseOffset ?? requestedOffset,
|
|
906
|
+
});
|
|
907
|
+
while (true) {
|
|
908
|
+
const unit = await readUnit();
|
|
909
|
+
if (unit === undefined) {
|
|
910
|
+
if (position !== range.end || decodedOffset !== stringIndex.byteLength)
|
|
911
|
+
throw new SyntaxError("escaped string index does not match JSON snapshot");
|
|
912
|
+
return result(true);
|
|
913
|
+
}
|
|
914
|
+
const unitBytes = Buffer.byteLength(unit);
|
|
915
|
+
if (responseOffset === undefined) {
|
|
916
|
+
if (decodedOffset < requestedOffset) {
|
|
917
|
+
decodedOffset += unitBytes;
|
|
918
|
+
if (decodedOffset <= requestedOffset) continue;
|
|
919
|
+
responseOffset = decodedOffset;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
responseOffset = decodedOffset;
|
|
923
|
+
}
|
|
924
|
+
if (bodyBytes + unitBytes > length) return bodyBytes === 0 ? undefined : result(false);
|
|
925
|
+
append(unit);
|
|
926
|
+
bodyBytes += unitBytes;
|
|
927
|
+
decodedOffset += unitBytes;
|
|
928
|
+
if (decodedOffset === stringIndex.byteLength) {
|
|
929
|
+
if ((await readUnit()) !== undefined || position !== range.end)
|
|
930
|
+
throw new SyntaxError("escaped string index does not match JSON snapshot");
|
|
931
|
+
return result(true);
|
|
932
|
+
}
|
|
933
|
+
if (bodyBytes === length) return result(false);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async #encodeIndexedItem(
|
|
938
|
+
value: unknown,
|
|
939
|
+
append: (text: string) => Promise<void>,
|
|
940
|
+
position: () => number,
|
|
941
|
+
): Promise<{
|
|
942
|
+
entryId?: string;
|
|
943
|
+
fields?: Record<string, IndexedField>;
|
|
944
|
+
}> {
|
|
945
|
+
const root =
|
|
946
|
+
value && typeof value === "object" && typeof (value as { toJSON?: unknown }).toJSON === "function"
|
|
947
|
+
? (value as { toJSON: (key: string) => unknown }).toJSON("")
|
|
948
|
+
: value;
|
|
949
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) {
|
|
950
|
+
await this.#encode(root, append, true);
|
|
951
|
+
return {};
|
|
952
|
+
}
|
|
953
|
+
const fields: Record<string, IndexedField> = {};
|
|
954
|
+
let entryId: string | undefined;
|
|
955
|
+
await append("{");
|
|
956
|
+
let first = true;
|
|
957
|
+
for (const [key, child] of Object.entries(root as Record<string, unknown>)) {
|
|
958
|
+
if (child === undefined || typeof child === "function" || typeof child === "symbol") continue;
|
|
959
|
+
if (!first) await append(",");
|
|
960
|
+
first = false;
|
|
961
|
+
await this.#encodeString(key, append);
|
|
962
|
+
await append(":");
|
|
963
|
+
const start = position();
|
|
964
|
+
const field = await this.#encodeIndexedValue(child, append);
|
|
965
|
+
fields[key] = { start, end: position(), ...field };
|
|
966
|
+
if (key === "id") entryId = String(child ?? "");
|
|
967
|
+
}
|
|
968
|
+
await append("}");
|
|
969
|
+
return { ...(entryId === undefined ? {} : { entryId }), fields };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async #readBytes(revision: Revision, start: number, end: number): Promise<Buffer> {
|
|
973
|
+
this.#onReadRange?.(start, end);
|
|
974
|
+
if (revision.payload !== undefined) return revision.payload.subarray(start, end);
|
|
975
|
+
if (!this.#directory || !revision.chunks || !revision.chunkLengths)
|
|
976
|
+
throw new SyntaxError("snapshot range is unavailable");
|
|
977
|
+
const values: Buffer[] = [];
|
|
978
|
+
let position = 0;
|
|
979
|
+
for (const [index, chunk] of revision.chunks.entries()) {
|
|
980
|
+
const length = revision.chunkLengths[index]!;
|
|
981
|
+
const chunkEnd = position + length;
|
|
982
|
+
if (chunkEnd > start && position < end) {
|
|
983
|
+
const data = await readFile(join(this.#directory, "objects", chunk));
|
|
984
|
+
if (data.length !== length || createHash("sha256").update(data).digest("hex") !== chunk)
|
|
985
|
+
throw new SyntaxError("snapshot chunk does not match manifest");
|
|
986
|
+
const sliceStart = Math.max(0, start - position);
|
|
987
|
+
const sliceEnd = Math.min(length, end - position);
|
|
988
|
+
values.push(data.subarray(sliceStart, sliceEnd));
|
|
989
|
+
this.#peakReadBufferedBytes = Math.max(this.#peakReadBufferedBytes, data.length + sliceEnd - sliceStart);
|
|
990
|
+
}
|
|
991
|
+
position = chunkEnd;
|
|
992
|
+
if (position >= end) break;
|
|
993
|
+
}
|
|
994
|
+
return Buffer.concat(values);
|
|
995
|
+
}
|
|
996
|
+
async #readRange(revision: Revision, range: { start: number; end: number }): Promise<string> {
|
|
997
|
+
return (await this.#readBytes(revision, range.start, range.end)).toString("utf8");
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async #readChunks(chunks: string[], lengths: number[]): Promise<Buffer | undefined> {
|
|
1001
|
+
if (!this.#directory || chunks.length === 0) return undefined;
|
|
1002
|
+
const values: Buffer[] = [];
|
|
1003
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
1004
|
+
const data = await readFile(join(this.#directory, "objects", chunk));
|
|
1005
|
+
if (data.length !== lengths[index] || createHash("sha256").update(data).digest("hex") !== chunk)
|
|
1006
|
+
throw new SyntaxError("snapshot chunk does not match manifest");
|
|
1007
|
+
values.push(data);
|
|
1008
|
+
}
|
|
1009
|
+
return Buffer.concat(values);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async #parseChunks(chunks: string[], lengths: number[]): Promise<unknown> {
|
|
1013
|
+
if (!this.#directory || chunks.length === 0) return undefined;
|
|
1014
|
+
return new ChunkJsonParser(this.#directory, chunks, lengths, bytes => {
|
|
1015
|
+
this.#peakReadBufferedBytes = Math.max(this.#peakReadBufferedBytes, bytes);
|
|
1016
|
+
}).parse();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async #spillDirectory(): Promise<string> {
|
|
1020
|
+
if (!this.#directory) this.#directory = await mkdtemp(join(tmpdir(), "gjc-sdk-snapshots-"));
|
|
1021
|
+
await mkdir(join(this.#directory, "objects"), { recursive: true, mode: 0o700 });
|
|
1022
|
+
await mkdir(join(this.#directory, "manifests"), { recursive: true, mode: 0o700 });
|
|
1023
|
+
await chmod(this.#directory, 0o700);
|
|
1024
|
+
return this.#directory;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async #writeAtomic(file: string, data: Buffer): Promise<void> {
|
|
1028
|
+
const temporary = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
1029
|
+
const handle = await open(temporary, "w", 0o600);
|
|
1030
|
+
try {
|
|
1031
|
+
await handle.writeFile(data);
|
|
1032
|
+
await handle.sync();
|
|
1033
|
+
} finally {
|
|
1034
|
+
await handle.close();
|
|
1035
|
+
}
|
|
1036
|
+
await chmod(temporary, 0o600);
|
|
1037
|
+
await rename(temporary, file);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
#retainSpill(revision: Revision): void {
|
|
1041
|
+
if (revision.manifest)
|
|
1042
|
+
this.#manifestRefs.set(revision.manifest, (this.#manifestRefs.get(revision.manifest) ?? 0) + 1);
|
|
1043
|
+
for (const chunk of revision.chunks ?? []) this.#chunkRefs.set(chunk, (this.#chunkRefs.get(chunk) ?? 0) + 1);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
#drop(revision: Revision): void {
|
|
1047
|
+
if (revision.payload) this.#memoryBytes -= revision.bytes;
|
|
1048
|
+
for (const cursorId of revision.pins) this.#pinIndex.delete(cursorId);
|
|
1049
|
+
for (const chunk of revision.chunks ?? []) this.#releaseChunk(chunk);
|
|
1050
|
+
if (revision.manifest) this.#releaseManifest(revision.manifest);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
#releaseChunk(chunk: string): void {
|
|
1054
|
+
const refs = (this.#chunkRefs.get(chunk) ?? 1) - 1;
|
|
1055
|
+
if (refs > 0) this.#chunkRefs.set(chunk, refs);
|
|
1056
|
+
else {
|
|
1057
|
+
this.#chunkRefs.delete(chunk);
|
|
1058
|
+
if (this.#directory) void unlink(join(this.#directory, "objects", chunk)).catch(() => undefined);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
#releaseManifest(manifest: string): void {
|
|
1063
|
+
const refs = (this.#manifestRefs.get(manifest) ?? 1) - 1;
|
|
1064
|
+
if (refs > 0) this.#manifestRefs.set(manifest, refs);
|
|
1065
|
+
else {
|
|
1066
|
+
this.#manifestRefs.delete(manifest);
|
|
1067
|
+
void unlink(manifest).catch(() => undefined);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async #discardUnreferenced(chunks: string[], manifest: string): Promise<void> {
|
|
1072
|
+
await Promise.all(
|
|
1073
|
+
chunks
|
|
1074
|
+
.filter(chunk => !this.#chunkRefs.has(chunk))
|
|
1075
|
+
.map(chunk =>
|
|
1076
|
+
this.#directory ? unlink(join(this.#directory, "objects", chunk)).catch(() => undefined) : undefined,
|
|
1077
|
+
),
|
|
1078
|
+
);
|
|
1079
|
+
if (!this.#manifestRefs.has(manifest)) await unlink(manifest).catch(() => undefined);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function isPlainJsonString(value: unknown): value is string {
|
|
1084
|
+
return typeof value === "string" && !/["\\\u0000-\u001f\ud800-\udfff]/.test(value);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Finds the furthest UTF-16 boundary whose UTF-8 encoding fits in maxBytes. */
|
|
1088
|
+
function utf8ChunkEnd(text: string, start: number, maxBytes: number): number {
|
|
1089
|
+
let low = start;
|
|
1090
|
+
let high = Math.min(text.length, start + maxBytes);
|
|
1091
|
+
while (low < high) {
|
|
1092
|
+
const middle = low + Math.ceil((high - low) / 2);
|
|
1093
|
+
if (Buffer.byteLength(text.slice(start, middle)) <= maxBytes) low = middle;
|
|
1094
|
+
else high = middle - 1;
|
|
1095
|
+
}
|
|
1096
|
+
if (
|
|
1097
|
+
low > start &&
|
|
1098
|
+
low < text.length &&
|
|
1099
|
+
text.charCodeAt(low - 1) >= 0xd800 &&
|
|
1100
|
+
text.charCodeAt(low - 1) <= 0xdbff &&
|
|
1101
|
+
text.charCodeAt(low) >= 0xdc00 &&
|
|
1102
|
+
text.charCodeAt(low) <= 0xdfff
|
|
1103
|
+
)
|
|
1104
|
+
low--;
|
|
1105
|
+
return low;
|
|
1106
|
+
}
|
|
1107
|
+
function stringCheckpointAtOrBefore(checkpoints: readonly StringCheckpoint[], offset: number): StringCheckpoint {
|
|
1108
|
+
if (checkpoints.length === 0) throw new SyntaxError("escaped string index has no checkpoints");
|
|
1109
|
+
let low = 0;
|
|
1110
|
+
let high = checkpoints.length;
|
|
1111
|
+
while (low < high) {
|
|
1112
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
1113
|
+
if (checkpoints[middle]!.decodedOffset <= offset) low = middle + 1;
|
|
1114
|
+
else high = middle;
|
|
1115
|
+
}
|
|
1116
|
+
return checkpoints[Math.max(0, low - 1)]!;
|
|
1117
|
+
}
|
|
1118
|
+
function utf8BoundaryAtOrBefore(bytes: Buffer, offset: number): number {
|
|
1119
|
+
while (offset > 0 && offset < bytes.length && (bytes[offset]! & 0xc0) === 0x80) offset--;
|
|
1120
|
+
return offset;
|
|
1121
|
+
}
|
|
1122
|
+
function utf8ContinuationPrefixLength(bytes: Buffer): number {
|
|
1123
|
+
let offset = 0;
|
|
1124
|
+
while (offset < bytes.length && (bytes[offset]! & 0xc0) === 0x80) offset++;
|
|
1125
|
+
return offset;
|
|
1126
|
+
}
|