@vellumai/assistant 0.12.2-staging.7 → 0.12.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/docs/architecture/memory.md +2 -11
- package/docs/desktop-browser-cli.md +2 -4
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/openapi.yaml +2 -6
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +0 -124
- package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
- package/src/__tests__/channel-approval.test.ts +14 -9
- package/src/__tests__/conversation-agent-loop.test.ts +0 -25
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
- package/src/__tests__/terminal-tools.test.ts +0 -8
- package/src/__tests__/unicode.test.ts +0 -36
- package/src/agent/loop.ts +0 -19
- package/src/api/index.ts +0 -6
- package/src/approvals/approval-primitive.ts +2 -5
- package/src/approvals/scoped-approval-grants.ts +2 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
- package/src/daemon/conversation-agent-loop.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +1 -57
- package/src/daemon/conversation.ts +0 -17
- package/src/daemon/daemon-control.ts +6 -2
- package/src/daemon/orphan-reaper.ts +3 -4
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-conversation-ops.ts +15 -38
- package/src/desktop/desktop-automation-lease.test.ts +0 -143
- package/src/desktop/desktop-automation-lease.ts +3 -39
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +0 -70
- package/src/notifications/copy-composer.ts +3 -11
- package/src/oauth/seed-providers.ts +30 -0
- package/src/persistence/conversation-plugin-facade.ts +0 -13
- package/src/persistence/schema/index.ts +0 -1
- package/src/persistence/steps.ts +0 -2
- package/src/plugin-api/conversation-turn.ts +7 -31
- package/src/plugin-api/index.ts +1 -9
- package/src/plugins/defaults/memory/AGENTS.md +2 -14
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
- package/src/plugins/defaults/memory/buffer-format.ts +0 -40
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
- package/src/plugins/defaults/memory/context-search/format.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +3 -4
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
- package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
- 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 +1 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
- package/src/plugins/defaults/memory/v3/card.ts +1 -2
- package/src/plugins/defaults/memory/v3/injector.ts +178 -212
- package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
- package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
- package/src/plugins/defaults/memory/v3/sections.ts +1 -2
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +1 -86
- package/src/runtime/agent-wake.ts +4 -20
- package/src/runtime/guardian-action-service.ts +17 -2
- package/src/runtime/guardian-reply-router.ts +8 -1
- package/src/runtime/routes/channel-route-shared.ts +9 -1
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +3 -7
- package/src/runtime/routes/guardian-approval-interception.ts +0 -24
- package/src/runtime/routes/inbound-message-handler.ts +3 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +6 -12
- package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
- package/src/tools/shared/shell-output.test.ts +0 -10
- package/src/tools/shared/shell-output.ts +2 -14
- package/src/tools/skills/sandbox-runner.ts +2 -13
- package/src/tools/skills/scaffold-managed.ts +1 -2
- package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
- package/src/tools/terminal/safe-env.ts +1 -30
- package/src/tools/terminal/sanitized-bash.ts +2 -15
- package/src/tools/terminal/shell.test.ts +0 -29
- package/src/tools/terminal/shell.ts +7 -13
- package/src/util/host-process.test.ts +1 -17
- package/src/util/host-process.ts +0 -24
- package/src/util/unicode.ts +0 -29
- package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
- package/src/api/events/desktop-activity-changed.ts +0 -10
- package/src/persistence/conversation-tool-surface.ts +0 -86
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
- package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
- package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
- package/src/plugins/defaults/memory/buffer-file.ts +0 -354
- package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
- package/src/tools/terminal/shell-launch.test.ts +0 -162
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
import {
|
|
44
44
|
addMessage,
|
|
45
45
|
type AgentLoopExitReason,
|
|
46
|
+
type ContentBlock,
|
|
46
47
|
type ConversationRow,
|
|
47
48
|
deleteConversation,
|
|
48
49
|
getConversation,
|
|
49
50
|
getConversationProcessingStartedAt,
|
|
50
|
-
getConversationToolSurface,
|
|
51
51
|
isConversationProcessing,
|
|
52
52
|
} from "@vellumai/plugin-api";
|
|
53
53
|
|
|
@@ -105,12 +105,6 @@ 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";
|
|
114
108
|
|
|
115
109
|
const log = getLogger("memory-retrospective-job");
|
|
116
110
|
|
|
@@ -506,9 +500,10 @@ export async function runForkBasedRetrospective(
|
|
|
506
500
|
// Persona + tool-context parity pins derived from the source conversation
|
|
507
501
|
// (see `resolveSourceParityPins`), both passed unconditionally. The persona
|
|
508
502
|
// override keeps the system-prompt prefix in parity (and is a review-quality
|
|
509
|
-
// fix on its own); the tool-context pin
|
|
510
|
-
//
|
|
511
|
-
// surface
|
|
503
|
+
// fix on its own); the tool-context pin keeps the wire tool surface in
|
|
504
|
+
// parity — the fork always runs execution gate mode below, so the source's
|
|
505
|
+
// full tool surface stays on the wire while the allowlist holds at
|
|
506
|
+
// execution time.
|
|
512
507
|
// Warm both guardian-delivery cache keys (vellum + unfiltered) so the sync
|
|
513
508
|
// slug resolution inside resolveSourceParityPins (resolveUserSlug(undefined)),
|
|
514
509
|
// including its any-channel fallback, hits fresh keys instead of falling
|
|
@@ -519,16 +514,6 @@ export async function runForkBasedRetrospective(
|
|
|
519
514
|
newMessages,
|
|
520
515
|
);
|
|
521
516
|
|
|
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
|
-
|
|
532
517
|
// `skipHintInjection: true` because the instruction is already a
|
|
533
518
|
// persisted message — the wake's hint sandwich would only duplicate it.
|
|
534
519
|
let wakeSucceeded = false;
|
|
@@ -557,18 +542,16 @@ export async function runForkBasedRetrospective(
|
|
|
557
542
|
"find_similar_skills",
|
|
558
543
|
]
|
|
559
544
|
: ["remember"],
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
// with the source's live turns, re-creating the cached prefix instead of
|
|
545
|
+
// Always keep the source's full tool surface on the wire and resolve it
|
|
546
|
+
// under the source's client context (`toolContextPin`). The wire tool
|
|
547
|
+
// block is the first tier of the provider cache prefix
|
|
548
|
+
// (tools → system → messages), so a wire filter busts cache parity with
|
|
549
|
+
// the source's live turns — re-creating the cached prefix instead of
|
|
566
550
|
// reading it. The allowlist still holds at execution time: non-allowlisted
|
|
567
551
|
// calls are rejected before any executor or side effect runs. See
|
|
568
552
|
// {@link SubagentToolGateMode} and {@link WakeToolContextPin}.
|
|
569
553
|
toolGateMode: "execution" as const,
|
|
570
554
|
toolContextPin,
|
|
571
|
-
...(sourceToolSurface ? { wireToolDefinitions: sourceToolSurface } : {}),
|
|
572
555
|
// Preactivate skill-management so its authoring tools (`find_similar_skills`
|
|
573
556
|
// / `scaffold_managed_skill` / the `skill_load` target) are in the turn's
|
|
574
557
|
// active set from turn 1; the checker's origin-scoped grant then makes them
|
|
@@ -748,24 +731,6 @@ function enqueueFollowUpJobs(): string[] {
|
|
|
748
731
|
return followUpJobIds;
|
|
749
732
|
}
|
|
750
733
|
|
|
751
|
-
/**
|
|
752
|
-
* The source's recorded wire tool array, or `null` when none is recorded or
|
|
753
|
-
* the read fails (logged; the fork then derives its wire surface).
|
|
754
|
-
*/
|
|
755
|
-
async function readSourceToolSurface(
|
|
756
|
-
sourceConversationId: string,
|
|
757
|
-
): ReturnType<typeof getConversationToolSurface> {
|
|
758
|
-
try {
|
|
759
|
-
return await getConversationToolSurface(sourceConversationId);
|
|
760
|
-
} catch (err) {
|
|
761
|
-
log.warn(
|
|
762
|
-
{ err, sourceConversationId },
|
|
763
|
-
"memory-retrospective (fork): failed to read the source's recorded tool surface; deriving the wire surface instead",
|
|
764
|
-
);
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
734
|
/**
|
|
770
735
|
* The source-derived parity pins the fork wake runs under: the system-prompt
|
|
771
736
|
* persona override and the tool-resolution context pin. Both exist so the
|
|
@@ -1287,16 +1252,177 @@ async function collectRetrospectiveRunEvidence(
|
|
|
1287
1252
|
const succeededIds = collectSuccessfulToolResultIds(runMessages);
|
|
1288
1253
|
return {
|
|
1289
1254
|
remembers: extractRememberContents(runMessages, succeededIds),
|
|
1290
|
-
durableToolCallCount: countDurableToolUses(
|
|
1291
|
-
|
|
1292
|
-
DURABLE_RETROSPECTIVE_TOOLS,
|
|
1293
|
-
succeededIds,
|
|
1294
|
-
),
|
|
1295
|
-
durableToolAttemptCount: countDurableToolUses(
|
|
1296
|
-
runMessages,
|
|
1297
|
-
DURABLE_RETROSPECTIVE_TOOLS,
|
|
1298
|
-
null,
|
|
1299
|
-
),
|
|
1255
|
+
durableToolCallCount: countDurableToolUses(runMessages, succeededIds),
|
|
1256
|
+
durableToolAttemptCount: countDurableToolUses(runMessages, null),
|
|
1300
1257
|
committedTextReply: hasCommittedTextReply(runMessages),
|
|
1301
1258
|
};
|
|
1302
1259
|
}
|
|
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
|
+
}
|