byterover-cli 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -63
- package/bin/dev.js +8 -1
- package/bin/run.js +7 -0
- package/dist/commands/cipher-agent/run.d.ts +30 -0
- package/dist/commands/cipher-agent/run.js +123 -61
- package/dist/commands/cipher-agent/set-prompt.d.ts +2 -0
- package/dist/commands/cipher-agent/set-prompt.js +13 -8
- package/dist/commands/cipher-agent/show-prompt.d.ts +2 -0
- package/dist/commands/cipher-agent/show-prompt.js +17 -12
- package/dist/commands/curate.d.ts +3 -60
- package/dist/commands/curate.js +45 -341
- package/dist/commands/foo.d.ts +4 -2
- package/dist/commands/foo.js +21 -16
- package/dist/commands/main.d.ts +9 -0
- package/dist/commands/main.js +34 -0
- package/dist/commands/query.d.ts +2 -48
- package/dist/commands/query.js +19 -287
- package/dist/commands/status.d.ts +2 -13
- package/dist/commands/status.js +12 -91
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +23 -19
- package/dist/config/environment.d.ts +1 -1
- package/dist/config/environment.js +2 -2
- package/dist/constants.d.ts +4 -5
- package/dist/constants.js +5 -5
- package/dist/core/domain/cipher/errors/storage-error.d.ts +89 -0
- package/dist/core/domain/cipher/errors/storage-error.js +130 -0
- package/dist/core/domain/cipher/queue/types.d.ts +71 -0
- package/dist/core/domain/cipher/queue/types.js +9 -0
- package/dist/core/domain/cipher/storage/message-storage-types.d.ts +218 -0
- package/dist/core/domain/cipher/storage/message-storage-types.js +18 -0
- package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
- package/dist/core/domain/cipher/tools/constants.js +1 -0
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +8 -1
- package/dist/core/domain/entities/global-config.d.ts +36 -0
- package/dist/core/domain/entities/global-config.js +66 -0
- package/dist/core/domain/knowledge/directory-manager.d.ts +10 -0
- package/dist/core/domain/knowledge/directory-manager.js +18 -0
- package/dist/core/domain/knowledge/markdown-writer.d.ts +9 -0
- package/dist/core/domain/knowledge/markdown-writer.js +51 -1
- package/dist/core/interfaces/cipher/i-agent-storage.d.ts +152 -0
- package/dist/core/interfaces/cipher/i-agent-storage.js +1 -0
- package/dist/core/interfaces/cipher/i-cipher-agent.d.ts +2 -0
- package/dist/core/interfaces/cipher/i-key-storage.d.ts +91 -0
- package/dist/core/interfaces/cipher/i-key-storage.js +1 -0
- package/dist/core/interfaces/i-global-config-store.d.ts +34 -0
- package/dist/core/interfaces/i-global-config-store.js +1 -0
- package/dist/core/interfaces/i-onboarding-preference-store.d.ts +20 -0
- package/dist/core/interfaces/i-onboarding-preference-store.js +1 -0
- package/dist/core/interfaces/i-terminal.d.ts +146 -0
- package/dist/core/interfaces/i-terminal.js +1 -0
- package/dist/core/interfaces/i-workspace-detector-service.d.ts +8 -0
- package/dist/core/interfaces/i-workspace-detector-service.js +1 -0
- package/dist/core/interfaces/usecase/i-clear-use-case.d.ts +6 -0
- package/dist/core/interfaces/usecase/i-clear-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +10 -0
- package/dist/core/interfaces/usecase/i-curate-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +5 -0
- package/dist/core/interfaces/usecase/i-init-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-login-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-logout-use-case.d.ts +5 -0
- package/dist/core/interfaces/usecase/i-logout-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -0
- package/dist/core/interfaces/usecase/i-pull-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -0
- package/dist/core/interfaces/usecase/i-push-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +9 -0
- package/dist/core/interfaces/usecase/i-query-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-space-list-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-space-list-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-space-switch-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-space-switch-use-case.js +1 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +5 -0
- package/dist/core/interfaces/usecase/i-status-use-case.js +1 -0
- package/dist/hooks/init/welcome.js +10 -26
- package/dist/infra/cipher/agent-service-factory.d.ts +13 -6
- package/dist/infra/cipher/agent-service-factory.js +40 -16
- package/dist/infra/cipher/cipher-agent.js +4 -4
- package/dist/infra/cipher/consumer/consumer-lock.d.ts +20 -0
- package/dist/infra/cipher/consumer/consumer-lock.js +40 -0
- package/dist/infra/cipher/consumer/consumer-service.d.ts +99 -0
- package/dist/infra/cipher/consumer/consumer-service.js +165 -0
- package/dist/infra/cipher/consumer/execution-consumer.d.ts +121 -0
- package/dist/infra/cipher/consumer/execution-consumer.js +523 -0
- package/dist/infra/cipher/consumer/index.d.ts +33 -0
- package/dist/infra/cipher/consumer/index.js +33 -0
- package/dist/infra/cipher/consumer/queue-polling-service.d.ts +120 -0
- package/dist/infra/cipher/consumer/queue-polling-service.js +248 -0
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +94 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +118 -0
- package/dist/infra/cipher/llm/context/compaction/compaction-service.d.ts +106 -0
- package/dist/infra/cipher/llm/context/compaction/compaction-service.js +132 -0
- package/dist/infra/cipher/llm/context/compaction/index.d.ts +9 -0
- package/dist/infra/cipher/llm/context/compaction/index.js +9 -0
- package/dist/infra/cipher/llm/context/context-manager.d.ts +46 -2
- package/dist/infra/cipher/llm/context/context-manager.js +68 -4
- package/dist/infra/cipher/llm/context/rw-lock.d.ts +72 -0
- package/dist/infra/cipher/llm/context/rw-lock.js +145 -0
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +7 -7
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +8 -8
- package/dist/infra/cipher/llm/internal-llm-service.js +2 -0
- package/dist/infra/cipher/session/session-manager.d.ts +4 -4
- package/dist/infra/cipher/session/session-manager.js +5 -5
- package/dist/infra/cipher/storage/agent-storage.d.ts +246 -0
- package/dist/infra/cipher/storage/agent-storage.js +956 -0
- package/dist/infra/cipher/storage/dual-format-history-storage.d.ts +77 -0
- package/dist/infra/cipher/storage/dual-format-history-storage.js +149 -0
- package/dist/infra/cipher/storage/granular-history-storage.d.ts +65 -0
- package/dist/infra/cipher/storage/granular-history-storage.js +118 -0
- package/dist/infra/cipher/storage/message-storage-service.d.ts +108 -0
- package/dist/infra/cipher/storage/message-storage-service.js +529 -0
- package/dist/infra/cipher/storage/process-utils.d.ts +16 -0
- package/dist/infra/cipher/storage/process-utils.js +43 -0
- package/dist/infra/cipher/storage/sqlite-key-storage.d.ts +105 -0
- package/dist/infra/cipher/storage/sqlite-key-storage.js +404 -0
- package/dist/infra/cipher/system-prompt/simple-prompt-factory.d.ts +1 -0
- package/dist/infra/cipher/system-prompt/simple-prompt-factory.js +7 -0
- package/dist/infra/cipher/tools/default-policy-rules.js +1 -1
- package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +10 -0
- package/dist/infra/cipher/tools/implementations/curate-tool.js +371 -0
- package/dist/infra/cipher/tools/implementations/find-knowledge-topics-tool.js +11 -8
- package/dist/infra/cipher/tools/tool-manager.d.ts +8 -2
- package/dist/infra/cipher/tools/tool-manager.js +29 -2
- package/dist/infra/cipher/tools/tool-registry.js +7 -0
- package/dist/infra/http/authenticated-http-client.d.ts +21 -0
- package/dist/infra/http/authenticated-http-client.js +38 -0
- package/dist/infra/repl/commands/arg-parser.d.ts +97 -0
- package/dist/infra/repl/commands/arg-parser.js +129 -0
- package/dist/infra/repl/commands/clear-command.d.ts +5 -0
- package/dist/infra/repl/commands/clear-command.js +61 -0
- package/dist/infra/repl/commands/curate-command.d.ts +9 -0
- package/dist/infra/repl/commands/curate-command.js +88 -0
- package/dist/infra/repl/commands/gen-rules-command.d.ts +7 -0
- package/dist/infra/repl/commands/gen-rules-command.js +38 -0
- package/dist/infra/repl/commands/index.d.ts +8 -0
- package/dist/infra/repl/commands/index.js +36 -0
- package/dist/infra/repl/commands/init-command.d.ts +7 -0
- package/dist/infra/repl/commands/init-command.js +83 -0
- package/dist/infra/repl/commands/login-command.d.ts +7 -0
- package/dist/infra/repl/commands/login-command.js +50 -0
- package/dist/infra/repl/commands/logout-command.d.ts +5 -0
- package/dist/infra/repl/commands/logout-command.js +48 -0
- package/dist/infra/repl/commands/pull-command.d.ts +5 -0
- package/dist/infra/repl/commands/pull-command.js +61 -0
- package/dist/infra/repl/commands/push-command.d.ts +5 -0
- package/dist/infra/repl/commands/push-command.js +66 -0
- package/dist/infra/repl/commands/query-command.d.ts +5 -0
- package/dist/infra/repl/commands/query-command.js +66 -0
- package/dist/infra/repl/commands/space/index.d.ts +5 -0
- package/dist/infra/repl/commands/space/index.js +14 -0
- package/dist/infra/repl/commands/space/list-command.d.ts +5 -0
- package/dist/infra/repl/commands/space/list-command.js +70 -0
- package/dist/infra/repl/commands/space/switch-command.d.ts +5 -0
- package/dist/infra/repl/commands/space/switch-command.js +37 -0
- package/dist/infra/repl/commands/status-command.d.ts +5 -0
- package/dist/infra/repl/commands/status-command.js +39 -0
- package/dist/infra/repl/repl-startup.d.ts +18 -0
- package/dist/infra/repl/repl-startup.js +28 -0
- package/dist/infra/storage/file-global-config-store.d.ts +22 -0
- package/dist/infra/storage/file-global-config-store.js +65 -0
- package/dist/infra/storage/file-onboarding-preference-store.d.ts +10 -0
- package/dist/infra/storage/file-onboarding-preference-store.js +46 -0
- package/dist/infra/terminal/oclif-terminal.d.ts +19 -0
- package/dist/infra/terminal/oclif-terminal.js +60 -0
- package/dist/infra/terminal/repl-terminal.d.ts +31 -0
- package/dist/infra/terminal/repl-terminal.js +116 -0
- package/dist/infra/tracking/mixpanel-tracking-service.d.ts +11 -1
- package/dist/infra/tracking/mixpanel-tracking-service.js +18 -13
- package/dist/infra/usecase/clear-use-case.d.ts +20 -0
- package/dist/infra/usecase/clear-use-case.js +58 -0
- package/dist/infra/usecase/curate-use-case.d.ts +66 -0
- package/dist/infra/usecase/curate-use-case.js +288 -0
- package/dist/{commands/gen-rules.d.ts → infra/usecase/generate-rules-use-case.d.ts} +14 -20
- package/dist/{commands/gen-rules.js → infra/usecase/generate-rules-use-case.js} +59 -78
- package/dist/infra/usecase/init-use-case.d.ts +139 -0
- package/dist/{commands/init.js → infra/usecase/init-use-case.js} +197 -233
- package/dist/infra/usecase/login-use-case.d.ts +28 -0
- package/dist/infra/usecase/login-use-case.js +94 -0
- package/dist/infra/usecase/logout-use-case.d.ts +22 -0
- package/dist/infra/usecase/logout-use-case.js +51 -0
- package/dist/infra/usecase/pull-use-case.d.ts +35 -0
- package/dist/infra/usecase/pull-use-case.js +89 -0
- package/dist/infra/usecase/push-use-case.d.ts +37 -0
- package/dist/infra/usecase/push-use-case.js +124 -0
- package/dist/infra/usecase/query-use-case.d.ts +78 -0
- package/dist/infra/usecase/query-use-case.js +402 -0
- package/dist/infra/usecase/space-list-use-case.d.ts +27 -0
- package/dist/infra/usecase/space-list-use-case.js +64 -0
- package/dist/infra/usecase/space-switch-use-case.d.ts +36 -0
- package/dist/infra/usecase/space-switch-use-case.js +140 -0
- package/dist/infra/usecase/status-use-case.d.ts +27 -0
- package/dist/infra/usecase/status-use-case.js +97 -0
- package/dist/infra/workspace/workspace-detector-service.d.ts +3 -6
- package/dist/resources/prompts/curate-context-tree-curation.yml +23 -11
- package/dist/resources/prompts/query-context-tree-retrieval.yml +3 -4
- package/dist/resources/prompts/system-prompt.yml +1 -1
- package/dist/resources/prompts/tool-outputs.yml +4 -3
- package/dist/templates/sections/command-reference.md +12 -0
- package/dist/templates/sections/workflow.md +10 -1
- package/dist/tui/app.d.ts +9 -0
- package/dist/tui/app.js +26 -0
- package/dist/tui/components/enter-prompt.d.ts +13 -0
- package/dist/tui/components/enter-prompt.js +15 -0
- package/dist/tui/components/execution/execution-changes.d.ts +14 -0
- package/dist/tui/components/execution/execution-changes.js +15 -0
- package/dist/tui/components/execution/execution-content.d.ts +25 -0
- package/dist/tui/components/execution/execution-content.js +67 -0
- package/dist/tui/components/execution/execution-input.d.ts +12 -0
- package/dist/tui/components/execution/execution-input.js +16 -0
- package/dist/tui/components/execution/execution-progress.d.ts +21 -0
- package/dist/tui/components/execution/execution-progress.js +21 -0
- package/dist/tui/components/execution/execution-status.d.ts +13 -0
- package/dist/tui/components/execution/execution-status.js +19 -0
- package/dist/tui/components/execution/index.d.ts +11 -0
- package/dist/tui/components/execution/index.js +11 -0
- package/dist/tui/components/execution/log-item.d.ts +17 -0
- package/dist/tui/components/execution/log-item.js +25 -0
- package/dist/tui/components/footer.d.ts +5 -0
- package/dist/tui/components/footer.js +12 -0
- package/dist/tui/components/header.d.ts +18 -0
- package/dist/tui/components/header.js +18 -0
- package/dist/tui/components/index.d.ts +17 -0
- package/dist/tui/components/index.js +14 -0
- package/dist/tui/components/inline-prompts/index.d.ts +15 -0
- package/dist/tui/components/inline-prompts/index.js +10 -0
- package/dist/tui/components/inline-prompts/inline-confirm.d.ts +17 -0
- package/dist/tui/components/inline-prompts/inline-confirm.js +32 -0
- package/dist/tui/components/inline-prompts/inline-file-selector.d.ts +43 -0
- package/dist/tui/components/inline-prompts/inline-file-selector.js +185 -0
- package/dist/tui/components/inline-prompts/inline-input.d.ts +19 -0
- package/dist/tui/components/inline-prompts/inline-input.js +32 -0
- package/dist/tui/components/inline-prompts/inline-search.d.ts +20 -0
- package/dist/tui/components/inline-prompts/inline-search.js +50 -0
- package/dist/tui/components/inline-prompts/inline-select.d.ts +20 -0
- package/dist/tui/components/inline-prompts/inline-select.js +34 -0
- package/dist/tui/components/logo.d.ts +43 -0
- package/dist/tui/components/logo.js +103 -0
- package/dist/tui/components/message-item.d.ts +12 -0
- package/dist/tui/components/message-item.js +12 -0
- package/dist/tui/components/onboarding/copyable-prompt.d.ts +15 -0
- package/dist/tui/components/onboarding/copyable-prompt.js +65 -0
- package/dist/tui/components/onboarding/index.d.ts +7 -0
- package/dist/tui/components/onboarding/index.js +6 -0
- package/dist/tui/components/onboarding/onboarding-flow.d.ts +13 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +304 -0
- package/dist/tui/components/onboarding/onboarding-step.d.ts +23 -0
- package/dist/tui/components/onboarding/onboarding-step.js +12 -0
- package/dist/tui/components/output-log.d.ts +14 -0
- package/dist/tui/components/output-log.js +13 -0
- package/dist/tui/components/scrollable-list.d.ts +30 -0
- package/dist/tui/components/scrollable-list.js +121 -0
- package/dist/tui/components/suggestions.d.ts +16 -0
- package/dist/tui/components/suggestions.js +162 -0
- package/dist/tui/components/tab-bar.d.ts +10 -0
- package/dist/tui/components/tab-bar.js +12 -0
- package/dist/tui/constants.d.ts +11 -0
- package/dist/tui/constants.js +13 -0
- package/dist/tui/contexts/auth-context.d.ts +30 -0
- package/dist/tui/contexts/auth-context.js +153 -0
- package/dist/tui/contexts/consumer.d.ts +31 -0
- package/dist/tui/contexts/consumer.js +56 -0
- package/dist/tui/contexts/index.d.ts +6 -0
- package/dist/tui/contexts/index.js +6 -0
- package/dist/tui/contexts/onboarding-context.d.ts +43 -0
- package/dist/tui/contexts/onboarding-context.js +181 -0
- package/dist/tui/contexts/services-context.d.ts +29 -0
- package/dist/tui/contexts/services-context.js +20 -0
- package/dist/tui/contexts/use-commands.d.ts +29 -0
- package/dist/tui/contexts/use-commands.js +54 -0
- package/dist/tui/contexts/use-mode.d.ts +43 -0
- package/dist/tui/contexts/use-mode.js +76 -0
- package/dist/tui/contexts/use-theme.d.ts +53 -0
- package/dist/tui/contexts/use-theme.js +60 -0
- package/dist/tui/hooks/index.d.ts +17 -0
- package/dist/tui/hooks/index.js +14 -0
- package/dist/tui/hooks/use-activity-logs.d.ts +26 -0
- package/dist/tui/hooks/use-activity-logs.js +90 -0
- package/dist/tui/hooks/use-consumer.d.ts +12 -0
- package/dist/tui/hooks/use-consumer.js +50 -0
- package/dist/tui/hooks/use-onboarding.d.ts +7 -0
- package/dist/tui/hooks/use-onboarding.js +6 -0
- package/dist/tui/hooks/use-queue-polling.d.ts +31 -0
- package/dist/tui/hooks/use-queue-polling.js +90 -0
- package/dist/tui/hooks/use-slash-command-processor.d.ts +16 -0
- package/dist/tui/hooks/use-slash-command-processor.js +132 -0
- package/dist/tui/hooks/use-slash-completion.d.ts +30 -0
- package/dist/tui/hooks/use-slash-completion.js +230 -0
- package/dist/tui/hooks/use-tab-navigation.d.ts +10 -0
- package/dist/tui/hooks/use-tab-navigation.js +35 -0
- package/dist/tui/hooks/use-visible-window.d.ts +22 -0
- package/dist/tui/hooks/use-visible-window.js +37 -0
- package/dist/tui/index.d.ts +1 -0
- package/dist/tui/index.js +1 -0
- package/dist/tui/providers/app-providers.d.ts +25 -0
- package/dist/tui/providers/app-providers.js +9 -0
- package/dist/tui/types/commands.d.ts +252 -0
- package/dist/tui/types/commands.js +16 -0
- package/dist/tui/types/dialogs.d.ts +37 -0
- package/dist/tui/types/dialogs.js +4 -0
- package/dist/tui/types/index.d.ts +11 -0
- package/dist/tui/types/index.js +7 -0
- package/dist/tui/types/messages.d.ts +55 -0
- package/dist/tui/types/messages.js +4 -0
- package/dist/tui/types/prompts.d.ts +100 -0
- package/dist/tui/types/prompts.js +4 -0
- package/dist/tui/types/ui.d.ts +14 -0
- package/dist/tui/types/ui.js +4 -0
- package/dist/tui/types.d.ts +1 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/views/command-view.d.ts +12 -0
- package/dist/tui/views/command-view.js +451 -0
- package/dist/tui/views/index.d.ts +6 -0
- package/dist/tui/views/index.js +6 -0
- package/dist/tui/views/login-view.d.ts +10 -0
- package/dist/tui/views/login-view.js +30 -0
- package/dist/tui/views/logs-view.d.ts +11 -0
- package/dist/tui/views/logs-view.js +73 -0
- package/dist/utils/file-validator.d.ts +16 -0
- package/dist/utils/file-validator.js +81 -0
- package/dist/utils/global-config-path.d.ts +15 -0
- package/dist/utils/global-config-path.js +38 -0
- package/oclif.manifest.json +29 -315
- package/package.json +11 -4
- package/dist/commands/clear.d.ts +0 -19
- package/dist/commands/clear.js +0 -78
- package/dist/commands/init.d.ts +0 -130
- package/dist/commands/login.d.ts +0 -22
- package/dist/commands/login.js +0 -108
- package/dist/commands/logout.d.ts +0 -16
- package/dist/commands/logout.js +0 -61
- package/dist/commands/pull.d.ts +0 -33
- package/dist/commands/pull.js +0 -115
- package/dist/commands/push.d.ts +0 -35
- package/dist/commands/push.js +0 -160
- package/dist/commands/space/list.d.ts +0 -25
- package/dist/commands/space/list.js +0 -114
- package/dist/commands/space/switch.d.ts +0 -36
- package/dist/commands/space/switch.js +0 -160
- package/dist/infra/cipher/grpc/internal-llm-grpc-service.d.ts +0 -149
- package/dist/infra/cipher/grpc/internal-llm-grpc-service.js +0 -364
- package/dist/infra/cipher/grpc/internal-llm-grpc.proto +0 -94
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import * as fsSync from 'node:fs';
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { BLOBS_DIR, BRV_DIR } from '../../../constants.js';
|
|
7
|
+
import { isProcessRunning } from './process-utils.js';
|
|
8
|
+
// Query execution timeout (5 minutes of no activity)
|
|
9
|
+
const QUERY_ACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
// ==================== SQL SCHEMA ====================
|
|
11
|
+
const SCHEMA_SQL = `
|
|
12
|
+
-- Consumer Locks: track active consumers for orphan detection
|
|
13
|
+
CREATE TABLE IF NOT EXISTS consumer_locks (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
pid INTEGER NOT NULL,
|
|
16
|
+
started_at INTEGER NOT NULL,
|
|
17
|
+
last_heartbeat INTEGER NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- Executions: job queue (status='queued') + execution tracking
|
|
21
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
type TEXT NOT NULL,
|
|
24
|
+
input TEXT NOT NULL,
|
|
25
|
+
status TEXT NOT NULL,
|
|
26
|
+
consumer_id TEXT,
|
|
27
|
+
pid INTEGER,
|
|
28
|
+
result TEXT,
|
|
29
|
+
error TEXT,
|
|
30
|
+
created_at INTEGER NOT NULL,
|
|
31
|
+
started_at INTEGER,
|
|
32
|
+
completed_at INTEGER,
|
|
33
|
+
updated_at INTEGER NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- Tool Calls: tool call details for UI polling
|
|
37
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
execution_id TEXT NOT NULL,
|
|
40
|
+
name TEXT NOT NULL,
|
|
41
|
+
description TEXT,
|
|
42
|
+
args TEXT,
|
|
43
|
+
status TEXT NOT NULL,
|
|
44
|
+
result TEXT,
|
|
45
|
+
result_summary TEXT,
|
|
46
|
+
started_at INTEGER NOT NULL,
|
|
47
|
+
completed_at INTEGER,
|
|
48
|
+
-- Metrics for FE display (raw int values)
|
|
49
|
+
duration_ms INTEGER,
|
|
50
|
+
lines_count INTEGER,
|
|
51
|
+
chars_count INTEGER,
|
|
52
|
+
FOREIGN KEY (execution_id) REFERENCES executions(id) ON DELETE CASCADE
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- Indexes for performance
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_executions_status ON executions(status);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_executions_type ON executions(type);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_executions_updated ON executions(updated_at);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_executions_consumer_id ON executions(consumer_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_execution ON tool_calls(execution_id);
|
|
61
|
+
`;
|
|
62
|
+
// Migration SQL for existing databases (add new columns if missing)
|
|
63
|
+
const MIGRATION_SQL = `
|
|
64
|
+
-- Add new columns to tool_calls if they don't exist
|
|
65
|
+
ALTER TABLE tool_calls ADD COLUMN duration_ms INTEGER;
|
|
66
|
+
ALTER TABLE tool_calls ADD COLUMN lines_count INTEGER;
|
|
67
|
+
ALTER TABLE tool_calls ADD COLUMN chars_count INTEGER;
|
|
68
|
+
-- Add consumer_id to executions for orphan tracking
|
|
69
|
+
ALTER TABLE executions ADD COLUMN consumer_id TEXT;
|
|
70
|
+
-- Add pid to executions for query orphan detection
|
|
71
|
+
ALTER TABLE executions ADD COLUMN pid INTEGER;
|
|
72
|
+
-- Add index for consumer_id lookups (orphan detection queries)
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_executions_consumer_id ON executions(consumer_id);
|
|
74
|
+
`;
|
|
75
|
+
// ==================== CLASS ====================
|
|
76
|
+
/**
|
|
77
|
+
* AgentStorage - SQLite-based storage for execution queue and tool calls
|
|
78
|
+
*
|
|
79
|
+
* Features:
|
|
80
|
+
* - Single database file at .brv/blobs/agent.db
|
|
81
|
+
* - Job queue via executions.status = 'queued'
|
|
82
|
+
* - Tool call tracking for UI polling
|
|
83
|
+
* - Prepared statement caching (no memory leak)
|
|
84
|
+
* - WAL mode for concurrent read/write
|
|
85
|
+
* - Orphan cleanup on startup
|
|
86
|
+
* - Old execution cleanup (max 100)
|
|
87
|
+
*/
|
|
88
|
+
export class AgentStorage {
|
|
89
|
+
initialized = false;
|
|
90
|
+
db = null;
|
|
91
|
+
// Track DB file inode to detect if file was replaced
|
|
92
|
+
dbFileInode = null;
|
|
93
|
+
dbPath;
|
|
94
|
+
inMemory;
|
|
95
|
+
stmtAddToolCall = null;
|
|
96
|
+
stmtCleanupOrphans = null;
|
|
97
|
+
stmtCountCompletedFailed = null;
|
|
98
|
+
// Cached prepared statements (lazy init, reuse to avoid memory leak)
|
|
99
|
+
stmtCreateExecution = null;
|
|
100
|
+
stmtDeleteConsumerLock = null;
|
|
101
|
+
stmtDeleteOldExecutions = null;
|
|
102
|
+
stmtDequeueBatchSelect = null;
|
|
103
|
+
stmtDequeueSelect = null;
|
|
104
|
+
stmtFailQuery = null;
|
|
105
|
+
stmtGetExecution = null;
|
|
106
|
+
stmtGetExecutionsSince = null;
|
|
107
|
+
stmtGetQueuedExecutions = null;
|
|
108
|
+
stmtGetRecentExecutions = null;
|
|
109
|
+
stmtGetRunningExecutions = null;
|
|
110
|
+
stmtGetToolCalls = null;
|
|
111
|
+
stmtOrphanFromConsumer = null;
|
|
112
|
+
stmtOrphanMissingConsumer = null;
|
|
113
|
+
stmtOrphanNullConsumer = null;
|
|
114
|
+
stmtUpdateStatus = null;
|
|
115
|
+
stmtUpdateToolCall = null;
|
|
116
|
+
storageDir;
|
|
117
|
+
constructor(config) {
|
|
118
|
+
this.inMemory = config?.inMemory ?? false;
|
|
119
|
+
this.storageDir = config?.storageDir || join(process.cwd(), BRV_DIR, BLOBS_DIR);
|
|
120
|
+
this.dbPath = this.inMemory ? ':memory:' : join(this.storageDir, 'agent.db');
|
|
121
|
+
}
|
|
122
|
+
// ==================== LIFECYCLE ====================
|
|
123
|
+
/**
|
|
124
|
+
* Acquire consumer lock (register this consumer)
|
|
125
|
+
* Only ONE consumer can run at a time - checks for any active consumer first
|
|
126
|
+
* @returns true if lock acquired, false if another consumer is already running
|
|
127
|
+
*/
|
|
128
|
+
acquireConsumerLock(consumerId) {
|
|
129
|
+
this.ensureInitialized();
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const { pid } = process;
|
|
132
|
+
// Use transaction to ensure atomic check-then-insert
|
|
133
|
+
const acquireLock = this.getDb().transaction(() => {
|
|
134
|
+
// Check if any active consumer exists (with recent heartbeat)
|
|
135
|
+
const cutoff = now - 30_000; // 30 second timeout
|
|
136
|
+
const existing = this.getDb().prepare(`SELECT id FROM consumer_locks WHERE last_heartbeat >= ?`).get(cutoff);
|
|
137
|
+
if (existing) {
|
|
138
|
+
// Another consumer is active
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// No active consumer - acquire lock
|
|
142
|
+
this.getDb()
|
|
143
|
+
.prepare(`INSERT INTO consumer_locks (id, pid, started_at, last_heartbeat)
|
|
144
|
+
VALUES (?, ?, ?, ?)`)
|
|
145
|
+
.run(consumerId, pid, now, now);
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
return acquireLock();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Add a tool call record
|
|
152
|
+
* @returns tool call id
|
|
153
|
+
*/
|
|
154
|
+
addToolCall(executionId, info) {
|
|
155
|
+
this.ensureInitialized();
|
|
156
|
+
const id = randomUUID();
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
if (!this.stmtAddToolCall) {
|
|
159
|
+
this.stmtAddToolCall = this.getDb().prepare(`
|
|
160
|
+
INSERT INTO tool_calls (id, execution_id, name, description, args, status, started_at)
|
|
161
|
+
VALUES (?, ?, ?, ?, ?, 'running', ?)
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
this.stmtAddToolCall.run(id, executionId, info.name, info.description ?? null, JSON.stringify(info.args), now);
|
|
165
|
+
return id;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Cleanup old executions, keep only maxKeep most recent completed/failed
|
|
169
|
+
*/
|
|
170
|
+
cleanupOldExecutions(maxKeep = 100) {
|
|
171
|
+
this.ensureInitialized();
|
|
172
|
+
// Count completed/failed executions
|
|
173
|
+
if (!this.stmtCountCompletedFailed) {
|
|
174
|
+
this.stmtCountCompletedFailed = this.getDb().prepare(`
|
|
175
|
+
SELECT COUNT(*) as count FROM executions
|
|
176
|
+
WHERE status IN ('completed', 'failed')
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
const countResult = this.stmtCountCompletedFailed.get();
|
|
180
|
+
const toDelete = countResult.count - maxKeep;
|
|
181
|
+
if (toDelete <= 0) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
// Delete oldest executions (tool_calls auto-deleted via CASCADE)
|
|
185
|
+
if (!this.stmtDeleteOldExecutions) {
|
|
186
|
+
this.stmtDeleteOldExecutions = this.getDb().prepare(`
|
|
187
|
+
DELETE FROM executions
|
|
188
|
+
WHERE id IN (
|
|
189
|
+
SELECT id FROM executions
|
|
190
|
+
WHERE status IN ('completed', 'failed')
|
|
191
|
+
ORDER BY created_at ASC
|
|
192
|
+
LIMIT ?
|
|
193
|
+
)
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
const result = this.stmtDeleteOldExecutions.run(toDelete);
|
|
197
|
+
return result.changes;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Cleanup orphaned executions (status='running') from previous session crash
|
|
201
|
+
* Should be called on startup
|
|
202
|
+
*/
|
|
203
|
+
cleanupOrphanedExecutions() {
|
|
204
|
+
this.ensureInitialized();
|
|
205
|
+
if (!this.stmtCleanupOrphans) {
|
|
206
|
+
this.stmtCleanupOrphans = this.getDb().prepare(`
|
|
207
|
+
UPDATE executions
|
|
208
|
+
SET status = 'failed',
|
|
209
|
+
error = 'Orphaned from previous session',
|
|
210
|
+
updated_at = ?
|
|
211
|
+
WHERE status = 'running'
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const result = this.stmtCleanupOrphans.run(now);
|
|
216
|
+
return result.changes;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Cleanup stale consumers and orphan their executions
|
|
220
|
+
* A consumer is stale if its heartbeat is older than timeoutMs
|
|
221
|
+
* @param timeoutMs - heartbeat timeout (default 30 seconds)
|
|
222
|
+
* @returns number of orphaned executions
|
|
223
|
+
*/
|
|
224
|
+
cleanupStaleConsumers(timeoutMs = 30_000) {
|
|
225
|
+
this.ensureInitialized();
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const cutoff = now - timeoutMs;
|
|
228
|
+
let totalOrphaned = 0;
|
|
229
|
+
// Case 1: Find consumers with stale heartbeat
|
|
230
|
+
const staleConsumers = this.getDb()
|
|
231
|
+
.prepare(`
|
|
232
|
+
SELECT id FROM consumer_locks WHERE last_heartbeat < ?
|
|
233
|
+
`)
|
|
234
|
+
.all(cutoff);
|
|
235
|
+
if (staleConsumers.length > 0) {
|
|
236
|
+
// Orphan executions from stale consumers (cached statement)
|
|
237
|
+
if (!this.stmtOrphanFromConsumer) {
|
|
238
|
+
this.stmtOrphanFromConsumer = this.getDb().prepare(`
|
|
239
|
+
UPDATE executions
|
|
240
|
+
SET status = 'failed',
|
|
241
|
+
error = 'Consumer died unexpectedly',
|
|
242
|
+
consumer_id = NULL,
|
|
243
|
+
updated_at = ?
|
|
244
|
+
WHERE consumer_id = ? AND status = 'running'
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
// Delete stale consumer locks (cached statement)
|
|
248
|
+
if (!this.stmtDeleteConsumerLock) {
|
|
249
|
+
this.stmtDeleteConsumerLock = this.getDb().prepare(`
|
|
250
|
+
DELETE FROM consumer_locks WHERE id = ?
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
for (const consumer of staleConsumers) {
|
|
254
|
+
const result = this.stmtOrphanFromConsumer.run(now, consumer.id);
|
|
255
|
+
totalOrphaned += result.changes;
|
|
256
|
+
this.stmtDeleteConsumerLock.run(consumer.id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Case 2: Find "running" executions whose consumer_id doesn't exist in consumer_locks
|
|
260
|
+
// This handles cases where consumer crashed without proper cleanup
|
|
261
|
+
if (!this.stmtOrphanMissingConsumer) {
|
|
262
|
+
this.stmtOrphanMissingConsumer = this.getDb().prepare(`
|
|
263
|
+
UPDATE executions
|
|
264
|
+
SET status = 'failed',
|
|
265
|
+
error = 'Consumer no longer exists',
|
|
266
|
+
completed_at = ?,
|
|
267
|
+
updated_at = ?
|
|
268
|
+
WHERE status = 'running'
|
|
269
|
+
AND consumer_id IS NOT NULL
|
|
270
|
+
AND consumer_id NOT IN (SELECT id FROM consumer_locks)
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
const orphanedFromMissingConsumers = this.stmtOrphanMissingConsumer.run(now, now);
|
|
274
|
+
totalOrphaned += orphanedFromMissingConsumers.changes;
|
|
275
|
+
// Case 3: Find "running" CURATE executions with NULL consumer_id (orphaned from releaseConsumerLock)
|
|
276
|
+
// These are stuck executions where consumer stopped but didn't complete the job
|
|
277
|
+
// NOTE: Query runs inline (not via consumer), so consumer_id=NULL is normal for query
|
|
278
|
+
if (!this.stmtOrphanNullConsumer) {
|
|
279
|
+
this.stmtOrphanNullConsumer = this.getDb().prepare(`
|
|
280
|
+
UPDATE executions
|
|
281
|
+
SET status = 'failed',
|
|
282
|
+
error = 'Execution orphaned (no consumer)',
|
|
283
|
+
completed_at = ?,
|
|
284
|
+
updated_at = ?
|
|
285
|
+
WHERE status = 'running'
|
|
286
|
+
AND consumer_id IS NULL
|
|
287
|
+
AND type = 'curate'
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
const orphanedNullConsumer = this.stmtOrphanNullConsumer.run(now, now);
|
|
291
|
+
totalOrphaned += orphanedNullConsumer.changes;
|
|
292
|
+
// Case 4: Stuck query executions
|
|
293
|
+
// Two-layer detection:
|
|
294
|
+
// 1. PID dead → immediately fail
|
|
295
|
+
// 2. PID alive but no activity for 2 min → fail (process hung)
|
|
296
|
+
const runningQueries = this.getDb()
|
|
297
|
+
.prepare(`
|
|
298
|
+
SELECT
|
|
299
|
+
e.id,
|
|
300
|
+
e.pid,
|
|
301
|
+
e.started_at,
|
|
302
|
+
COALESCE(
|
|
303
|
+
MAX(tc.completed_at),
|
|
304
|
+
MAX(tc.started_at),
|
|
305
|
+
e.started_at
|
|
306
|
+
) as last_activity
|
|
307
|
+
FROM executions e
|
|
308
|
+
LEFT JOIN tool_calls tc ON tc.execution_id = e.id
|
|
309
|
+
WHERE e.status = 'running'
|
|
310
|
+
AND e.consumer_id IS NULL
|
|
311
|
+
AND e.type = 'query'
|
|
312
|
+
GROUP BY e.id
|
|
313
|
+
`)
|
|
314
|
+
.all();
|
|
315
|
+
const activityCutoff = now - QUERY_ACTIVITY_TIMEOUT_MS;
|
|
316
|
+
// Cached statement for failing stuck queries
|
|
317
|
+
if (!this.stmtFailQuery) {
|
|
318
|
+
this.stmtFailQuery = this.getDb().prepare(`
|
|
319
|
+
UPDATE executions
|
|
320
|
+
SET status = 'failed',
|
|
321
|
+
error = ?,
|
|
322
|
+
completed_at = ?,
|
|
323
|
+
updated_at = ?
|
|
324
|
+
WHERE id = ?
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
327
|
+
for (const query of runningQueries) {
|
|
328
|
+
let shouldFail = false;
|
|
329
|
+
let errorMessage = '';
|
|
330
|
+
// Check 1: PID-based detection (instant)
|
|
331
|
+
if (query.pid) {
|
|
332
|
+
const processAlive = isProcessRunning(query.pid);
|
|
333
|
+
if (processAlive === false) {
|
|
334
|
+
// Process is definitely dead
|
|
335
|
+
shouldFail = true;
|
|
336
|
+
errorMessage = `Query process died (PID ${query.pid} no longer exists)`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Check 2: Activity-based detection (fallback)
|
|
340
|
+
if (!shouldFail && query.last_activity < activityCutoff) {
|
|
341
|
+
shouldFail = true;
|
|
342
|
+
errorMessage = 'Query execution timed out (no activity for 2 minutes)';
|
|
343
|
+
}
|
|
344
|
+
if (shouldFail) {
|
|
345
|
+
this.stmtFailQuery.run(errorMessage, now, now, query.id);
|
|
346
|
+
totalOrphaned++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return totalOrphaned;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Close database connection
|
|
353
|
+
*/
|
|
354
|
+
close() {
|
|
355
|
+
if (this.db) {
|
|
356
|
+
this.db.close();
|
|
357
|
+
this.db = null;
|
|
358
|
+
this.initialized = false;
|
|
359
|
+
// Clear cached statements
|
|
360
|
+
this.stmtCreateExecution = null;
|
|
361
|
+
this.stmtDequeueBatchSelect = null;
|
|
362
|
+
this.stmtDequeueSelect = null;
|
|
363
|
+
this.stmtUpdateStatus = null;
|
|
364
|
+
this.stmtGetExecution = null;
|
|
365
|
+
this.stmtGetQueuedExecutions = null;
|
|
366
|
+
this.stmtGetRunningExecutions = null;
|
|
367
|
+
this.stmtGetRecentExecutions = null;
|
|
368
|
+
this.stmtAddToolCall = null;
|
|
369
|
+
this.stmtUpdateToolCall = null;
|
|
370
|
+
this.stmtGetToolCalls = null;
|
|
371
|
+
this.stmtCleanupOrphans = null;
|
|
372
|
+
this.stmtCountCompletedFailed = null;
|
|
373
|
+
this.stmtDeleteOldExecutions = null;
|
|
374
|
+
this.stmtGetExecutionsSince = null;
|
|
375
|
+
this.stmtOrphanFromConsumer = null;
|
|
376
|
+
this.stmtOrphanMissingConsumer = null;
|
|
377
|
+
this.stmtOrphanNullConsumer = null;
|
|
378
|
+
this.stmtDeleteConsumerLock = null;
|
|
379
|
+
this.stmtFailQuery = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Create a new execution
|
|
384
|
+
* @param type - 'curate' or 'query'
|
|
385
|
+
* @param input - content (curate) or query string (query)
|
|
386
|
+
* @returns execution id
|
|
387
|
+
*/
|
|
388
|
+
createExecution(type, input) {
|
|
389
|
+
this.ensureInitialized();
|
|
390
|
+
const id = randomUUID();
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
// curate starts as 'queued', query starts as 'running'
|
|
393
|
+
const status = type === 'curate' ? 'queued' : 'running';
|
|
394
|
+
const startedAt = type === 'query' ? now : null;
|
|
395
|
+
const { pid } = process; // Capture current process ID for orphan detection
|
|
396
|
+
if (!this.stmtCreateExecution) {
|
|
397
|
+
this.stmtCreateExecution = this.getDb().prepare(`
|
|
398
|
+
INSERT INTO executions (id, type, input, status, pid, created_at, started_at, updated_at)
|
|
399
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
this.stmtCreateExecution.run(id, type, input, status, pid, now, startedAt, now);
|
|
403
|
+
return id;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Dequeue multiple executions at once (atomic batch SELECT + UPDATE)
|
|
407
|
+
* This is more efficient than calling dequeueExecution() multiple times
|
|
408
|
+
* and ensures all queued items are seen in a single transaction snapshot
|
|
409
|
+
* @param limit - max number of executions to dequeue
|
|
410
|
+
* @param consumerId - ID of the consumer claiming these executions
|
|
411
|
+
* @returns array of executions (may be empty if queue is empty)
|
|
412
|
+
*/
|
|
413
|
+
dequeueBatch(limit, consumerId) {
|
|
414
|
+
this.ensureInitialized();
|
|
415
|
+
if (limit <= 0)
|
|
416
|
+
return [];
|
|
417
|
+
if (!this.stmtDequeueBatchSelect) {
|
|
418
|
+
// Use a parameterized LIMIT - better-sqlite3 handles this correctly
|
|
419
|
+
this.stmtDequeueBatchSelect = this.getDb().prepare(`
|
|
420
|
+
SELECT * FROM executions
|
|
421
|
+
WHERE status = 'queued'
|
|
422
|
+
ORDER BY created_at ASC
|
|
423
|
+
LIMIT ?
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
// Capture for type safety inside transaction closure
|
|
427
|
+
const selectStmt = this.stmtDequeueBatchSelect;
|
|
428
|
+
// Use dynamic statement to handle optional consumer_id
|
|
429
|
+
const updateSql = consumerId
|
|
430
|
+
? `UPDATE executions SET status = 'running', consumer_id = ?, started_at = ?, updated_at = ? WHERE id = ?`
|
|
431
|
+
: `UPDATE executions SET status = 'running', started_at = ?, updated_at = ? WHERE id = ?`;
|
|
432
|
+
// Use transaction for atomicity - all SELECTs and UPDATEs in one snapshot
|
|
433
|
+
const dequeueBatch = this.getDb().transaction((batchLimit) => {
|
|
434
|
+
const rows = selectStmt.all(batchLimit);
|
|
435
|
+
if (rows.length === 0) {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
const executions = [];
|
|
440
|
+
const updateStmt = this.getDb().prepare(updateSql);
|
|
441
|
+
for (const row of rows) {
|
|
442
|
+
if (consumerId) {
|
|
443
|
+
updateStmt.run(consumerId, now, now, row.id);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
updateStmt.run(now, now, row.id);
|
|
447
|
+
}
|
|
448
|
+
// eslint-disable-next-line camelcase
|
|
449
|
+
executions.push(this.rowToExecution({ ...row, started_at: now, status: 'running', updated_at: now }));
|
|
450
|
+
}
|
|
451
|
+
return executions;
|
|
452
|
+
});
|
|
453
|
+
return dequeueBatch(limit);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Dequeue next queued execution (atomic SELECT + UPDATE)
|
|
457
|
+
* @param consumerId - ID of the consumer claiming this execution
|
|
458
|
+
* @returns execution or null if queue is empty
|
|
459
|
+
*/
|
|
460
|
+
dequeueExecution(consumerId) {
|
|
461
|
+
this.ensureInitialized();
|
|
462
|
+
if (!this.stmtDequeueSelect) {
|
|
463
|
+
this.stmtDequeueSelect = this.getDb().prepare(`
|
|
464
|
+
SELECT * FROM executions
|
|
465
|
+
WHERE status = 'queued'
|
|
466
|
+
ORDER BY created_at ASC
|
|
467
|
+
LIMIT 1
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
// Capture for type safety inside transaction closure
|
|
471
|
+
const selectStmt = this.stmtDequeueSelect;
|
|
472
|
+
// Use dynamic statement to handle optional consumer_id
|
|
473
|
+
const updateSql = consumerId
|
|
474
|
+
? `UPDATE executions SET status = 'running', consumer_id = ?, started_at = ?, updated_at = ? WHERE id = ?`
|
|
475
|
+
: `UPDATE executions SET status = 'running', started_at = ?, updated_at = ? WHERE id = ?`;
|
|
476
|
+
// Use transaction for atomicity
|
|
477
|
+
const dequeue = this.getDb().transaction(() => {
|
|
478
|
+
const row = selectStmt.get();
|
|
479
|
+
if (!row) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
const updateStmt = this.getDb().prepare(updateSql);
|
|
484
|
+
if (consumerId) {
|
|
485
|
+
updateStmt.run(consumerId, now, now, row.id);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
updateStmt.run(now, now, row.id);
|
|
489
|
+
}
|
|
490
|
+
// eslint-disable-next-line camelcase
|
|
491
|
+
return this.rowToExecution({ ...row, started_at: now, status: 'running', updated_at: now });
|
|
492
|
+
});
|
|
493
|
+
return dequeue();
|
|
494
|
+
}
|
|
495
|
+
// ==================== EXECUTION METHODS ====================
|
|
496
|
+
/**
|
|
497
|
+
* Get execution by id
|
|
498
|
+
*/
|
|
499
|
+
getExecution(id) {
|
|
500
|
+
this.ensureInitialized();
|
|
501
|
+
if (!this.stmtGetExecution) {
|
|
502
|
+
this.stmtGetExecution = this.getDb().prepare(`
|
|
503
|
+
SELECT * FROM executions WHERE id = ?
|
|
504
|
+
`);
|
|
505
|
+
}
|
|
506
|
+
const row = this.stmtGetExecution.get(id);
|
|
507
|
+
return row ? this.rowToExecution(row) : null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get executions updated since timestamp (for incremental polling)
|
|
511
|
+
*/
|
|
512
|
+
getExecutionsSince(timestamp) {
|
|
513
|
+
this.ensureInitialized();
|
|
514
|
+
if (!this.stmtGetExecutionsSince) {
|
|
515
|
+
this.stmtGetExecutionsSince = this.getDb().prepare(`
|
|
516
|
+
SELECT * FROM executions
|
|
517
|
+
WHERE updated_at > ?
|
|
518
|
+
ORDER BY updated_at ASC
|
|
519
|
+
LIMIT 100
|
|
520
|
+
`);
|
|
521
|
+
}
|
|
522
|
+
const rows = this.stmtGetExecutionsSince.all(timestamp);
|
|
523
|
+
return rows.map((row) => this.rowToExecution(row));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get execution with all its tool calls (for UI display)
|
|
527
|
+
*/
|
|
528
|
+
getExecutionWithToolCalls(id) {
|
|
529
|
+
const execution = this.getExecution(id);
|
|
530
|
+
if (!execution) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
const toolCalls = this.getToolCalls(id);
|
|
534
|
+
return { execution, toolCalls };
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get all queued executions
|
|
538
|
+
*/
|
|
539
|
+
getQueuedExecutions() {
|
|
540
|
+
this.ensureInitialized();
|
|
541
|
+
if (!this.stmtGetQueuedExecutions) {
|
|
542
|
+
this.stmtGetQueuedExecutions = this.getDb().prepare(`
|
|
543
|
+
SELECT * FROM executions
|
|
544
|
+
WHERE status = 'queued'
|
|
545
|
+
ORDER BY created_at ASC
|
|
546
|
+
LIMIT 100
|
|
547
|
+
`);
|
|
548
|
+
}
|
|
549
|
+
const rows = this.stmtGetQueuedExecutions.all();
|
|
550
|
+
return rows.map((row) => this.rowToExecution(row));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Get recent executions (for UI display)
|
|
554
|
+
*/
|
|
555
|
+
getRecentExecutions(limit = 20) {
|
|
556
|
+
this.ensureInitialized();
|
|
557
|
+
if (!this.stmtGetRecentExecutions) {
|
|
558
|
+
this.stmtGetRecentExecutions = this.getDb().prepare(`
|
|
559
|
+
SELECT * FROM executions
|
|
560
|
+
ORDER BY created_at DESC
|
|
561
|
+
LIMIT ?
|
|
562
|
+
`);
|
|
563
|
+
}
|
|
564
|
+
const rows = this.stmtGetRecentExecutions.all(limit);
|
|
565
|
+
return rows.map((row) => this.rowToExecution(row));
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get all running executions
|
|
569
|
+
*/
|
|
570
|
+
getRunningExecutions() {
|
|
571
|
+
this.ensureInitialized();
|
|
572
|
+
if (!this.stmtGetRunningExecutions) {
|
|
573
|
+
this.stmtGetRunningExecutions = this.getDb().prepare(`
|
|
574
|
+
SELECT * FROM executions
|
|
575
|
+
WHERE status = 'running'
|
|
576
|
+
ORDER BY started_at ASC
|
|
577
|
+
LIMIT 100
|
|
578
|
+
`);
|
|
579
|
+
}
|
|
580
|
+
const rows = this.stmtGetRunningExecutions.all();
|
|
581
|
+
return rows.map((row) => this.rowToExecution(row));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Get all executions belonging to a specific consumer session.
|
|
585
|
+
* Returns executions ordered by created_at ASC (oldest first, newest last).
|
|
586
|
+
* Includes:
|
|
587
|
+
* - Curate executions with matching consumer_id
|
|
588
|
+
* - Query executions created after consumer started (no consumer_id)
|
|
589
|
+
*/
|
|
590
|
+
getSessionExecutions(consumerId) {
|
|
591
|
+
this.ensureInitialized();
|
|
592
|
+
// Get the consumer's start time from locks table
|
|
593
|
+
const lockRow = this.getDb().prepare(`SELECT started_at FROM consumer_locks WHERE id = ?`).get(consumerId);
|
|
594
|
+
if (!lockRow) {
|
|
595
|
+
// Consumer not found, return empty
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
// Get all executions belonging to this session:
|
|
599
|
+
// 1. Curate executions with this consumer_id
|
|
600
|
+
// 2. Query executions created after consumer started (have NULL consumer_id)
|
|
601
|
+
const rows = this.getDb()
|
|
602
|
+
.prepare(`
|
|
603
|
+
SELECT * FROM executions
|
|
604
|
+
WHERE consumer_id = ? OR (consumer_id IS NULL AND created_at >= ?)
|
|
605
|
+
ORDER BY created_at ASC
|
|
606
|
+
`)
|
|
607
|
+
.all(consumerId, lockRow.started_at);
|
|
608
|
+
return rows.map((row) => this.rowToExecution(row));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Get queue statistics (queries DB directly for accurate counts)
|
|
612
|
+
*/
|
|
613
|
+
getStats() {
|
|
614
|
+
this.ensureInitialized();
|
|
615
|
+
const result = this.getDb()
|
|
616
|
+
.prepare(`
|
|
617
|
+
SELECT
|
|
618
|
+
COUNT(*) as total,
|
|
619
|
+
SUM(CASE WHEN status = 'queued' THEN 1 ELSE 0 END) as queued,
|
|
620
|
+
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
|
|
621
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
622
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
|
623
|
+
FROM executions
|
|
624
|
+
`)
|
|
625
|
+
.get();
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Get all tool calls for an execution
|
|
630
|
+
*/
|
|
631
|
+
getToolCalls(executionId) {
|
|
632
|
+
this.ensureInitialized();
|
|
633
|
+
if (!this.stmtGetToolCalls) {
|
|
634
|
+
this.stmtGetToolCalls = this.getDb().prepare(`
|
|
635
|
+
SELECT * FROM tool_calls
|
|
636
|
+
WHERE execution_id = ?
|
|
637
|
+
ORDER BY started_at ASC
|
|
638
|
+
`);
|
|
639
|
+
}
|
|
640
|
+
const rows = this.stmtGetToolCalls.all(executionId);
|
|
641
|
+
return rows.map((row) => this.rowToToolCall(row));
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Check if any consumer is currently active (has recent heartbeat)
|
|
645
|
+
* @param timeoutMs - heartbeat timeout (default 30 seconds)
|
|
646
|
+
*/
|
|
647
|
+
hasActiveConsumer(timeoutMs = 30_000) {
|
|
648
|
+
this.ensureInitialized();
|
|
649
|
+
const now = Date.now();
|
|
650
|
+
const cutoff = now - timeoutMs;
|
|
651
|
+
const result = this.getDb()
|
|
652
|
+
.prepare(`
|
|
653
|
+
SELECT COUNT(*) as count FROM consumer_locks WHERE last_heartbeat >= ?
|
|
654
|
+
`)
|
|
655
|
+
.get(cutoff);
|
|
656
|
+
return result.count > 0;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Check if a specific consumer lock exists in the database
|
|
660
|
+
* Used by Consumer to verify its lock is still valid after DB reconnection
|
|
661
|
+
*/
|
|
662
|
+
hasConsumerLock(consumerId) {
|
|
663
|
+
this.ensureInitialized();
|
|
664
|
+
const result = this.getDb().prepare(`SELECT 1 FROM consumer_locks WHERE id = ?`).get(consumerId);
|
|
665
|
+
return result !== undefined;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Initialize storage - create tables, enable WAL
|
|
669
|
+
* @param options - Initialization options
|
|
670
|
+
* @param options.cleanupOrphans - If true, cleanup orphaned executions (only Consumer should set this)
|
|
671
|
+
*/
|
|
672
|
+
async initialize(options) {
|
|
673
|
+
if (this.initialized) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// Ensure storage directory exists (skip for in-memory)
|
|
677
|
+
if (!this.inMemory) {
|
|
678
|
+
await fs.mkdir(this.storageDir, { recursive: true });
|
|
679
|
+
}
|
|
680
|
+
// Open/create database
|
|
681
|
+
this.db = new Database(this.dbPath);
|
|
682
|
+
// Enable WAL mode for better concurrent performance
|
|
683
|
+
this.db.pragma('journal_mode = WAL');
|
|
684
|
+
// Enable foreign keys
|
|
685
|
+
this.db.pragma('foreign_keys = ON');
|
|
686
|
+
// Set busy timeout to avoid SQLITE_BUSY errors
|
|
687
|
+
this.db.pragma('busy_timeout = 5000');
|
|
688
|
+
// Create schema
|
|
689
|
+
this.db.exec(SCHEMA_SQL);
|
|
690
|
+
// Run migrations for existing databases (ignore errors for columns that already exist)
|
|
691
|
+
for (const line of MIGRATION_SQL.split('\n')) {
|
|
692
|
+
const trimmed = line.trim();
|
|
693
|
+
if (trimmed && !trimmed.startsWith('--')) {
|
|
694
|
+
try {
|
|
695
|
+
this.db.exec(trimmed);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// Column already exists, ignore
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Capture inode to detect if file is replaced later
|
|
703
|
+
if (!this.inMemory) {
|
|
704
|
+
try {
|
|
705
|
+
const stat = fsSync.statSync(this.dbPath);
|
|
706
|
+
this.dbFileInode = stat.ino;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
// File may not exist yet, ignore
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this.initialized = true;
|
|
713
|
+
// Cleanup orphaned executions from previous session (only if requested)
|
|
714
|
+
// IMPORTANT: Only Consumer should set cleanupOrphans=true
|
|
715
|
+
// Dashboard/other readers should NOT cleanup, or they will mark consumer's running executions as failed
|
|
716
|
+
if (options?.cleanupOrphans) {
|
|
717
|
+
this.cleanupOrphanedExecutions();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Check if the DB file has been replaced (different inode)
|
|
722
|
+
* Returns true if DB needs reconnection
|
|
723
|
+
*/
|
|
724
|
+
isDbFileChanged() {
|
|
725
|
+
if (this.inMemory || !this.dbFileInode) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
const stat = fsSync.statSync(this.dbPath);
|
|
730
|
+
return stat.ino !== this.dbFileInode;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// File doesn't exist - definitely changed
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Reconnect to the database (close and reinitialize)
|
|
739
|
+
* Use when DB file has been replaced by another process (e.g., brv init)
|
|
740
|
+
*/
|
|
741
|
+
async reconnect() {
|
|
742
|
+
// Close existing connection
|
|
743
|
+
this.close();
|
|
744
|
+
// Reinitialize (will create new connection and capture new inode)
|
|
745
|
+
await this.initialize();
|
|
746
|
+
}
|
|
747
|
+
// ==================== TOOL CALL METHODS ====================
|
|
748
|
+
/**
|
|
749
|
+
* Release consumer lock (unregister this consumer)
|
|
750
|
+
*/
|
|
751
|
+
releaseConsumerLock(consumerId) {
|
|
752
|
+
this.ensureInitialized();
|
|
753
|
+
// First, clear consumer_id from any running executions
|
|
754
|
+
const now = Date.now();
|
|
755
|
+
this.getDb()
|
|
756
|
+
.prepare(`
|
|
757
|
+
UPDATE executions
|
|
758
|
+
SET consumer_id = NULL, updated_at = ?
|
|
759
|
+
WHERE consumer_id = ? AND status = 'running'
|
|
760
|
+
`)
|
|
761
|
+
.run(now, consumerId);
|
|
762
|
+
// Then delete the lock
|
|
763
|
+
this.getDb()
|
|
764
|
+
.prepare(`
|
|
765
|
+
DELETE FROM consumer_locks WHERE id = ?
|
|
766
|
+
`)
|
|
767
|
+
.run(consumerId);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Update consumer heartbeat
|
|
771
|
+
*/
|
|
772
|
+
updateConsumerHeartbeat(consumerId) {
|
|
773
|
+
this.ensureInitialized();
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
this.getDb()
|
|
776
|
+
.prepare(`
|
|
777
|
+
UPDATE consumer_locks SET last_heartbeat = ? WHERE id = ?
|
|
778
|
+
`)
|
|
779
|
+
.run(now, consumerId);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Update execution status
|
|
783
|
+
*/
|
|
784
|
+
updateExecutionStatus(id, status, result, error) {
|
|
785
|
+
this.ensureInitialized();
|
|
786
|
+
if (!this.stmtUpdateStatus) {
|
|
787
|
+
this.stmtUpdateStatus = this.getDb().prepare(`
|
|
788
|
+
UPDATE executions
|
|
789
|
+
SET status = ?, result = ?, error = ?, completed_at = ?, updated_at = ?
|
|
790
|
+
WHERE id = ?
|
|
791
|
+
`);
|
|
792
|
+
}
|
|
793
|
+
const now = Date.now();
|
|
794
|
+
const completedAt = status === 'completed' || status === 'failed' ? now : null;
|
|
795
|
+
this.stmtUpdateStatus.run(status, result ?? null, error ?? null, completedAt, now, id);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Update tool call status and result
|
|
799
|
+
*/
|
|
800
|
+
updateToolCall(id, status, options) {
|
|
801
|
+
this.ensureInitialized();
|
|
802
|
+
if (!this.stmtUpdateToolCall) {
|
|
803
|
+
this.stmtUpdateToolCall = this.getDb().prepare(`
|
|
804
|
+
UPDATE tool_calls
|
|
805
|
+
SET status = ?, result = ?, result_summary = ?, completed_at = ?,
|
|
806
|
+
duration_ms = ?, lines_count = ?, chars_count = ?
|
|
807
|
+
WHERE id = ?
|
|
808
|
+
`);
|
|
809
|
+
}
|
|
810
|
+
const now = Date.now();
|
|
811
|
+
const completedAt = status === 'completed' || status === 'failed' ? now : null;
|
|
812
|
+
// Get started_at to calculate duration
|
|
813
|
+
const getStarted = this.getDb().prepare('SELECT started_at FROM tool_calls WHERE id = ?');
|
|
814
|
+
const row = getStarted.get(id);
|
|
815
|
+
const durationMs = row && completedAt ? completedAt - row.started_at : null;
|
|
816
|
+
this.stmtUpdateToolCall.run(status, options?.result ?? null, options?.resultSummary ?? null, completedAt, durationMs, options?.linesCount ?? null, options?.charsCount ?? null, id);
|
|
817
|
+
}
|
|
818
|
+
// ==================== PRIVATE HELPERS ====================
|
|
819
|
+
/**
|
|
820
|
+
* Ensure storage has been initialized
|
|
821
|
+
*/
|
|
822
|
+
ensureInitialized() {
|
|
823
|
+
if (!this.initialized || !this.db) {
|
|
824
|
+
throw new Error('AgentStorage not initialized. Call initialize() first.');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get database instance with type safety (throws if not initialized)
|
|
829
|
+
* Use this instead of this.getDb() for proper type narrowing
|
|
830
|
+
*/
|
|
831
|
+
getDb() {
|
|
832
|
+
if (!this.db) {
|
|
833
|
+
throw new Error('AgentStorage not initialized. Call initialize() first.');
|
|
834
|
+
}
|
|
835
|
+
return this.db;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Convert database row to Execution object
|
|
839
|
+
*/
|
|
840
|
+
rowToExecution(row) {
|
|
841
|
+
const execution = {
|
|
842
|
+
createdAt: row.created_at,
|
|
843
|
+
id: row.id,
|
|
844
|
+
input: row.input,
|
|
845
|
+
status: row.status,
|
|
846
|
+
type: row.type,
|
|
847
|
+
updatedAt: row.updated_at,
|
|
848
|
+
};
|
|
849
|
+
if (row.result)
|
|
850
|
+
execution.result = row.result;
|
|
851
|
+
if (row.error)
|
|
852
|
+
execution.error = row.error;
|
|
853
|
+
if (row.started_at)
|
|
854
|
+
execution.startedAt = row.started_at;
|
|
855
|
+
if (row.completed_at)
|
|
856
|
+
execution.completedAt = row.completed_at;
|
|
857
|
+
if (row.pid)
|
|
858
|
+
execution.pid = row.pid;
|
|
859
|
+
return execution;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Convert database row to ToolCall object
|
|
863
|
+
*/
|
|
864
|
+
rowToToolCall(row) {
|
|
865
|
+
const toolCall = {
|
|
866
|
+
args: row.args ?? '{}',
|
|
867
|
+
executionId: row.execution_id,
|
|
868
|
+
id: row.id,
|
|
869
|
+
name: row.name,
|
|
870
|
+
startedAt: row.started_at,
|
|
871
|
+
status: row.status,
|
|
872
|
+
};
|
|
873
|
+
if (row.description)
|
|
874
|
+
toolCall.description = row.description;
|
|
875
|
+
if (row.result)
|
|
876
|
+
toolCall.result = row.result;
|
|
877
|
+
if (row.result_summary)
|
|
878
|
+
toolCall.resultSummary = row.result_summary;
|
|
879
|
+
if (row.completed_at)
|
|
880
|
+
toolCall.completedAt = row.completed_at;
|
|
881
|
+
if (row.duration_ms)
|
|
882
|
+
toolCall.durationMs = row.duration_ms;
|
|
883
|
+
if (row.lines_count)
|
|
884
|
+
toolCall.linesCount = row.lines_count;
|
|
885
|
+
if (row.chars_count)
|
|
886
|
+
toolCall.charsCount = row.chars_count;
|
|
887
|
+
return toolCall;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// ==================== SINGLETON ====================
|
|
891
|
+
let instance = null;
|
|
892
|
+
let initPromise = null;
|
|
893
|
+
/**
|
|
894
|
+
* Get the singleton AgentStorage instance (auto-initializes if needed)
|
|
895
|
+
*
|
|
896
|
+
* This is the PRIMARY API - just call this and it handles everything.
|
|
897
|
+
* First call will initialize with provided config, subsequent calls return cached instance.
|
|
898
|
+
*
|
|
899
|
+
* @param config - Configuration options
|
|
900
|
+
* @param config.cleanupOrphans - Cleanup orphaned executions (only Consumer should set this)
|
|
901
|
+
* @param config.inMemory - Use in-memory database (for testing)
|
|
902
|
+
* @param config.storageDir - Directory for agent.db (default: .brv/blobs)
|
|
903
|
+
*/
|
|
904
|
+
export async function getAgentStorage(config) {
|
|
905
|
+
// Already initialized - return immediately
|
|
906
|
+
if (instance?.initialized) {
|
|
907
|
+
return instance;
|
|
908
|
+
}
|
|
909
|
+
// Initialization in progress - wait for it
|
|
910
|
+
if (initPromise) {
|
|
911
|
+
return initPromise;
|
|
912
|
+
}
|
|
913
|
+
// Start initialization (only one concurrent init allowed)
|
|
914
|
+
initPromise = (async () => {
|
|
915
|
+
if (!instance) {
|
|
916
|
+
instance = new AgentStorage(config);
|
|
917
|
+
}
|
|
918
|
+
await instance.initialize({ cleanupOrphans: config?.cleanupOrphans });
|
|
919
|
+
return instance;
|
|
920
|
+
})();
|
|
921
|
+
try {
|
|
922
|
+
return await initPromise;
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
initPromise = null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get the singleton AgentStorage instance (sync version)
|
|
930
|
+
* THROWS if not initialized - use getAgentStorage() instead for auto-init
|
|
931
|
+
*
|
|
932
|
+
* Use this only when you KNOW storage is already initialized (e.g., in Consumer after start)
|
|
933
|
+
*/
|
|
934
|
+
export function getAgentStorageSync() {
|
|
935
|
+
if (!instance?.initialized) {
|
|
936
|
+
throw new Error('AgentStorage not initialized. Use await getAgentStorage() instead.');
|
|
937
|
+
}
|
|
938
|
+
return instance;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Initialize the singleton AgentStorage instance
|
|
942
|
+
* @deprecated Use getAgentStorage() directly - it auto-initializes
|
|
943
|
+
*/
|
|
944
|
+
export async function initializeAgentStorage(config) {
|
|
945
|
+
return getAgentStorage(config);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Close and clear the singleton AgentStorage instance
|
|
949
|
+
*/
|
|
950
|
+
export function closeAgentStorage() {
|
|
951
|
+
if (instance) {
|
|
952
|
+
instance.close();
|
|
953
|
+
instance = null;
|
|
954
|
+
}
|
|
955
|
+
initPromise = null;
|
|
956
|
+
}
|