@superblocksteam/vite-plugin-file-sync 2.0.129 → 2.0.130-next.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/dist/ai-service/agent/middleware.d.ts.map +1 -1
- package/dist/ai-service/agent/middleware.js +51 -1
- package/dist/ai-service/agent/middleware.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +44 -4
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/subagents/coding/run-coding-subagent.d.ts.map +1 -1
- package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js +25 -0
- package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api-artifact.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api-artifact.js +4 -3
- package/dist/ai-service/agent/tools/apis/build-api-artifact.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js +22 -0
- package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +25 -0
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +117 -11
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.js +22 -0
- package/dist/ai-service/agent/tools/build-manage-checklist.js.map +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts +291 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.js +386 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tasks.js.map +1 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts +17 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tools.js +19 -0
- package/dist/ai-service/agent/tools/databases/dev-database-tools.js.map +1 -0
- package/dist/ai-service/agent/tools/index.d.ts +0 -1
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +0 -1
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.js +20 -9
- package/dist/ai-service/agent/tools/integrations/execute-request.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.d.ts +5 -1
- package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.js +2 -1
- package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
- package/dist/ai-service/agent/tools/screenshot-selector.d.ts +22 -0
- package/dist/ai-service/agent/tools/screenshot-selector.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/screenshot-selector.js +158 -0
- package/dist/ai-service/agent/tools/screenshot-selector.js.map +1 -0
- package/dist/ai-service/agent/tools.d.ts +17 -0
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +65 -6
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/access-control.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/access-control.js +6 -1
- package/dist/ai-service/agent/tools2/access-control.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/cancel-task.d.ts +20 -0
- package/dist/ai-service/agent/tools2/tools/cancel-task.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/cancel-task.js +39 -0
- package/dist/ai-service/agent/tools2/tools/cancel-task.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/check-task.d.ts +37 -0
- package/dist/ai-service/agent/tools2/tools/check-task.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/check-task.js +53 -0
- package/dist/ai-service/agent/tools2/tools/check-task.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +10 -0
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/git.d.ts +1 -1
- package/dist/ai-service/agent/tools2/tools/git.js +1 -1
- package/dist/ai-service/agent/tools2/tools/git.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/grep.d.ts +6 -1
- package/dist/ai-service/agent/tools2/tools/grep.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/grep.js +37 -13
- package/dist/ai-service/agent/tools2/tools/grep.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/index.d.ts +4 -0
- package/dist/ai-service/agent/tools2/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/index.js +8 -0
- package/dist/ai-service/agent/tools2/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/list-tasks.d.ts +21 -0
- package/dist/ai-service/agent/tools2/tools/list-tasks.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/list-tasks.js +35 -0
- package/dist/ai-service/agent/tools2/tools/list-tasks.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/wait-for-task.d.ts +46 -0
- package/dist/ai-service/agent/tools2/tools/wait-for-task.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/wait-for-task.js +83 -0
- package/dist/ai-service/agent/tools2/tools/wait-for-task.js.map +1 -0
- package/dist/ai-service/agent/tools2/types.d.ts +3 -0
- package/dist/ai-service/agent/tools2/types.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/types.js.map +1 -1
- package/dist/ai-service/agent/utils.d.ts.map +1 -1
- package/dist/ai-service/agent/utils.js +37 -2
- package/dist/ai-service/agent/utils.js.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts +22 -0
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.js +88 -1
- package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.d.ts +7 -0
- package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.js +74 -4
- package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
- package/dist/ai-service/app-interface/shell.d.ts +32 -1
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +86 -3
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/app-skills/manager.d.ts +4 -1
- package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
- package/dist/ai-service/app-skills/manager.js +16 -1
- package/dist/ai-service/app-skills/manager.js.map +1 -1
- package/dist/ai-service/chat/chat-session-store.d.ts +10 -0
- package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
- package/dist/ai-service/chat/chat-session-store.js +55 -28
- package/dist/ai-service/chat/chat-session-store.js.map +1 -1
- package/dist/ai-service/context-download.d.ts.map +1 -1
- package/dist/ai-service/context-download.js +50 -40
- package/dist/ai-service/context-download.js.map +1 -1
- package/dist/ai-service/dev-database-client.d.ts +27 -4
- package/dist/ai-service/dev-database-client.d.ts.map +1 -1
- package/dist/ai-service/dev-database-client.js +35 -1
- package/dist/ai-service/dev-database-client.js.map +1 -1
- package/dist/ai-service/dev-database-file-sync.d.ts +41 -0
- package/dist/ai-service/dev-database-file-sync.d.ts.map +1 -0
- package/dist/ai-service/dev-database-file-sync.js +29 -0
- package/dist/ai-service/dev-database-file-sync.js.map +1 -0
- package/dist/ai-service/features.d.ts +14 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +14 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/index.d.ts +136 -5
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +748 -99
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/integrations/store.d.ts +2 -0
- package/dist/ai-service/integrations/store.d.ts.map +1 -1
- package/dist/ai-service/integrations/store.js +4 -0
- package/dist/ai-service/integrations/store.js.map +1 -1
- package/dist/ai-service/knowledge/knowledge-manager.d.ts +8 -10
- package/dist/ai-service/knowledge/knowledge-manager.d.ts.map +1 -1
- package/dist/ai-service/knowledge/knowledge-manager.js +135 -119
- package/dist/ai-service/knowledge/knowledge-manager.js.map +1 -1
- package/dist/ai-service/llm/chaos-fetch.d.ts.map +1 -1
- package/dist/ai-service/llm/chaos-fetch.js +7 -3
- package/dist/ai-service/llm/chaos-fetch.js.map +1 -1
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +23 -7
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -0
- package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/adapter.js +9 -0
- package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +2 -0
- package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/compaction/server-side.js +6 -2
- package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context-management.d.ts +3 -9
- package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context-management.js +3 -9
- package/dist/ai-service/llm/context-v2/context-management.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts +23 -0
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -0
- package/dist/ai-service/llm/context-v2/context-metrics.js +144 -0
- package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -0
- package/dist/ai-service/llm/context-v2/context.d.ts +16 -1
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +138 -26
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts +26 -1
- package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts +15 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.js +97 -40
- package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/local.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/storage/local.js +3 -0
- package/dist/ai-service/llm/context-v2/storage/local.js.map +1 -1
- package/dist/ai-service/llm/context-v2/types.d.ts +27 -0
- package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/types.js +108 -23
- package/dist/ai-service/llm/context-v2/types.js.map +1 -1
- package/dist/ai-service/llm/error.d.ts +21 -0
- package/dist/ai-service/llm/error.d.ts.map +1 -1
- package/dist/ai-service/llm/error.js +72 -12
- package/dist/ai-service/llm/error.js.map +1 -1
- package/dist/ai-service/llm/stream/compaction-text-block.d.ts +14 -0
- package/dist/ai-service/llm/stream/compaction-text-block.d.ts.map +1 -0
- package/dist/ai-service/llm/stream/compaction-text-block.js +29 -0
- package/dist/ai-service/llm/stream/compaction-text-block.js.map +1 -0
- package/dist/ai-service/llm/stream/idle-monitor.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/idle-monitor.js +12 -28
- package/dist/ai-service/llm/stream/idle-monitor.js.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.js +22 -0
- package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.d.ts +4 -0
- package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/context.js +57 -5
- package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
- package/dist/ai-service/policy-gate-step.d.ts +17 -0
- package/dist/ai-service/policy-gate-step.d.ts.map +1 -0
- package/dist/ai-service/policy-gate-step.js +52 -0
- package/dist/ai-service/policy-gate-step.js.map +1 -0
- package/dist/ai-service/quota-client.d.ts +28 -0
- package/dist/ai-service/quota-client.d.ts.map +1 -0
- package/dist/ai-service/quota-client.js +68 -0
- package/dist/ai-service/quota-client.js.map +1 -0
- package/dist/ai-service/security/index.d.ts +1 -1
- package/dist/ai-service/security/index.d.ts.map +1 -1
- package/dist/ai-service/security/index.js +1 -1
- package/dist/ai-service/security/index.js.map +1 -1
- package/dist/ai-service/security/safety-classifier.d.ts +9 -26
- package/dist/ai-service/security/safety-classifier.d.ts.map +1 -1
- package/dist/ai-service/security/safety-classifier.js +29 -44
- package/dist/ai-service/security/safety-classifier.js.map +1 -1
- package/dist/ai-service/skills/system/_registry.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/_registry.generated.js +2 -0
- package/dist/ai-service/skills/system/_registry.generated.js.map +1 -1
- package/dist/ai-service/skills/system/common-import-issues/skill.generated.d.ts +2 -0
- package/dist/ai-service/skills/system/common-import-issues/skill.generated.d.ts.map +1 -0
- package/dist/ai-service/skills/system/common-import-issues/skill.generated.js +93 -0
- package/dist/ai-service/skills/system/common-import-issues/skill.generated.js.map +1 -0
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js +15 -0
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +7 -2
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +74 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +26 -7
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +223 -9
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/idle.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/idle.js +42 -6
- package/dist/ai-service/state-machine/handlers/idle.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +16 -6
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.js +9 -1
- package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
- package/dist/ai-service/tasks/access.d.ts +23 -0
- package/dist/ai-service/tasks/access.d.ts.map +1 -0
- package/dist/ai-service/tasks/access.js +26 -0
- package/dist/ai-service/tasks/access.js.map +1 -0
- package/dist/ai-service/tasks/factory.d.ts +92 -0
- package/dist/ai-service/tasks/factory.d.ts.map +1 -0
- package/dist/ai-service/tasks/factory.js +54 -0
- package/dist/ai-service/tasks/factory.js.map +1 -0
- package/dist/ai-service/tasks/index.d.ts +8 -0
- package/dist/ai-service/tasks/index.d.ts.map +1 -0
- package/dist/ai-service/tasks/index.js +7 -0
- package/dist/ai-service/tasks/index.js.map +1 -0
- package/dist/ai-service/tasks/peer-push.d.ts +78 -0
- package/dist/ai-service/tasks/peer-push.d.ts.map +1 -0
- package/dist/ai-service/tasks/peer-push.js +74 -0
- package/dist/ai-service/tasks/peer-push.js.map +1 -0
- package/dist/ai-service/tasks/poll-loop.d.ts +55 -0
- package/dist/ai-service/tasks/poll-loop.d.ts.map +1 -0
- package/dist/ai-service/tasks/poll-loop.js +73 -0
- package/dist/ai-service/tasks/poll-loop.js.map +1 -0
- package/dist/ai-service/tasks/task-store.d.ts +106 -0
- package/dist/ai-service/tasks/task-store.d.ts.map +1 -0
- package/dist/ai-service/tasks/task-store.js +292 -0
- package/dist/ai-service/tasks/task-store.js.map +1 -0
- package/dist/ai-service/tasks/test-utils.d.ts +23 -0
- package/dist/ai-service/tasks/test-utils.d.ts.map +1 -0
- package/dist/ai-service/tasks/test-utils.js +53 -0
- package/dist/ai-service/tasks/test-utils.js.map +1 -0
- package/dist/ai-service/tasks/types.d.ts +205 -0
- package/dist/ai-service/tasks/types.d.ts.map +1 -0
- package/dist/ai-service/tasks/types.js +24 -0
- package/dist/ai-service/tasks/types.js.map +1 -0
- package/dist/ai-service/template-renderer.d.ts.map +1 -1
- package/dist/ai-service/template-renderer.js +1 -0
- package/dist/ai-service/template-renderer.js.map +1 -1
- package/dist/ai-service/test-utils/metrics-harness.d.ts +32 -0
- package/dist/ai-service/test-utils/metrics-harness.d.ts.map +1 -0
- package/dist/ai-service/test-utils/metrics-harness.js +67 -0
- package/dist/ai-service/test-utils/metrics-harness.js.map +1 -0
- package/dist/ai-service/test-utils/tracer-harness.d.ts +8 -0
- package/dist/ai-service/test-utils/tracer-harness.d.ts.map +1 -0
- package/dist/ai-service/test-utils/tracer-harness.js +26 -0
- package/dist/ai-service/test-utils/tracer-harness.js.map +1 -0
- package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.d.ts +40 -0
- package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.d.ts.map +1 -0
- package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.js +118 -0
- package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.js.map +1 -0
- package/dist/ai-service/util/llm-config-utils.d.ts +2 -6
- package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
- package/dist/ai-service/util/llm-config-utils.js +2 -7
- package/dist/ai-service/util/llm-config-utils.js.map +1 -1
- package/dist/ai-service/util/rpc-timeout.d.ts +17 -7
- package/dist/ai-service/util/rpc-timeout.d.ts.map +1 -1
- package/dist/ai-service/util/rpc-timeout.js +17 -7
- package/dist/ai-service/util/rpc-timeout.js.map +1 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +76 -6
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/inject-index-vite-plugin.d.ts.map +1 -1
- package/dist/inject-index-vite-plugin.js +4 -0
- package/dist/inject-index-vite-plugin.js.map +1 -1
- package/dist/migration/migration-routes.d.ts +14 -0
- package/dist/migration/migration-routes.d.ts.map +1 -1
- package/dist/migration/migration-routes.js +284 -5
- package/dist/migration/migration-routes.js.map +1 -1
- package/dist/migration/migration-session-reconcile.d.ts +23 -0
- package/dist/migration/migration-session-reconcile.d.ts.map +1 -0
- package/dist/migration/migration-session-reconcile.js +129 -0
- package/dist/migration/migration-session-reconcile.js.map +1 -0
- package/dist/migration/restructure.d.ts.map +1 -1
- package/dist/migration/restructure.js +2 -0
- package/dist/migration/restructure.js.map +1 -1
- package/dist/migration-templates/app-fullstack/css.d.ts +3 -0
- package/dist/migration-templates/app-fullstack/package.json +2 -2
- package/dist/npm/normalize-workspace-protocol-for-npm.d.ts +15 -0
- package/dist/npm/normalize-workspace-protocol-for-npm.d.ts.map +1 -0
- package/dist/npm/normalize-workspace-protocol-for-npm.js +36 -0
- package/dist/npm/normalize-workspace-protocol-for-npm.js.map +1 -0
- package/dist/npm/prepare-package-json-for-cloud-npm.d.ts +21 -0
- package/dist/npm/prepare-package-json-for-cloud-npm.d.ts.map +1 -0
- package/dist/npm/prepare-package-json-for-cloud-npm.js +44 -0
- package/dist/npm/prepare-package-json-for-cloud-npm.js.map +1 -0
- package/dist/npm/rewrite-platform-workspace-deps.d.ts +17 -0
- package/dist/npm/rewrite-platform-workspace-deps.d.ts.map +1 -0
- package/dist/npm/rewrite-platform-workspace-deps.js +37 -0
- package/dist/npm/rewrite-platform-workspace-deps.js.map +1 -0
- package/dist/plugin-options.d.ts +8 -0
- package/dist/plugin-options.d.ts.map +1 -1
- package/dist/plugin-options.js.map +1 -1
- package/dist/policy-gate-run-registry.d.ts +28 -0
- package/dist/policy-gate-run-registry.d.ts.map +1 -0
- package/dist/policy-gate-run-registry.js +40 -0
- package/dist/policy-gate-run-registry.js.map +1 -0
- package/dist/policy-gate-runner.d.ts +2 -0
- package/dist/policy-gate-runner.d.ts.map +1 -1
- package/dist/policy-gate-runner.js +14 -1
- package/dist/policy-gate-runner.js.map +1 -1
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +55 -0
- package/dist/socket-manager.js.map +1 -1
- package/dist/sync-service/download.d.ts.map +1 -1
- package/dist/sync-service/download.js +6 -4
- package/dist/sync-service/download.js.map +1 -1
- package/dist/sync-service/draft-reconcile.d.ts +22 -0
- package/dist/sync-service/draft-reconcile.d.ts.map +1 -0
- package/dist/sync-service/draft-reconcile.js +24 -0
- package/dist/sync-service/draft-reconcile.js.map +1 -0
- package/dist/sync-service/index.d.ts +30 -5
- package/dist/sync-service/index.d.ts.map +1 -1
- package/dist/sync-service/index.js +59 -8
- package/dist/sync-service/index.js.map +1 -1
- package/package.json +10 -10
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts +0 -103
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts.map +0 -1
- package/dist/ai-service/agent/tools/databases/dev-database.js +0 -117
- package/dist/ai-service/agent/tools/databases/dev-database.js.map +0 -1
package/dist/ai-service/index.js
CHANGED
|
@@ -3,17 +3,20 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { generateObject, hasToolCall, stepCountIs, streamText as aiStreamText, } from "ai";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
|
|
7
|
-
import { addTracingToMethods, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, } from "@superblocksteam/shared";
|
|
6
|
+
import { AiGenerationState, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
|
|
7
|
+
import { addTracingToMethods, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, getAiQuotaPaywallReasonFromMessage, } from "@superblocksteam/shared";
|
|
8
|
+
import { npmRegistryEmitter } from "@superblocksteam/telemetry";
|
|
8
9
|
import { buildTranslationPrompt } from "../migration/translation-prompt.js";
|
|
10
|
+
import { POLICY_GATE_RUN_IN_PROGRESS_ERROR, policyGateRunRegistry, } from "../policy-gate-run-registry.js";
|
|
9
11
|
import { getErrorMeta, getLogger, setDefaultLogger } from "../util/logger.js";
|
|
10
12
|
import { composeSecurityAgentPrompt, } from "./agent/prompts/build-security-scan-prompt.js";
|
|
11
13
|
import { getToolCallArguments, getToolErrorCode, getToolOutput, } from "./agent/tool-message-utils.js";
|
|
12
|
-
import { buildSecurityScanTools, } from "./agent/tools.js";
|
|
14
|
+
import { buildSecurityScanTools, isBackgroundTaskFrameworkEnabled, } from "./agent/tools.js";
|
|
13
15
|
import { hasPendingEscalations, flushPendingEscalations, } from "./agent/tools/apis/api-validation-orchestrator.js";
|
|
14
16
|
import { safeSampleJson } from "./agent/tools/apis/sample-json.js";
|
|
15
17
|
import { resolveSdkApiEntryPoint } from "./agent/tools/apis/sdk-registry.js";
|
|
16
18
|
import { readFile } from "./agent/tools/build-read-file.js";
|
|
19
|
+
import { createDevDatabaseTaskTypes, wireDevDatabaseIntegrationPriming, } from "./agent/tools/databases/dev-database-tasks.js";
|
|
17
20
|
import { debugCache } from "./agent/tools/debug-cache.js";
|
|
18
21
|
import { buildReadFileToolFactory } from "./agent/tools/index.js";
|
|
19
22
|
import { securityScanResultSchema, } from "./agent/tools/report-security-findings.js";
|
|
@@ -33,6 +36,8 @@ import { API_MIGRATION_ORIGINS, readPersistedChecklist, updateChecklistItem, } f
|
|
|
33
36
|
import { downloadContextFromBucketeer, downloadLatestContextFromBucketeer, restoreContextFromCheckpoint, } from "./context-download.js";
|
|
34
37
|
import { uploadContextToBucketeer } from "./context-upload.js";
|
|
35
38
|
import { AppContextStore } from "./context/app-context.js";
|
|
39
|
+
import { createDevDatabaseHttpTransport, DevDatabaseClient, } from "./dev-database-client.js";
|
|
40
|
+
import { buildEnsureDevDatabaseFilesSynced, } from "./dev-database-file-sync.js";
|
|
36
41
|
import { filterDisabledToolsForMigration } from "./filter-disabled-tools-for-migration.js";
|
|
37
42
|
import { IntegrationStore } from "./integrations/store.js";
|
|
38
43
|
import { JudgeService } from "./judge/judge-service.js";
|
|
@@ -49,11 +54,15 @@ import { traceLLM } from "./llmobs/helpers.js";
|
|
|
49
54
|
import { getJwtTraceTags } from "./llmobs/helpers.js";
|
|
50
55
|
import llmobs from "./llmobs/index.js";
|
|
51
56
|
import { PlaywrightMcpServerManager } from "./mcp/playwright-server.js";
|
|
57
|
+
import { policyGateStreamPartToAgentStep } from "./policy-gate-step.js";
|
|
52
58
|
import { ClarkProfiler } from "./profiler/clark-profiler.js";
|
|
53
59
|
import { explainEventHandlerCodePrompt } from "./prompts/explain-code.js";
|
|
54
60
|
import { buildSummarizeApiUsagePrompt } from "./prompts/summarize-api-usage.js";
|
|
61
|
+
import { checkAiQuota } from "./quota-client.js";
|
|
55
62
|
import { RecordingManager, createRecordingFetch } from "./recording/index.js";
|
|
56
63
|
import { RequestDeduplicator } from "./request-deduplicator.js";
|
|
64
|
+
import { scanContentForSecrets } from "./security/secret-scanner-service.js";
|
|
65
|
+
import { SecretRedactor } from "./security/secret-scanner.js";
|
|
57
66
|
import { ClarkStateNames, Clark, SERVICE_STARTED_WITH_DRAFT, USER_ACCEPTED_DRAFT, USER_REJECTED_DRAFT, USER_SENT_PROMPT, USER_CANCELED, AGENT_CANCELED, } from "./state-machine/clark-fsm.js";
|
|
58
67
|
import { doAgentPlanning } from "./state-machine/handlers/agent-planning.js";
|
|
59
68
|
import { doAwaitingUser } from "./state-machine/handlers/awaiting-user.js";
|
|
@@ -64,13 +73,14 @@ import { doPostProcessing } from "./state-machine/handlers/post-processing.js";
|
|
|
64
73
|
import { doRuntimeReviewing } from "./state-machine/handlers/runtime-reviewing.js";
|
|
65
74
|
import { classificationHelper } from "./state-machine/helpers/classification.js";
|
|
66
75
|
import { getContextId } from "./state-machine/helpers/context-id.js";
|
|
76
|
+
import { sendUserGenerationStateChannel } from "./state-machine/helpers/peer.js";
|
|
67
77
|
import { sendUserMessageChannel, clearPendingToolPermissionRequest, } from "./state-machine/helpers/peer.js";
|
|
68
78
|
import { StablePeer } from "./state-machine/helpers/stable-peer.js";
|
|
69
79
|
import { transitionFrom } from "./state-machine/helpers/transition.js";
|
|
80
|
+
import { isTaskAccessibleTo, registerBackgroundTaskTypes, resyncActiveTaskStatusesToPeer, TaskStore, wireTaskStatusPushToPeer, } from "./tasks/index.js";
|
|
70
81
|
import { TemplateRenderer } from "./template-renderer.js";
|
|
71
82
|
import { createJsonStreamParser } from "./util/json-stream-parser.js";
|
|
72
83
|
import { processLLMConfig } from "./util/llm-config-utils.js";
|
|
73
|
-
import { buildModeMessage } from "./util/mode-message.js";
|
|
74
84
|
import { parseJwt } from "./util/parse-jwt.js";
|
|
75
85
|
import { getToolCallSignature } from "./util/tool-signature.js";
|
|
76
86
|
export { AiServiceFeatureFlags } from "./features.js";
|
|
@@ -292,6 +302,14 @@ const mergePlanContexts = (existing, incoming) => {
|
|
|
292
302
|
enableTesting: incoming?.enableTesting ?? existing?.enableTesting,
|
|
293
303
|
};
|
|
294
304
|
};
|
|
305
|
+
const APP_CLARK_GENERATION_METADATA = {
|
|
306
|
+
origin: "app_clark_generation",
|
|
307
|
+
surface: "app",
|
|
308
|
+
};
|
|
309
|
+
const POLICY_GATE_USAGE_METADATA = {
|
|
310
|
+
origin: "policy_gate",
|
|
311
|
+
surface: "policy_gate",
|
|
312
|
+
};
|
|
295
313
|
export class AiService extends TracedEventEmitter {
|
|
296
314
|
config;
|
|
297
315
|
templateRenderer;
|
|
@@ -312,6 +330,19 @@ export class AiService extends TracedEventEmitter {
|
|
|
312
330
|
requestDeduplicator = new RequestDeduplicator();
|
|
313
331
|
entityPermissionStore = new SessionEntityPermissionStore();
|
|
314
332
|
_onGenerationCompleteCallback;
|
|
333
|
+
/** JWT-derived userId of the currently-authenticated peer, set by
|
|
334
|
+
* `notifyAuthenticatedRpc` and cleared on disconnect / peer swap.
|
|
335
|
+
* Only trusted while `authenticatedPeerId` still matches the bound
|
|
336
|
+
* `stablePeer.peerId` (see `getAuthenticatedPeerUserId`). Used by
|
|
337
|
+
* `wireTaskStatusPushToPeer` to owner-filter live status pushes. */
|
|
338
|
+
authenticatedPeerUserId;
|
|
339
|
+
/** The `peerId` that `authenticatedPeerUserId` was authenticated for.
|
|
340
|
+
* `handleUserConnected` can bind a replacement peer before the
|
|
341
|
+
* previous peer's disconnect is processed; keying the identity to its
|
|
342
|
+
* peerId means a live push in that window is gated against `undefined`
|
|
343
|
+
* (suppressed) instead of the previous user, closing the cross-user
|
|
344
|
+
* leak from PR #19718 Comment 59. */
|
|
345
|
+
authenticatedPeerId;
|
|
315
346
|
tokenManagerJwt;
|
|
316
347
|
/**
|
|
317
348
|
* Listener body for `tokenManager.on("tokenUpdated", ...)`. Defined as a
|
|
@@ -355,6 +386,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
355
386
|
_runtimeGitOperationLock;
|
|
356
387
|
_onGitOperationComplete;
|
|
357
388
|
_syncContextProvider;
|
|
389
|
+
taskStore;
|
|
358
390
|
get appContext() {
|
|
359
391
|
return this._appContext;
|
|
360
392
|
}
|
|
@@ -506,21 +538,21 @@ export class AiService extends TracedEventEmitter {
|
|
|
506
538
|
useSuperblocksAuthorizationHeader: config.providerSettings?.useSuperblocksAuthorizationHeader,
|
|
507
539
|
};
|
|
508
540
|
// Seed `tokenManagerJwt` synchronously from the manager's current
|
|
509
|
-
// value before
|
|
541
|
+
// value before the npm-registry client calls `getNpmRegistryJwt()`. The
|
|
510
542
|
// `tokenUpdated` listener below covers FUTURE refreshes; this read
|
|
511
543
|
// covers the cold-start case where the CLI (or any earlier code) has
|
|
512
544
|
// already called `tokenManager.updateToken(...)` before this AiService
|
|
513
545
|
// was constructed. Without it, the prior emission is lost — Node
|
|
514
546
|
// `EventEmitter` has no replay — and `NpmRegistryClient.getJwt()`
|
|
515
|
-
// would time out via `
|
|
547
|
+
// would time out via `waitForNpmRegistryJwt(...)`, downgrading `syncHomeNpmrc`
|
|
516
548
|
// to `unreachable` and leaving `~/.npmrc` unwritten on cold boot.
|
|
517
549
|
this.tokenManagerJwt = this.config.tokenManager?.getCurrentToken();
|
|
518
550
|
// Resolve the npm registry client once and reuse across TemplateRenderer
|
|
519
551
|
// and AppShell so both install paths share the same in-memory cache and
|
|
520
552
|
// 401-refresh state. Explicit injection wins (tests); otherwise we
|
|
521
553
|
// construct a default that reads the LD flag from `config.features` and
|
|
522
|
-
// routes JWT through the AiService's
|
|
523
|
-
// tokenManager token and falls back to clark.context.jwt). Empty
|
|
554
|
+
// routes JWT through the AiService's npm-registry JWT source (which
|
|
555
|
+
// prefers the tokenManager token and falls back to clark.context.jwt). Empty
|
|
524
556
|
// tokens are refused upstream of `attempt()` so the server doesn't
|
|
525
557
|
// see `Bearer ` and reply 400 (which the client would now treat as a
|
|
526
558
|
// hard fail rather than a transient outage).
|
|
@@ -541,7 +573,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
541
573
|
// `tokenUpdated` event rather than throwing immediately, so the
|
|
542
574
|
// first install in the shared template dir actually gets a
|
|
543
575
|
// `.npmrc` instead of silently downgrading to "not configured".
|
|
544
|
-
getJwt: () => this.
|
|
576
|
+
getJwt: () => this.waitForNpmRegistryJwt(NPM_REGISTRY_JWT_WAIT_MS),
|
|
545
577
|
// Push-based refresh hook: on 401, wait briefly for the next
|
|
546
578
|
// `tokenUpdated` event before re-reading the JWT. Without this,
|
|
547
579
|
// a 401 retry fires immediately with the same expired token and
|
|
@@ -556,6 +588,12 @@ export class AiService extends TracedEventEmitter {
|
|
|
556
588
|
}),
|
|
557
589
|
});
|
|
558
590
|
this.npmRegistryClient = npmRegistryClient;
|
|
591
|
+
// Kill-switch gauge (APPS-4378): `superblocks.npm.killswitch.enabled` reads
|
|
592
|
+
// `1` when the npm-registry feature is suppressed (LD flag off), `0`
|
|
593
|
+
// otherwise — one series per process. Registered here, the single place
|
|
594
|
+
// that owns the flag value, so on-call can answer "did anyone hit the
|
|
595
|
+
// kill-switch in the last 24h?" without reading logs.
|
|
596
|
+
npmRegistryEmitter.setKillswitch(() => config.features.npmRegistryEnabled !== true);
|
|
559
597
|
// One process-lifetime `NpmPackageLookup` shared across every
|
|
560
598
|
// `buildTools()` call. The `build_lookupNpmPackage` tool factory used
|
|
561
599
|
// to construct a fresh instance per LLM turn; each construction
|
|
@@ -569,7 +607,11 @@ export class AiService extends TracedEventEmitter {
|
|
|
569
607
|
// `npmRegistryClient` / `templateRenderer` / `appShell` — means the
|
|
570
608
|
// gauge wires up exactly once and the 60s TTL cache stays warm across
|
|
571
609
|
// turns instead of being thrown away.
|
|
572
|
-
this.npmPackageLookup = new NpmPackageLookup({
|
|
610
|
+
this.npmPackageLookup = new NpmPackageLookup({
|
|
611
|
+
client: npmRegistryClient,
|
|
612
|
+
// org_id rides on the `npm_registry.lookup_package` span only.
|
|
613
|
+
orgId: config.organizationId,
|
|
614
|
+
});
|
|
573
615
|
this.templateRenderer =
|
|
574
616
|
config.templateRenderer ??
|
|
575
617
|
new TemplateRenderer({
|
|
@@ -589,6 +631,24 @@ export class AiService extends TracedEventEmitter {
|
|
|
589
631
|
draftInterface: config.draftInterface,
|
|
590
632
|
templateRenderer: this.templateRenderer,
|
|
591
633
|
npmRegistryClient,
|
|
634
|
+
// APPS-4191 / P6.3: ship blocked-install audit events to the server over
|
|
635
|
+
// the authenticated socket. Fire-and-forget — the server derives org,
|
|
636
|
+
// actor, AND app attribution from the authenticated connection
|
|
637
|
+
// (`ctx.jwtClaims.app_id` on the scoped JWT this socket carries), then
|
|
638
|
+
// throttles, sanitizes, and persists. A transport failure must never
|
|
639
|
+
// surface to (or break) the install. We intentionally do NOT spread the
|
|
640
|
+
// local `applicationId` into the payload — that would be client-
|
|
641
|
+
// controlled and forgeable by any authenticated socket caller.
|
|
642
|
+
reportNpmInstallBlocked: (report) => {
|
|
643
|
+
void this.config.rpcClient
|
|
644
|
+
.call(async (client) => {
|
|
645
|
+
const rpcClient = client;
|
|
646
|
+
return await rpcClient.call.v1.audit.npmInstallBlocked(report);
|
|
647
|
+
})
|
|
648
|
+
.catch((error) => {
|
|
649
|
+
this.getLogger().debug("[ai-service] Failed to report npm-install-blocked audit event", getErrorMeta(error));
|
|
650
|
+
});
|
|
651
|
+
},
|
|
592
652
|
virtualFileSystem: createDefaultVirtualFileSystem({
|
|
593
653
|
useSystemSkills: config.features.useSystemSkills,
|
|
594
654
|
knowledgeManager: this.knowledgeManager,
|
|
@@ -616,6 +676,68 @@ export class AiService extends TracedEventEmitter {
|
|
|
616
676
|
});
|
|
617
677
|
this.fileSystemInterface = new FileSystemInterface();
|
|
618
678
|
this.integrationStore = new IntegrationStore(undefined, config.appRootDirPath, config.features.metadataCacheMaxTotalSizeMB, config.features.metadataCacheMaxSingleFileSizeMB, config.pluginExecutionVersions, config.superblocksBaseUrl, config.applicationId, () => this.clark?.context?.jwt);
|
|
679
|
+
// Background-task framework: per-session TaskStore + registered task
|
|
680
|
+
// types. Gated by `isBackgroundTaskFrameworkEnabled` — the same
|
|
681
|
+
// `managedNativeDbEnabled` rollout check that `agent/tools.ts`
|
|
682
|
+
// consults. When the gate is off, `taskStore` stays undefined and
|
|
683
|
+
// `agent/tools.ts` skips registering the start*/check*/wait*/cancel*/list*
|
|
684
|
+
// tools (those have an `&& services.taskStore` guard, plus the same
|
|
685
|
+
// flag check).
|
|
686
|
+
if (isBackgroundTaskFrameworkEnabled(config.features)) {
|
|
687
|
+
this.taskStore = new TaskStore();
|
|
688
|
+
// Heterogeneous list of task types funneled into
|
|
689
|
+
// `registerBackgroundTaskTypes` (which itself takes ReadonlyArray<TaskType<any, any, any>>),
|
|
690
|
+
// hence the `any` widening: `TaskType` is contravariant in `Input`.
|
|
691
|
+
const taskTypes = [];
|
|
692
|
+
{
|
|
693
|
+
// Dev-database lifecycle is unconditionally wired as a pair of
|
|
694
|
+
// background tasks (provision + applyMigrations) — the legacy
|
|
695
|
+
// blocking triplet (createDevDb / applyMigrations /
|
|
696
|
+
// getDevDbProgress) was removed in favor of this path. Gated only
|
|
697
|
+
// by the framework flag above.
|
|
698
|
+
const devDbTypes = createDevDatabaseTaskTypes({
|
|
699
|
+
applicationId: config.applicationId,
|
|
700
|
+
// The working-state SQL files in the user's local repo —
|
|
701
|
+
// including drafts the LLM just wrote this turn — must be
|
|
702
|
+
// flushed to the server before the lifecycle worker reads
|
|
703
|
+
// them, otherwise migrations apply against stale content and
|
|
704
|
+
// the new migration silently succeeds against an unchanged
|
|
705
|
+
// schema. Matches master's `applyMigrationsToolFactory`
|
|
706
|
+
// registration, which threaded `syncCurrentDirectoryWithDraftsNow`
|
|
707
|
+
// — NOT the autosync drain `uploadDirectoryNowIfNeeded`.
|
|
708
|
+
// `_runtimeSyncService` is injected by `setRuntimeGitServices`
|
|
709
|
+
// AFTER this constructor runs, so the helper resolves it
|
|
710
|
+
// lazily at task-start time.
|
|
711
|
+
ensureFilesSynced: buildEnsureDevDatabaseFilesSynced(() => this._runtimeSyncService),
|
|
712
|
+
getClient: () => {
|
|
713
|
+
const jwt = this.clark?.context?.jwt;
|
|
714
|
+
if (!jwt) {
|
|
715
|
+
throw new Error("[devDatabase] cannot start background task: no JWT available on Clark context");
|
|
716
|
+
}
|
|
717
|
+
return new DevDatabaseClient({
|
|
718
|
+
transport: createDevDatabaseHttpTransport({
|
|
719
|
+
jwt,
|
|
720
|
+
superblocksBaseUrl: config.superblocksBaseUrl,
|
|
721
|
+
}),
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
taskTypes.push(devDbTypes.provisionDevDatabaseTaskType, devDbTypes.applyDevDatabaseMigrationsTaskType);
|
|
726
|
+
wireDevDatabaseIntegrationPriming(this.taskStore, this.integrationStore);
|
|
727
|
+
}
|
|
728
|
+
registerBackgroundTaskTypes(this.taskStore, taskTypes);
|
|
729
|
+
// Push every status transition (including the initial in-store
|
|
730
|
+
// snapshot after `start`) to the editor for the bottom-of-chat
|
|
731
|
+
// indicator. StablePeer handles disconnect buffering so the helper
|
|
732
|
+
// itself doesn't retry. The unsubscribe fn is intentionally not
|
|
733
|
+
// captured — the TaskStore is per-AiService and disposed alongside
|
|
734
|
+
// it, which clears all listeners.
|
|
735
|
+
wireTaskStatusPushToPeer(this.taskStore, {
|
|
736
|
+
getPeer: () => this.stablePeer,
|
|
737
|
+
getCallerUserId: () => this.getAuthenticatedPeerUserId(),
|
|
738
|
+
logger: this.getLogger(),
|
|
739
|
+
});
|
|
740
|
+
}
|
|
619
741
|
this._appContext = new AppContextStore(this.fileSystemInterface);
|
|
620
742
|
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
621
743
|
this.chatSessionStore = new ChatSessionStore({
|
|
@@ -653,7 +775,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
653
775
|
unifiedProviderEnabled: config.features.unifiedProviderEnabled ?? false,
|
|
654
776
|
organizationId: config.organizationId,
|
|
655
777
|
applicationId: config.applicationId,
|
|
656
|
-
getJwt: () => this.
|
|
778
|
+
getJwt: () => this.requireInferenceJwt(),
|
|
657
779
|
onUsageRecorded: (usage) => {
|
|
658
780
|
// Accumulate workflow-level metrics
|
|
659
781
|
this.clark.updateContext((context) => {
|
|
@@ -845,15 +967,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
845
967
|
integrationStore: this.integrationStore,
|
|
846
968
|
knowledgeManager: this.knowledgeManager,
|
|
847
969
|
appContextStore: this.appContext,
|
|
848
|
-
llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.
|
|
849
|
-
prompt_id: clark.context.promptId,
|
|
850
|
-
chatmessage_id: clark.context.chatmessageId,
|
|
851
|
-
application_id: this.config.applicationId,
|
|
852
|
-
session_id: this.sessionId,
|
|
853
|
-
// Note: retry_count for LLM retries is added by orchestrator via providerOptions
|
|
854
|
-
mode: clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
|
|
855
|
-
plan_size: clark.context.planSize,
|
|
856
|
-
})),
|
|
970
|
+
llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.requireInferenceJwt(), clark.context.llmConfig, () => this.getLlmAiAttributes(clark)),
|
|
857
971
|
chatSessionStore: this.chatSessionStore,
|
|
858
972
|
clarkProfiler: this.clarkProfiler,
|
|
859
973
|
contextManagerV2: this.contextManagerV2,
|
|
@@ -867,6 +981,18 @@ export class AiService extends TracedEventEmitter {
|
|
|
867
981
|
syncService: this._runtimeSyncService,
|
|
868
982
|
gitOperationLock: this._runtimeGitOperationLock,
|
|
869
983
|
onGitOperationComplete: this._onGitOperationComplete,
|
|
984
|
+
taskStore: this.taskStore,
|
|
985
|
+
// Resolve `ownerUserId` from the *current* JWT at each call site
|
|
986
|
+
// (kickoff / read / cancel). Reading lazily — instead of capturing
|
|
987
|
+
// the JWT at handler construction — means the wired identity stays
|
|
988
|
+
// correct across `updateClarkJwt` refreshes that happen mid-turn
|
|
989
|
+
// when a user's token rotates. Tests pin this lazy contract.
|
|
990
|
+
getCurrentOwnerId: () => {
|
|
991
|
+
const jwt = clark.context.jwt;
|
|
992
|
+
if (!jwt)
|
|
993
|
+
return undefined;
|
|
994
|
+
return parseJwt(jwt)?.userId;
|
|
995
|
+
},
|
|
870
996
|
};
|
|
871
997
|
const clarkStateHandlers = Object.fromEntries(Object.entries(clarkStateHandlerFactories).map(([state, handlerFactory]) => [state, handlerFactory(clark, params)]));
|
|
872
998
|
const { from, to, event } = transition;
|
|
@@ -885,6 +1011,14 @@ export class AiService extends TracedEventEmitter {
|
|
|
885
1011
|
// Trigger callback when Clark enters idle states (generation complete)
|
|
886
1012
|
if (to === ClarkStateNames.Idle ||
|
|
887
1013
|
to === ClarkStateNames.AwaitingUser) {
|
|
1014
|
+
// The turn is over: drop per-turn billing attribution so stray
|
|
1015
|
+
// LLM calls before the next prompt bill as default generation.
|
|
1016
|
+
if (clark.context.usageMetadata) {
|
|
1017
|
+
clark.updateContext((context) => ({
|
|
1018
|
+
...context,
|
|
1019
|
+
usageMetadata: undefined,
|
|
1020
|
+
}));
|
|
1021
|
+
}
|
|
888
1022
|
this.triggerGenerationComplete();
|
|
889
1023
|
}
|
|
890
1024
|
}
|
|
@@ -907,6 +1041,16 @@ export class AiService extends TracedEventEmitter {
|
|
|
907
1041
|
};
|
|
908
1042
|
};
|
|
909
1043
|
}
|
|
1044
|
+
getInferenceJwt() {
|
|
1045
|
+
return this.clark?.context?.jwt;
|
|
1046
|
+
}
|
|
1047
|
+
requireInferenceJwt() {
|
|
1048
|
+
const jwt = this.getInferenceJwt();
|
|
1049
|
+
if (!jwt) {
|
|
1050
|
+
throw new Error("[ai-service] no Clark JWT available for AI inference");
|
|
1051
|
+
}
|
|
1052
|
+
return jwt;
|
|
1053
|
+
}
|
|
910
1054
|
/**
|
|
911
1055
|
* Resolves the conversation context for a contextId that is not present
|
|
912
1056
|
* locally, by downloading it from Bucketeer. Wired into ContextManagerV2 as
|
|
@@ -925,7 +1069,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
925
1069
|
* commitId — and GET by commit. The resolution is logged either way.
|
|
926
1070
|
*/
|
|
927
1071
|
async remoteRestoreFallback(id) {
|
|
928
|
-
const jwt = this.
|
|
1072
|
+
const jwt = this.getInferenceJwt();
|
|
929
1073
|
const branchName = this._syncContextProvider?.getBranchName();
|
|
930
1074
|
if (!jwt || !branchName) {
|
|
931
1075
|
// A missing JWT is expected startup churn on a fresh pod (this runs
|
|
@@ -980,43 +1124,51 @@ export class AiService extends TracedEventEmitter {
|
|
|
980
1124
|
appRootDirPath: this.config.appRootDirPath,
|
|
981
1125
|
});
|
|
982
1126
|
}
|
|
983
|
-
|
|
1127
|
+
requireProvidedInferenceJwt(jwt, source) {
|
|
1128
|
+
if (!jwt) {
|
|
1129
|
+
throw new Error(`[ai-service] no JWT available for ${source} AI inference`);
|
|
1130
|
+
}
|
|
1131
|
+
return jwt;
|
|
1132
|
+
}
|
|
1133
|
+
getNpmRegistryJwt() {
|
|
984
1134
|
if (this.tokenManagerJwt) {
|
|
985
1135
|
return this.tokenManagerJwt;
|
|
986
1136
|
}
|
|
987
|
-
//
|
|
1137
|
+
// Fallback to Clark context if no token manager token is provided.
|
|
988
1138
|
return this.clark?.context?.jwt;
|
|
989
1139
|
}
|
|
990
1140
|
buildContextManagementForCurrentClark(llmConfig) {
|
|
991
|
-
const modeInstructions = this.clark.context.currentMode
|
|
992
|
-
? `Preserve the following mode context in your summary: ${buildModeMessage(this.clark.context.currentMode, this.clark.context.planContext?.approved)}`
|
|
993
|
-
: undefined;
|
|
994
1141
|
return selectContextManagementStrategy({
|
|
995
1142
|
serverContextManagementEnabled: llmConfig?.serverContextManagement?.enabled,
|
|
996
|
-
modeInstructions,
|
|
997
1143
|
thresholds: llmConfig?.serverContextManagement,
|
|
998
|
-
summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.
|
|
999
|
-
prompt_id: this.clark.context.promptId,
|
|
1000
|
-
chatmessage_id: this.clark.context.chatmessageId,
|
|
1001
|
-
application_id: this.config.applicationId,
|
|
1002
|
-
session_id: this.sessionId,
|
|
1003
|
-
mode: this.clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
|
|
1004
|
-
plan_size: this.clark.context.planSize,
|
|
1005
|
-
})).modelForTask("summarizeMessages"),
|
|
1144
|
+
summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getInferenceJwt(), llmConfig, () => this.getLlmAiAttributes(this.clark)).modelForTask("summarizeMessages"),
|
|
1006
1145
|
});
|
|
1007
1146
|
}
|
|
1147
|
+
getLlmAiAttributes(clark) {
|
|
1148
|
+
const usageMetadata = clark.context.usageMetadata ?? APP_CLARK_GENERATION_METADATA;
|
|
1149
|
+
return {
|
|
1150
|
+
prompt_id: clark.context.promptId,
|
|
1151
|
+
chatmessage_id: clark.context.chatmessageId,
|
|
1152
|
+
application_id: this.config.applicationId,
|
|
1153
|
+
session_id: this.sessionId,
|
|
1154
|
+
// Note: retry_count for LLM retries is added by orchestrator via providerOptions
|
|
1155
|
+
mode: clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
|
|
1156
|
+
plan_size: clark.context.planSize,
|
|
1157
|
+
metadata: usageMetadata,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1008
1160
|
/**
|
|
1009
1161
|
* Resolve a JWT for the npm-registry fetch, blocking briefly on the
|
|
1010
1162
|
* first `tokenUpdated` event when no JWT is yet primed. The
|
|
1011
1163
|
* `TemplateRenderer` prefetch fires during AiService construction —
|
|
1012
1164
|
* before the `tokenManager` subscription has emitted — so a synchronous
|
|
1013
|
-
* `
|
|
1165
|
+
* `getNpmRegistryJwt()` would observe `undefined` and silently downgrade the
|
|
1014
1166
|
* private-registry install to public npm. Blocking here is the
|
|
1015
1167
|
* narrowest fix: callers that already have a primed JWT take the fast
|
|
1016
1168
|
* path and never await.
|
|
1017
1169
|
*/
|
|
1018
|
-
async
|
|
1019
|
-
const immediate = this.
|
|
1170
|
+
async waitForNpmRegistryJwt(timeoutMs) {
|
|
1171
|
+
const immediate = this.getNpmRegistryJwt();
|
|
1020
1172
|
if (immediate) {
|
|
1021
1173
|
return immediate;
|
|
1022
1174
|
}
|
|
@@ -1024,7 +1176,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1024
1176
|
throw new Error("[npm-registry] no JWT available and no tokenManager wired to wait for one");
|
|
1025
1177
|
}
|
|
1026
1178
|
await this.waitForTokenUpdate(timeoutMs);
|
|
1027
|
-
const after = this.
|
|
1179
|
+
const after = this.getNpmRegistryJwt();
|
|
1028
1180
|
if (!after) {
|
|
1029
1181
|
throw new Error("[npm-registry] no JWT available after waiting for tokenManager update");
|
|
1030
1182
|
}
|
|
@@ -1038,7 +1190,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1038
1190
|
*
|
|
1039
1191
|
* Resolution only signals "a wait completed" — never that a JWT actually
|
|
1040
1192
|
* changed. The constructor's `tokenUpdated` listener (above) is the only
|
|
1041
|
-
* code that mutates `tokenManagerJwt`. Callers (`
|
|
1193
|
+
* code that mutates `tokenManagerJwt`. Callers (`waitForNpmRegistryJwt`, the
|
|
1042
1194
|
* npm-registry `refreshJwt` adapter) must re-read the JWT after this
|
|
1043
1195
|
* resolves and treat an unchanged value the same as a refresh that
|
|
1044
1196
|
* never happened.
|
|
@@ -1071,20 +1223,6 @@ export class AiService extends TracedEventEmitter {
|
|
|
1071
1223
|
});
|
|
1072
1224
|
};
|
|
1073
1225
|
}
|
|
1074
|
-
async callServer({ jwt, path, }) {
|
|
1075
|
-
const url = `${this.config.superblocksBaseUrl}/api/${path}`;
|
|
1076
|
-
const response = await fetch(url, {
|
|
1077
|
-
method: "GET",
|
|
1078
|
-
headers: {
|
|
1079
|
-
Authorization: `Bearer ${jwt}`,
|
|
1080
|
-
"Content-Type": "application/json",
|
|
1081
|
-
},
|
|
1082
|
-
});
|
|
1083
|
-
if (!response.ok) {
|
|
1084
|
-
throw new Error(`Call to ${path} failed with status ${response.status}`);
|
|
1085
|
-
}
|
|
1086
|
-
return (await response.json());
|
|
1087
|
-
}
|
|
1088
1226
|
/**
|
|
1089
1227
|
* Non-throwing AI quota check. Returns `{ allowed: true }` when the org is
|
|
1090
1228
|
* within its limit, and also when the check itself can't run (no JWT, or the
|
|
@@ -1092,33 +1230,14 @@ export class AiService extends TracedEventEmitter {
|
|
|
1092
1230
|
* blocks legitimate work. When the org is over limit, returns the reason and
|
|
1093
1231
|
* a user-facing message instead of throwing, so callers that don't surface
|
|
1094
1232
|
* exceptions (e.g. the background migration turn) can react.
|
|
1233
|
+
* Logic lives in `quota-client.ts` (with the `quota.check` span).
|
|
1095
1234
|
*/
|
|
1096
1235
|
async checkAiQuota(jwt) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
try {
|
|
1103
|
-
const quota = await this.callServer({
|
|
1104
|
-
jwt: token,
|
|
1105
|
-
path: "v1/ai/quota",
|
|
1106
|
-
});
|
|
1107
|
-
if (!quota.allowed) {
|
|
1108
|
-
const reason = quota.reason ?? "token_limit_exceeded";
|
|
1109
|
-
const message = reason === "trial_expired"
|
|
1110
|
-
? "Your AI trial period has ended."
|
|
1111
|
-
: reason === "no_seat_assigned"
|
|
1112
|
-
? "You don't have an AI Builder license assigned. Ask your admin to assign a license."
|
|
1113
|
-
: "Your organization has reached its AI usage limit.";
|
|
1114
|
-
return { allowed: false, reason, message };
|
|
1115
|
-
}
|
|
1116
|
-
return { allowed: true };
|
|
1117
|
-
}
|
|
1118
|
-
catch (error) {
|
|
1119
|
-
this.getLogger().warn("[ai-service] Quota check failed, allowing request", getErrorMeta(error));
|
|
1120
|
-
return { allowed: true };
|
|
1121
|
-
}
|
|
1236
|
+
return checkAiQuota({
|
|
1237
|
+
superblocksBaseUrl: this.config.superblocksBaseUrl,
|
|
1238
|
+
jwt: jwt ?? this.getInferenceJwt(),
|
|
1239
|
+
logger: this.getLogger(),
|
|
1240
|
+
});
|
|
1122
1241
|
}
|
|
1123
1242
|
async ensureAiQuotaAvailable(jwt) {
|
|
1124
1243
|
const quota = await this.checkAiQuota(jwt);
|
|
@@ -1200,9 +1319,20 @@ export class AiService extends TracedEventEmitter {
|
|
|
1200
1319
|
llmConfig: migrationLlmConfig,
|
|
1201
1320
|
mode: AiMode.BUILD,
|
|
1202
1321
|
};
|
|
1322
|
+
const promptId = randomUUID();
|
|
1323
|
+
const branchName = this._syncContextProvider?.getBranchName();
|
|
1324
|
+
const migrationUsageMetadata = {
|
|
1325
|
+
applicationId: this.config.applicationId,
|
|
1326
|
+
branchName,
|
|
1327
|
+
migrationSource: "superblocks_2_0",
|
|
1328
|
+
origin: "app_migration",
|
|
1329
|
+
pendingSeedApiCount,
|
|
1330
|
+
retryMode: request.retryMode ?? "none",
|
|
1331
|
+
};
|
|
1203
1332
|
this.clark.updateContext((context) => ({
|
|
1204
1333
|
...context,
|
|
1205
|
-
promptId
|
|
1334
|
+
promptId,
|
|
1335
|
+
usageMetadata: migrationUsageMetadata,
|
|
1206
1336
|
llmConfig: migrationLlmConfig,
|
|
1207
1337
|
}));
|
|
1208
1338
|
const transitionTo = transitionFrom(this.clark);
|
|
@@ -1234,6 +1364,312 @@ export class AiService extends TracedEventEmitter {
|
|
|
1234
1364
|
return result.config;
|
|
1235
1365
|
}
|
|
1236
1366
|
migrationTurnTriggered = false;
|
|
1367
|
+
/**
|
|
1368
|
+
* Rejects prompt submission while a policy gate run is executing against the
|
|
1369
|
+
* current application so concurrent Clark activity cannot mutate the app
|
|
1370
|
+
* mid-run (APPS-4466). Enforced here at the dev server, not only in the UI.
|
|
1371
|
+
*/
|
|
1372
|
+
assertNoActivePolicyGateRun() {
|
|
1373
|
+
if (policyGateRunRegistry.isRunning(this.config.applicationId)) {
|
|
1374
|
+
this.getLogger().warn("[ai-service] Rejecting prompt while a policy gate run is in progress", { applicationId: this.config.applicationId });
|
|
1375
|
+
throw new Error(POLICY_GATE_RUN_IN_PROGRESS_ERROR);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Run the pre-call quota check for an incoming user request. If quota is
|
|
1380
|
+
* denied with a recognized paywall reason, persist the prompt + advice
|
|
1381
|
+
* and open the modal via `handlePreCallPaywall`, and return `true` so
|
|
1382
|
+
* the caller can short-circuit. Returns `false` if quota is fine.
|
|
1383
|
+
* Non-paywall errors from the quota check are re-thrown unchanged.
|
|
1384
|
+
*
|
|
1385
|
+
* Shared between `handleAiGenerate` (the normal entry point) and
|
|
1386
|
+
* `interruptAndContinue` (the interrupt-mid-generation entry point) so
|
|
1387
|
+
* both surfaces produce the same user-visible behavior on quota denial —
|
|
1388
|
+
* chat record + modal — instead of the interrupt path silently rejecting
|
|
1389
|
+
* with no UI state. (APPS-4080)
|
|
1390
|
+
*/
|
|
1391
|
+
async tryHandlePaywallForRequest(request) {
|
|
1392
|
+
try {
|
|
1393
|
+
await this.ensureAiQuotaAvailable(request.jwt);
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
if (error instanceof Error) {
|
|
1398
|
+
const reason = getAiQuotaPaywallReasonFromMessage(error.message);
|
|
1399
|
+
if (reason) {
|
|
1400
|
+
await this.handlePreCallPaywall(request, reason);
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Persist the user's prompt + the "credits ran out" advice and open the
|
|
1409
|
+
* paywall modal when the pre-call quota check denies the request. Mirrors
|
|
1410
|
+
* what `idle.ts:AGENT_CANCELED` does for the mid-stream paywall path so
|
|
1411
|
+
* both paths leave an identical chat record. Used only when
|
|
1412
|
+
* `ensureAiQuotaAvailable` throws a paywall error in `handleAiGenerate`.
|
|
1413
|
+
*
|
|
1414
|
+
* - For organic user prompts (no `responseMetadata`), persists via
|
|
1415
|
+
* `recordUser`.
|
|
1416
|
+
* - For widget responses (`multi_choice_response`, `plan_response`,
|
|
1417
|
+
* `confirm_response`, `searchable_dropdown_response`,
|
|
1418
|
+
* `remember_knowledge_draft_response`), persists via
|
|
1419
|
+
* `recordUserResponse` with the full metadata — without this, the
|
|
1420
|
+
* user's exact choice is silently dropped (they lose their selection
|
|
1421
|
+
* from chat history AND the resume turn's LLM has no record of what
|
|
1422
|
+
* they picked). Mirrors `agent-planning.ts:USER_SENT_PROMPT`.
|
|
1423
|
+
* - `tool_permission_response` is intentionally NOT persisted — those
|
|
1424
|
+
* responses don't have a chat representation; the approval is just
|
|
1425
|
+
* side-effect state. On resume the user will see the permission
|
|
1426
|
+
* prompt again, which is acceptable UX.
|
|
1427
|
+
* - Uses fire-and-forget peer pushes after each `await` on the store so
|
|
1428
|
+
* the chat is durable across refresh even if the live peer push drops.
|
|
1429
|
+
*/
|
|
1430
|
+
async handlePreCallPaywall(request, paywallReason) {
|
|
1431
|
+
this.getLogger().info(`[ai-service] Pre-call paywall denial; persisting prompt + advice for chat history`, { paywallReason });
|
|
1432
|
+
// APPS-4080: pre-cache the CANCELLED + paywallReason snapshot into
|
|
1433
|
+
// `lastGenerationState` BEFORE any chat-persistence await. Without
|
|
1434
|
+
// this, a socket reconnect during the awaits in steps 1-2 below
|
|
1435
|
+
// would replay the stale IDLE snapshot held over from before the
|
|
1436
|
+
// request, and the UI saga (`paywallReason && state === CANCELLED`
|
|
1437
|
+
// -> openAiQuotaPaywallModal) would never see the paywall payload —
|
|
1438
|
+
// so the modal would not reopen on refresh. The full
|
|
1439
|
+
// `sendUserGenerationStateChannel({...})` in step 3 below re-caches
|
|
1440
|
+
// and pushes to the peer so the LIVE modal opens AFTER the advice
|
|
1441
|
+
// is in chat. (Cursor PR-19341 review.)
|
|
1442
|
+
this.clark.updateContext({
|
|
1443
|
+
lastGenerationState: {
|
|
1444
|
+
state: AiGenerationState.CANCELLED,
|
|
1445
|
+
hasError: true,
|
|
1446
|
+
paywallReason,
|
|
1447
|
+
},
|
|
1448
|
+
});
|
|
1449
|
+
// 1. Persist + echo the user's prompt so the chat doesn't lose it.
|
|
1450
|
+
const responseMetadata = request.responseMetadata;
|
|
1451
|
+
if (!responseMetadata) {
|
|
1452
|
+
try {
|
|
1453
|
+
const chatmessageId = await this.chatSessionStore.recordUser(request.prompt, { attachments: request.promptContext?.attachments });
|
|
1454
|
+
if (chatmessageId && this.clark.context.peer) {
|
|
1455
|
+
void this.clark.context.peer.call
|
|
1456
|
+
.aiPushMessage({
|
|
1457
|
+
role: "user",
|
|
1458
|
+
type: "text",
|
|
1459
|
+
content: request.prompt,
|
|
1460
|
+
id: chatmessageId,
|
|
1461
|
+
timestamp: Date.now(),
|
|
1462
|
+
attachments: request.promptContext?.attachments,
|
|
1463
|
+
})
|
|
1464
|
+
.catch((err) => {
|
|
1465
|
+
this.getLogger().warn("[ai-service] Failed to echo pre-call paywall user prompt", getErrorMeta(err));
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
catch (err) {
|
|
1470
|
+
this.getLogger().warn("[ai-service] Failed to persist pre-call paywall user prompt", getErrorMeta(err));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
else if (responseMetadata.type !== "tool_permission_response") {
|
|
1474
|
+
// 1b. Widget response (multi_choice_response, plan_response, etc.).
|
|
1475
|
+
// Persist via `recordUserResponse` with the same metadata mapping
|
|
1476
|
+
// `agent-planning.ts:USER_SENT_PROMPT` uses, so the user's exact
|
|
1477
|
+
// choice survives in chat history and is visible on the resume
|
|
1478
|
+
// turn. Without this branch, the user's selection is silently
|
|
1479
|
+
// dropped and they lose their selection from chat history.
|
|
1480
|
+
//
|
|
1481
|
+
// `tool_permission_response` is intentionally skipped — those
|
|
1482
|
+
// don't have a persisted chat representation. The user will see
|
|
1483
|
+
// the permission prompt again on resume.
|
|
1484
|
+
const metadata = responseMetadata.type === "multi_choice_response"
|
|
1485
|
+
? {
|
|
1486
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1487
|
+
selectedChoiceIndices: responseMetadata.selectedChoiceIndices,
|
|
1488
|
+
selectedScopes: responseMetadata.selectedScopes,
|
|
1489
|
+
}
|
|
1490
|
+
: responseMetadata.type === "remember_knowledge_draft_response"
|
|
1491
|
+
? {
|
|
1492
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1493
|
+
selectedCandidateIndices: responseMetadata.selectedCandidateIndices,
|
|
1494
|
+
selectedScopes: responseMetadata.selectedScopes,
|
|
1495
|
+
}
|
|
1496
|
+
: responseMetadata.type === "confirm_response"
|
|
1497
|
+
? {
|
|
1498
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1499
|
+
approved: responseMetadata.approved,
|
|
1500
|
+
selectedScopes: responseMetadata.selectedScopes,
|
|
1501
|
+
}
|
|
1502
|
+
: responseMetadata.type === "searchable_dropdown_response"
|
|
1503
|
+
? {
|
|
1504
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1505
|
+
selectedValue: responseMetadata.selectedValue,
|
|
1506
|
+
selectedLabel: responseMetadata.selectedLabel,
|
|
1507
|
+
}
|
|
1508
|
+
: {
|
|
1509
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1510
|
+
approved: responseMetadata.approved,
|
|
1511
|
+
enableTesting: responseMetadata.enableTesting,
|
|
1512
|
+
};
|
|
1513
|
+
try {
|
|
1514
|
+
const chatmessageId = await this.chatSessionStore.recordUserResponse(request.prompt, responseMetadata.type, metadata);
|
|
1515
|
+
if (chatmessageId && this.clark.context.peer) {
|
|
1516
|
+
void this.clark.context.peer.call
|
|
1517
|
+
.aiPushMessage({
|
|
1518
|
+
role: "user",
|
|
1519
|
+
type: responseMetadata.type,
|
|
1520
|
+
content: request.prompt,
|
|
1521
|
+
id: chatmessageId,
|
|
1522
|
+
timestamp: Date.now(),
|
|
1523
|
+
responseToMessageId: responseMetadata.responseToMessageId,
|
|
1524
|
+
})
|
|
1525
|
+
.catch((err) => {
|
|
1526
|
+
this.getLogger().warn("[ai-service] Failed to echo pre-call paywall widget response", getErrorMeta(err));
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
catch (err) {
|
|
1531
|
+
this.getLogger().warn("[ai-service] Failed to persist pre-call paywall widget response", getErrorMeta(err));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
// 2. Persist + echo the credit advice. Same canned text as
|
|
1535
|
+
// `agent-planning.ts:LLM_ERRORED` so users see consistent guidance
|
|
1536
|
+
// across pre-call and mid-stream paywall paths.
|
|
1537
|
+
const advice = "I stopped because credits ran out. Add credits, then send any message to pick up from here.";
|
|
1538
|
+
const adviceId = randomUUID();
|
|
1539
|
+
try {
|
|
1540
|
+
await this.chatSessionStore.recordAssistant({
|
|
1541
|
+
role: "assistant",
|
|
1542
|
+
type: "error",
|
|
1543
|
+
content: advice,
|
|
1544
|
+
id: adviceId,
|
|
1545
|
+
timestamp: Date.now(),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
catch (err) {
|
|
1549
|
+
this.getLogger().warn("[ai-service] Failed to persist pre-call paywall advice", getErrorMeta(err));
|
|
1550
|
+
}
|
|
1551
|
+
if (this.clark.context.peer) {
|
|
1552
|
+
void this.clark.context.peer.call
|
|
1553
|
+
.aiPushMessage({
|
|
1554
|
+
role: "assistant",
|
|
1555
|
+
type: "error",
|
|
1556
|
+
content: advice,
|
|
1557
|
+
id: adviceId,
|
|
1558
|
+
timestamp: Date.now(),
|
|
1559
|
+
})
|
|
1560
|
+
.catch((err) => {
|
|
1561
|
+
this.getLogger().warn("[ai-service] Failed to echo pre-call paywall advice", getErrorMeta(err));
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
// 3. Open the paywall modal via the same generation-state event the
|
|
1565
|
+
// mid-stream path uses. The UI saga listens for
|
|
1566
|
+
// `paywallReason && state === CANCELLED` and opens the modal.
|
|
1567
|
+
// Route through `sendUserGenerationStateChannel` (NOT raw
|
|
1568
|
+
// `peer.call.aiSetGenerationState`) so the CANCELLED + hasError +
|
|
1569
|
+
// paywallReason payload is cached in `lastGenerationState`. On
|
|
1570
|
+
// socket reconnect, `handleUserConnected` replays that cached
|
|
1571
|
+
// state — without this, a refresh / reconnect after a pre-call
|
|
1572
|
+
// paywall would replay stale generation state and the modal
|
|
1573
|
+
// would not reopen. (APPS-4080)
|
|
1574
|
+
void sendUserGenerationStateChannel(this.clark)({
|
|
1575
|
+
state: AiGenerationState.CANCELLED,
|
|
1576
|
+
hasError: true,
|
|
1577
|
+
paywallReason,
|
|
1578
|
+
}).catch((err) => {
|
|
1579
|
+
this.getLogger().warn("[ai-service] Failed to send pre-call paywall generation state", getErrorMeta(err));
|
|
1580
|
+
});
|
|
1581
|
+
// 4. Mark this turn as paused-for-credits so the resume hint fires on
|
|
1582
|
+
// the next `USER_SENT_PROMPT` (same as the mid-stream LLM_ERRORED path
|
|
1583
|
+
// does in agent-planning.ts).
|
|
1584
|
+
this.clark.updateContext({ pausedForCreditsReason: paywallReason });
|
|
1585
|
+
// 5. Persist the failed prompt into ContextV2 so the LLM actually sees
|
|
1586
|
+
// it on the resume turn. ContextV2 is a per-turn context layer that
|
|
1587
|
+
// only grows when an LLM call runs (`ctx.startTurn(user)` +
|
|
1588
|
+
// `ctx.addResponse(...)` inside `streamText`). Pre-call paywall
|
|
1589
|
+
// throws BEFORE `transitionTo<$AgentPlanning>`, so `streamText`
|
|
1590
|
+
// never runs and the failed prompt is invisible to the LLM on the
|
|
1591
|
+
// next turn — recordUser above only writes to chat_message + the
|
|
1592
|
+
// live UI socket, not to ContextV2. Without this step the resume
|
|
1593
|
+
// LLM call has no memory of the failed task and (correctly!)
|
|
1594
|
+
// answers "I have no memory of prior tasks" because for *this*
|
|
1595
|
+
// context that's literally true. Add the failed prompt directly
|
|
1596
|
+
// so the resume turn sees it.
|
|
1597
|
+
// Add the failed prompt/response text to ContextV2 for both organic
|
|
1598
|
+
// user prompts AND widget responses (multi_choice, plan, etc.) so the
|
|
1599
|
+
// resume turn's LLM sees what the user said. The only case we skip is
|
|
1600
|
+
// tool_permission_response — those have no persisted chat
|
|
1601
|
+
// representation; the approval is just side-effect state that gets
|
|
1602
|
+
// re-prompted on resume.
|
|
1603
|
+
if (request.responseMetadata?.type !== "tool_permission_response") {
|
|
1604
|
+
try {
|
|
1605
|
+
// `getContextId` reads `clark.context.jwt` for the userId, but the
|
|
1606
|
+
// JWT is normally only stored into clark context by the
|
|
1607
|
+
// AgentPlanning transition further down the flow — which we never
|
|
1608
|
+
// reach on the pre-call paywall path. Always overwrite from
|
|
1609
|
+
// `request.jwt` (the authoritative source for THIS turn) rather
|
|
1610
|
+
// than reusing whatever happens to be in clark.context.jwt — a
|
|
1611
|
+
// stale token left over from a prior peer/reconnect would resolve
|
|
1612
|
+
// to the wrong userId and write this user's prompt into another
|
|
1613
|
+
// user's ContextV2 history.
|
|
1614
|
+
if (request.jwt) {
|
|
1615
|
+
this.clark.updateContext({ jwt: request.jwt });
|
|
1616
|
+
}
|
|
1617
|
+
const contextId = getContextId(this.clark, this.config);
|
|
1618
|
+
const llmConfig = request.llmConfig;
|
|
1619
|
+
if (contextId && llmConfig) {
|
|
1620
|
+
// Redact secrets from the prompt before adding to ContextV2.
|
|
1621
|
+
// The resume turn's safety pass uses `scope: "last"`, which only
|
|
1622
|
+
// covers the NEXT user message — so this replayed message is
|
|
1623
|
+
// outside the normal redaction window. Without this redact-on-
|
|
1624
|
+
// persist step, a user pasting a token into a quota-denied
|
|
1625
|
+
// prompt would still forward that token to the model provider
|
|
1626
|
+
// on resume. Mirrors the same scanContentForSecrets +
|
|
1627
|
+
// SecretRedactor pipeline `llm/client.ts:streamText` runs for
|
|
1628
|
+
// organic user prompts. Fail-open on scan errors (same as
|
|
1629
|
+
// mainline) so a flaky scanner doesn't break the resume path —
|
|
1630
|
+
// logged so we can audit if this ever fires in prod.
|
|
1631
|
+
let promptForContext = request.prompt;
|
|
1632
|
+
try {
|
|
1633
|
+
const scanResult = await scanContentForSecrets(request.prompt);
|
|
1634
|
+
if (scanResult.success && scanResult.hasSecrets) {
|
|
1635
|
+
this.getLogger().warn(`[secret-scan] Redacting ${scanResult.findings.length} secret(s) from pre-call paywall prompt before ContextV2 persist`, {
|
|
1636
|
+
findingCount: scanResult.findings.length,
|
|
1637
|
+
hasVerified: scanResult.hasVerifiedSecrets,
|
|
1638
|
+
detectorTypes: scanResult.findings.map((f) => f.detectorType),
|
|
1639
|
+
});
|
|
1640
|
+
const redactor = new SecretRedactor(scanResult.findings);
|
|
1641
|
+
promptForContext = redactor.redact(request.prompt);
|
|
1642
|
+
}
|
|
1643
|
+
else if (!scanResult.success) {
|
|
1644
|
+
// Do NOT log `scanResult.error` directly — it can contain
|
|
1645
|
+
// raw TruffleHog stdout/stderr payloads, which on some
|
|
1646
|
+
// failure paths include excerpts of the scanned prompt
|
|
1647
|
+
// (i.e. unredacted user secret material). Log only that the
|
|
1648
|
+
// scan failed; operators can correlate via timestamp +
|
|
1649
|
+
// peerId. (APPS-4080 security review)
|
|
1650
|
+
this.getLogger().warn("[secret-scan] Secret scan failed on pre-call paywall prompt; persisting unredacted (fail-open, matches mainline)");
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
catch (scanErr) {
|
|
1654
|
+
// Same reason — don't pipe raw scanner error meta to logs.
|
|
1655
|
+
// `getErrorMeta` extracts message/stack which can include
|
|
1656
|
+
// unredacted prompt content from the scanner subprocess.
|
|
1657
|
+
this.getLogger().warn("[secret-scan] Secret scan threw on pre-call paywall prompt; persisting unredacted (fail-open, matches mainline)", {
|
|
1658
|
+
errorKind: scanErr instanceof Error ? scanErr.name : typeof scanErr,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
const ctx = await this.contextManagerV2.getOrCreateContext(contextId, llmConfig.contextOptionsV2);
|
|
1662
|
+
await ctx.addUserMessage({
|
|
1663
|
+
role: "user",
|
|
1664
|
+
content: promptForContext,
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
catch (err) {
|
|
1669
|
+
this.getLogger().warn("[ai-service] Failed to add pre-call paywall user prompt to ContextV2 (chat_message persistence unaffected)", getErrorMeta(err));
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1237
1673
|
async handleAiGenerate(request) {
|
|
1238
1674
|
this.getLogger().info(`[ai-service] handleAiGenerate: peerId=${this.stablePeer.peerId ?? "unknown"}`);
|
|
1239
1675
|
if (this.clark.state === ClarkStateNames.Dead) {
|
|
@@ -1244,11 +1680,24 @@ export class AiService extends TracedEventEmitter {
|
|
|
1244
1680
|
this.getLogger().info("[ai-service] Ignoring duplicate aiGenerate re-send", { requestId: request.requestId });
|
|
1245
1681
|
return;
|
|
1246
1682
|
}
|
|
1683
|
+
this.assertNoActivePolicyGateRun();
|
|
1247
1684
|
if (this.isBusy()) {
|
|
1248
1685
|
this.getLogger().warn("[ai-service] Service is busy. State:", this.clark.state);
|
|
1249
1686
|
throw new Error("Service is busy");
|
|
1250
1687
|
}
|
|
1251
|
-
|
|
1688
|
+
// Pre-call quota check. When denied, `ensureAiQuotaAvailable` throws so
|
|
1689
|
+
// the UI gets a paywall reason and opens the modal. Without intervention
|
|
1690
|
+
// the throw means we never reach `transitionTo<$AgentPlanning>` below,
|
|
1691
|
+
// so `agent-planning.ts:USER_SENT_PROMPT` never records the user prompt
|
|
1692
|
+
// and `idle.ts:AGENT_CANCELED` never persists the credit advice — the UI
|
|
1693
|
+
// ends up with the optimistic user message removed (thunks.ts catch path)
|
|
1694
|
+
// and no record of what the user tried. Catch it here, persist prompt +
|
|
1695
|
+
// advice + open modal via the same `aiSetGenerationState(CANCELLED,
|
|
1696
|
+
// paywallReason)` saga path the mid-stream paywall uses, then resolve
|
|
1697
|
+
// the socket call so the UI's catch doesn't run. (APPS-4080)
|
|
1698
|
+
if (await this.tryHandlePaywallForRequest(request)) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1252
1701
|
if (request.requestId &&
|
|
1253
1702
|
!this.requestDeduplicator.markIfNew(request.requestId)) {
|
|
1254
1703
|
this.getLogger().warn("[ai-service] Ignoring duplicate aiGenerate re-send (lost dedup race)", { requestId: request.requestId });
|
|
@@ -1278,6 +1727,11 @@ export class AiService extends TracedEventEmitter {
|
|
|
1278
1727
|
return {
|
|
1279
1728
|
...context,
|
|
1280
1729
|
promptId,
|
|
1730
|
+
// Third-party import turns are billed as app_migration; everything
|
|
1731
|
+
// else falls back to the default in-app Clark generation metadata.
|
|
1732
|
+
usageMetadata: request.importSource
|
|
1733
|
+
? { origin: "app_migration", source: request.importSource }
|
|
1734
|
+
: undefined,
|
|
1281
1735
|
useMockGeneration: request.mockGeneration ?? false,
|
|
1282
1736
|
planContext: mergePlanContexts(context.planContext, request.planContext),
|
|
1283
1737
|
browserContext: request.browserContext,
|
|
@@ -1372,7 +1826,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1372
1826
|
promptId,
|
|
1373
1827
|
applicationId: this.config.applicationId,
|
|
1374
1828
|
});
|
|
1375
|
-
const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
|
|
1829
|
+
const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
|
|
1376
1830
|
prompt_id: promptId,
|
|
1377
1831
|
application_id: this.config.applicationId,
|
|
1378
1832
|
session_id: this.sessionId,
|
|
@@ -1539,7 +1993,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1539
1993
|
const llmProviderSettings = this.getLLMProviderSettings(llmConfig);
|
|
1540
1994
|
const promptId = randomUUID();
|
|
1541
1995
|
getLogger().info("[ai-service] Generated prompt_id for summarize api usage", { promptId, applicationId: this.config.applicationId });
|
|
1542
|
-
const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
|
|
1996
|
+
const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
|
|
1543
1997
|
prompt_id: promptId,
|
|
1544
1998
|
application_id: this.config.applicationId,
|
|
1545
1999
|
session_id: this.sessionId,
|
|
@@ -1655,7 +2109,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1655
2109
|
promptId,
|
|
1656
2110
|
applicationId: this.config.applicationId,
|
|
1657
2111
|
});
|
|
1658
|
-
const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
|
|
2112
|
+
const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
|
|
1659
2113
|
prompt_id: promptId,
|
|
1660
2114
|
application_id: this.config.applicationId,
|
|
1661
2115
|
session_id: this.sessionId,
|
|
@@ -1749,7 +2203,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1749
2203
|
promptId,
|
|
1750
2204
|
applicationId: this.config.applicationId,
|
|
1751
2205
|
});
|
|
1752
|
-
const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
|
|
2206
|
+
const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
|
|
1753
2207
|
prompt_id: promptId,
|
|
1754
2208
|
application_id: this.config.applicationId,
|
|
1755
2209
|
session_id: this.sessionId,
|
|
@@ -1920,9 +2374,52 @@ export class AiService extends TracedEventEmitter {
|
|
|
1920
2374
|
this.clark.updateContext((context) => ({
|
|
1921
2375
|
...context,
|
|
1922
2376
|
promptId: undefined,
|
|
2377
|
+
usageMetadata: undefined,
|
|
1923
2378
|
}));
|
|
1924
2379
|
void this.clark.send({ type: USER_CANCELED });
|
|
1925
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* UI-initiated cancellation of a background task. Delegates to
|
|
2383
|
+
* `TaskStore.cancel`, which aborts the local poll loop and best-effort
|
|
2384
|
+
* invokes the task type's `cancel` hook on the external system. The
|
|
2385
|
+
* status listener wired in the constructor pushes the resulting
|
|
2386
|
+
* `cancelled` summary to the UI; this method just returns whether a
|
|
2387
|
+
* transition actually happened so the thunk can short-circuit retries.
|
|
2388
|
+
*
|
|
2389
|
+
* `callerUserId` is the JWT-derived identity of the RPC caller (the
|
|
2390
|
+
* socket handler extracts it from `peerAuthorization`). When the
|
|
2391
|
+
* target task has a recorded `ownerUserId` that does not match, we
|
|
2392
|
+
* return the same `{ cancelled: false }` no-op as the unknown /
|
|
2393
|
+
* already-terminal cases. This is intentional: surfacing a distinct
|
|
2394
|
+
* "not your task" error would let a malicious caller probe the
|
|
2395
|
+
* existence of other users' task IDs. See PR #19718 review feedback.
|
|
2396
|
+
*
|
|
2397
|
+
* Returns `{ cancelled: false }` when:
|
|
2398
|
+
* - the background-tasks framework is disabled
|
|
2399
|
+
* - the task is unknown or already terminal
|
|
2400
|
+
* - the caller is not the task's owner
|
|
2401
|
+
*/
|
|
2402
|
+
async cancelBackgroundTask(taskId, callerUserId) {
|
|
2403
|
+
if (!this.taskStore) {
|
|
2404
|
+
this.getLogger().warn(`[ai-service] cancelBackgroundTask called but background-tasks framework is disabled; taskId=${taskId}`);
|
|
2405
|
+
return { cancelled: false };
|
|
2406
|
+
}
|
|
2407
|
+
const before = this.taskStore.get(taskId);
|
|
2408
|
+
if (!before || before.state !== "running") {
|
|
2409
|
+
return { cancelled: false };
|
|
2410
|
+
}
|
|
2411
|
+
if (!isTaskAccessibleTo(before, callerUserId)) {
|
|
2412
|
+
// Opaque deny: same shape as unknown/terminal so callers cannot
|
|
2413
|
+
// distinguish "your task is not running" from "this is not your
|
|
2414
|
+
// task". Log at warn so cross-user cancellation attempts are
|
|
2415
|
+
// observable in ops without leaking to the client.
|
|
2416
|
+
this.getLogger().warn(`[ai-service] cancelBackgroundTask denied: cross-owner attempt; taskId=${taskId}`);
|
|
2417
|
+
return { cancelled: false };
|
|
2418
|
+
}
|
|
2419
|
+
await this.taskStore.cancel(taskId);
|
|
2420
|
+
const after = this.taskStore.get(taskId);
|
|
2421
|
+
return { cancelled: after?.state === "cancelled" };
|
|
2422
|
+
}
|
|
1926
2423
|
/**
|
|
1927
2424
|
* Submit a prompt with interrupt behavior.
|
|
1928
2425
|
* If not busy, starts immediately. Otherwise interrupts and continues.
|
|
@@ -1939,6 +2436,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
1939
2436
|
this.getLogger().info("[ai-service] Ignoring duplicate aiGenerateWithQueue re-send", { requestId: request.requestId, mode });
|
|
1940
2437
|
return { status: "started" };
|
|
1941
2438
|
}
|
|
2439
|
+
this.assertNoActivePolicyGateRun();
|
|
1942
2440
|
if (!this.isBusy()) {
|
|
1943
2441
|
await this.handleAiGenerate(request);
|
|
1944
2442
|
return { status: "started" };
|
|
@@ -1951,7 +2449,15 @@ export class AiService extends TracedEventEmitter {
|
|
|
1951
2449
|
* The partial response is preserved in chat history for context.
|
|
1952
2450
|
*/
|
|
1953
2451
|
async interruptAndContinue(request) {
|
|
1954
|
-
|
|
2452
|
+
// The user interrupted — abort the in-flight generation FIRST, before
|
|
2453
|
+
// anything else. The caller (UI saga) signals `interrupted` immediately
|
|
2454
|
+
// on this method's return, so the old run must actually be dead by
|
|
2455
|
+
// then. An earlier version of this method checked quota before
|
|
2456
|
+
// aborting, which let the old run keep producing tokens while the
|
|
2457
|
+
// paywall UI was rendering — racing the paywall state on the wire.
|
|
2458
|
+
// The right shape: abort -> wait -> install fresh AbortController ->
|
|
2459
|
+
// persist any partial -> then decide whether to continue with a new
|
|
2460
|
+
// generation (or paywall the new prompt). (APPS-4080)
|
|
1955
2461
|
const partialResponse = this.clark.context.partialResponse?.text ?? "";
|
|
1956
2462
|
const preservedContext = {
|
|
1957
2463
|
jwt: this.clark.context.jwt,
|
|
@@ -1962,14 +2468,19 @@ export class AiService extends TracedEventEmitter {
|
|
|
1962
2468
|
this.clark.updateContext({ isInterruptAbort: true });
|
|
1963
2469
|
this.clark.context.abortController?.abort();
|
|
1964
2470
|
await this.waitForAbortComplete();
|
|
2471
|
+
// Note: `interruptedPreviousGeneration` and `interruptOriginalPrompt`
|
|
2472
|
+
// are NOT set yet — they're set below, AFTER the paywall check, so
|
|
2473
|
+
// they don't survive into the next turn on a paywall return. If we
|
|
2474
|
+
// set them here and then the paywall path returned, `agent-planning.ts`
|
|
2475
|
+
// would preferentially use the stale `interruptOriginalPrompt` over
|
|
2476
|
+
// the user's actual follow-up message on the resume turn (it has a
|
|
2477
|
+
// `clark.context.interruptOriginalPrompt ?? request.prompt` fallback).
|
|
1965
2478
|
this.clark.updateContext({
|
|
1966
2479
|
jwt: preservedContext.jwt,
|
|
1967
2480
|
llmConfig: preservedContext.llmConfig,
|
|
1968
2481
|
availableIntegrations: preservedContext.availableIntegrations,
|
|
1969
2482
|
abortController: new AbortController(),
|
|
1970
2483
|
partialResponse: undefined,
|
|
1971
|
-
interruptedPreviousGeneration: true,
|
|
1972
|
-
interruptOriginalPrompt: request.prompt,
|
|
1973
2484
|
});
|
|
1974
2485
|
if (partialResponse.trim().length > 10) {
|
|
1975
2486
|
await this.chatSessionStore.recordAssistant({
|
|
@@ -1978,6 +2489,24 @@ export class AiService extends TracedEventEmitter {
|
|
|
1978
2489
|
type: "text",
|
|
1979
2490
|
});
|
|
1980
2491
|
}
|
|
2492
|
+
// Now that the old run is reliably dead, check quota for the new
|
|
2493
|
+
// prompt. If paywalled, persist + open modal + return — same flow as
|
|
2494
|
+
// `handleAiGenerate`. The aborted run is NOT resumed; the user can
|
|
2495
|
+
// start fresh once credits are restored via the same "send any
|
|
2496
|
+
// message" affordance the standard paywall uses.
|
|
2497
|
+
if (await this.tryHandlePaywallForRequest(request)) {
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
// Only set the interrupt-state fields once we're committed to calling
|
|
2501
|
+
// `handleAiGenerate` below — `agent-planning.ts` consumes
|
|
2502
|
+
// `interruptOriginalPrompt` on the immediately-next `USER_SENT_PROMPT`
|
|
2503
|
+
// and clears it. Setting them only on this happy path keeps them from
|
|
2504
|
+
// leaking into a future unrelated turn if anything between here and
|
|
2505
|
+
// the LLM call throws.
|
|
2506
|
+
this.clark.updateContext({
|
|
2507
|
+
interruptedPreviousGeneration: true,
|
|
2508
|
+
interruptOriginalPrompt: request.prompt,
|
|
2509
|
+
});
|
|
1981
2510
|
const enhancedPrompt = `[The user interrupted the previous generation to say this. It may be an update/correction to what you were doing, or a new direction entirely - use your judgment based on the message:]\n\n${request.prompt}`;
|
|
1982
2511
|
const enhancedRequest = {
|
|
1983
2512
|
...request,
|
|
@@ -2372,6 +2901,17 @@ export class AiService extends TracedEventEmitter {
|
|
|
2372
2901
|
// disconnected flush in order as part of setInner.
|
|
2373
2902
|
this.stablePeer.setInner({ peer, peerId });
|
|
2374
2903
|
this.getLogger().info(`[ai-service] Peer set: ${peerId}`);
|
|
2904
|
+
// A replacement (or freshly reconnecting) peer must re-prove its
|
|
2905
|
+
// identity via `notifyAuthenticatedRpc` before live task pushes
|
|
2906
|
+
// trust it. Drop any identity carried over from the peer we just
|
|
2907
|
+
// replaced so the bound peerId and the authenticated identity can't
|
|
2908
|
+
// disagree — otherwise a peer that binds before the previous peer's
|
|
2909
|
+
// disconnect is processed would receive the previous user's task
|
|
2910
|
+
// pushes (PR #19718 Comment 59).
|
|
2911
|
+
if (this.authenticatedPeerId !== peerId) {
|
|
2912
|
+
this.authenticatedPeerId = undefined;
|
|
2913
|
+
this.authenticatedPeerUserId = undefined;
|
|
2914
|
+
}
|
|
2375
2915
|
// 1. Sync generation state if generation is in progress
|
|
2376
2916
|
if (this.clark.context.lastGenerationState) {
|
|
2377
2917
|
this.getLogger().info(`[ai-service] Syncing generation state on reconnect: ${this.clark.context.lastGenerationState.state}`);
|
|
@@ -2415,6 +2955,84 @@ export class AiService extends TracedEventEmitter {
|
|
|
2415
2955
|
this.getLogger().warn(`[ai-service] Failed to clear stale tool permission on reconnect`, error);
|
|
2416
2956
|
});
|
|
2417
2957
|
}
|
|
2958
|
+
// 3. Defer the background-task summary resync until the peer
|
|
2959
|
+
// proves auth via its first authenticated RPC. The socket
|
|
2960
|
+
// upgrade path only validates `applicationId`; auth is enforced
|
|
2961
|
+
// by RPC middleware, which hasn't run yet at connect time. An
|
|
2962
|
+
// unauthenticated client that manages to open the socket would
|
|
2963
|
+
// otherwise receive the active task list. The resync drains
|
|
2964
|
+
// inside `notifyAuthenticatedRpc(peerId)`, called by the
|
|
2965
|
+
// auth middleware (see `socket-manager.ts`) on first success.
|
|
2966
|
+
if (this.taskStore) {
|
|
2967
|
+
this.pendingTaskResyncPeerIds.add(peerId);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Per-session set of peer IDs that connected since their last task
|
|
2972
|
+
* resync drained. Populated by `handleUserConnected`, drained inside
|
|
2973
|
+
* `notifyAuthenticatedRpc` exactly once per (connect, first auth
|
|
2974
|
+
* success) pair — so a fresh tab gets the active task list as soon
|
|
2975
|
+
* as auth is proven, but never before.
|
|
2976
|
+
*/
|
|
2977
|
+
pendingTaskResyncPeerIds = new Set();
|
|
2978
|
+
/**
|
|
2979
|
+
* The authenticated peer's identity, but only while that same peer is
|
|
2980
|
+
* still the bound `stablePeer`. `handleUserConnected` can swap in a
|
|
2981
|
+
* replacement peer before the previous peer's disconnect is processed;
|
|
2982
|
+
* keying the identity to its peerId means a live status push in that
|
|
2983
|
+
* window is gated against `undefined` (suppressed) rather than the
|
|
2984
|
+
* previous user — closing the cross-user leak in PR #19718 Comment 59.
|
|
2985
|
+
* Returns `undefined` until the currently-bound peer proves auth via
|
|
2986
|
+
* `notifyAuthenticatedRpc`, so owned-task pushes fail closed.
|
|
2987
|
+
*/
|
|
2988
|
+
getAuthenticatedPeerUserId() {
|
|
2989
|
+
return this.authenticatedPeerId === this.stablePeer.peerId
|
|
2990
|
+
? this.authenticatedPeerUserId
|
|
2991
|
+
: undefined;
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Hook for the socket-manager auth middleware to call after every
|
|
2995
|
+
* successful authorization check. The first call per peer drains any
|
|
2996
|
+
* deferred state pushes registered by `handleUserConnected` (today:
|
|
2997
|
+
* the active background-task summary resync). Subsequent calls for
|
|
2998
|
+
* the same connection are no-ops; the set is repopulated on the
|
|
2999
|
+
* next connect/reconnect.
|
|
3000
|
+
*
|
|
3001
|
+
* `callerUserId` is the JWT-derived identity of the just-authenticated
|
|
3002
|
+
* RPC caller. The resync filters by `isTaskAccessibleTo(task, callerUserId)`
|
|
3003
|
+
* so user B reconnecting to an AiService that's running user A's task
|
|
3004
|
+
* does not receive A's `taskId` / label / message. Tasks with no
|
|
3005
|
+
* recorded `ownerUserId` (legacy/local-dev) remain visible to anyone,
|
|
3006
|
+
* matching the LLM-facing `listTasks` semantics.
|
|
3007
|
+
*
|
|
3008
|
+
* NOTE: The legacy `aiSetGenerationState` / tool-permission pushes
|
|
3009
|
+
* in `handleUserConnected` have the same pre-auth exposure but are
|
|
3010
|
+
* out of scope for this gate — they were not introduced by the
|
|
3011
|
+
* background-task framework. Move them here when tightening that
|
|
3012
|
+
* threat surface (see PR #19718 review feedback).
|
|
3013
|
+
*/
|
|
3014
|
+
notifyAuthenticatedRpc(peerId, callerUserId) {
|
|
3015
|
+
// Track the authenticated peer's identity (keyed to its peerId) so
|
|
3016
|
+
// `wireTaskStatusPushToPeer` can owner-filter live status
|
|
3017
|
+
// transitions (Comment 57, PR #19718).
|
|
3018
|
+
if (this.stablePeer.peerId === peerId) {
|
|
3019
|
+
this.authenticatedPeerId = peerId;
|
|
3020
|
+
this.authenticatedPeerUserId = callerUserId;
|
|
3021
|
+
}
|
|
3022
|
+
if (!this.pendingTaskResyncPeerIds.has(peerId))
|
|
3023
|
+
return;
|
|
3024
|
+
// Delete before invoking the push so concurrent middleware calls
|
|
3025
|
+
// for the same peer don't double-fire.
|
|
3026
|
+
this.pendingTaskResyncPeerIds.delete(peerId);
|
|
3027
|
+
// Defense in depth: only push for the currently-bound peer. A
|
|
3028
|
+
// late auth notification for a peer that's already been replaced
|
|
3029
|
+
// shouldn't leak state to the now-current peer's history.
|
|
3030
|
+
if (this.taskStore && this.stablePeer.peerId === peerId) {
|
|
3031
|
+
resyncActiveTaskStatusesToPeer(this.taskStore, {
|
|
3032
|
+
getPeer: () => this.stablePeer,
|
|
3033
|
+
logger: this.getLogger(),
|
|
3034
|
+
}, callerUserId);
|
|
3035
|
+
}
|
|
2418
3036
|
}
|
|
2419
3037
|
handleUserDisconnected(peerId) {
|
|
2420
3038
|
const currentPeerId = this.stablePeer.peerId;
|
|
@@ -2427,6 +3045,13 @@ export class AiService extends TracedEventEmitter {
|
|
|
2427
3045
|
return; // Don't clear the current peer on stale disconnect
|
|
2428
3046
|
}
|
|
2429
3047
|
this.getLogger().info(`[ai-service] peer disconnecting: ${peerId}`);
|
|
3048
|
+
// Drop any deferred state-push registration for this peer so a
|
|
3049
|
+
// disconnect that races a never-arrived auth proof doesn't leave
|
|
3050
|
+
// the entry around forever (the auth middleware never gets to
|
|
3051
|
+
// drain it on a dead connection).
|
|
3052
|
+
this.pendingTaskResyncPeerIds.delete(peerId);
|
|
3053
|
+
this.authenticatedPeerUserId = undefined;
|
|
3054
|
+
this.authenticatedPeerId = undefined;
|
|
2430
3055
|
const contextId = this.getContextId();
|
|
2431
3056
|
if (contextId) {
|
|
2432
3057
|
this.contextManagerV2.clearContext(contextId);
|
|
@@ -2617,10 +3242,6 @@ export class AiService extends TracedEventEmitter {
|
|
|
2617
3242
|
*/
|
|
2618
3243
|
async handleJudgeEvaluation(request) {
|
|
2619
3244
|
this.getLogger().info(`[ai-service] Judge evaluation requested for promptId=${request.promptId}, appId=${request.appId}`);
|
|
2620
|
-
// Temporarily set the JWT for this evaluation
|
|
2621
|
-
const previousJwt = this.tokenManagerJwt;
|
|
2622
|
-
this.tokenManagerJwt = request.jwt;
|
|
2623
|
-
this.getLogger().info(`[ai-service] Set tokenManagerJwt for judge evaluation. hasJwt=${!!request.jwt}, jwtLength=${request.jwt?.length || 0}`);
|
|
2624
3245
|
// Judge storage location is configurable via JUDGE_STORAGE_PATH env var
|
|
2625
3246
|
// Supports both directory paths (writes to evaluations.csv) and full file paths
|
|
2626
3247
|
// Default: <appRoot>/.superblocks/judge-evaluations/evaluations.csv
|
|
@@ -2651,7 +3272,7 @@ export class AiService extends TracedEventEmitter {
|
|
|
2651
3272
|
this.config.logger.info(`[ai-service] Judge storage configured: ${path.join(storageDir, storageFilename)}`);
|
|
2652
3273
|
// Create LLM provider for judge with JWT getter
|
|
2653
3274
|
// Use getLLMProviderSettings to ensure proper configuration (including upstreamProvider)
|
|
2654
|
-
const judgeLlmProvider = createLLMProvider(this.getLLMProviderSettings(request.llmConfig), () => this.
|
|
3275
|
+
const judgeLlmProvider = createLLMProvider(this.getLLMProviderSettings(request.llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "judge evaluation"), request.llmConfig, undefined);
|
|
2655
3276
|
const judgeService = new JudgeService({
|
|
2656
3277
|
llmClient: this.llmClient,
|
|
2657
3278
|
llmProvider: judgeLlmProvider,
|
|
@@ -2692,10 +3313,6 @@ export class AiService extends TracedEventEmitter {
|
|
|
2692
3313
|
this.getLogger().error(`[ai-service] Judge evaluation failed: ${String(error)}`, getErrorMeta(error));
|
|
2693
3314
|
throw error;
|
|
2694
3315
|
}
|
|
2695
|
-
finally {
|
|
2696
|
-
// Restore previous JWT
|
|
2697
|
-
this.tokenManagerJwt = previousJwt;
|
|
2698
|
-
}
|
|
2699
3316
|
}
|
|
2700
3317
|
async handlePolicyGateExecution(request) {
|
|
2701
3318
|
const logger = this.getLogger();
|
|
@@ -2703,9 +3320,10 @@ export class AiService extends TracedEventEmitter {
|
|
|
2703
3320
|
const requestLlmConfig = request.llmConfig;
|
|
2704
3321
|
const contextLlmConfig = this.clark.context.llmConfig;
|
|
2705
3322
|
const llmConfig = requestLlmConfig ?? contextLlmConfig;
|
|
2706
|
-
const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.
|
|
3323
|
+
const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "policy gate"), llmConfig, () => ({
|
|
2707
3324
|
ai_agent_run_id: request.aiAgentRunId,
|
|
2708
3325
|
application_id: request.applicationId,
|
|
3326
|
+
metadata: POLICY_GATE_USAGE_METADATA,
|
|
2709
3327
|
session_id: this.sessionId,
|
|
2710
3328
|
trigger_type: "policy_gate",
|
|
2711
3329
|
}));
|
|
@@ -2777,7 +3395,30 @@ export class AiService extends TracedEventEmitter {
|
|
|
2777
3395
|
tools,
|
|
2778
3396
|
stopWhen: stepCountIs(30),
|
|
2779
3397
|
});
|
|
3398
|
+
// Best-effort: surface the agent's latest tool/reasoning step in the editor
|
|
3399
|
+
// so a running policy row shows live activity instead of an opaque spinner.
|
|
3400
|
+
// Only meaningful when an editor session is connected; never blocks the run.
|
|
3401
|
+
const peer = this.clark.context.peer;
|
|
3402
|
+
const emitAgentStep = (part) => {
|
|
3403
|
+
if (!this.hasConnectedPeer() || !peer) {
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
const step = policyGateStreamPartToAgentStep(part);
|
|
3407
|
+
if (!step) {
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
void peer.call
|
|
3411
|
+
.aiPushPolicyAgentStep({
|
|
3412
|
+
applicationId: request.applicationId,
|
|
3413
|
+
policyId: request.reviewPolicyId,
|
|
3414
|
+
step,
|
|
3415
|
+
})
|
|
3416
|
+
.catch((error) => {
|
|
3417
|
+
logger.warn("[ai-service] Failed to push policy agent step", getErrorMeta(error));
|
|
3418
|
+
});
|
|
3419
|
+
};
|
|
2780
3420
|
for await (const part of result.fullStream) {
|
|
3421
|
+
emitAgentStep(part);
|
|
2781
3422
|
const partType = typeof part.type === "string"
|
|
2782
3423
|
? part.type
|
|
2783
3424
|
: "unknown";
|
|
@@ -2915,13 +3556,21 @@ export class AiService extends TracedEventEmitter {
|
|
|
2915
3556
|
pendingToolPermissionRequest: undefined,
|
|
2916
3557
|
});
|
|
2917
3558
|
}
|
|
3559
|
+
/** Clears local LLM context so migration-era memory does not survive abort. */
|
|
3560
|
+
async clearMigrationAbortLocalContext() {
|
|
3561
|
+
const contextId = getContextId(this.clark, this.config);
|
|
3562
|
+
await this.contextManagerV2.clearAllContexts();
|
|
3563
|
+
if (contextId) {
|
|
3564
|
+
await this.contextManagerV2.persistEmptyContext(contextId);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
2918
3567
|
/**
|
|
2919
3568
|
* Upload the (now-empty) context to Bucketeer.
|
|
2920
3569
|
* commitId is captured before newChat() clears the session.
|
|
2921
3570
|
* Failures are logged but do not fail clear chat.
|
|
2922
3571
|
*/
|
|
2923
3572
|
async uploadEmptyContextToBucketeer(userId, commitId) {
|
|
2924
|
-
const jwt = this.
|
|
3573
|
+
const jwt = this.getInferenceJwt();
|
|
2925
3574
|
const branchName = this._syncContextProvider?.getBranchName();
|
|
2926
3575
|
if (!jwt || !commitId || !branchName) {
|
|
2927
3576
|
return;
|