@vellumai/assistant 0.11.3 → 0.11.4-staging.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/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../persistence/conversation-crud.js";
|
|
26
26
|
import type { Message } from "../providers/types.js";
|
|
27
27
|
import type { TrustContext } from "./trust-context-types.js";
|
|
28
|
+
import { restingTrust } from "./trust-context-types.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Subset of Conversation state that notifier callbacks need to read at
|
|
@@ -58,7 +59,14 @@ export function registerConversationNotifiers(
|
|
|
58
59
|
JSON.stringify([{ type: "text", text: questionText }]),
|
|
59
60
|
{
|
|
60
61
|
metadata: {
|
|
61
|
-
...provenanceFromTrustContext(
|
|
62
|
+
...provenanceFromTrustContext(
|
|
63
|
+
// This callback fires after the voice turn has settled: the
|
|
64
|
+
// per-turn field may still hold that turn's actor (nothing
|
|
65
|
+
// clears it at release), while voice cleanup has restored the
|
|
66
|
+
// conversation's resting trust. The owner is the right actor
|
|
67
|
+
// for a row persisted outside any turn.
|
|
68
|
+
restingTrust(ctx),
|
|
69
|
+
),
|
|
62
70
|
userMessageChannel: "phone",
|
|
63
71
|
assistantMessageChannel: "phone",
|
|
64
72
|
userMessageInterface: "phone",
|
|
@@ -63,6 +63,7 @@ import { preactivateHostProxySkills } from "./host-proxy-preactivation.js";
|
|
|
63
63
|
import type { UserMessageAttachment } from "./message-protocol.js";
|
|
64
64
|
import { buildTransportHints } from "./transport-hints.js";
|
|
65
65
|
import { sameTrustIdentity, type TrustContext } from "./trust-context-types.js";
|
|
66
|
+
import { turnOrRestingTrust } from "./trust-context-types.js";
|
|
66
67
|
import { resolveVerificationSessionIntent } from "./verification-session-intent.js";
|
|
67
68
|
|
|
68
69
|
const log = getLogger("conversation-process");
|
|
@@ -823,7 +824,7 @@ async function drainSingleMessage(
|
|
|
823
824
|
if (slashResult.kind === "unknown") {
|
|
824
825
|
try {
|
|
825
826
|
const drainProvenance = provenanceFromTrustContext(
|
|
826
|
-
conversation
|
|
827
|
+
turnOrRestingTrust(conversation),
|
|
827
828
|
);
|
|
828
829
|
const drainImageSourcePaths: Record<string, string> = {};
|
|
829
830
|
for (let i = 0; i < next.attachments.length; i++) {
|
|
@@ -938,7 +939,7 @@ async function drainSingleMessage(
|
|
|
938
939
|
let persistedCompactMessage = false;
|
|
939
940
|
try {
|
|
940
941
|
const drainProvenance = provenanceFromTrustContext(
|
|
941
|
-
conversation
|
|
942
|
+
turnOrRestingTrust(conversation),
|
|
942
943
|
);
|
|
943
944
|
const drainChannelMeta = {
|
|
944
945
|
...drainProvenance,
|
|
@@ -1031,7 +1032,7 @@ async function drainSingleMessage(
|
|
|
1031
1032
|
let persistedCleanMessage = false;
|
|
1032
1033
|
try {
|
|
1033
1034
|
const drainProvenance = provenanceFromTrustContext(
|
|
1034
|
-
conversation
|
|
1035
|
+
turnOrRestingTrust(conversation),
|
|
1035
1036
|
);
|
|
1036
1037
|
const drainChannelMeta = {
|
|
1037
1038
|
...drainProvenance,
|
|
@@ -2024,7 +2025,9 @@ export async function processMessage(
|
|
|
2024
2025
|
if (slashResult.kind === "unknown") {
|
|
2025
2026
|
const pmTurnCtx = conversation.getTurnChannelContext();
|
|
2026
2027
|
const pmInterfaceCtx = conversation.getTurnInterfaceContext();
|
|
2027
|
-
const pmProvenance = provenanceFromTrustContext(
|
|
2028
|
+
const pmProvenance = provenanceFromTrustContext(
|
|
2029
|
+
turnOrRestingTrust(conversation),
|
|
2030
|
+
);
|
|
2028
2031
|
const pmImageSourcePaths: Record<string, string> = {};
|
|
2029
2032
|
for (let i = 0; i < attachments.length; i++) {
|
|
2030
2033
|
const a = attachments[i];
|
|
@@ -2116,7 +2119,7 @@ export async function processMessage(
|
|
|
2116
2119
|
const pmTurnCtx = conversation.getTurnChannelContext();
|
|
2117
2120
|
const pmInterfaceCtx = conversation.getTurnInterfaceContext();
|
|
2118
2121
|
const pmProvenance = provenanceFromTrustContext(
|
|
2119
|
-
conversation
|
|
2122
|
+
turnOrRestingTrust(conversation),
|
|
2120
2123
|
);
|
|
2121
2124
|
const pmChannelMeta = {
|
|
2122
2125
|
...pmProvenance,
|
|
@@ -2197,7 +2200,7 @@ export async function processMessage(
|
|
|
2197
2200
|
const pmTurnCtx = conversation.getTurnChannelContext();
|
|
2198
2201
|
const pmInterfaceCtx = conversation.getTurnInterfaceContext();
|
|
2199
2202
|
const pmProvenance = provenanceFromTrustContext(
|
|
2200
|
-
conversation
|
|
2203
|
+
turnOrRestingTrust(conversation),
|
|
2201
2204
|
);
|
|
2202
2205
|
const pmChannelMeta = {
|
|
2203
2206
|
...pmProvenance,
|
|
@@ -2319,7 +2319,7 @@ export async function applyRuntimeInjections(
|
|
|
2319
2319
|
// folded into the summary are excluded.
|
|
2320
2320
|
const slackActiveThreadFocusBlock = channelCapabilities
|
|
2321
2321
|
? loadSlackActiveThreadFocusBlock(conversationId, channelCapabilities, {
|
|
2322
|
-
trustClass: liveConversation?.
|
|
2322
|
+
trustClass: liveConversation?.getTrustContext()?.trustClass,
|
|
2323
2323
|
contextCompactedMessageCount:
|
|
2324
2324
|
liveConversation?.contextCompactedMessageCount,
|
|
2325
2325
|
slackContextCompactionWatermarkTs:
|
|
@@ -2337,7 +2337,7 @@ export async function applyRuntimeInjections(
|
|
|
2337
2337
|
// boundary without the orchestrator threading a snapshot in.
|
|
2338
2338
|
const slackChronologicalMessages = channelCapabilities
|
|
2339
2339
|
? loadSlackChronologicalMessages(conversationId, channelCapabilities, {
|
|
2340
|
-
trustClass: liveConversation?.
|
|
2340
|
+
trustClass: liveConversation?.getTrustContext()?.trustClass,
|
|
2341
2341
|
contextSummary: liveConversation?.contextSummary,
|
|
2342
2342
|
contextCompactedMessageCount:
|
|
2343
2343
|
liveConversation?.contextCompactedMessageCount,
|
|
@@ -2380,8 +2380,7 @@ export async function applyRuntimeInjections(
|
|
|
2380
2380
|
turnIndex: options.turnIndex ?? liveConversation?.turnCount ?? 0,
|
|
2381
2381
|
trust:
|
|
2382
2382
|
options.trust ??
|
|
2383
|
-
liveConversation?.
|
|
2384
|
-
liveConversation?.trustContext ??
|
|
2383
|
+
liveConversation?.getTurnOrRestingTrust() ??
|
|
2385
2384
|
fallbackTurnTrust(channelCapabilities),
|
|
2386
2385
|
callSite: options.callSite ?? liveConversation?.currentCallSite,
|
|
2387
2386
|
...injectionInputs,
|
|
@@ -389,8 +389,7 @@ export function createToolExecutor(
|
|
|
389
389
|
// Per-turn trust snapshot: prefer the snapshot captured at turn start so
|
|
390
390
|
// a concurrent owner meta command (/status, /clean) that mutates the live
|
|
391
391
|
// trustContext cannot elevate the in-flight turn to guardian.
|
|
392
|
-
const turnTrust =
|
|
393
|
-
ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
|
|
392
|
+
const turnTrust = ctx.getTurnOrRestingTrust() ?? FALLBACK_TURN_TRUST;
|
|
394
393
|
|
|
395
394
|
const toolContext: ToolContext = {
|
|
396
395
|
workingDir: ctx.workingDir,
|
|
@@ -2488,6 +2488,54 @@ export class Conversation {
|
|
|
2488
2488
|
return this.authContext;
|
|
2489
2489
|
}
|
|
2490
2490
|
|
|
2491
|
+
/**
|
|
2492
|
+
* Trust the in-flight turn is executing under.
|
|
2493
|
+
*
|
|
2494
|
+
* Use this for authorization and for routing a reply to the requester:
|
|
2495
|
+
* cases where substituting the conversation's owner would be wrong rather
|
|
2496
|
+
* than approximate. Provenance is not such a case; see
|
|
2497
|
+
* {@link getTurnOrRestingTrust} and `docs/architecture/turn-actor.md`.
|
|
2498
|
+
*
|
|
2499
|
+
* `undefined` when no turn recorded one, which is a gap in the entry point
|
|
2500
|
+
* rather than an answer. Deliberately does not fall back to the
|
|
2501
|
+
* conversation's trust: a caller that can accept the conversation's owner
|
|
2502
|
+
* instead spells `?? getTrustContext()`, so the substitution is visible
|
|
2503
|
+
* where it happens.
|
|
2504
|
+
*/
|
|
2505
|
+
getTurnTrust(): TrustContext | undefined {
|
|
2506
|
+
return this.currentTurnTrustContext;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Trust of the actor the conversation belongs to, independent of any turn.
|
|
2511
|
+
*
|
|
2512
|
+
* Use this where there is no turn to speak of: routes reporting on a
|
|
2513
|
+
* conversation, hydration, and persisting conversation-level options. A
|
|
2514
|
+
* caller that wants the conversation's owner *rather than* whoever is
|
|
2515
|
+
* currently acting should be obviously doing so; if it is not obvious,
|
|
2516
|
+
* {@link getTurnTrust} is probably the one meant.
|
|
2517
|
+
*/
|
|
2518
|
+
getTrustContext(): TrustContext | undefined {
|
|
2519
|
+
return this.trustContext;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Trust of the in-flight turn, or the conversation's owner when the turn
|
|
2524
|
+
* recorded none. The substitution is in the name: callers that can accept
|
|
2525
|
+
* the owner as a stand-in ask this, including provenance stamping, whose
|
|
2526
|
+
* readers treat an absent trust class as more trusted than `"unknown"`.
|
|
2527
|
+
* Callers for which the owner would be wrong rather than approximate call
|
|
2528
|
+
* {@link getTurnTrust} and handle `undefined`.
|
|
2529
|
+
*
|
|
2530
|
+
* The fallback half is load-bearing, not transitional politeness: a
|
|
2531
|
+
* deferred wake fires with no inbound actor, and refusing it an answer
|
|
2532
|
+
* denies every sensitive tool in the resumed turn (LUM-2929). It becomes
|
|
2533
|
+
* removable in one place when every entry point records a turn actor.
|
|
2534
|
+
*/
|
|
2535
|
+
getTurnOrRestingTrust(): TrustContext | undefined {
|
|
2536
|
+
return this.currentTurnTrustContext ?? this.trustContext;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2491
2539
|
/**
|
|
2492
2540
|
* The actor principal that owns the current turn, for host-proxy routing.
|
|
2493
2541
|
* Prefers the in-flight turn's actor over the conversation's resting
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getConfig, invalidateConfigCache } from "../config/loader.js";
|
|
9
|
+
import {
|
|
10
|
+
buildEffectiveMcpConfig,
|
|
11
|
+
pluginMcpServersChangedSinceLastBuild,
|
|
12
|
+
} from "../mcp/effective-config.js";
|
|
9
13
|
import { getMcpServerManager } from "../mcp/manager.js";
|
|
10
14
|
import { migrateLegacyMcpHeaders } from "../mcp/mcp-header-store.js";
|
|
11
15
|
import { createMcpToolsFromServer } from "../tools/mcp/mcp-tool-factory.js";
|
|
@@ -52,6 +56,29 @@ export function reloadMcpServers(): Promise<McpReloadResult> {
|
|
|
52
56
|
return reloadInProgress;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Reload MCP servers if — and only if — the plugin-declared set moved.
|
|
61
|
+
*
|
|
62
|
+
* Called by the plugin source reconcile, which every install / uninstall /
|
|
63
|
+
* upgrade / enable / disable already funnels through, so a plugin's MCP
|
|
64
|
+
* tools come up and go down with the plugin instead of lingering until an
|
|
65
|
+
* explicit reload, a config edit, or a restart.
|
|
66
|
+
*
|
|
67
|
+
* The guard matters: a reconcile also fires for plugin edits that touch no
|
|
68
|
+
* `mcp.json` at all, and an unconditional reload would tear down every
|
|
69
|
+
* healthy workspace connection to re-establish it for nothing.
|
|
70
|
+
*
|
|
71
|
+
* Never throws — the reconcile is best-effort and must not fail an install
|
|
72
|
+
* whose files already landed on disk.
|
|
73
|
+
*/
|
|
74
|
+
export async function reconcilePluginMcpServers(): Promise<void> {
|
|
75
|
+
if (!pluginMcpServersChangedSinceLastBuild()) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
log.info("Plugin-declared MCP servers changed; reloading");
|
|
79
|
+
await reloadMcpServers();
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
async function doReload(): Promise<McpReloadResult> {
|
|
56
83
|
try {
|
|
57
84
|
const manager = getMcpServerManager();
|
|
@@ -74,17 +101,20 @@ async function doReload(): Promise<McpReloadResult> {
|
|
|
74
101
|
// 2. Stop existing MCP servers + unregister their tools
|
|
75
102
|
await manager.stop();
|
|
76
103
|
unregisterAllMcpTools();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
|
|
105
|
+
// Plugins are re-read here too: installing or removing one changes the
|
|
106
|
+
// server set exactly like editing config.json does, and both arrive
|
|
107
|
+
// through this same reload.
|
|
108
|
+
const mcpConfig = buildEffectiveMcpConfig(config.mcp);
|
|
109
|
+
const serverIds = Object.keys(mcpConfig.servers);
|
|
80
110
|
|
|
81
111
|
// 3. Restart MCP servers
|
|
82
112
|
let serverCount = 0;
|
|
83
113
|
let toolCount = 0;
|
|
84
114
|
const servers: McpReloadServerResult[] = [];
|
|
85
115
|
|
|
86
|
-
if (
|
|
87
|
-
const serverToolInfos = await manager.start(
|
|
116
|
+
if (serverIds.length > 0) {
|
|
117
|
+
const serverToolInfos = await manager.start(mcpConfig);
|
|
88
118
|
for (const { serverId, serverConfig, tools } of serverToolInfos) {
|
|
89
119
|
const mcpTools = createMcpToolsFromServer(
|
|
90
120
|
tools,
|
|
@@ -105,7 +135,7 @@ async function doReload(): Promise<McpReloadResult> {
|
|
|
105
135
|
// Include servers that were configured but failed to connect or are disabled
|
|
106
136
|
for (const id of serverIds) {
|
|
107
137
|
if (!servers.some((s) => s.id === id)) {
|
|
108
|
-
const serverConfig =
|
|
138
|
+
const serverConfig = mcpConfig.servers[id];
|
|
109
139
|
const isDisabled = serverConfig?.enabled === false;
|
|
110
140
|
servers.push({
|
|
111
141
|
id,
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
shouldAttachHostProxyForCapability,
|
|
72
72
|
} from "./host-proxy-preactivation.js";
|
|
73
73
|
import type { SubagentToolGateMode } from "./tool-setup-types.js";
|
|
74
|
+
import { restingTrust } from "./trust-context-types.js";
|
|
74
75
|
|
|
75
76
|
const log = getLogger("process-message");
|
|
76
77
|
|
|
@@ -455,7 +456,10 @@ export async function processMessage(
|
|
|
455
456
|
if (slashResult.kind === "unknown") {
|
|
456
457
|
const serverTurnCtx = conversation.getTurnChannelContext();
|
|
457
458
|
const serverProvenance = provenanceFromTrustContext(
|
|
458
|
-
|
|
459
|
+
// Ingress persists before any per-turn stamp; the slot was just written
|
|
460
|
+
// by this message's own resolution, and the per-turn field may still
|
|
461
|
+
// hold the previous turn's actor.
|
|
462
|
+
restingTrust(conversation),
|
|
459
463
|
);
|
|
460
464
|
const imageSourcePaths: Record<string, string> = {};
|
|
461
465
|
for (let i = 0; i < attachments.length; i++) {
|
|
@@ -557,7 +561,10 @@ export async function processMessage(
|
|
|
557
561
|
if (slashResult.kind === "compact") {
|
|
558
562
|
const serverTurnCtx = conversation.getTurnChannelContext();
|
|
559
563
|
const serverProvenance = provenanceFromTrustContext(
|
|
560
|
-
|
|
564
|
+
// Ingress persists before any per-turn stamp; the slot was just written
|
|
565
|
+
// by this message's own resolution, and the per-turn field may still
|
|
566
|
+
// hold the previous turn's actor.
|
|
567
|
+
restingTrust(conversation),
|
|
561
568
|
);
|
|
562
569
|
const compactChannelMeta = {
|
|
563
570
|
...serverProvenance,
|
|
@@ -612,7 +619,10 @@ export async function processMessage(
|
|
|
612
619
|
if (slashResult.kind === "clean") {
|
|
613
620
|
const serverTurnCtx = conversation.getTurnChannelContext();
|
|
614
621
|
const serverProvenance = provenanceFromTrustContext(
|
|
615
|
-
|
|
622
|
+
// Ingress persists before any per-turn stamp; the slot was just written
|
|
623
|
+
// by this message's own resolution, and the per-turn field may still
|
|
624
|
+
// hold the previous turn's actor.
|
|
625
|
+
restingTrust(conversation),
|
|
616
626
|
);
|
|
617
627
|
const cleanChannelMeta = {
|
|
618
628
|
...serverProvenance,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { maybeDefaultSpeechToManaged } from "../config/managed-speech-defaults.js";
|
|
2
2
|
import { rehydratePlatformCredentials } from "../config/platform-rehydration.js";
|
|
3
3
|
import type { AssistantConfig } from "../config/types.js";
|
|
4
|
+
import { buildEffectiveMcpConfig } from "../mcp/effective-config.js";
|
|
4
5
|
import { getMcpServerManager } from "../mcp/manager.js";
|
|
5
6
|
import { gmailMessagingProvider } from "../messaging/providers/gmail/adapter.js";
|
|
6
7
|
import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
|
|
@@ -61,11 +62,13 @@ export async function initializeProvidersAndTools(
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// Start MCP servers and
|
|
65
|
-
|
|
65
|
+
// Start MCP servers — workspace-configured and plugin-declared alike —
|
|
66
|
+
// and register their tools.
|
|
67
|
+
const mcpConfig = buildEffectiveMcpConfig(config.mcp);
|
|
68
|
+
if (Object.keys(mcpConfig.servers).length > 0) {
|
|
66
69
|
const manager = getMcpServerManager();
|
|
67
70
|
try {
|
|
68
|
-
const serverToolInfos = await manager.start(
|
|
71
|
+
const serverToolInfos = await manager.start(mcpConfig);
|
|
69
72
|
for (const { serverId, serverConfig, tools } of serverToolInfos) {
|
|
70
73
|
const mcpTools = createMcpToolsFromServer(
|
|
71
74
|
tools,
|
|
@@ -106,3 +106,32 @@ export function sameTrustIdentity(
|
|
|
106
106
|
a.guardianPrincipalId === b.guardianPrincipalId
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/** The two trust fields a conversation-shaped value carries. */
|
|
111
|
+
export interface TrustCarrier {
|
|
112
|
+
currentTurnTrustContext?: TrustContext;
|
|
113
|
+
trustContext?: TrustContext;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The acting turn's trust, else the owner's. Structural counterpart of
|
|
118
|
+
* `Conversation.getTurnOrRestingTrust` for call sites handed a
|
|
119
|
+
* conversation-shaped context rather than the class, so a partial test double
|
|
120
|
+
* needs only the fields it already has.
|
|
121
|
+
*/
|
|
122
|
+
export function turnOrRestingTrust(
|
|
123
|
+
carrier: TrustCarrier | undefined,
|
|
124
|
+
): TrustContext | undefined {
|
|
125
|
+
return carrier?.currentTurnTrustContext ?? carrier?.trustContext;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The owner's trust, independent of any turn. Structural counterpart of
|
|
130
|
+
* `Conversation.getTrustContext`; see `docs/architecture/turn-actor.md` for
|
|
131
|
+
* when the owner is the right actor.
|
|
132
|
+
*/
|
|
133
|
+
export function restingTrust(
|
|
134
|
+
carrier: Pick<TrustCarrier, "trustContext"> | undefined,
|
|
135
|
+
): TrustContext | undefined {
|
|
136
|
+
return carrier?.trustContext;
|
|
137
|
+
}
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
SubagentToolGateMode,
|
|
33
33
|
WakeToolContextPin,
|
|
34
34
|
} from "./tool-setup-types.js";
|
|
35
|
+
import { restingTrust } from "./trust-context-types.js";
|
|
35
36
|
|
|
36
37
|
const log = getLogger("wake-conversation-ops");
|
|
37
38
|
|
|
@@ -222,7 +223,7 @@ export async function persistWakeTailMessage(
|
|
|
222
223
|
const turnChannelCtx = conversation.getTurnChannelContext();
|
|
223
224
|
const turnInterfaceCtx = conversation.getTurnInterfaceContext();
|
|
224
225
|
const metadata: Record<string, unknown> = {
|
|
225
|
-
...provenanceFromTrustContext(conversation
|
|
226
|
+
...provenanceFromTrustContext(restingTrust(conversation)),
|
|
226
227
|
userMessageChannel: turnChannelCtx?.userMessageChannel ?? "vellum",
|
|
227
228
|
assistantMessageChannel:
|
|
228
229
|
turnChannelCtx?.assistantMessageChannel ?? "vellum",
|
|
@@ -301,7 +302,7 @@ export async function persistWakeTriggerMessage(
|
|
|
301
302
|
const turnChannelCtx = conversation.getTurnChannelContext();
|
|
302
303
|
const turnInterfaceCtx = conversation.getTurnInterfaceContext();
|
|
303
304
|
const metadata: Record<string, unknown> = {
|
|
304
|
-
...provenanceFromTrustContext(conversation
|
|
305
|
+
...provenanceFromTrustContext(restingTrust(conversation)),
|
|
305
306
|
userMessageChannel: turnChannelCtx?.userMessageChannel ?? "vellum",
|
|
306
307
|
assistantMessageChannel:
|
|
307
308
|
turnChannelCtx?.assistantMessageChannel ?? "vellum",
|
|
@@ -798,12 +798,125 @@ export function replaceInDocument(
|
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Append idempotency
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
|
|
805
|
+
/** A blank-line-delimited block of markdown, located in its source string. */
|
|
806
|
+
interface MarkdownBlock {
|
|
807
|
+
/** Whitespace-trimmed block text, the unit duplicate detection compares. */
|
|
808
|
+
text: string;
|
|
809
|
+
/** Offset of the block's first character in the source string. */
|
|
810
|
+
start: number;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Split markdown into blank-line-delimited blocks, dropping empty ones but
|
|
815
|
+
* keeping each surviving block's offset so a suffix of the source can be
|
|
816
|
+
* recovered verbatim.
|
|
817
|
+
*/
|
|
818
|
+
function splitIntoBlocks(content: string): MarkdownBlock[] {
|
|
819
|
+
const blocks: MarkdownBlock[] = [];
|
|
820
|
+
const separator = /\n[^\S\n]*\n/g;
|
|
821
|
+
let cursor = 0;
|
|
822
|
+
let match: RegExpExecArray | null;
|
|
823
|
+
const push = (start: number, end: number): void => {
|
|
824
|
+
const text = content.slice(start, end).trim();
|
|
825
|
+
if (text.length > 0) {
|
|
826
|
+
blocks.push({ text, start });
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
while ((match = separator.exec(content)) !== null) {
|
|
830
|
+
push(cursor, match.index);
|
|
831
|
+
cursor = match.index + match[0].length;
|
|
832
|
+
}
|
|
833
|
+
push(cursor, content.length);
|
|
834
|
+
return blocks;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* An append has to repeat at least this many characters of the document's tail
|
|
839
|
+
* before the repetition is treated as an accident.
|
|
840
|
+
*
|
|
841
|
+
* The floor is what keeps legitimate repetition intact. A document can honestly
|
|
842
|
+
* repeat a heading, a refrain, a table row, or a bare "TODO" back to back, and
|
|
843
|
+
* those are all short. Only a substantial restatement (an opening paragraph
|
|
844
|
+
* passed to both `document_create` and the first append) clears the bar.
|
|
845
|
+
*/
|
|
846
|
+
const MIN_DUPLICATE_APPEND_CHARS = 40;
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Drop a leading run of blocks from `markdown` that exactly repeats the tail of
|
|
850
|
+
* `existing`, so an append that restates content already committed does not
|
|
851
|
+
* write it a second time.
|
|
852
|
+
*
|
|
853
|
+
* The guard is deliberately narrow: the repeated run must be an exact,
|
|
854
|
+
* block-aligned match, it must sit at the very start of the append and the very
|
|
855
|
+
* end of the document, and it must be at least
|
|
856
|
+
* {@link MIN_DUPLICATE_APPEND_CHARS} long. Anything else, including the same
|
|
857
|
+
* text reappearing further down the append, is passed through untouched.
|
|
858
|
+
*/
|
|
859
|
+
function stripDuplicateLeadingBlocks(
|
|
860
|
+
existing: string,
|
|
861
|
+
markdown: string,
|
|
862
|
+
): string {
|
|
863
|
+
const existingBlocks = splitIntoBlocks(existing);
|
|
864
|
+
const incomingBlocks = splitIntoBlocks(markdown);
|
|
865
|
+
const maxOverlap = Math.min(existingBlocks.length, incomingBlocks.length);
|
|
866
|
+
|
|
867
|
+
let overlap = 0;
|
|
868
|
+
for (let candidate = maxOverlap; candidate >= 1; candidate--) {
|
|
869
|
+
const offset = existingBlocks.length - candidate;
|
|
870
|
+
let matches = true;
|
|
871
|
+
for (let i = 0; i < candidate; i++) {
|
|
872
|
+
if (existingBlocks[offset + i].text !== incomingBlocks[i].text) {
|
|
873
|
+
matches = false;
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (matches) {
|
|
878
|
+
overlap = candidate;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (overlap === 0) {
|
|
883
|
+
return markdown;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const duplicatedChars = incomingBlocks
|
|
887
|
+
.slice(0, overlap)
|
|
888
|
+
.reduce((total, block) => total + block.text.length, 0);
|
|
889
|
+
if (duplicatedChars < MIN_DUPLICATE_APPEND_CHARS) {
|
|
890
|
+
return markdown;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (overlap === incomingBlocks.length) {
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
return markdown
|
|
897
|
+
.slice(incomingBlocks[overlap].start)
|
|
898
|
+
.replace(/^(?:[^\S\n]*\n)+/, "");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Outcome of a successful {@link updateDocumentContent} call. */
|
|
902
|
+
export interface DocumentContentUpdated {
|
|
903
|
+
success: true;
|
|
904
|
+
/**
|
|
905
|
+
* The markdown that actually landed. Equals the submitted markdown except
|
|
906
|
+
* when an append's leading blocks duplicated the document's tail, in which
|
|
907
|
+
* case it is the remainder. Clients render this so their view matches the row.
|
|
908
|
+
*/
|
|
909
|
+
appliedMarkdown: string;
|
|
910
|
+
/** True when a duplicated leading run was dropped from an append. */
|
|
911
|
+
duplicateLeadingContentSkipped: boolean;
|
|
912
|
+
}
|
|
913
|
+
|
|
801
914
|
/** Update persisted document content (append or replace). */
|
|
802
915
|
export function updateDocumentContent(
|
|
803
916
|
surfaceId: string,
|
|
804
917
|
markdown: string,
|
|
805
918
|
mode: string,
|
|
806
|
-
):
|
|
919
|
+
): DocumentContentUpdated | { success: false; error: string } {
|
|
807
920
|
try {
|
|
808
921
|
const existing = rawGet<{ content: string }>(
|
|
809
922
|
"documents:updateDocumentContent:get",
|
|
@@ -814,9 +927,19 @@ export function updateDocumentContent(
|
|
|
814
927
|
log.info({ surfaceId }, "No persisted document to update");
|
|
815
928
|
return { success: false, error: "Document not found" };
|
|
816
929
|
}
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
930
|
+
const appending = mode === "append";
|
|
931
|
+
const appliedMarkdown = appending
|
|
932
|
+
? stripDuplicateLeadingBlocks(existing.content, markdown)
|
|
933
|
+
: markdown;
|
|
934
|
+
const duplicateLeadingContentSkipped =
|
|
935
|
+
appending && appliedMarkdown !== markdown;
|
|
936
|
+
const sep =
|
|
937
|
+
appending && existing.content.length > 0 && appliedMarkdown.length > 0
|
|
938
|
+
? "\n\n"
|
|
939
|
+
: "";
|
|
940
|
+
const newContent = appending
|
|
941
|
+
? existing.content + sep + appliedMarkdown
|
|
942
|
+
: appliedMarkdown;
|
|
820
943
|
writeThroughToWorkspaceFile(surfaceId, newContent);
|
|
821
944
|
const wordCount = countWords(newContent);
|
|
822
945
|
rawRun(
|
|
@@ -827,8 +950,18 @@ export function updateDocumentContent(
|
|
|
827
950
|
Date.now(),
|
|
828
951
|
surfaceId,
|
|
829
952
|
);
|
|
953
|
+
if (duplicateLeadingContentSkipped) {
|
|
954
|
+
log.info(
|
|
955
|
+
{ surfaceId, skippedChars: markdown.length - appliedMarkdown.length },
|
|
956
|
+
"Skipped append content duplicating the document tail",
|
|
957
|
+
);
|
|
958
|
+
}
|
|
830
959
|
log.info({ surfaceId, mode }, "Updated document content");
|
|
831
|
-
return {
|
|
960
|
+
return {
|
|
961
|
+
success: true,
|
|
962
|
+
appliedMarkdown,
|
|
963
|
+
duplicateLeadingContentSkipped,
|
|
964
|
+
};
|
|
832
965
|
} catch (error) {
|
|
833
966
|
log.error({ err: error, surfaceId }, "Document content update error");
|
|
834
967
|
return {
|
package/src/hooks/hook-loader.ts
CHANGED
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* registry, so the next read re-resolves fresh.
|
|
40
40
|
*
|
|
41
41
|
* Plugin *discovery* (which plugin directories exist, in what order) lives in
|
|
42
|
-
* `../plugins/mtime-cache.ts`;
|
|
43
|
-
*
|
|
44
|
-
* this module lets it sit below the plugin cache with no import cycle.
|
|
42
|
+
* `../plugins/mtime-cache.ts`; hook lookup in `./registry.ts` passes the
|
|
43
|
+
* discovered names into {@link collectUserHookEntries}. Keeping discovery out
|
|
44
|
+
* of this module lets it sit below the plugin cache with no import cycle.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import {
|
package/src/hooks/registry.ts
CHANGED
|
@@ -6,20 +6,22 @@
|
|
|
6
6
|
* hook-chain ordering.
|
|
7
7
|
*
|
|
8
8
|
* User-land hooks (from the filesystem) are owned by
|
|
9
|
-
* {@link ./hook-loader.ts} and surfaced through
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* {@link ./hook-loader.ts} and surfaced through {@link getUserHooksFor}.
|
|
10
|
+
* This module owns the in-process hooks that default plugins register at
|
|
11
|
+
* boot, plus the user-land lookup that walks discovered plugin names from
|
|
12
|
+
* the plugin cache.
|
|
12
13
|
*
|
|
13
14
|
* {@link getHooksFor} combines both sources: in-process hooks from this
|
|
14
15
|
* registry (filtered by `isPluginDisabled` at read time) and user-land hooks
|
|
15
16
|
* from the plugin cache. The read-time filtering is what makes `assistant
|
|
16
|
-
* plugins disable default-*` take effect immediately in a running assistant
|
|
17
|
-
*
|
|
17
|
+
* plugins disable default-*` take effect immediately in a running assistant:
|
|
18
|
+
* the hooks stay registered but are filtered out on the next turn.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
21
|
-
import {
|
|
22
|
+
import { getDiscoveredUserPluginNames } from "../plugins/mtime-cache.js";
|
|
22
23
|
import type { HookEntry, HookFunction } from "../plugins/types.js";
|
|
24
|
+
import { collectUserHookEntries } from "./hook-loader.js";
|
|
23
25
|
|
|
24
26
|
// ─── Internal state ──────────────────────────────────────────────────────────
|
|
25
27
|
|
|
@@ -75,6 +77,48 @@ export function unregisterPluginHooks(pluginName: string): void {
|
|
|
75
77
|
|
|
76
78
|
// ─── Queries ─────────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Get all hooks for a given event name from user plugins and standalone
|
|
82
|
+
* workspace hooks, from the hook loader's in-memory cache. Plugin hooks run
|
|
83
|
+
* in install-date order, the workspace hook runs last.
|
|
84
|
+
*
|
|
85
|
+
* This is a pure cache read: it never scans disk, activates a plugin, or
|
|
86
|
+
* runs `init`. Activation happens only at boot (`populateCacheAtBoot`)
|
|
87
|
+
* and through the imperative install/uninstall poke
|
|
88
|
+
* (`reconcilePluginSourcesNow`), both main-daemon paths, so a
|
|
89
|
+
* sidecar process that dispatches hooks (a worker running conversation
|
|
90
|
+
* turns) can never bring a plugin up in its own process.
|
|
91
|
+
*
|
|
92
|
+
* `effectiveEnabledPlugins` carries the per-chat plugin scope: when non-null,
|
|
93
|
+
* user plugins outside the set are skipped (standalone workspace hooks always
|
|
94
|
+
* run). `null`/omitted means no per-chat restriction.
|
|
95
|
+
*/
|
|
96
|
+
export async function getUserHookEntriesFor<TCtx = unknown>(
|
|
97
|
+
hookName: string,
|
|
98
|
+
effectiveEnabledPlugins?: Set<string> | null,
|
|
99
|
+
): Promise<HookEntry<TCtx>[]> {
|
|
100
|
+
return collectUserHookEntries<TCtx>(
|
|
101
|
+
hookName,
|
|
102
|
+
getDiscoveredUserPluginNames(),
|
|
103
|
+
effectiveEnabledPlugins,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* {@link getUserHookEntriesFor} without owner attribution. Returns just the
|
|
109
|
+
* hook functions in the same order.
|
|
110
|
+
*/
|
|
111
|
+
export async function getUserHooksFor<TCtx = unknown>(
|
|
112
|
+
hookName: string,
|
|
113
|
+
effectiveEnabledPlugins?: Set<string> | null,
|
|
114
|
+
): Promise<HookFunction<TCtx>[]> {
|
|
115
|
+
const entries = await getUserHookEntriesFor<TCtx>(
|
|
116
|
+
hookName,
|
|
117
|
+
effectiveEnabledPlugins,
|
|
118
|
+
);
|
|
119
|
+
return entries.map((e) => e.fn);
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
/**
|
|
79
123
|
* Collect every registered hook for the given name, in registration order.
|
|
80
124
|
* Plugins that don't declare a hook for `name` are skipped. Used by the
|