@vellumai/assistant 0.11.8 → 0.11.9-staging.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 +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* decoupling route handlers and IPC callbacks from the DaemonServer
|
|
7
7
|
* class.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* creation/reuse lifecycle
|
|
11
|
-
* prompt assembly, and DB hydration. Idle eviction lives in
|
|
9
|
+
* {@link getOrCreateConversation} and {@link getConversationIfExists} share
|
|
10
|
+
* one body that owns the full creation/reuse lifecycle: provider wiring, rate
|
|
11
|
+
* limiting, system prompt assembly, and DB hydration. Idle eviction lives in
|
|
12
12
|
* `conversation-evictor`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -128,8 +128,8 @@ function clearConversationOptions(): void {
|
|
|
128
128
|
|
|
129
129
|
// ── Conversation lifecycle ─────────────────────────────────────────
|
|
130
130
|
|
|
131
|
-
/** Dedup guard: in-flight
|
|
132
|
-
const conversationCreating = new Map<string, Promise<Conversation>>();
|
|
131
|
+
/** Dedup guard: in-flight acquisitions keyed by conversation ID. */
|
|
132
|
+
const conversationCreating = new Map<string, Promise<Conversation | null>>();
|
|
133
133
|
|
|
134
134
|
function applyTransportMetadata(
|
|
135
135
|
conversation: Conversation,
|
|
@@ -163,6 +163,88 @@ export async function getOrCreateConversation(
|
|
|
163
163
|
conversationId: string,
|
|
164
164
|
options?: ConversationCreateOptions,
|
|
165
165
|
): Promise<Conversation> {
|
|
166
|
+
// Null is the non-creating acquire's answer and nobody else's.
|
|
167
|
+
return (await acquireConversation(conversationId, options, true))!;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Acquire an active conversation, or null when the one asked about is gone.
|
|
172
|
+
*
|
|
173
|
+
* For a caller whose work is only meaningful inside a conversation that still
|
|
174
|
+
* exists: a queued job whose conversation can be deleted between the moment it
|
|
175
|
+
* was accepted and the moment it runs. {@link getOrCreateConversation} would
|
|
176
|
+
* write the row back for it, which resurrects a conversation the user deleted.
|
|
177
|
+
*
|
|
178
|
+
* The question it answers is about one incarnation, not about an id. A row
|
|
179
|
+
* deleted and written back under the same id is a different conversation, and
|
|
180
|
+
* this reports it as gone rather than persisting into a stranger that inherited
|
|
181
|
+
* the name.
|
|
182
|
+
*
|
|
183
|
+
* Distinct from the two cheaper reads it sits above:
|
|
184
|
+
* `findConversation` returns only an instance already in memory, and the
|
|
185
|
+
* persistence layer's `getConversation` returns only the row. This builds and
|
|
186
|
+
* hydrates the instance the way the creating acquire does, and answers null
|
|
187
|
+
* exactly where that one would insert.
|
|
188
|
+
*/
|
|
189
|
+
export function getConversationIfExists(
|
|
190
|
+
conversationId: string,
|
|
191
|
+
options?: ConversationCreateOptions,
|
|
192
|
+
): Promise<Conversation | null> {
|
|
193
|
+
return acquireConversation(conversationId, options, false);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether the conversation is still the incarnation the caller was asked about.
|
|
198
|
+
*
|
|
199
|
+
* `created_at` is stamped at insert and nothing else rewrites it, so a row
|
|
200
|
+
* deleted and written back under the same id carries a different one.
|
|
201
|
+
* `createConversation` issues that stamp monotonically per process, which is
|
|
202
|
+
* what makes the two distinguishable even when they land in the same
|
|
203
|
+
* millisecond. Bare existence cannot answer this: a caller sharing flight with
|
|
204
|
+
* a creating acquire can find a row that acquire wrote moments ago and read it
|
|
205
|
+
* as the one it was asked about, and so can one that held an instance across a
|
|
206
|
+
* wait.
|
|
207
|
+
*
|
|
208
|
+
* A conversation restored from its on-disk view (the recovery migration, the
|
|
209
|
+
* `db repair` backfill) is written back with the stamp its meta file records,
|
|
210
|
+
* so it answers as the incarnation it is rather than as a new one. Both
|
|
211
|
+
* restores skip an id whose row is present, so neither can displace a live
|
|
212
|
+
* conversation this way.
|
|
213
|
+
*
|
|
214
|
+
* Exported for callers that keep working after their acquire returns: holding
|
|
215
|
+
* the instance says nothing about the row still being the one behind it.
|
|
216
|
+
*/
|
|
217
|
+
export function isSameIncarnation(
|
|
218
|
+
conversationId: string,
|
|
219
|
+
createdAt: number | null,
|
|
220
|
+
): boolean {
|
|
221
|
+
return (
|
|
222
|
+
createdAt !== null &&
|
|
223
|
+
getConversation(conversationId)?.createdAt === createdAt
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The body both acquires share. `createIfMissing` decides one thing: whether a
|
|
229
|
+
* missing row is written or reported.
|
|
230
|
+
*/
|
|
231
|
+
async function acquireConversation(
|
|
232
|
+
conversationId: string,
|
|
233
|
+
options: ConversationCreateOptions | undefined,
|
|
234
|
+
createIfMissing: boolean,
|
|
235
|
+
): Promise<Conversation | null> {
|
|
236
|
+
// A caller that will not create wants nothing to do with a conversation
|
|
237
|
+
// whose row is gone, resident instance or not. Also spares an already-gone
|
|
238
|
+
// conversation the provider and tool setup below. The row it finds is the
|
|
239
|
+
// incarnation every later acceptance point below answers for.
|
|
240
|
+
let incarnation: number | null = null;
|
|
241
|
+
if (!createIfMissing) {
|
|
242
|
+
incarnation = getConversation(conversationId)?.createdAt ?? null;
|
|
243
|
+
if (incarnation === null) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
166
248
|
let conversation = findConversation(conversationId);
|
|
167
249
|
|
|
168
250
|
// `taskRunId` and `ephemeral` are per-call scopes, not durable conversation
|
|
@@ -200,10 +282,54 @@ export async function getOrCreateConversation(
|
|
|
200
282
|
conversation.dispose();
|
|
201
283
|
}
|
|
202
284
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
285
|
+
// Joining is a loop rather than a single look because a flight can decline
|
|
286
|
+
// to create, and a caller that was asked to create must then re-consult
|
|
287
|
+
// the map instead of building unconditionally: two creating callers that
|
|
288
|
+
// fell through together would each build an instance for one id, the
|
|
289
|
+
// second overwriting the first in the registry and leaving the first to
|
|
290
|
+
// run outside the shared processing and queue state. A non-creating caller
|
|
291
|
+
// re-consults for the mirrored reason: a flight that declined answered for
|
|
292
|
+
// the incarnation IT was asked about, which a delete and recreate can have
|
|
293
|
+
// made a different one from this caller's.
|
|
294
|
+
//
|
|
295
|
+
// It terminates. The owner of a flight attaches its own continuation in
|
|
296
|
+
// the same synchronous run that publishes the flight, so its `finally`
|
|
297
|
+
// has already removed the entry by the time any joiner resumes, and a
|
|
298
|
+
// joiner therefore never re-joins the flight it just awaited. What it
|
|
299
|
+
// finds instead is a newer flight or nothing, and a creating flight only
|
|
300
|
+
// ever resolves non-null, so joining one returns. A non-creating caller
|
|
301
|
+
// passes again only while the row it was asked about is still there, so
|
|
302
|
+
// the delete that a declining flight reports ends its loop as well.
|
|
303
|
+
for (;;) {
|
|
304
|
+
const pending = conversationCreating.get(conversationId);
|
|
305
|
+
if (!pending) {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
const joined = await pending;
|
|
309
|
+
if (!createIfMissing) {
|
|
310
|
+
// A non-creating acquire answers for the incarnation it was asked
|
|
311
|
+
// about, never for what an acquire it happened to share flight with
|
|
312
|
+
// decided to write. The shared flight can belong to a creating caller,
|
|
313
|
+
// whose own contract is to build the conversation whatever became of
|
|
314
|
+
// the row, and a delete during that caller's setup is answered by
|
|
315
|
+
// writing a fresh row under the same id. Present is therefore not the
|
|
316
|
+
// question: that row is a different conversation, and this caller was
|
|
317
|
+
// never asked to persist into it.
|
|
318
|
+
if (joined) {
|
|
319
|
+
return isSameIncarnation(conversationId, incarnation) ? joined : null;
|
|
320
|
+
}
|
|
321
|
+
// A declining flight reports its own incarnation gone, which says
|
|
322
|
+
// nothing about a later one this caller may hold. Taking that null
|
|
323
|
+
// would refuse work the id can still accept, so this reconsults, and
|
|
324
|
+
// only its own row being gone ends it.
|
|
325
|
+
if (!isSameIncarnation(conversationId, incarnation)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (joined) {
|
|
331
|
+
return joined;
|
|
332
|
+
}
|
|
207
333
|
}
|
|
208
334
|
|
|
209
335
|
const storedOptions = conversationOptions.get(conversationId);
|
|
@@ -238,6 +364,14 @@ export async function getOrCreateConversation(
|
|
|
238
364
|
const { initializeTools } = await import("../tools/registry.js");
|
|
239
365
|
await initializeTools();
|
|
240
366
|
|
|
367
|
+
// Provider resolution, the system prompt, and tool setup all await, and
|
|
368
|
+
// a delete can land in any of them. This is the last instant before the
|
|
369
|
+
// row insert below and nothing between the two awaits, so a non-creating
|
|
370
|
+
// acquire that finds its incarnation gone here never writes it back.
|
|
371
|
+
if (!createIfMissing && !isSameIncarnation(conversationId, incarnation)) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
241
375
|
const newConversation = new Conversation(
|
|
242
376
|
conversationId,
|
|
243
377
|
provider,
|
|
@@ -310,16 +444,30 @@ export async function getOrCreateConversation(
|
|
|
310
444
|
if (options?.transport) {
|
|
311
445
|
newConversation.applyVisibleAppFromTransport(options.transport);
|
|
312
446
|
}
|
|
447
|
+
// Hydration awaits, so a delete can land between the check that let this
|
|
448
|
+
// build and the registry write. Registering then leaves an active
|
|
449
|
+
// conversation with no row behind it, which later session work finds and
|
|
450
|
+
// reuses. The creating path keeps its own window: it cannot answer null,
|
|
451
|
+
// and a caller that was asked to build owns what it built.
|
|
452
|
+
if (!createIfMissing && !isSameIncarnation(conversationId, incarnation)) {
|
|
453
|
+
newConversation.dispose();
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
313
456
|
setConversation(conversationId, newConversation);
|
|
314
457
|
return newConversation;
|
|
315
458
|
})();
|
|
316
459
|
|
|
317
460
|
conversationCreating.set(conversationId, createPromise);
|
|
461
|
+
let acquired: Conversation | null;
|
|
318
462
|
try {
|
|
319
|
-
|
|
463
|
+
acquired = await createPromise;
|
|
320
464
|
} finally {
|
|
321
465
|
conversationCreating.delete(conversationId);
|
|
322
466
|
}
|
|
467
|
+
if (!acquired) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
conversation = acquired;
|
|
323
471
|
touchConversation(conversationId);
|
|
324
472
|
} else {
|
|
325
473
|
if (!conversation.isProcessing()) {
|
|
@@ -330,7 +478,7 @@ export async function getOrCreateConversation(
|
|
|
330
478
|
}
|
|
331
479
|
touchConversation(conversationId);
|
|
332
480
|
}
|
|
333
|
-
return conversation;
|
|
481
|
+
return conversation ?? null;
|
|
334
482
|
}
|
|
335
483
|
|
|
336
484
|
/**
|
|
@@ -729,7 +729,9 @@ function resolveTurnClientOs(ctx: Conversation): {
|
|
|
729
729
|
const clientOs = pin
|
|
730
730
|
? pin.clientOs
|
|
731
731
|
: (parseClientOs(ctx.currentTurnClientOs ?? ctx.clientOs) ??
|
|
732
|
-
(transportInterface === "macos" ||
|
|
732
|
+
(transportInterface === "macos" ||
|
|
733
|
+
transportInterface === "windows" ||
|
|
734
|
+
transportInterface === "linux"
|
|
733
735
|
? transportInterface
|
|
734
736
|
: undefined));
|
|
735
737
|
return { clientOs, transportInterface };
|
|
@@ -900,7 +902,12 @@ export function isToolActiveForContext(
|
|
|
900
902
|
// Check the *client's* platform, not the daemon's process.platform.
|
|
901
903
|
// In Docker the daemon runs on Linux but the connected client may be macOS.
|
|
902
904
|
const { clientOs } = resolveTurnClientOs(ctx);
|
|
903
|
-
return (
|
|
905
|
+
return (
|
|
906
|
+
(clientOs === "macos" ||
|
|
907
|
+
clientOs === "windows" ||
|
|
908
|
+
clientOs === "linux") &&
|
|
909
|
+
!hasNoClient
|
|
910
|
+
);
|
|
904
911
|
}
|
|
905
912
|
if (SUBAGENT_ONLY_TOOL_NAMES.has(name)) {
|
|
906
913
|
return ctx.isSubagent === true;
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
|
|
79
79
|
} from "../plugins/defaults/memory/v3/ever-injected-store.js";
|
|
80
80
|
import { filterPrunedCardSections } from "../plugins/defaults/memory/v3/prune.js";
|
|
81
|
+
import { MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY } from "../plugins/defaults/memory/v3/types.js";
|
|
81
82
|
import {
|
|
82
83
|
applyBootstrapTemplate,
|
|
83
84
|
buildSystemPrompt,
|
|
@@ -306,12 +307,37 @@ function abortReasonOf(signal?: AbortSignal): unknown {
|
|
|
306
307
|
);
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Thrown by the processing fence when the claim it was asked about is no
|
|
312
|
+
* longer the live one, which is the conversation having moved on to another
|
|
313
|
+
* holder rather than anything failing.
|
|
314
|
+
*/
|
|
315
|
+
export class ProcessingClaimLostError extends Error {
|
|
316
|
+
constructor(conversationId: string) {
|
|
317
|
+
super(`Processing claim lost for conversation ${conversationId}`);
|
|
318
|
+
this.name = "ProcessingClaimLostError";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
309
322
|
export class Conversation {
|
|
310
323
|
public readonly conversationId: string;
|
|
311
324
|
/** @internal */ provider: Provider;
|
|
312
325
|
/** @internal */ messages: Message[] = [];
|
|
313
326
|
/** @internal */ agentLoop: AgentLoop;
|
|
314
327
|
private _processing = false;
|
|
328
|
+
/**
|
|
329
|
+
* Which claim on the processing flag is the live one, as a value that changes
|
|
330
|
+
* every time the flag is taken. Zero while it is free.
|
|
331
|
+
*
|
|
332
|
+
* The flag itself is a boolean and cannot say who holds it, so a release has
|
|
333
|
+
* no way to tell its own hold from one taken since. This is what lets
|
|
334
|
+
* {@link releaseProcessing} refuse to release someone else's.
|
|
335
|
+
*/
|
|
336
|
+
private processingOwner = 0;
|
|
337
|
+
private nextProcessingOwner = 0;
|
|
338
|
+
/** The live claim's marker write, awaited through the fence below. */
|
|
339
|
+
private processingMarker: { owner: number; landed: Promise<void> } | null =
|
|
340
|
+
null;
|
|
315
341
|
/**
|
|
316
342
|
* Pending {@link waitForIdle} resolvers, notified from the committed
|
|
317
343
|
* `processing → false` transition inside {@link setProcessing}. Every
|
|
@@ -535,7 +561,7 @@ export class Conversation {
|
|
|
535
561
|
* @internal
|
|
536
562
|
*/
|
|
537
563
|
currentTurnCronRunId?: string | null;
|
|
538
|
-
/** @internal */
|
|
564
|
+
/** @internal */ currentTurnIsNonInteractive?: boolean;
|
|
539
565
|
/** @internal */ currentTurnModelProfileNoticeKey?: string;
|
|
540
566
|
/** @internal */ currentTurnRequestOrigin?: string;
|
|
541
567
|
/** @internal */ authContext?: AuthContext;
|
|
@@ -1191,12 +1217,17 @@ export class Conversation {
|
|
|
1191
1217
|
const text = extractTextFromStoredMessageContent(row.content);
|
|
1192
1218
|
if (text) {
|
|
1193
1219
|
// A split reply posts several provider messages from one row;
|
|
1194
|
-
// a reaction may name any of them.
|
|
1220
|
+
// a reaction may name any of them. A post deleted on its own
|
|
1221
|
+
// (partial deletion of a split reply) stops being quotable
|
|
1222
|
+
// while its siblings remain.
|
|
1195
1223
|
for (const id of [
|
|
1196
1224
|
rowMeta.messageId,
|
|
1197
1225
|
...(rowMeta.additionalMessageIds ?? []),
|
|
1198
1226
|
]) {
|
|
1199
|
-
if (
|
|
1227
|
+
if (
|
|
1228
|
+
!reactionTargetIndexMemo.has(id) &&
|
|
1229
|
+
!rowMeta.deletedMessageIds?.includes(id)
|
|
1230
|
+
) {
|
|
1200
1231
|
reactionTargetIndexMemo.set(id, text);
|
|
1201
1232
|
}
|
|
1202
1233
|
}
|
|
@@ -1242,6 +1273,23 @@ export class Conversation {
|
|
|
1242
1273
|
// Neutral marker, no actor: Discord deletes can be authorless
|
|
1243
1274
|
// (`actorUnattributed`), so the marker never claims who deleted.
|
|
1244
1275
|
content = [{ type: "text", text: "[This message was deleted]" }];
|
|
1276
|
+
} else if (
|
|
1277
|
+
role === "assistant" &&
|
|
1278
|
+
providerMeta?.deletedAt !== undefined
|
|
1279
|
+
) {
|
|
1280
|
+
// The assistant's own deleted post keeps its content: erasure is
|
|
1281
|
+
// how a retracted USER message is honored, but rewriting what the
|
|
1282
|
+
// assistant said would falsify its memory of its own output. The
|
|
1283
|
+
// rendered fact is visibility: the message no longer exists on the
|
|
1284
|
+
// channel, so the assistant should not refer to it as something
|
|
1285
|
+
// participants can see.
|
|
1286
|
+
content = [
|
|
1287
|
+
...content,
|
|
1288
|
+
{
|
|
1289
|
+
type: "text",
|
|
1290
|
+
text: "[This message was deleted from the channel and is no longer visible to participants]",
|
|
1291
|
+
},
|
|
1292
|
+
];
|
|
1245
1293
|
}
|
|
1246
1294
|
}
|
|
1247
1295
|
|
|
@@ -1274,7 +1322,8 @@ export class Conversation {
|
|
|
1274
1322
|
// at the memory boundary after the `<info>` block but before
|
|
1275
1323
|
// now-md's earlier splice):
|
|
1276
1324
|
// [<workspace>, <turn_context>, <memory>dynamic</memory>,
|
|
1277
|
-
// <info>v2static</info>, <memory>v3cards</memory>,
|
|
1325
|
+
// <info>v2static</info>, <memory>v3cards</memory>,
|
|
1326
|
+
// <memory_spotlight>, <NOW.md>,
|
|
1278
1327
|
// <system_reminder>, <knowledge_base>, ...original]
|
|
1279
1328
|
// The v2 static block is replayed verbatim from stored metadata,
|
|
1280
1329
|
// so rows may carry either `<info>…</info>` or `<memory>…</memory>`
|
|
@@ -1304,6 +1353,28 @@ export class Conversation {
|
|
|
1304
1353
|
];
|
|
1305
1354
|
}
|
|
1306
1355
|
|
|
1356
|
+
// The memory-v3 per-turn `<memory_spotlight>` persists under its
|
|
1357
|
+
// own key as the wrapped block that was sent. Rehydrated on ALL
|
|
1358
|
+
// rows (tail included), matching frozen cards: after a reload the
|
|
1359
|
+
// last completed turn is the tail, and the next user message is
|
|
1360
|
+
// appended without re-running loadFromDb. Prepended here, after
|
|
1361
|
+
// now-md and before the v3 card block, so the inverted prepends
|
|
1362
|
+
// land as [cards, spotlight, now-md, ...]. Trust-gated on
|
|
1363
|
+
// `personalMemoryAllowed` like the cards: the spotlight carries
|
|
1364
|
+
// matched personal-memory sections.
|
|
1365
|
+
if (
|
|
1366
|
+
personalMemoryAllowed &&
|
|
1367
|
+
typeof meta[MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY] === "string"
|
|
1368
|
+
) {
|
|
1369
|
+
content = [
|
|
1370
|
+
{
|
|
1371
|
+
type: "text" as const,
|
|
1372
|
+
text: meta[MEMORY_V3_SPOTLIGHT_BLOCK_METADATA_KEY] as string,
|
|
1373
|
+
},
|
|
1374
|
+
...content,
|
|
1375
|
+
];
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1307
1378
|
// The memory-v3 frozen card block (net-new compact cards) persists
|
|
1308
1379
|
// under its own key, stored UNWRAPPED like v2's dynamic block below.
|
|
1309
1380
|
// Rehydrated on ALL rows (tail included): the next turn injects only
|
|
@@ -1606,27 +1677,56 @@ export class Conversation {
|
|
|
1606
1677
|
}
|
|
1607
1678
|
}
|
|
1608
1679
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1680
|
+
/**
|
|
1681
|
+
* Whether the resident history is the one an actor carrying `trustContext`
|
|
1682
|
+
* would be given.
|
|
1683
|
+
*
|
|
1684
|
+
* Personal memory is asked alongside the trust class because `loadFromDb`
|
|
1685
|
+
* gates personal-memory rehydration on `isPersonalMemoryAllowed`, which folds
|
|
1686
|
+
* in the disabled-auth elevation of an unbound actor: two contexts can share
|
|
1687
|
+
* a trust class and still differ here. A reuse that changes the answer has to
|
|
1688
|
+
* reload, or stale personal-memory blocks persist into a turn that must not
|
|
1689
|
+
* see them, or stay stripped from one that should.
|
|
1690
|
+
*/
|
|
1691
|
+
private historyMatchesScope(trustContext: TrustContext | undefined): boolean {
|
|
1692
|
+
return (
|
|
1621
1693
|
!this.loadedHistoryStale &&
|
|
1622
|
-
this.loadedHistoryTrustClass ===
|
|
1623
|
-
this.loadedHistoryPersonalMemoryAllowed ===
|
|
1624
|
-
|
|
1694
|
+
this.loadedHistoryTrustClass === trustContext?.trustClass &&
|
|
1695
|
+
this.loadedHistoryPersonalMemoryAllowed ===
|
|
1696
|
+
isPersonalMemoryAllowed(trustContext)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async ensureActorScopedHistory(): Promise<void> {
|
|
1701
|
+
if (this.historyMatchesScope(this.trustContext)) {
|
|
1625
1702
|
return;
|
|
1626
1703
|
}
|
|
1627
1704
|
await this.loadFromDb();
|
|
1628
1705
|
}
|
|
1629
1706
|
|
|
1707
|
+
/**
|
|
1708
|
+
* Mark stale when a row has been appended to the resident history under an
|
|
1709
|
+
* actor this history is not scoped for.
|
|
1710
|
+
*
|
|
1711
|
+
* A persist that runs outside any turn has nothing ensuring the history for
|
|
1712
|
+
* its sender first, so its row joins `this.messages` as it is. Left alone,
|
|
1713
|
+
* the next turn under the resident scope reuses that array rather than
|
|
1714
|
+
* reloading, and sends the model a row a reload would have filtered out.
|
|
1715
|
+
*
|
|
1716
|
+
* `trustContext` is the actor the row was attributed to, which for a caller
|
|
1717
|
+
* that resolved none is the conversation's own, matching what the persist
|
|
1718
|
+
* stamps. A scope that already matches is left alone: rows can arrive on a
|
|
1719
|
+
* camera's cadence, and a reload apiece is a cost worth the comparison.
|
|
1720
|
+
*/
|
|
1721
|
+
markHistoryStaleForForeignScope(
|
|
1722
|
+
trustContext: TrustContext | undefined,
|
|
1723
|
+
): void {
|
|
1724
|
+
if (this.historyMatchesScope(trustContext ?? this.trustContext)) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
this.markHistoryStale();
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1630
1730
|
/**
|
|
1631
1731
|
* Mark the resident history stale: a channel event (a reaction, edit, or
|
|
1632
1732
|
* delete) was persisted straight to the store while this conversation was
|
|
@@ -1799,7 +1899,12 @@ export class Conversation {
|
|
|
1799
1899
|
*/
|
|
1800
1900
|
setProcessing(value: boolean): void {
|
|
1801
1901
|
const wasProcessing = this._processing;
|
|
1902
|
+
const wasOwner = this.processingOwner;
|
|
1802
1903
|
this._processing = value;
|
|
1904
|
+
// Every set is a claim, including one over a flag another holder already
|
|
1905
|
+
// has: a caller setting it unconditionally is asserting the hold is now
|
|
1906
|
+
// theirs, and the previous holder's release must not undo that.
|
|
1907
|
+
this.processingOwner = value ? ++this.nextProcessingOwner : 0;
|
|
1803
1908
|
// Persist the cross-process source of truth so out-of-process callers
|
|
1804
1909
|
// (retrospective CLI, future detached workers) can detect mid-turn state
|
|
1805
1910
|
// by reading the conversations row directly.
|
|
@@ -1808,6 +1913,7 @@ export class Conversation {
|
|
|
1808
1913
|
setConversationProcessingStartedAt(this.conversationId, Date.now());
|
|
1809
1914
|
} catch (err) {
|
|
1810
1915
|
this._processing = wasProcessing;
|
|
1916
|
+
this.processingOwner = wasOwner;
|
|
1811
1917
|
throw err;
|
|
1812
1918
|
}
|
|
1813
1919
|
} else {
|
|
@@ -1830,6 +1936,171 @@ export class Conversation {
|
|
|
1830
1936
|
}
|
|
1831
1937
|
}
|
|
1832
1938
|
|
|
1939
|
+
/**
|
|
1940
|
+
* Take the processing flag if it is free, reporting the claim a later release
|
|
1941
|
+
* has to name. Null means someone already holds it.
|
|
1942
|
+
*
|
|
1943
|
+
* The read and the take are one synchronous step, so no second acquirer can
|
|
1944
|
+
* land between them, and the flag stays held while the marker write below
|
|
1945
|
+
* retries: reverting it because that write lost a race would publish an idle
|
|
1946
|
+
* conversation for as long as the retry sleeps, and anything polling for idle
|
|
1947
|
+
* takes the flag in that window while this caller believes its turn is
|
|
1948
|
+
* starting.
|
|
1949
|
+
*
|
|
1950
|
+
* The marker is a fence, not a hope. `processing_started_at` is what a
|
|
1951
|
+
* reconnecting client and the out-of-process retrospective worker read to
|
|
1952
|
+
* decide whether a turn is live, so a turn that proceeds while the column is
|
|
1953
|
+
* null lets a client stop waiting mid-turn and lets the worker fork partial
|
|
1954
|
+
* history. Every caller therefore awaits {@link ensureProcessingMarker}
|
|
1955
|
+
* before doing anything with the hold, and gives the hold back when it does
|
|
1956
|
+
* not land.
|
|
1957
|
+
*/
|
|
1958
|
+
acquireProcessing(): number | null {
|
|
1959
|
+
if (this._processing) {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
this._processing = true;
|
|
1963
|
+
const owner = ++this.nextProcessingOwner;
|
|
1964
|
+
this.processingOwner = owner;
|
|
1965
|
+
this.processingMarker = {
|
|
1966
|
+
owner,
|
|
1967
|
+
landed: this.mirrorProcessingStarted(owner, Date.now()),
|
|
1968
|
+
};
|
|
1969
|
+
return owner;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* Resolve once this claim's processing marker is durable. Reject when the
|
|
1974
|
+
* retry budget runs out without it landing, and reject when the claim is not
|
|
1975
|
+
* the live one.
|
|
1976
|
+
*
|
|
1977
|
+
* Lost ownership is a failure here, not a pass. The caller does its work
|
|
1978
|
+
* between this fence and its release, so answering "fine" for a hold that
|
|
1979
|
+
* Stop or a teardown has already cleared would let that work run under a
|
|
1980
|
+
* dead claim while a new turn acquires and writes alongside it.
|
|
1981
|
+
*
|
|
1982
|
+
* Asked again after the await because both can happen in one window: the
|
|
1983
|
+
* write lands, and the hold is claimed away before this resumes. The fence
|
|
1984
|
+
* has to describe the present, not the moment it started waiting.
|
|
1985
|
+
*/
|
|
1986
|
+
async ensureProcessingMarker(owner: number): Promise<void> {
|
|
1987
|
+
const marker = this.processingMarker;
|
|
1988
|
+
if (!marker || marker.owner !== owner || this.processingOwner !== owner) {
|
|
1989
|
+
throw new ProcessingClaimLostError(this.conversationId);
|
|
1990
|
+
}
|
|
1991
|
+
await marker.landed;
|
|
1992
|
+
if (this.processingOwner !== owner) {
|
|
1993
|
+
throw new ProcessingClaimLostError(this.conversationId);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* Take the flag and wait out its marker, giving the claim back rather than
|
|
1999
|
+
* ever leaving a live one unreleased.
|
|
2000
|
+
*
|
|
2001
|
+
* Null is "this conversation is someone else's", by either route: the flag
|
|
2002
|
+
* was already held, or the hold was claimed away while the marker was still
|
|
2003
|
+
* landing. Callers answer both with the busy behaviour they already owe.
|
|
2004
|
+
*
|
|
2005
|
+
* A throw is the marker itself refusing to persist, which is a real failure
|
|
2006
|
+
* rather than a busy conversation, and the claim is already released before
|
|
2007
|
+
* it is raised. Every acquire goes through here, so no call site can get the
|
|
2008
|
+
* ordering wrong: there is no window where a claim exists and its release is
|
|
2009
|
+
* not yet guaranteed.
|
|
2010
|
+
*/
|
|
2011
|
+
async acquireProcessingFenced(): Promise<number | null> {
|
|
2012
|
+
const owner = this.acquireProcessing();
|
|
2013
|
+
if (owner === null) {
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
try {
|
|
2017
|
+
await this.ensureProcessingMarker(owner);
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
// A no-op when the claim is already gone, which is exactly the case
|
|
2020
|
+
// this is here to make harmless.
|
|
2021
|
+
this.releaseProcessing(owner);
|
|
2022
|
+
if (err instanceof ProcessingClaimLostError) {
|
|
2023
|
+
return null;
|
|
2024
|
+
}
|
|
2025
|
+
throw err;
|
|
2026
|
+
}
|
|
2027
|
+
return owner;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Whether this claim is still the live hold on the conversation.
|
|
2032
|
+
*
|
|
2033
|
+
* For work that runs across awaits under a claim it took earlier. A Stop on
|
|
2034
|
+
* a hold with no live turn behind it force-clears the flag, and the next
|
|
2035
|
+
* request acquires, so a claim can go stale while its holder is mid-write.
|
|
2036
|
+
* Asking here is a plain field read, which is what lets a write fence ask it
|
|
2037
|
+
* in the same tick as the statement it guards.
|
|
2038
|
+
*/
|
|
2039
|
+
holdsProcessingClaim(owner: number): boolean {
|
|
2040
|
+
return this.processingOwner === owner;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Release a hold taken by {@link acquireProcessing}, and only that hold.
|
|
2045
|
+
* Reports whether it released.
|
|
2046
|
+
*
|
|
2047
|
+
* A hold can be claimed away by any unconditional `setProcessing(true)`,
|
|
2048
|
+
* which is how a turn starts. The earlier holder's release then has to do
|
|
2049
|
+
* nothing: clearing there would release a turn that is still running and let
|
|
2050
|
+
* the next one interleave into the rows it is still writing.
|
|
2051
|
+
*/
|
|
2052
|
+
releaseProcessing(owner: number): boolean {
|
|
2053
|
+
if (this.processingOwner !== owner) {
|
|
2054
|
+
log.debug(
|
|
2055
|
+
{
|
|
2056
|
+
conversationId: this.conversationId,
|
|
2057
|
+
owner,
|
|
2058
|
+
holder: this.processingOwner,
|
|
2059
|
+
},
|
|
2060
|
+
"Not releasing the processing flag: another holder claimed it",
|
|
2061
|
+
);
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
this.setProcessing(false);
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Write a taken processing lock into the `processing_started_at` column,
|
|
2070
|
+
* reporting through the promise {@link ensureProcessingMarker} hands back.
|
|
2071
|
+
*
|
|
2072
|
+
* The retry runs while the flag stays held, so the lock has no gap for
|
|
2073
|
+
* another acquirer, and it re-checks ownership rather than the flag: a hold
|
|
2074
|
+
* claimed since has written its own timestamp, and a late write from this one
|
|
2075
|
+
* would describe the wrong turn.
|
|
2076
|
+
*/
|
|
2077
|
+
private mirrorProcessingStarted(
|
|
2078
|
+
owner: number,
|
|
2079
|
+
startedAt: number,
|
|
2080
|
+
): Promise<void> {
|
|
2081
|
+
const landed = withSqliteRetry(
|
|
2082
|
+
() => {
|
|
2083
|
+
if (this.processingOwner !== owner) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
setConversationProcessingStartedAt(this.conversationId, startedAt);
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
op: "conversation:acquireProcessing",
|
|
2090
|
+
context: { conversationId: this.conversationId },
|
|
2091
|
+
},
|
|
2092
|
+
);
|
|
2093
|
+
// The caller awaits this, but only on its own next tick, so subscribe now
|
|
2094
|
+
// rather than let a rejection land with nothing attached to it.
|
|
2095
|
+
landed.catch((err: unknown) => {
|
|
2096
|
+
log.error(
|
|
2097
|
+
{ err, conversationId: this.conversationId },
|
|
2098
|
+
"Failed to persist the processing marker; the claim holding this conversation is given back",
|
|
2099
|
+
);
|
|
2100
|
+
});
|
|
2101
|
+
return landed;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1833
2104
|
/**
|
|
1834
2105
|
* Mirror a released processing lock into the advisory `processing_started_at`
|
|
1835
2106
|
* column, without ever reporting failure back to the release.
|
|
@@ -10,11 +10,6 @@ import type {
|
|
|
10
10
|
DictationSnippet,
|
|
11
11
|
} from "./dictation-profile-store.js";
|
|
12
12
|
|
|
13
|
-
/** Escape a string for safe use inside a RegExp. */
|
|
14
|
-
function escapeRegExp(s: string): string {
|
|
15
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
/**
|
|
19
14
|
* Wrap an escaped pattern with word boundaries.
|
|
20
15
|
*
|
|
@@ -65,7 +60,7 @@ export function expandSnippets(
|
|
|
65
60
|
|
|
66
61
|
// Build a single alternation pattern for single-pass replacement
|
|
67
62
|
const alternatives = sorted.map((s) =>
|
|
68
|
-
wrapWordBoundary(
|
|
63
|
+
wrapWordBoundary(RegExp.escape(s.trigger), s.trigger),
|
|
69
64
|
);
|
|
70
65
|
const pattern = new RegExp(`(?:${alternatives.join("|")})`, "gi");
|
|
71
66
|
|
|
@@ -116,7 +111,7 @@ export function applyDictionary(
|
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
const entries: EntryWithPattern[] = sorted.map((entry) => {
|
|
119
|
-
const escaped =
|
|
114
|
+
const escaped = RegExp.escape(entry.spoken);
|
|
120
115
|
const wholeWord = entry.wholeWord !== false; // default true
|
|
121
116
|
const pat = wholeWord ? wrapWordBoundary(escaped, entry.spoken) : escaped;
|
|
122
117
|
return { pattern: pat, entry };
|