@vellumai/assistant 0.5.1 → 0.5.2
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/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +71 -8
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getApp,
|
|
5
|
+
getAppPreview,
|
|
6
|
+
resolveAppDir,
|
|
7
|
+
updateApp,
|
|
8
|
+
} from "../memory/app-store.js";
|
|
4
9
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
5
10
|
import { getLogger } from "../util/logger.js";
|
|
6
11
|
import { isPlainObject } from "../util/object.js";
|
|
@@ -187,6 +192,7 @@ export interface SurfaceConversationContext {
|
|
|
187
192
|
}
|
|
188
193
|
>;
|
|
189
194
|
surfaceUndoStacks: Map<string, string[]>;
|
|
195
|
+
accumulatedSurfaceState: Map<string, Record<string, unknown>>;
|
|
190
196
|
/** Request IDs that originated from surface action button clicks (not regular user messages). */
|
|
191
197
|
surfaceActionRequestIds: Set<string>;
|
|
192
198
|
currentTurnSurfaces: Array<{
|
|
@@ -346,6 +352,39 @@ function handleDocumentContentChanged(
|
|
|
346
352
|
}
|
|
347
353
|
}
|
|
348
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Handle state_update action from a dynamic page.
|
|
357
|
+
* Accumulates state via shallow merge without triggering an LLM turn.
|
|
358
|
+
*/
|
|
359
|
+
function handleStateUpdate(
|
|
360
|
+
ctx: SurfaceConversationContext,
|
|
361
|
+
surfaceId: string,
|
|
362
|
+
data?: Record<string, unknown>,
|
|
363
|
+
): void {
|
|
364
|
+
if (!data) {
|
|
365
|
+
log.debug({ surfaceId }, "state_update action called with no data");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const surfaceState = ctx.surfaceState.get(surfaceId);
|
|
370
|
+
if (!surfaceState || surfaceState.surfaceType !== "dynamic_page") {
|
|
371
|
+
log.warn(
|
|
372
|
+
{ surfaceId, surfaceType: surfaceState?.surfaceType },
|
|
373
|
+
"state_update action received for non-dynamic_page surface",
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
|
|
379
|
+
const merged = { ...existing, ...data };
|
|
380
|
+
ctx.accumulatedSurfaceState.set(surfaceId, merged);
|
|
381
|
+
|
|
382
|
+
log.debug(
|
|
383
|
+
{ surfaceId, accumulatedState: merged },
|
|
384
|
+
"Accumulated surface state updated",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
349
388
|
export function pushUndoState(
|
|
350
389
|
surfaceUndoStacks: Map<string, string[]>,
|
|
351
390
|
surfaceId: string,
|
|
@@ -543,17 +582,60 @@ export function handleSurfaceAction(
|
|
|
543
582
|
const pending = ctx.pendingSurfaceActions.get(surfaceId);
|
|
544
583
|
|
|
545
584
|
// When surfaces are restored from history (e.g. onboarding cards), there is
|
|
546
|
-
// no in-memory pendingSurfaceActions entry.
|
|
547
|
-
//
|
|
548
|
-
// so we can handle them without stored state.
|
|
585
|
+
// no in-memory pendingSurfaceActions entry. Handle non-terminal actions
|
|
586
|
+
// directly, and forward custom/relay actions to the LLM.
|
|
549
587
|
if (!pending) {
|
|
588
|
+
// Non-terminal actions don't need stored state — handle directly.
|
|
589
|
+
if (actionId === "selection_changed") {
|
|
590
|
+
log.debug(
|
|
591
|
+
{ surfaceId, data },
|
|
592
|
+
"Selection changed (history-restored, not forwarding)",
|
|
593
|
+
);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (actionId === "content_changed") {
|
|
597
|
+
log.debug(
|
|
598
|
+
{ surfaceId },
|
|
599
|
+
"Content changed (history-restored, no surface state — skipping)",
|
|
600
|
+
);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (actionId === "state_update") {
|
|
604
|
+
if (data) {
|
|
605
|
+
const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
|
|
606
|
+
ctx.accumulatedSurfaceState.set(surfaceId, { ...existing, ...data });
|
|
607
|
+
}
|
|
608
|
+
log.debug(
|
|
609
|
+
{ surfaceId, data },
|
|
610
|
+
"Silent state accumulated (history-restored)",
|
|
611
|
+
);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Determine message content from the action.
|
|
550
616
|
const isRelay = actionId === "relay_prompt" || actionId === "agent_prompt";
|
|
551
617
|
const prompt =
|
|
552
618
|
isRelay && typeof data?.prompt === "string" ? data.prompt.trim() : "";
|
|
553
619
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
620
|
+
let content: string;
|
|
621
|
+
let displayContent: string | undefined;
|
|
622
|
+
if (prompt) {
|
|
623
|
+
content = prompt;
|
|
624
|
+
} else {
|
|
625
|
+
// Custom action from an app (e.g. sendAction('answer_selected', {...}))
|
|
626
|
+
const summary = actionId
|
|
627
|
+
.replace(/_/g, " ")
|
|
628
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
629
|
+
content = `[User action on app: ${summary}]`;
|
|
630
|
+
if (data && Object.keys(data).length > 0) {
|
|
631
|
+
content += `\n\nAction data: ${JSON.stringify(data)}`;
|
|
632
|
+
}
|
|
633
|
+
const accState = ctx.accumulatedSurfaceState.get(surfaceId);
|
|
634
|
+
if (accState && Object.keys(accState).length > 0) {
|
|
635
|
+
content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
|
|
636
|
+
ctx.accumulatedSurfaceState.delete(surfaceId);
|
|
637
|
+
}
|
|
638
|
+
displayContent = summary;
|
|
557
639
|
}
|
|
558
640
|
|
|
559
641
|
const requestId = uuid();
|
|
@@ -567,11 +649,15 @@ export function handleSurfaceAction(
|
|
|
567
649
|
});
|
|
568
650
|
|
|
569
651
|
const result = ctx.enqueueMessage(
|
|
570
|
-
|
|
652
|
+
content,
|
|
571
653
|
[],
|
|
572
654
|
onEvent,
|
|
573
655
|
requestId,
|
|
574
656
|
surfaceId,
|
|
657
|
+
undefined,
|
|
658
|
+
undefined,
|
|
659
|
+
undefined,
|
|
660
|
+
displayContent,
|
|
575
661
|
);
|
|
576
662
|
|
|
577
663
|
if (result.rejected) {
|
|
@@ -581,16 +667,18 @@ export function handleSurfaceAction(
|
|
|
581
667
|
|
|
582
668
|
// Echo the prompt to the client so it appears in the chat UI.
|
|
583
669
|
// Deferred until after rejection check to avoid ghost messages.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
670
|
+
if (prompt) {
|
|
671
|
+
ctx.sendToClient({
|
|
672
|
+
type: "user_message_echo",
|
|
673
|
+
text: prompt,
|
|
674
|
+
conversationId: ctx.conversationId,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
589
677
|
|
|
590
678
|
if (result.queued) {
|
|
591
679
|
log.info(
|
|
592
680
|
{ surfaceId, actionId, requestId },
|
|
593
|
-
"
|
|
681
|
+
"Surface action queued (conversation busy, history-restored)",
|
|
594
682
|
);
|
|
595
683
|
return;
|
|
596
684
|
}
|
|
@@ -598,22 +686,31 @@ export function handleSurfaceAction(
|
|
|
598
686
|
// Conversation is idle — process the message immediately.
|
|
599
687
|
log.info(
|
|
600
688
|
{ surfaceId, actionId, requestId },
|
|
601
|
-
"Processing
|
|
689
|
+
"Processing surface action immediately (history-restored)",
|
|
602
690
|
);
|
|
603
691
|
ctx
|
|
604
|
-
.processMessage(
|
|
692
|
+
.processMessage(
|
|
693
|
+
content,
|
|
694
|
+
[],
|
|
695
|
+
onEvent,
|
|
696
|
+
requestId,
|
|
697
|
+
surfaceId,
|
|
698
|
+
undefined,
|
|
699
|
+
undefined,
|
|
700
|
+
displayContent,
|
|
701
|
+
)
|
|
605
702
|
.catch((err) => {
|
|
606
703
|
const message = err instanceof Error ? err.message : String(err);
|
|
607
704
|
log.error(
|
|
608
705
|
{ err, surfaceId, actionId },
|
|
609
|
-
"Failed to process history-restored
|
|
706
|
+
"Failed to process history-restored surface action",
|
|
610
707
|
);
|
|
611
708
|
onEvent(
|
|
612
709
|
buildConversationErrorMessage(ctx.conversationId, {
|
|
613
710
|
code: "CONVERSATION_PROCESSING_FAILED",
|
|
614
711
|
userMessage: `Something went wrong: ${message}`,
|
|
615
712
|
retryable: false,
|
|
616
|
-
debugDetails: `History-restored
|
|
713
|
+
debugDetails: `History-restored surface action processing failed: ${message}`,
|
|
617
714
|
errorCategory: "processing_failed",
|
|
618
715
|
}),
|
|
619
716
|
);
|
|
@@ -637,6 +734,14 @@ export function handleSurfaceAction(
|
|
|
637
734
|
handleDocumentContentChanged(ctx, surfaceId, data);
|
|
638
735
|
return;
|
|
639
736
|
}
|
|
737
|
+
|
|
738
|
+
// state_update is a silent accumulation action — merge data into accumulated
|
|
739
|
+
// state without triggering an LLM turn.
|
|
740
|
+
if (actionId === "state_update") {
|
|
741
|
+
handleStateUpdate(ctx, surfaceId, data);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
640
745
|
// Merge stored action-level data (from ui_show definition) with client-sent
|
|
641
746
|
// data. This is critical for relay_prompt buttons: the client only sends the
|
|
642
747
|
// actionId, but the prompt payload lives in the action definition's data.
|
|
@@ -694,6 +799,10 @@ export function handleSurfaceAction(
|
|
|
694
799
|
selectedIds,
|
|
695
800
|
);
|
|
696
801
|
}
|
|
802
|
+
const accumulatedState = ctx.accumulatedSurfaceState.get(surfaceId);
|
|
803
|
+
if (accumulatedState && Object.keys(accumulatedState).length > 0) {
|
|
804
|
+
fallbackContent += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
|
|
805
|
+
}
|
|
697
806
|
// When a relay_prompt button also carries selection data (e.g. list/table
|
|
698
807
|
// surface with a canned prompt + user-selected rows), append the selection
|
|
699
808
|
// context so the LLM sees both the prompt and the user's selections.
|
|
@@ -707,6 +816,11 @@ export function handleSurfaceAction(
|
|
|
707
816
|
);
|
|
708
817
|
}
|
|
709
818
|
}
|
|
819
|
+
// When prompt is truthy, fallbackContent (which includes accumulated state)
|
|
820
|
+
// is discarded. Re-append accumulated state so the LLM sees it.
|
|
821
|
+
if (prompt && accumulatedState && Object.keys(accumulatedState).length > 0) {
|
|
822
|
+
content += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
|
|
823
|
+
}
|
|
710
824
|
// Show the user plain-text instead of raw JSON action data.
|
|
711
825
|
const displayContent = prompt
|
|
712
826
|
? undefined
|
|
@@ -743,6 +857,12 @@ export function handleSurfaceAction(
|
|
|
743
857
|
return;
|
|
744
858
|
}
|
|
745
859
|
|
|
860
|
+
// One-shot: clear accumulated state now that the message has been accepted.
|
|
861
|
+
// Deferred until after rejection check so state is preserved for retry on rejection.
|
|
862
|
+
if (accumulatedState && Object.keys(accumulatedState).length > 0) {
|
|
863
|
+
ctx.accumulatedSurfaceState.delete(surfaceId);
|
|
864
|
+
}
|
|
865
|
+
|
|
746
866
|
// Echo the user's prompt to the client so it appears in the chat UI.
|
|
747
867
|
// Deferred until after rejection check to avoid ghost messages.
|
|
748
868
|
if (shouldRelayPrompt && prompt) {
|
|
@@ -811,7 +931,7 @@ export function handleSurfaceAction(
|
|
|
811
931
|
}
|
|
812
932
|
|
|
813
933
|
/**
|
|
814
|
-
* After an
|
|
934
|
+
* After an app_refresh, refresh any active surface that displays the updated app.
|
|
815
935
|
*/
|
|
816
936
|
export function refreshSurfacesForApp(
|
|
817
937
|
ctx: SurfaceConversationContext,
|
|
@@ -860,7 +980,7 @@ export function refreshSurfacesForApp(
|
|
|
860
980
|
refreshed = true;
|
|
861
981
|
log.info(
|
|
862
982
|
{ conversationId: ctx.conversationId, surfaceId, appId },
|
|
863
|
-
"Auto-refreshed surface after
|
|
983
|
+
"Auto-refreshed surface after app_refresh",
|
|
864
984
|
);
|
|
865
985
|
}
|
|
866
986
|
return refreshed;
|
|
@@ -1201,6 +1321,7 @@ export async function surfaceProxyResolver(
|
|
|
1201
1321
|
ctx.surfaceState.delete(surfaceId);
|
|
1202
1322
|
ctx.surfaceUndoStacks.delete(surfaceId);
|
|
1203
1323
|
ctx.lastSurfaceAction.delete(surfaceId);
|
|
1324
|
+
ctx.accumulatedSurfaceState.delete(surfaceId);
|
|
1204
1325
|
return {
|
|
1205
1326
|
content: lastAction ? "Surface completed" : "Surface dismissed",
|
|
1206
1327
|
isError: false,
|
|
@@ -1219,9 +1340,11 @@ export async function surfaceProxyResolver(
|
|
|
1219
1340
|
const defaultPreview = { title: app.name, subtitle: app.description };
|
|
1220
1341
|
|
|
1221
1342
|
const storedPreview = getAppPreview(app.id);
|
|
1343
|
+
const { dirName } = resolveAppDir(app.id);
|
|
1222
1344
|
const surfaceData: DynamicPageSurfaceData = {
|
|
1223
1345
|
html: app.htmlDefinition,
|
|
1224
1346
|
appId: app.id,
|
|
1347
|
+
dirName,
|
|
1225
1348
|
preview: {
|
|
1226
1349
|
...defaultPreview,
|
|
1227
1350
|
...preview,
|
|
@@ -560,8 +560,6 @@ export interface SkillProjectionContext {
|
|
|
560
560
|
};
|
|
561
561
|
/** True when no client is connected (HTTP-only). */
|
|
562
562
|
readonly hasNoClient?: boolean;
|
|
563
|
-
/** True when the conversation has user-uploaded attachments. */
|
|
564
|
-
hasAttachments?: boolean;
|
|
565
563
|
}
|
|
566
564
|
|
|
567
565
|
// ── Conditional tool sets ────────────────────────────────────────────
|
|
@@ -573,7 +571,6 @@ const HOST_TOOL_NAMES = new Set([
|
|
|
573
571
|
"host_file_edit",
|
|
574
572
|
"host_bash",
|
|
575
573
|
]);
|
|
576
|
-
const ASSET_TOOL_NAMES = new Set(["asset_search", "asset_materialize"]);
|
|
577
574
|
const CLIENT_CAPABILITY_TOOL_NAMES = new Set(["app_open"]);
|
|
578
575
|
const PLATFORM_TOOL_NAMES = new Set(["request_system_permission"]);
|
|
579
576
|
|
|
@@ -597,9 +594,6 @@ export function isToolActiveForContext(
|
|
|
597
594
|
// unchecked host command execution on the daemon host.
|
|
598
595
|
return !ctx.hasNoClient;
|
|
599
596
|
}
|
|
600
|
-
if (ASSET_TOOL_NAMES.has(name)) {
|
|
601
|
-
return ctx.hasAttachments ?? false;
|
|
602
|
-
}
|
|
603
597
|
if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
|
|
604
598
|
return !ctx.hasNoClient;
|
|
605
599
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { getConversation } from "../memory/conversation-crud.js";
|
|
4
|
+
import { resolveConversationDirectoryPaths } from "../memory/conversation-directories.js";
|
|
1
5
|
import { renderWorkspaceTopLevelContext } from "../workspace/top-level-renderer.js";
|
|
2
6
|
import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
|
|
3
7
|
|
|
@@ -5,6 +9,7 @@ import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
|
|
|
5
9
|
* Subset of Conversation state that workspace context helpers need.
|
|
6
10
|
*/
|
|
7
11
|
export interface WorkspaceConversationContext {
|
|
12
|
+
conversationId: string;
|
|
8
13
|
workingDir: string;
|
|
9
14
|
workspaceTopLevelContext: string | null;
|
|
10
15
|
workspaceTopLevelDirty: boolean;
|
|
@@ -17,6 +22,21 @@ export function refreshWorkspaceTopLevelContextIfNeeded(
|
|
|
17
22
|
if (!ctx.workspaceTopLevelDirty && ctx.workspaceTopLevelContext != null)
|
|
18
23
|
return;
|
|
19
24
|
const snapshot = scanTopLevelDirectories(ctx.workingDir);
|
|
20
|
-
|
|
25
|
+
const conversation = getConversation(ctx.conversationId);
|
|
26
|
+
let currentConversationPath: string | null = null;
|
|
27
|
+
if (conversation && typeof conversation.createdAt === "number") {
|
|
28
|
+
const { resolvedDirName } = resolveConversationDirectoryPaths(
|
|
29
|
+
conversation.id,
|
|
30
|
+
conversation.createdAt,
|
|
31
|
+
join(ctx.workingDir, "conversations"),
|
|
32
|
+
);
|
|
33
|
+
currentConversationPath = `conversations/${resolvedDirName}/`;
|
|
34
|
+
}
|
|
35
|
+
ctx.workspaceTopLevelContext = renderWorkspaceTopLevelContext(snapshot, {
|
|
36
|
+
currentConversationPath,
|
|
37
|
+
currentConversationAttachmentsPath: currentConversationPath
|
|
38
|
+
? `${currentConversationPath}attachments/`
|
|
39
|
+
: null,
|
|
40
|
+
});
|
|
21
41
|
ctx.workspaceTopLevelDirty = false;
|
|
22
42
|
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { registerToolTraceListener } from "../events/tool-trace-listener.js";
|
|
39
39
|
import { getHookManager } from "../hooks/manager.js";
|
|
40
40
|
import { resolveCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
|
|
41
|
+
import { getMessages } from "../memory/conversation-crud.js";
|
|
41
42
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
42
43
|
import { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
43
44
|
import { patternMatchesCandidate } from "../permissions/trust-store.js";
|
|
@@ -81,7 +82,6 @@ import type {
|
|
|
81
82
|
ChannelCapabilities,
|
|
82
83
|
TrustContext,
|
|
83
84
|
} from "./conversation-runtime-assembly.js";
|
|
84
|
-
import { messagesContainAttachments } from "./conversation-runtime-assembly.js";
|
|
85
85
|
import type { SkillProjectionCache } from "./conversation-skill-tools.js";
|
|
86
86
|
import {
|
|
87
87
|
createSurfaceMutex,
|
|
@@ -164,7 +164,6 @@ export class Conversation {
|
|
|
164
164
|
/** @internal */ contextCompactedAt: number | null = null;
|
|
165
165
|
/** @internal */ currentRequestId?: string;
|
|
166
166
|
/** @internal */ hasNoClient = false;
|
|
167
|
-
/** @internal */ hasAttachments = false;
|
|
168
167
|
/** @internal */ headlessLock = false;
|
|
169
168
|
/** @internal */ taskRunId?: string;
|
|
170
169
|
/** @internal */ callSessionId?: string;
|
|
@@ -200,6 +199,10 @@ export class Conversation {
|
|
|
200
199
|
{ surfaceType: SurfaceType; data: SurfaceData; title?: string }
|
|
201
200
|
>();
|
|
202
201
|
/** @internal */ surfaceUndoStacks = new Map<string, string[]>();
|
|
202
|
+
/** @internal */ accumulatedSurfaceState = new Map<
|
|
203
|
+
string,
|
|
204
|
+
Record<string, unknown>
|
|
205
|
+
>();
|
|
203
206
|
/** @internal */ withSurface = createSurfaceMutex();
|
|
204
207
|
/** @internal */ currentTurnSurfaces: Array<{
|
|
205
208
|
surfaceId: string;
|
|
@@ -379,13 +382,35 @@ export class Conversation {
|
|
|
379
382
|
|
|
380
383
|
async loadFromDb(): Promise<void> {
|
|
381
384
|
await loadFromDbImpl(this);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
this.restoreSurfaceStateFromHistory();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Scan ALL persisted messages (including compacted ones) for ui_surface
|
|
390
|
+
* content blocks and populate surfaceState so findConversationBySurfaceId
|
|
391
|
+
* works for surfaces restored from history (e.g. after daemon restart).
|
|
392
|
+
*/
|
|
393
|
+
private restoreSurfaceStateFromHistory(): void {
|
|
394
|
+
const dbMessages = getMessages(this.conversationId);
|
|
395
|
+
for (const row of dbMessages) {
|
|
396
|
+
try {
|
|
397
|
+
const content = JSON.parse(row.content);
|
|
398
|
+
if (!Array.isArray(content)) continue;
|
|
399
|
+
for (const block of content) {
|
|
400
|
+
if (
|
|
401
|
+
block.type === "ui_surface" &&
|
|
402
|
+
typeof block.surfaceId === "string"
|
|
403
|
+
) {
|
|
404
|
+
this.surfaceState.set(block.surfaceId, {
|
|
405
|
+
surfaceType: (block.surfaceType ?? "dynamic_page") as SurfaceType,
|
|
406
|
+
data: (block.data ?? {}) as SurfaceData,
|
|
407
|
+
title: block.title as string | undefined,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// Content isn't valid JSON — skip
|
|
413
|
+
}
|
|
389
414
|
}
|
|
390
415
|
}
|
|
391
416
|
|
|
@@ -606,6 +631,18 @@ export class Conversation {
|
|
|
606
631
|
"Resuming after approval",
|
|
607
632
|
);
|
|
608
633
|
|
|
634
|
+
// Sync the canonical guardian request status so stale "pending" DB
|
|
635
|
+
// records don't get matched by later guardian reply routing. Best-effort:
|
|
636
|
+
// CAS may harmlessly fail if the canonical decision primitive already
|
|
637
|
+
// resolved the request (e.g. channel approval path).
|
|
638
|
+
try {
|
|
639
|
+
resolveCanonicalGuardianRequest(requestId, "pending", {
|
|
640
|
+
status: resolvedState,
|
|
641
|
+
});
|
|
642
|
+
} catch {
|
|
643
|
+
// Canonical request tracking should not break the primary approval flow.
|
|
644
|
+
}
|
|
645
|
+
|
|
609
646
|
// Cascade to other pending confirmations that match this decision
|
|
610
647
|
this.cascadePendingApprovals(requestId, decision, selectedPattern);
|
|
611
648
|
}
|
|
@@ -648,10 +685,11 @@ export class Conversation {
|
|
|
648
685
|
// Consume from pending-interactions tracker
|
|
649
686
|
pendingInteractions.resolve(candidateId);
|
|
650
687
|
|
|
651
|
-
// Resolve via handleConfirmationResponse which emits events
|
|
652
|
-
// Use simple "allow"/"deny" so the
|
|
653
|
-
// duplicate rules or re-activate
|
|
654
|
-
// terminates because allow/deny exit
|
|
688
|
+
// Resolve via handleConfirmationResponse which emits events and
|
|
689
|
+
// syncs canonical status. Use simple "allow"/"deny" so the
|
|
690
|
+
// permission-checker won't save duplicate rules or re-activate
|
|
691
|
+
// temporary modes. Recursion terminates because allow/deny exit
|
|
692
|
+
// cascadePendingApprovals early.
|
|
655
693
|
this.handleConfirmationResponse(
|
|
656
694
|
candidateId,
|
|
657
695
|
cascadeResult.allow ? "allow" : "deny",
|
|
@@ -663,17 +701,6 @@ export class Conversation {
|
|
|
663
701
|
causedByRequestId: primaryRequestId,
|
|
664
702
|
},
|
|
665
703
|
);
|
|
666
|
-
|
|
667
|
-
// Sync the canonical guardian request status for the cascaded request.
|
|
668
|
-
// Best-effort: canonical request tracking should not break the cascade flow.
|
|
669
|
-
try {
|
|
670
|
-
const targetStatus = cascadeResult.allow ? "approved" : "denied";
|
|
671
|
-
resolveCanonicalGuardianRequest(candidateId, "pending", {
|
|
672
|
-
status: targetStatus,
|
|
673
|
-
});
|
|
674
|
-
} catch {
|
|
675
|
-
// Ignore — canonical request tracking is best-effort
|
|
676
|
-
}
|
|
677
704
|
}
|
|
678
705
|
}
|
|
679
706
|
|
|
@@ -880,11 +907,6 @@ export class Conversation {
|
|
|
880
907
|
if (!this.processing) {
|
|
881
908
|
await this.ensureActorScopedHistory();
|
|
882
909
|
}
|
|
883
|
-
// One-way flag: once an attachment arrives, asset tools stay available
|
|
884
|
-
// for the remainder of the conversation.
|
|
885
|
-
if (!this.hasAttachments && attachments.length > 0) {
|
|
886
|
-
this.hasAttachments = true;
|
|
887
|
-
}
|
|
888
910
|
return persistUserMessageImpl(
|
|
889
911
|
this,
|
|
890
912
|
content,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The canned assistant response for the wake-up greeting on a fresh workspace.
|
|
7
|
+
* Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
|
|
8
|
+
* time," "I'm ready to be useful," and "you're in control."
|
|
9
|
+
*/
|
|
10
|
+
export const CANNED_FIRST_GREETING =
|
|
11
|
+
"Hey. I'm brand new, no name, no memories, nothing yet. The more we work together, the more context and memory I build, and the better I get. But let's not wait around. Throw a question at me, give me a task, or ask what I can do.";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns `true` when all of the following are true:
|
|
15
|
+
* - `conversationMessageCount === 0` (no prior messages in this conversation)
|
|
16
|
+
* - BOOTSTRAP.md exists at the workspace prompt path
|
|
17
|
+
* - The trimmed content matches the macOS wake-up greeting (case-insensitive)
|
|
18
|
+
*/
|
|
19
|
+
export function isWakeUpGreeting(
|
|
20
|
+
content: string,
|
|
21
|
+
conversationMessageCount: number,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (conversationMessageCount !== 0) return false;
|
|
24
|
+
if (!existsSync(getWorkspacePromptPath("BOOTSTRAP.md"))) return false;
|
|
25
|
+
return content.trim().toLowerCase() === "wake up, my friend.";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the canned first-greeting string. Simple getter that exists to keep
|
|
30
|
+
* the call site consistent and allow future flexibility (e.g., locale-aware
|
|
31
|
+
* greetings) without changing the API.
|
|
32
|
+
*/
|
|
33
|
+
export function getCannedFirstGreeting(): string {
|
|
34
|
+
return CANNED_FIRST_GREETING;
|
|
35
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfig,
|
|
3
|
+
loadRawConfig,
|
|
4
|
+
saveRawConfig,
|
|
5
|
+
} from "../../config/loader.js";
|
|
6
|
+
import { setMemoryEmbeddingField } from "../../config/raw-config-utils.js";
|
|
7
|
+
import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
|
|
8
|
+
import {
|
|
9
|
+
clearEmbeddingBackendCache,
|
|
10
|
+
getMemoryBackendStatus,
|
|
11
|
+
} from "../../memory/embedding-backend.js";
|
|
12
|
+
import type { ModelSetContext } from "./config-model.js";
|
|
13
|
+
import { CONFIG_RELOAD_DEBOUNCE_MS, log } from "./shared.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Embedding provider catalog
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const EMBEDDING_PROVIDER_CATALOG = [
|
|
20
|
+
{
|
|
21
|
+
id: "auto",
|
|
22
|
+
displayName: "Auto (Best Available)",
|
|
23
|
+
defaultModel: "",
|
|
24
|
+
requiresKey: false,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "local",
|
|
28
|
+
displayName: "Local (In-Process)",
|
|
29
|
+
defaultModel: "Xenova/bge-small-en-v1.5",
|
|
30
|
+
requiresKey: false,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "openai",
|
|
34
|
+
displayName: "OpenAI",
|
|
35
|
+
defaultModel: "text-embedding-3-small",
|
|
36
|
+
requiresKey: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "gemini",
|
|
40
|
+
displayName: "Gemini",
|
|
41
|
+
defaultModel: "gemini-embedding-2-preview",
|
|
42
|
+
requiresKey: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "ollama",
|
|
46
|
+
displayName: "Ollama",
|
|
47
|
+
defaultModel: "nomic-embed-text",
|
|
48
|
+
requiresKey: false,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Provider-specific model field names
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const PROVIDER_MODEL_FIELD: Record<string, string> = {
|
|
57
|
+
local: "localModel",
|
|
58
|
+
openai: "openaiModel",
|
|
59
|
+
gemini: "geminiModel",
|
|
60
|
+
ollama: "ollamaModel",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// GET — return current embedding config + resolved status
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export async function getEmbeddingConfigInfo(): Promise<{
|
|
68
|
+
provider: string;
|
|
69
|
+
model: string | null;
|
|
70
|
+
activeProvider: string | null;
|
|
71
|
+
activeModel: string | null;
|
|
72
|
+
availableProviders: typeof EMBEDDING_PROVIDER_CATALOG;
|
|
73
|
+
status: { enabled: boolean; degraded: boolean; reason: string | null };
|
|
74
|
+
}> {
|
|
75
|
+
const config = getConfig();
|
|
76
|
+
const embeddingConfig = config.memory.embeddings;
|
|
77
|
+
const backendStatus = await getMemoryBackendStatus(config);
|
|
78
|
+
|
|
79
|
+
// Derive the provider-specific model from config
|
|
80
|
+
const fieldName = PROVIDER_MODEL_FIELD[embeddingConfig.provider];
|
|
81
|
+
const model = fieldName
|
|
82
|
+
? (embeddingConfig as Record<string, unknown>)[fieldName]
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
provider: embeddingConfig.provider,
|
|
87
|
+
model: typeof model === "string" ? model : null,
|
|
88
|
+
activeProvider: backendStatus.provider,
|
|
89
|
+
activeModel: backendStatus.model,
|
|
90
|
+
availableProviders: EMBEDDING_PROVIDER_CATALOG,
|
|
91
|
+
status: {
|
|
92
|
+
enabled: backendStatus.enabled,
|
|
93
|
+
degraded: backendStatus.degraded,
|
|
94
|
+
reason: backendStatus.reason,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// PUT — persist embedding provider/model to config
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export async function setEmbeddingConfig(
|
|
104
|
+
provider: string,
|
|
105
|
+
model: string | undefined,
|
|
106
|
+
ctx: ModelSetContext,
|
|
107
|
+
): Promise<ReturnType<typeof getEmbeddingConfigInfo>> {
|
|
108
|
+
const validProviders = new Set<string>(VALID_MEMORY_EMBEDDING_PROVIDERS);
|
|
109
|
+
if (!validProviders.has(provider)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Invalid embedding provider "${provider}". Valid providers: ${[...validProviders].join(", ")}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const raw = loadRawConfig();
|
|
116
|
+
setMemoryEmbeddingField(raw, "provider", provider);
|
|
117
|
+
|
|
118
|
+
if (model !== undefined) {
|
|
119
|
+
const fieldName = PROVIDER_MODEL_FIELD[provider];
|
|
120
|
+
if (fieldName) {
|
|
121
|
+
setMemoryEmbeddingField(raw, fieldName, model);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Suppress the file watcher callback — we handle the reload ourselves.
|
|
126
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
127
|
+
ctx.setSuppressConfigReload(true);
|
|
128
|
+
try {
|
|
129
|
+
saveRawConfig(raw);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
ctx.debounceTimers.schedule(
|
|
135
|
+
"__suppress_reset__",
|
|
136
|
+
() => {
|
|
137
|
+
ctx.setSuppressConfigReload(false);
|
|
138
|
+
},
|
|
139
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
clearEmbeddingBackendCache();
|
|
143
|
+
ctx.updateConfigFingerprint();
|
|
144
|
+
|
|
145
|
+
log.info({ provider, model }, "Embedding config updated");
|
|
146
|
+
|
|
147
|
+
return getEmbeddingConfigInfo();
|
|
148
|
+
}
|