@vellumai/assistant 0.12.2 → 0.12.3-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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
import {
|
|
44
44
|
addMessage,
|
|
45
45
|
type AgentLoopExitReason,
|
|
46
|
-
type ContentBlock,
|
|
47
46
|
type ConversationRow,
|
|
48
47
|
deleteConversation,
|
|
49
48
|
getConversation,
|
|
50
49
|
getConversationProcessingStartedAt,
|
|
50
|
+
getRecordedConversationToolSurface,
|
|
51
51
|
isConversationProcessing,
|
|
52
52
|
} from "@vellumai/plugin-api";
|
|
53
53
|
|
|
@@ -105,6 +105,12 @@ import {
|
|
|
105
105
|
upsertRetrospectiveState,
|
|
106
106
|
} from "./memory-retrospective-state.js";
|
|
107
107
|
import { effectiveSweepLookbackMs } from "./memory-retrospective-sweep.js";
|
|
108
|
+
import {
|
|
109
|
+
collectSuccessfulToolResultIds,
|
|
110
|
+
countDurableToolUses,
|
|
111
|
+
extractRememberContents,
|
|
112
|
+
hasCommittedTextReply,
|
|
113
|
+
} from "./memory-run-evidence.js";
|
|
108
114
|
|
|
109
115
|
const log = getLogger("memory-retrospective-job");
|
|
110
116
|
|
|
@@ -500,10 +506,9 @@ export async function runForkBasedRetrospective(
|
|
|
500
506
|
// Persona + tool-context parity pins derived from the source conversation
|
|
501
507
|
// (see `resolveSourceParityPins`), both passed unconditionally. The persona
|
|
502
508
|
// override keeps the system-prompt prefix in parity (and is a review-quality
|
|
503
|
-
// fix on its own); the tool-context pin
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// execution time.
|
|
509
|
+
// fix on its own); the tool-context pin resolves the fork's execution-side
|
|
510
|
+
// tool inventory under the source's client context and carries the wire
|
|
511
|
+
// surface when the source has no recorded one (`sourceToolSurface` below).
|
|
507
512
|
// Warm both guardian-delivery cache keys (vellum + unfiltered) so the sync
|
|
508
513
|
// slug resolution inside resolveSourceParityPins (resolveUserSlug(undefined)),
|
|
509
514
|
// including its any-channel fallback, hits fresh keys instead of falling
|
|
@@ -514,6 +519,16 @@ export async function runForkBasedRetrospective(
|
|
|
514
519
|
newMessages,
|
|
515
520
|
);
|
|
516
521
|
|
|
522
|
+
// The tools array the source's most recent live turn actually sent,
|
|
523
|
+
// replayed verbatim on the fork. The wire tool block is the first tier of
|
|
524
|
+
// the provider cache prefix (tools → system → messages), and re-deriving it
|
|
525
|
+
// on the fork cannot reproduce the source's bytes: this worker's registry
|
|
526
|
+
// has no user-plugin tools, host-tool gates read connected clients the
|
|
527
|
+
// worker never has, and presence derived from a persisted interface stamp
|
|
528
|
+
// misreads clientless turns. `null` (no live turn has recorded a surface)
|
|
529
|
+
// leaves the pin's derivation as the wire surface.
|
|
530
|
+
const sourceToolSurface = await readSourceToolSurface(sourceConversationId);
|
|
531
|
+
|
|
517
532
|
// `skipHintInjection: true` because the instruction is already a
|
|
518
533
|
// persisted message — the wake's hint sandwich would only duplicate it.
|
|
519
534
|
let wakeSucceeded = false;
|
|
@@ -542,16 +557,32 @@ export async function runForkBasedRetrospective(
|
|
|
542
557
|
"find_similar_skills",
|
|
543
558
|
]
|
|
544
559
|
: ["remember"],
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
560
|
+
// Keep the source's full tool surface on the wire: replay the array its
|
|
561
|
+
// last live turn sent when one is recorded, otherwise resolve the fork's
|
|
562
|
+
// own surface under the source's client context (`toolContextPin`). The
|
|
563
|
+
// wire tool block is the first tier of the provider cache prefix
|
|
564
|
+
// (tools → system → messages), so any wire difference busts cache parity
|
|
565
|
+
// with the source's live turns, re-creating the cached prefix instead of
|
|
566
|
+
// reading it. The recorded delegation-section state rides with the array
|
|
567
|
+
// into the fork's system prompt (the second tier) for the same reason:
|
|
568
|
+
// this wake's own scope cannot spawn, so deriving the section here
|
|
569
|
+
// renders it off where an interactive source rendered it on. The
|
|
570
|
+
// allowlist still holds at execution time: non-allowlisted calls are
|
|
571
|
+
// rejected before any executor or side effect runs. See
|
|
552
572
|
// {@link SubagentToolGateMode} and {@link WakeToolContextPin}.
|
|
553
573
|
toolGateMode: "execution" as const,
|
|
554
574
|
toolContextPin,
|
|
575
|
+
...(sourceToolSurface
|
|
576
|
+
? {
|
|
577
|
+
wireToolDefinitions: sourceToolSurface.tools,
|
|
578
|
+
...(sourceToolSurface.delegateIndependentTasks === null
|
|
579
|
+
? {}
|
|
580
|
+
: {
|
|
581
|
+
delegateIndependentTasks:
|
|
582
|
+
sourceToolSurface.delegateIndependentTasks,
|
|
583
|
+
}),
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
555
586
|
// Preactivate skill-management so its authoring tools (`find_similar_skills`
|
|
556
587
|
// / `scaffold_managed_skill` / the `skill_load` target) are in the turn's
|
|
557
588
|
// active set from turn 1; the checker's origin-scoped grant then makes them
|
|
@@ -731,6 +762,25 @@ function enqueueFollowUpJobs(): string[] {
|
|
|
731
762
|
return followUpJobIds;
|
|
732
763
|
}
|
|
733
764
|
|
|
765
|
+
/**
|
|
766
|
+
* The source's recorded wire surface (its tool array and the delegation
|
|
767
|
+
* section state of its system prompt), or `null` when none is recorded or the
|
|
768
|
+
* read fails (logged; the fork then derives both for itself).
|
|
769
|
+
*/
|
|
770
|
+
async function readSourceToolSurface(
|
|
771
|
+
sourceConversationId: string,
|
|
772
|
+
): ReturnType<typeof getRecordedConversationToolSurface> {
|
|
773
|
+
try {
|
|
774
|
+
return await getRecordedConversationToolSurface(sourceConversationId);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
log.warn(
|
|
777
|
+
{ err, sourceConversationId },
|
|
778
|
+
"memory-retrospective (fork): failed to read the source's recorded tool surface; deriving the wire surface instead",
|
|
779
|
+
);
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
734
784
|
/**
|
|
735
785
|
* The source-derived parity pins the fork wake runs under: the system-prompt
|
|
736
786
|
* persona override and the tool-resolution context pin. Both exist so the
|
|
@@ -1252,177 +1302,16 @@ async function collectRetrospectiveRunEvidence(
|
|
|
1252
1302
|
const succeededIds = collectSuccessfulToolResultIds(runMessages);
|
|
1253
1303
|
return {
|
|
1254
1304
|
remembers: extractRememberContents(runMessages, succeededIds),
|
|
1255
|
-
durableToolCallCount: countDurableToolUses(
|
|
1256
|
-
|
|
1305
|
+
durableToolCallCount: countDurableToolUses(
|
|
1306
|
+
runMessages,
|
|
1307
|
+
DURABLE_RETROSPECTIVE_TOOLS,
|
|
1308
|
+
succeededIds,
|
|
1309
|
+
),
|
|
1310
|
+
durableToolAttemptCount: countDurableToolUses(
|
|
1311
|
+
runMessages,
|
|
1312
|
+
DURABLE_RETROSPECTIVE_TOOLS,
|
|
1313
|
+
null,
|
|
1314
|
+
),
|
|
1257
1315
|
committedTextReply: hasCommittedTextReply(runMessages),
|
|
1258
1316
|
};
|
|
1259
1317
|
}
|
|
1260
|
-
|
|
1261
|
-
/**
|
|
1262
|
-
* Whether the LAST persisted assistant row on the run's tail carries a text
|
|
1263
|
-
* block with non-whitespace content. Paired with a model-driven stop and
|
|
1264
|
-
* zero memory-write attempts, that closing reply is the persisted artifact
|
|
1265
|
-
* of a pass that read its window and had nothing to save: the model spoke
|
|
1266
|
-
* and then chose to end the run. Reading only the final row separates it
|
|
1267
|
-
* from a run whose narration went live but whose actual conclusion was
|
|
1268
|
-
* empty, as well as from a response that committed nothing at all
|
|
1269
|
-
* (thinking-only output, an empty content array).
|
|
1270
|
-
*/
|
|
1271
|
-
function hasCommittedTextReply(messages: MessageLike[]): boolean {
|
|
1272
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1273
|
-
const msg = messages[i]!;
|
|
1274
|
-
if (msg.role !== "assistant") {
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
const blocks = parseMessageBlocks(msg);
|
|
1278
|
-
if (blocks === null) {
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
return blocks.some(
|
|
1282
|
-
(b) =>
|
|
1283
|
-
b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
return false;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Ids of `tool_result` blocks on the run's user rows whose execution did not
|
|
1291
|
-
* report an error. Robust to malformed content JSON the same way
|
|
1292
|
-
* `extractRememberContents` is.
|
|
1293
|
-
*/
|
|
1294
|
-
function collectSuccessfulToolResultIds(messages: MessageLike[]): Set<string> {
|
|
1295
|
-
const ids = new Set<string>();
|
|
1296
|
-
for (const msg of messages) {
|
|
1297
|
-
if (msg.role !== "user") {
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
1301
|
-
// guard:allow-tool-result-only: success evidence for locally-executed
|
|
1302
|
-
// durable memory tools; server-side web_search_tool_result never
|
|
1303
|
-
// corresponds to a durable write and carries no is_error flag.
|
|
1304
|
-
if (
|
|
1305
|
-
b.type === "tool_result" &&
|
|
1306
|
-
typeof b.tool_use_id === "string" &&
|
|
1307
|
-
b.is_error !== true
|
|
1308
|
-
) {
|
|
1309
|
-
ids.add(b.tool_use_id);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return ids;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
/**
|
|
1317
|
-
* Count persisted `tool_use` blocks whose `name` is in
|
|
1318
|
-
* {@link DURABLE_RETROSPECTIVE_TOOLS} across the run's assistant rows.
|
|
1319
|
-
* With a `succeededIds` set, only calls whose id has a matching successful
|
|
1320
|
-
* `tool_result` count (verified executions); with `null`, every attempt
|
|
1321
|
-
* counts regardless of outcome.
|
|
1322
|
-
*/
|
|
1323
|
-
function countDurableToolUses(
|
|
1324
|
-
messages: MessageLike[],
|
|
1325
|
-
succeededIds: ReadonlySet<string> | null,
|
|
1326
|
-
): number {
|
|
1327
|
-
let count = 0;
|
|
1328
|
-
for (const msg of messages) {
|
|
1329
|
-
if (msg.role !== "assistant") {
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
1333
|
-
if (
|
|
1334
|
-
b.type === "tool_use" &&
|
|
1335
|
-
DURABLE_RETROSPECTIVE_TOOLS.has(String(b.name)) &&
|
|
1336
|
-
(succeededIds === null ||
|
|
1337
|
-
(typeof b.id === "string" && succeededIds.has(b.id)))
|
|
1338
|
-
) {
|
|
1339
|
-
count += 1;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
return count;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
interface MessageLike {
|
|
1347
|
-
role: string;
|
|
1348
|
-
content: string | ContentBlock[];
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
/**
|
|
1352
|
-
* Parse a message row's content into its block objects, or `null` when the
|
|
1353
|
-
* content is malformed (unparseable JSON, not an array). Non-object entries
|
|
1354
|
-
* are dropped. Every evidence reader in this module goes through this so
|
|
1355
|
-
* malformed rows degrade the same way everywhere: skipped, not propagated.
|
|
1356
|
-
*/
|
|
1357
|
-
function parseMessageBlocks(
|
|
1358
|
-
msg: MessageLike,
|
|
1359
|
-
): Record<string, unknown>[] | null {
|
|
1360
|
-
let blocks: unknown = msg.content;
|
|
1361
|
-
if (typeof blocks === "string") {
|
|
1362
|
-
try {
|
|
1363
|
-
blocks = JSON.parse(blocks);
|
|
1364
|
-
} catch {
|
|
1365
|
-
return null;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
if (!Array.isArray(blocks)) {
|
|
1369
|
-
return null;
|
|
1370
|
-
}
|
|
1371
|
-
return blocks.filter(
|
|
1372
|
-
(block): block is Record<string, unknown> =>
|
|
1373
|
-
typeof block === "object" && block !== null,
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
/**
|
|
1378
|
-
* Scan an array of message rows for `tool_use` blocks where `name` is
|
|
1379
|
-
* `"remember"` and return the `input.content` strings in order. Robust to
|
|
1380
|
-
* malformed content JSON — unparseable rows are skipped, not propagated.
|
|
1381
|
-
*/
|
|
1382
|
-
function extractRememberContents(
|
|
1383
|
-
messages: MessageLike[],
|
|
1384
|
-
succeededIds?: ReadonlySet<string>,
|
|
1385
|
-
): string[] {
|
|
1386
|
-
const contents: string[] = [];
|
|
1387
|
-
for (const msg of messages) {
|
|
1388
|
-
if (msg.role !== "assistant") {
|
|
1389
|
-
continue;
|
|
1390
|
-
}
|
|
1391
|
-
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
1392
|
-
if (b.type !== "tool_use") {
|
|
1393
|
-
continue;
|
|
1394
|
-
}
|
|
1395
|
-
if (b.name !== "remember") {
|
|
1396
|
-
continue;
|
|
1397
|
-
}
|
|
1398
|
-
// When a success set is provided, only executions that reported a
|
|
1399
|
-
// non-error tool_result contribute facts: a failed remember never
|
|
1400
|
-
// wrote the buffer, and logging its facts would suppress the retry's
|
|
1401
|
-
// re-save via <already_remembered>.
|
|
1402
|
-
if (
|
|
1403
|
-
succeededIds !== undefined &&
|
|
1404
|
-
(typeof b.id !== "string" || !succeededIds.has(b.id))
|
|
1405
|
-
) {
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
const input = b.input;
|
|
1409
|
-
if (!input || typeof input !== "object") {
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
const content = (input as Record<string, unknown>).content;
|
|
1413
|
-
// `remember` accepts a single string or an array of facts (batch form);
|
|
1414
|
-
// flatten both so batched saves still feed the dedup baseline.
|
|
1415
|
-
const facts = Array.isArray(content) ? content : [content];
|
|
1416
|
-
for (const fact of facts) {
|
|
1417
|
-
if (typeof fact !== "string") {
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
const trimmed = fact.trim();
|
|
1421
|
-
if (trimmed.length > 0) {
|
|
1422
|
-
contents.push(trimmed);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
return contents;
|
|
1428
|
-
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory run evidence: what a background memory run durably produced.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// A background run's persisted message rows are the one record of what it
|
|
6
|
+
// did. A `tool_use` block proves only that the model asked; the durable write
|
|
7
|
+
// happens inside the executor, so a call counts as evidence only when a
|
|
8
|
+
// matching non-error `tool_result` is persisted on the same rows. The
|
|
9
|
+
// readers here apply that rule uniformly so every memory job that gates a
|
|
10
|
+
// state transition on "the run verifiably wrote something" (the
|
|
11
|
+
// retrospective's cursor advance, consolidation's buffer consumption)
|
|
12
|
+
// reads the same evidence the same way.
|
|
13
|
+
//
|
|
14
|
+
// This module sits at the plugin root (shared infra, not a tier) and imports
|
|
15
|
+
// nothing from the plugin, so `substrate/` and the spine can both reach it
|
|
16
|
+
// without a tier importing spine.
|
|
17
|
+
|
|
18
|
+
import type { ContentBlock } from "@vellumai/plugin-api";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the LAST persisted assistant row on the run's tail carries a text
|
|
22
|
+
* block with non-whitespace content. Paired with a model-driven stop and
|
|
23
|
+
* zero memory-write attempts, that closing reply is the persisted artifact
|
|
24
|
+
* of a pass that read its window and had nothing to save: the model spoke
|
|
25
|
+
* and then chose to end the run. Reading only the final row separates it
|
|
26
|
+
* from a run whose narration went live but whose actual conclusion was
|
|
27
|
+
* empty, as well as from a response that committed nothing at all
|
|
28
|
+
* (thinking-only output, an empty content array).
|
|
29
|
+
*/
|
|
30
|
+
export function hasCommittedTextReply(messages: MessageLike[]): boolean {
|
|
31
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
32
|
+
const msg = messages[i]!;
|
|
33
|
+
if (msg.role !== "assistant") {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const blocks = parseMessageBlocks(msg);
|
|
37
|
+
if (blocks === null) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return blocks.some(
|
|
41
|
+
(b) =>
|
|
42
|
+
b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the run's FINAL persisted row is an assistant reply in its own
|
|
50
|
+
* words: text with non-whitespace content and no `tool_use` block. This is
|
|
51
|
+
* the shape of a run the model ended itself, since the loop stops when the
|
|
52
|
+
* assistant answers without asking for a tool. It is stricter than
|
|
53
|
+
* {@link hasCommittedTextReply} on purpose: narration on a row that also
|
|
54
|
+
* calls a tool ("fixing that page now" followed by `file_write`) is not a
|
|
55
|
+
* conclusion, and a run whose last row is a tool result stopped mid-loop.
|
|
56
|
+
*/
|
|
57
|
+
export function endsWithTextReply(messages: MessageLike[]): boolean {
|
|
58
|
+
const last = messages[messages.length - 1];
|
|
59
|
+
if (last === undefined || last.role !== "assistant") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const blocks = parseMessageBlocks(last);
|
|
63
|
+
if (blocks === null || blocks.some((b) => b.type === "tool_use")) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return blocks.some(
|
|
67
|
+
(b) =>
|
|
68
|
+
b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ids of `tool_result` blocks on the run's user rows whose execution did not
|
|
74
|
+
* report an error. Robust to malformed content JSON the same way
|
|
75
|
+
* `extractRememberContents` is.
|
|
76
|
+
*/
|
|
77
|
+
export function collectSuccessfulToolResultIds(
|
|
78
|
+
messages: MessageLike[],
|
|
79
|
+
): Set<string> {
|
|
80
|
+
const ids = new Set<string>();
|
|
81
|
+
for (const msg of messages) {
|
|
82
|
+
if (msg.role !== "user") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
86
|
+
// guard:allow-tool-result-only: success evidence for locally-executed
|
|
87
|
+
// durable memory tools; server-side web_search_tool_result never
|
|
88
|
+
// corresponds to a durable write and carries no is_error flag.
|
|
89
|
+
if (
|
|
90
|
+
b.type === "tool_result" &&
|
|
91
|
+
typeof b.tool_use_id === "string" &&
|
|
92
|
+
b.is_error !== true
|
|
93
|
+
) {
|
|
94
|
+
ids.add(b.tool_use_id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ids;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Count persisted `tool_use` blocks whose `name` is in `durableTools` across
|
|
103
|
+
* the run's assistant rows. With a `succeededIds` set, only calls whose id
|
|
104
|
+
* has a matching successful `tool_result` count (verified executions); with
|
|
105
|
+
* `null`, every attempt counts regardless of outcome.
|
|
106
|
+
*/
|
|
107
|
+
export function countDurableToolUses(
|
|
108
|
+
messages: MessageLike[],
|
|
109
|
+
durableTools: ReadonlySet<string>,
|
|
110
|
+
succeededIds: ReadonlySet<string> | null,
|
|
111
|
+
): number {
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (const msg of messages) {
|
|
114
|
+
if (msg.role !== "assistant") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
118
|
+
if (
|
|
119
|
+
b.type === "tool_use" &&
|
|
120
|
+
durableTools.has(String(b.name)) &&
|
|
121
|
+
(succeededIds === null ||
|
|
122
|
+
(typeof b.id === "string" && succeededIds.has(b.id)))
|
|
123
|
+
) {
|
|
124
|
+
count += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MessageLike {
|
|
132
|
+
role: string;
|
|
133
|
+
content: string | ContentBlock[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse a message row's content into its block objects, or `null` when the
|
|
138
|
+
* content is malformed (unparseable JSON, not an array). Non-object entries
|
|
139
|
+
* are dropped. Every evidence reader in this module goes through this so
|
|
140
|
+
* malformed rows degrade the same way everywhere: skipped, not propagated.
|
|
141
|
+
*/
|
|
142
|
+
function parseMessageBlocks(
|
|
143
|
+
msg: MessageLike,
|
|
144
|
+
): Record<string, unknown>[] | null {
|
|
145
|
+
let blocks: unknown = msg.content;
|
|
146
|
+
if (typeof blocks === "string") {
|
|
147
|
+
try {
|
|
148
|
+
blocks = JSON.parse(blocks);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(blocks)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return blocks.filter(
|
|
157
|
+
(block): block is Record<string, unknown> =>
|
|
158
|
+
typeof block === "object" && block !== null,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Scan an array of message rows for `tool_use` blocks where `name` is
|
|
164
|
+
* `"remember"` and return the `input.content` strings in order. Robust to
|
|
165
|
+
* malformed content JSON: unparseable rows are skipped, not propagated.
|
|
166
|
+
*/
|
|
167
|
+
export function extractRememberContents(
|
|
168
|
+
messages: MessageLike[],
|
|
169
|
+
succeededIds?: ReadonlySet<string>,
|
|
170
|
+
): string[] {
|
|
171
|
+
const contents: string[] = [];
|
|
172
|
+
for (const msg of messages) {
|
|
173
|
+
if (msg.role !== "assistant") {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const b of parseMessageBlocks(msg) ?? []) {
|
|
177
|
+
if (b.type !== "tool_use") {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (b.name !== "remember") {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// When a success set is provided, only executions that reported a
|
|
184
|
+
// non-error tool_result contribute facts: a failed remember never
|
|
185
|
+
// wrote the buffer, and logging its facts would suppress the retry's
|
|
186
|
+
// re-save via <already_remembered>.
|
|
187
|
+
if (
|
|
188
|
+
succeededIds !== undefined &&
|
|
189
|
+
(typeof b.id !== "string" || !succeededIds.has(b.id))
|
|
190
|
+
) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const input = b.input;
|
|
194
|
+
if (!input || typeof input !== "object") {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const content = (input as Record<string, unknown>).content;
|
|
198
|
+
// `remember` accepts a single string or an array of facts (batch form);
|
|
199
|
+
// flatten both so batched saves still feed the dedup baseline.
|
|
200
|
+
const facts = Array.isArray(content) ? content : [content];
|
|
201
|
+
for (const fact of facts) {
|
|
202
|
+
if (typeof fact !== "string") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const trimmed = fact.trim();
|
|
206
|
+
if (trimmed.length > 0) {
|
|
207
|
+
contents.push(trimmed);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return contents;
|
|
213
|
+
}
|