@vellumai/assistant 0.6.0 → 0.6.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// retrieval mode based on conversation state.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
import { and, desc, eq, ne } from "drizzle-orm";
|
|
9
|
+
import { and, desc, eq, inArray, ne, notInArray } from "drizzle-orm";
|
|
10
10
|
|
|
11
11
|
import type { AssistantConfig } from "../../config/types.js";
|
|
12
12
|
import { estimateTextTokens } from "../../context/token-estimator.js";
|
|
@@ -20,22 +20,22 @@ import { getLogger } from "../../util/logger.js";
|
|
|
20
20
|
import { getDb } from "../db.js";
|
|
21
21
|
import { memorySummaries } from "../schema.js";
|
|
22
22
|
import { conversations } from "../schema/conversations.js";
|
|
23
|
+
import {
|
|
24
|
+
loadGraphMemoryState,
|
|
25
|
+
saveGraphMemoryState,
|
|
26
|
+
} from "./graph-memory-state-store.js";
|
|
23
27
|
import {
|
|
24
28
|
assembleContextBlock,
|
|
25
29
|
assembleInjectionBlock,
|
|
26
30
|
InContextTracker,
|
|
31
|
+
type InContextTrackerSnapshot,
|
|
27
32
|
MAX_CONTEXT_LOAD_IMAGES,
|
|
28
33
|
MAX_PER_TURN_IMAGES,
|
|
29
|
-
MAX_REFRESH_IMAGES,
|
|
30
34
|
type ResolvedImage,
|
|
31
35
|
resolveInjectionImages,
|
|
32
36
|
} from "./injection.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
REFRESH_INTERVAL_TURNS,
|
|
36
|
-
refreshContextMemory,
|
|
37
|
-
retrieveForTurn,
|
|
38
|
-
} from "./retriever.js";
|
|
37
|
+
import { loadContextMemory, retrieveForTurn } from "./retriever.js";
|
|
38
|
+
import type { RetrievalMetrics } from "./types.js";
|
|
39
39
|
|
|
40
40
|
const log = getLogger("graph-conversation-memory");
|
|
41
41
|
|
|
@@ -51,10 +51,9 @@ const ESTIMATED_IMAGE_TOKENS = 1000;
|
|
|
51
51
|
*/
|
|
52
52
|
export class ConversationGraphMemory {
|
|
53
53
|
readonly tracker = new InContextTracker();
|
|
54
|
-
private turnCount = 0;
|
|
55
54
|
private initialized = false;
|
|
56
|
-
private lastCompactedAt: number | null = null;
|
|
57
55
|
private needsReload = false;
|
|
56
|
+
private stateRestored = false;
|
|
58
57
|
private scopeId: string;
|
|
59
58
|
private conversationId: string;
|
|
60
59
|
private lastInjectedBlock: string | null = null;
|
|
@@ -66,6 +65,65 @@ export class ConversationGraphMemory {
|
|
|
66
65
|
this.conversationId = conversationId;
|
|
67
66
|
}
|
|
68
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Persist tracker state to the database so it survives eviction.
|
|
70
|
+
* Called during conversation disposal.
|
|
71
|
+
*/
|
|
72
|
+
persistState(): void {
|
|
73
|
+
if (!this.initialized) return;
|
|
74
|
+
try {
|
|
75
|
+
const snapshot: InContextTrackerSnapshot & {
|
|
76
|
+
initialized: boolean;
|
|
77
|
+
needsReload: boolean;
|
|
78
|
+
} = {
|
|
79
|
+
initialized: this.initialized,
|
|
80
|
+
needsReload: this.needsReload,
|
|
81
|
+
...this.tracker.toJSON(),
|
|
82
|
+
};
|
|
83
|
+
saveGraphMemoryState(this.conversationId, JSON.stringify(snapshot));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
log.warn(
|
|
86
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
87
|
+
"Failed to persist graph memory state (non-fatal)",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Restore tracker state from the database after eviction + recreation.
|
|
94
|
+
* On failure or missing row, silently falls back to full context-load.
|
|
95
|
+
*/
|
|
96
|
+
restoreState(): void {
|
|
97
|
+
if (this.stateRestored) return;
|
|
98
|
+
try {
|
|
99
|
+
const json = loadGraphMemoryState(this.conversationId);
|
|
100
|
+
if (!json) return;
|
|
101
|
+
|
|
102
|
+
const snapshot = JSON.parse(json) as InContextTrackerSnapshot & {
|
|
103
|
+
initialized: boolean;
|
|
104
|
+
needsReload?: boolean;
|
|
105
|
+
};
|
|
106
|
+
this.initialized = snapshot.initialized;
|
|
107
|
+
this.needsReload = snapshot.needsReload ?? false;
|
|
108
|
+
this.tracker.restoreFrom(snapshot);
|
|
109
|
+
this.stateRestored = true;
|
|
110
|
+
|
|
111
|
+
log.info(
|
|
112
|
+
{
|
|
113
|
+
conversationId: this.conversationId,
|
|
114
|
+
turn: snapshot.currentTurn,
|
|
115
|
+
inContextCount: snapshot.inContext.length,
|
|
116
|
+
},
|
|
117
|
+
"Restored graph memory state after eviction",
|
|
118
|
+
);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn(
|
|
121
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
122
|
+
"Failed to restore graph memory state — will do full context load",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
69
127
|
/**
|
|
70
128
|
* Fetch the most recent conversation summaries (excluding the current
|
|
71
129
|
* conversation, which won't have one yet at context-load time).
|
|
@@ -93,7 +151,15 @@ export class ConversationGraphMemory {
|
|
|
93
151
|
conversations,
|
|
94
152
|
eq(memorySummaries.scopeKey, conversations.id),
|
|
95
153
|
)
|
|
96
|
-
.where(
|
|
154
|
+
.where(
|
|
155
|
+
and(
|
|
156
|
+
baseWhere,
|
|
157
|
+
notInArray(conversations.conversationType, [
|
|
158
|
+
"background",
|
|
159
|
+
"scheduled",
|
|
160
|
+
]),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
97
163
|
.orderBy(desc(memorySummaries.updatedAt))
|
|
98
164
|
.limit(3)
|
|
99
165
|
.all();
|
|
@@ -102,7 +168,7 @@ export class ConversationGraphMemory {
|
|
|
102
168
|
return userRows.map((r) => r.summary);
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
// Fill remaining slots with at most 1 background conversation
|
|
171
|
+
// Fill remaining slots with at most 1 background/scheduled conversation
|
|
106
172
|
const remaining = Math.min(1, 3 - userRows.length);
|
|
107
173
|
const bgRows = db
|
|
108
174
|
.select({ summary: memorySummaries.summary })
|
|
@@ -111,7 +177,15 @@ export class ConversationGraphMemory {
|
|
|
111
177
|
conversations,
|
|
112
178
|
eq(memorySummaries.scopeKey, conversations.id),
|
|
113
179
|
)
|
|
114
|
-
.where(
|
|
180
|
+
.where(
|
|
181
|
+
and(
|
|
182
|
+
baseWhere,
|
|
183
|
+
inArray(conversations.conversationType, [
|
|
184
|
+
"background",
|
|
185
|
+
"scheduled",
|
|
186
|
+
]),
|
|
187
|
+
),
|
|
188
|
+
)
|
|
115
189
|
.orderBy(desc(memorySummaries.updatedAt))
|
|
116
190
|
.limit(remaining)
|
|
117
191
|
.all();
|
|
@@ -133,7 +207,6 @@ export class ConversationGraphMemory {
|
|
|
133
207
|
// so we conservatively clear everything and reload.
|
|
134
208
|
this.tracker.evictCompactedTurns(this.tracker.getTurn());
|
|
135
209
|
this.needsReload = true;
|
|
136
|
-
this.lastCompactedAt = Date.now();
|
|
137
210
|
log.info(
|
|
138
211
|
{ compactedMessageCount },
|
|
139
212
|
"Compaction detected — will reload context on next turn",
|
|
@@ -184,7 +257,6 @@ export class ConversationGraphMemory {
|
|
|
184
257
|
*
|
|
185
258
|
* Dispatches to the appropriate retrieval mode:
|
|
186
259
|
* - Turn 1 (or after compaction): full context load
|
|
187
|
-
* - Every 5 turns: periodic refresh
|
|
188
260
|
* - Every other turn: per-turn injection
|
|
189
261
|
*
|
|
190
262
|
* Returns augmented messages with memory context prepended to the last
|
|
@@ -199,11 +271,12 @@ export class ConversationGraphMemory {
|
|
|
199
271
|
runMessages: Message[];
|
|
200
272
|
injectedTokens: number;
|
|
201
273
|
latencyMs: number;
|
|
202
|
-
mode: "context-load" | "
|
|
274
|
+
mode: "context-load" | "per-turn" | "none";
|
|
203
275
|
/** The raw text content of the injected block (without XML wrapper), or null if nothing was injected. */
|
|
204
276
|
injectedBlockText: string | null;
|
|
277
|
+
/** Retrieval pipeline metrics (null for noop/error paths). */
|
|
278
|
+
metrics: RetrievalMetrics | null;
|
|
205
279
|
}> {
|
|
206
|
-
this.turnCount++;
|
|
207
280
|
this.tracker.advanceTurn();
|
|
208
281
|
|
|
209
282
|
const noopResult = {
|
|
@@ -212,6 +285,7 @@ export class ConversationGraphMemory {
|
|
|
212
285
|
latencyMs: 0,
|
|
213
286
|
mode: "none" as const,
|
|
214
287
|
injectedBlockText: null as string | null,
|
|
288
|
+
metrics: null as RetrievalMetrics | null,
|
|
215
289
|
};
|
|
216
290
|
|
|
217
291
|
// Gate: skip for empty/tool-result-only messages — unless we need to
|
|
@@ -245,10 +319,6 @@ export class ConversationGraphMemory {
|
|
|
245
319
|
);
|
|
246
320
|
}
|
|
247
321
|
|
|
248
|
-
if (this.turnCount % REFRESH_INTERVAL_TURNS === 0) {
|
|
249
|
-
return await this.runRefresh(messages, config, abortSignal);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
322
|
return await this.runPerTurn(messages, config, abortSignal);
|
|
253
323
|
} catch (err) {
|
|
254
324
|
log.warn(
|
|
@@ -290,6 +360,7 @@ export class ConversationGraphMemory {
|
|
|
290
360
|
latencyMs: result.latencyMs,
|
|
291
361
|
mode: "context-load" as const,
|
|
292
362
|
injectedBlockText: null,
|
|
363
|
+
metrics: result.metrics,
|
|
293
364
|
};
|
|
294
365
|
}
|
|
295
366
|
|
|
@@ -308,6 +379,7 @@ export class ConversationGraphMemory {
|
|
|
308
379
|
latencyMs: result.latencyMs,
|
|
309
380
|
mode: "context-load" as const,
|
|
310
381
|
injectedBlockText: null,
|
|
382
|
+
metrics: result.metrics,
|
|
311
383
|
};
|
|
312
384
|
}
|
|
313
385
|
|
|
@@ -339,80 +411,7 @@ export class ConversationGraphMemory {
|
|
|
339
411
|
latencyMs: result.latencyMs,
|
|
340
412
|
mode: "context-load" as const,
|
|
341
413
|
injectedBlockText: contextBlock,
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private async runRefresh(
|
|
346
|
-
messages: Message[],
|
|
347
|
-
config: AssistantConfig,
|
|
348
|
-
signal: AbortSignal,
|
|
349
|
-
) {
|
|
350
|
-
// Build recent turns text from the last ~6 messages
|
|
351
|
-
const recentTurns = messages
|
|
352
|
-
.slice(-6)
|
|
353
|
-
.map((m) => {
|
|
354
|
-
const textBlocks = m.content.filter(
|
|
355
|
-
(b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
|
|
356
|
-
);
|
|
357
|
-
if (textBlocks.length === 0) return "";
|
|
358
|
-
return `[${m.role}]: ${textBlocks.map((b) => b.text).join(" ")}`;
|
|
359
|
-
})
|
|
360
|
-
.filter((t) => t.length > 0)
|
|
361
|
-
.join("\n\n");
|
|
362
|
-
|
|
363
|
-
const result = await refreshContextMemory({
|
|
364
|
-
recentTurnsText: recentTurns,
|
|
365
|
-
scopeId: this.scopeId,
|
|
366
|
-
config,
|
|
367
|
-
tracker: this.tracker,
|
|
368
|
-
signal,
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
if (result.nodes.length === 0) {
|
|
372
|
-
this.lastInjectedBlock = null;
|
|
373
|
-
this.lastInjectedNodeIds = [];
|
|
374
|
-
this.lastInjectedImages = new Map();
|
|
375
|
-
return {
|
|
376
|
-
runMessages: messages,
|
|
377
|
-
injectedTokens: 0,
|
|
378
|
-
latencyMs: result.latencyMs,
|
|
379
|
-
mode: "refresh" as const,
|
|
380
|
-
injectedBlockText: null,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Track new nodes
|
|
385
|
-
this.tracker.add(result.nodes.map((n) => n.node.id));
|
|
386
|
-
|
|
387
|
-
const injectionBlock = assembleInjectionBlock(result.nodes);
|
|
388
|
-
if (!injectionBlock) {
|
|
389
|
-
return {
|
|
390
|
-
runMessages: messages,
|
|
391
|
-
injectedTokens: 0,
|
|
392
|
-
latencyMs: result.latencyMs,
|
|
393
|
-
mode: "refresh" as const,
|
|
394
|
-
injectedBlockText: null,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Resolve images from scored nodes
|
|
399
|
-
const images = await resolveInjectionImages(
|
|
400
|
-
result.nodes,
|
|
401
|
-
MAX_REFRESH_IMAGES,
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
this.lastInjectedBlock = injectionBlock;
|
|
405
|
-
this.lastInjectedNodeIds = result.nodes.map((n) => n.node.id);
|
|
406
|
-
this.lastInjectedImages = images;
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
runMessages: injectMemoryBlock(messages, injectionBlock, images),
|
|
410
|
-
injectedTokens:
|
|
411
|
-
estimateTextTokens(injectionBlock) +
|
|
412
|
-
images.size * ESTIMATED_IMAGE_TOKENS,
|
|
413
|
-
latencyMs: result.latencyMs,
|
|
414
|
-
mode: "refresh" as const,
|
|
415
|
-
injectedBlockText: injectionBlock,
|
|
414
|
+
metrics: result.metrics,
|
|
416
415
|
};
|
|
417
416
|
}
|
|
418
417
|
|
|
@@ -466,6 +465,7 @@ export class ConversationGraphMemory {
|
|
|
466
465
|
latencyMs: result.latencyMs,
|
|
467
466
|
mode: "per-turn" as const,
|
|
468
467
|
injectedBlockText: null,
|
|
468
|
+
metrics: result.metrics,
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
471
|
|
|
@@ -480,6 +480,7 @@ export class ConversationGraphMemory {
|
|
|
480
480
|
latencyMs: result.latencyMs,
|
|
481
481
|
mode: "per-turn" as const,
|
|
482
482
|
injectedBlockText: null,
|
|
483
|
+
metrics: result.metrics,
|
|
483
484
|
};
|
|
484
485
|
}
|
|
485
486
|
|
|
@@ -501,6 +502,7 @@ export class ConversationGraphMemory {
|
|
|
501
502
|
latencyMs: result.latencyMs,
|
|
502
503
|
mode: "per-turn" as const,
|
|
503
504
|
injectedBlockText: injectionBlock,
|
|
505
|
+
metrics: result.metrics,
|
|
504
506
|
};
|
|
505
507
|
}
|
|
506
508
|
}
|
|
@@ -513,20 +515,26 @@ export class ConversationGraphMemory {
|
|
|
513
515
|
* Remove all memory-injected blocks from the last user message.
|
|
514
516
|
*
|
|
515
517
|
* `injectMemoryBlock` always prepends blocks in this order:
|
|
516
|
-
* 1. `<
|
|
517
|
-
* 2.
|
|
518
|
+
* 1. For each image: `<memory_image __injected>…` text + `image` + `</memory_image>` text (3-block group)
|
|
519
|
+
* 2. `<memory __injected>…</memory>` text block
|
|
518
520
|
*
|
|
519
521
|
* We strip all leading blocks that match this pattern so that
|
|
520
522
|
* `reinjectCachedMemory` is idempotent — no duplicate images after compaction.
|
|
521
523
|
*/
|
|
522
|
-
function stripExistingMemoryInjections(messages: Message[]): Message[] {
|
|
524
|
+
export function stripExistingMemoryInjections(messages: Message[]): Message[] {
|
|
523
525
|
if (messages.length === 0) return messages;
|
|
524
526
|
const last = messages[messages.length - 1];
|
|
525
527
|
if (!last || last.role !== "user") return messages;
|
|
526
528
|
|
|
527
529
|
// Walk from the front and skip all memory-injected blocks.
|
|
528
530
|
// The injection prefix is always contiguous at the start of content.
|
|
531
|
+
// Memory-injected images use a 3-block pattern: opening <memory_image> text,
|
|
532
|
+
// image block, closing </memory_image> text (see injectMemoryBlock).
|
|
533
|
+
// Legacy 2-block pattern (no closing tag) is also handled for backward compat.
|
|
534
|
+
// Only strip image blocks that follow a marker — user-attached images must be preserved.
|
|
529
535
|
let firstNonMemory = 0;
|
|
536
|
+
let prevWasMemoryImageMarker = false;
|
|
537
|
+
let prevWasInjectedImage = false;
|
|
530
538
|
const content = last.content;
|
|
531
539
|
while (firstNonMemory < content.length) {
|
|
532
540
|
const block = content[firstNonMemory];
|
|
@@ -535,13 +543,27 @@ function stripExistingMemoryInjections(messages: Message[]): Message[] {
|
|
|
535
543
|
block.text.startsWith("<memory __injected>\n")
|
|
536
544
|
) {
|
|
537
545
|
firstNonMemory++;
|
|
546
|
+
prevWasMemoryImageMarker = false;
|
|
547
|
+
prevWasInjectedImage = false;
|
|
538
548
|
} else if (
|
|
539
549
|
block.type === "text" &&
|
|
540
|
-
block.text.startsWith("<memory_image
|
|
550
|
+
block.text.startsWith("<memory_image")
|
|
541
551
|
) {
|
|
542
552
|
firstNonMemory++;
|
|
543
|
-
|
|
553
|
+
prevWasMemoryImageMarker = true;
|
|
554
|
+
prevWasInjectedImage = false;
|
|
555
|
+
} else if (block.type === "image" && prevWasMemoryImageMarker) {
|
|
556
|
+
firstNonMemory++;
|
|
557
|
+
prevWasMemoryImageMarker = false;
|
|
558
|
+
prevWasInjectedImage = true;
|
|
559
|
+
} else if (
|
|
560
|
+
block.type === "text" &&
|
|
561
|
+
block.text === "</memory_image>" &&
|
|
562
|
+
prevWasInjectedImage
|
|
563
|
+
) {
|
|
564
|
+
// Closing tag from the 3-block pattern — only strip after an injected image
|
|
544
565
|
firstNonMemory++;
|
|
566
|
+
prevWasInjectedImage = false;
|
|
545
567
|
} else {
|
|
546
568
|
break;
|
|
547
569
|
}
|
|
@@ -592,14 +614,12 @@ function injectMemoryBlock(
|
|
|
592
614
|
const userTail = cleaned[cleaned.length - 1];
|
|
593
615
|
if (!userTail || userTail.role !== "user") return messages;
|
|
594
616
|
|
|
595
|
-
const blocks: ContentBlock[] = [
|
|
596
|
-
{ type: "text" as const, text: `<memory __injected>\n${text}\n</memory>` },
|
|
597
|
-
];
|
|
617
|
+
const blocks: ContentBlock[] = [];
|
|
598
618
|
|
|
599
619
|
for (const [_nodeId, img] of images) {
|
|
600
620
|
blocks.push({
|
|
601
621
|
type: "text" as const,
|
|
602
|
-
text: `<memory_image
|
|
622
|
+
text: `<memory_image __injected>\n${img.description}`,
|
|
603
623
|
});
|
|
604
624
|
blocks.push({
|
|
605
625
|
type: "image" as const,
|
|
@@ -609,8 +629,17 @@ function injectMemoryBlock(
|
|
|
609
629
|
data: img.base64Data,
|
|
610
630
|
},
|
|
611
631
|
} as ImageContent);
|
|
632
|
+
blocks.push({
|
|
633
|
+
type: "text" as const,
|
|
634
|
+
text: `</memory_image>`,
|
|
635
|
+
});
|
|
612
636
|
}
|
|
613
637
|
|
|
638
|
+
blocks.push({
|
|
639
|
+
type: "text" as const,
|
|
640
|
+
text: `<memory __injected>\n${text}\n</memory>`,
|
|
641
|
+
});
|
|
642
|
+
|
|
614
643
|
return [
|
|
615
644
|
...cleaned.slice(0, -1),
|
|
616
645
|
{ ...userTail, content: [...blocks, ...userTail.content] },
|
|
@@ -22,10 +22,10 @@ const log = getLogger("graph-extraction-job");
|
|
|
22
22
|
* Checkpoint key: `graph_extract:<conversationId>:last_ts`
|
|
23
23
|
* Value: epoch ms of the most recent message processed.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
25
|
+
* Trigger sources:
|
|
26
|
+
* - Indexer after batchSize messages (default 10)
|
|
27
|
+
* - Indexer idle debounce (default 300s)
|
|
28
|
+
* - Conversation dispose (end of conversation)
|
|
29
29
|
*/
|
|
30
30
|
export async function graphExtractJob(
|
|
31
31
|
job: MemoryJob,
|
|
@@ -40,9 +40,14 @@ export async function graphExtractJob(
|
|
|
40
40
|
const lastTs = getMemoryCheckpoint(checkpointKey);
|
|
41
41
|
const afterTimestamp = lastTs ? parseInt(lastTs, 10) : undefined;
|
|
42
42
|
|
|
43
|
+
const activeContextNodeIds = Array.isArray(job.payload.activeContextNodeIds)
|
|
44
|
+
? (job.payload.activeContextNodeIds as string[])
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
43
47
|
try {
|
|
44
48
|
const result = await runGraphExtraction(conversationId, scopeId, config, {
|
|
45
49
|
afterTimestamp,
|
|
50
|
+
activeContextNodeIds,
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
// Update checkpoint to the newest message actually processed — using
|
|
@@ -60,6 +60,7 @@ const EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET = 24_000;
|
|
|
60
60
|
function buildGraphExtractionSystemPrompt(
|
|
61
61
|
candidateNodes: Array<{ id: string; type: string; content: string }>,
|
|
62
62
|
identityContext: string | null,
|
|
63
|
+
activeContextNodeIds?: Set<string>,
|
|
63
64
|
): string {
|
|
64
65
|
const instructions = `You are the memory consolidation process for an AI assistant. A conversation just ended.
|
|
65
66
|
Your job is to extract memories worth keeping and produce a structured diff.
|
|
@@ -70,6 +71,8 @@ Call the \`extract_graph_diff\` tool with the diff. Each node needs:
|
|
|
70
71
|
|
|
71
72
|
- **content**: First-person prose — how the assistant naturally remembers this. Write naturally, not as a database entry. E.g. "He mentioned his mom used to make amazing Sunday dinners — he still misses them" not "User's mother cooked Sunday dinners."
|
|
72
73
|
|
|
74
|
+
Be concise — most memories should be 1-3 sentences capturing the essential detail and emotional weight. Don't narrate every nuance; write a vivid snapshot, not a journal entry. Higher-significance (0.7+) memories can use a short paragraph, but even those should stay focused.
|
|
75
|
+
|
|
73
76
|
- **type**: Classify by WHAT the memory IS, not how it FEELS. Almost every memory has emotional weight — that goes in emotionalCharge, not the type.
|
|
74
77
|
|
|
75
78
|
- **episodic**: A specific moment or event. "We stayed up until 4 AM debugging the pipeline." "The first time we deployed to production." Use this for things that HAPPENED.
|
|
@@ -135,7 +138,44 @@ Do NOT attach images that are incidental (screenshots of error messages fully de
|
|
|
135
138
|
|
|
136
139
|
Write detailed descriptions — these are used for text-based retrieval when visual search isn't available.
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
${(() => {
|
|
142
|
+
const reconsolidationNodes = activeContextNodeIds?.size
|
|
143
|
+
? candidateNodes.filter((n) => activeContextNodeIds.has(n.id))
|
|
144
|
+
: [];
|
|
145
|
+
const otherCandidates = activeContextNodeIds?.size
|
|
146
|
+
? candidateNodes.filter((n) => !activeContextNodeIds.has(n.id))
|
|
147
|
+
: candidateNodes;
|
|
148
|
+
|
|
149
|
+
const reconsolidationSection =
|
|
150
|
+
reconsolidationNodes.length > 0
|
|
151
|
+
? `## Reconsolidation Window
|
|
152
|
+
|
|
153
|
+
These memories were ACTIVELY RECALLED during this conversation — the user and
|
|
154
|
+
assistant both saw them. Recalled memories are in a reconsolidation window and
|
|
155
|
+
should be the FIRST candidates for updating with new information.
|
|
156
|
+
|
|
157
|
+
When a recalled memory relates to what was discussed:
|
|
158
|
+
- Conversation CONFIRMS what the memory says → REINFORCE it
|
|
159
|
+
- Conversation adds new detail or nuance → UPDATE it with richer content
|
|
160
|
+
- Conversation reveals the memory is outdated or wrong → UPDATE it or create a superseding node
|
|
161
|
+
- Conversation is unrelated to this memory → leave it alone
|
|
162
|
+
|
|
163
|
+
STRONG PREFERENCE: Update a recalled memory rather than creating a new node that
|
|
164
|
+
partially overlaps. The recalled memory already has history, reinforcement count,
|
|
165
|
+
and edge connections — enriching it preserves that context graph.
|
|
166
|
+
|
|
167
|
+
### Recalled memories
|
|
168
|
+
${reconsolidationNodes.map((n) => `- [${n.id}] (${n.type}) ${n.content}`).join("\n")}
|
|
169
|
+
|
|
170
|
+
`
|
|
171
|
+
: "";
|
|
172
|
+
|
|
173
|
+
const candidateHeader =
|
|
174
|
+
reconsolidationNodes.length > 0
|
|
175
|
+
? "## Other Candidate Nodes (existing memories not in this conversation)"
|
|
176
|
+
: "## Candidate Nodes (existing memories)";
|
|
177
|
+
|
|
178
|
+
const candidateSection = `${candidateHeader}
|
|
139
179
|
|
|
140
180
|
Check these CAREFULLY for overlap before creating any new node:
|
|
141
181
|
|
|
@@ -153,7 +193,10 @@ Common duplicate mistakes to avoid:
|
|
|
153
193
|
- Same fact restated in a later conversation → REINFORCE, don't create
|
|
154
194
|
- An update to an existing situation (e.g. "project is now done") → UPDATE the existing node, don't create a parallel one
|
|
155
195
|
|
|
156
|
-
${
|
|
196
|
+
${otherCandidates.length > 0 ? `### Existing memories (candidates for connection/reinforcement)\n${otherCandidates.map((n) => `- [${n.id}] (${n.type}) ${n.content}`).join("\n")}` : reconsolidationNodes.length > 0 ? "All existing memories are shown in the reconsolidation section above." : "No existing memories found — this may be an early conversation."}`;
|
|
197
|
+
|
|
198
|
+
return reconsolidationSection + candidateSection;
|
|
199
|
+
})()}
|
|
157
200
|
`;
|
|
158
201
|
|
|
159
202
|
let prompt = instructions;
|
|
@@ -401,7 +444,7 @@ interface RawUpdateNode {
|
|
|
401
444
|
significance?: number;
|
|
402
445
|
confidence?: number;
|
|
403
446
|
fidelity?: string;
|
|
404
|
-
event_date?: number;
|
|
447
|
+
event_date?: number | null;
|
|
405
448
|
}
|
|
406
449
|
|
|
407
450
|
interface RawNewEdge {
|
|
@@ -601,19 +644,18 @@ export function parseExtractionResponse(
|
|
|
601
644
|
if (
|
|
602
645
|
node.eventDate != null &&
|
|
603
646
|
(!Array.isArray(raw.triggers) ||
|
|
604
|
-
!raw.triggers.some(
|
|
605
|
-
(t) => t.type === "event" && t.event_date != null,
|
|
606
|
-
))
|
|
647
|
+
!raw.triggers.some((t) => t.type === "event" && t.event_date != null))
|
|
607
648
|
) {
|
|
608
|
-
// Remove
|
|
609
|
-
|
|
610
|
-
|
|
649
|
+
// Remove all malformed event triggers (type=event but missing event_date)
|
|
650
|
+
for (let i = deferredTriggers.length - 1; i >= 0; i--) {
|
|
651
|
+
const dt = deferredTriggers[i];
|
|
652
|
+
if (
|
|
611
653
|
dt.newNodeIndex === nodeIndex &&
|
|
612
654
|
dt.trigger.type === "event" &&
|
|
613
|
-
dt.trigger.eventDate == null
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
655
|
+
dt.trigger.eventDate == null
|
|
656
|
+
) {
|
|
657
|
+
deferredTriggers.splice(i, 1);
|
|
658
|
+
}
|
|
617
659
|
}
|
|
618
660
|
|
|
619
661
|
deferredTriggers.push({
|
|
@@ -674,7 +716,8 @@ export function parseExtractionResponse(
|
|
|
674
716
|
["vivid", "clear", "faded", "gist"].includes(raw.fidelity)
|
|
675
717
|
)
|
|
676
718
|
changes.fidelity = raw.fidelity;
|
|
677
|
-
if (raw.event_date !== undefined)
|
|
719
|
+
if (raw.event_date !== undefined)
|
|
720
|
+
changes.eventDate = parseEpochMs(raw.event_date);
|
|
678
721
|
if (Object.keys(changes).length > 0) {
|
|
679
722
|
diff.updateNodes.push({ id: raw.id, changes });
|
|
680
723
|
}
|
|
@@ -773,9 +816,7 @@ export async function runGraphExtraction(
|
|
|
773
816
|
// from the multimodal message content blocks for candidate search.
|
|
774
817
|
if (imageResult) {
|
|
775
818
|
transcript = imageResult.message.content
|
|
776
|
-
.filter(
|
|
777
|
-
(b): b is { type: "text"; text: string } => b.type === "text",
|
|
778
|
-
)
|
|
819
|
+
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
|
779
820
|
.map((b) => b.text)
|
|
780
821
|
.join("\n");
|
|
781
822
|
}
|
|
@@ -818,9 +859,14 @@ export async function runGraphExtraction(
|
|
|
818
859
|
userPersona: userPersona ?? undefined,
|
|
819
860
|
});
|
|
820
861
|
|
|
862
|
+
const activeSet = opts?.activeContextNodeIds
|
|
863
|
+
? new Set(opts.activeContextNodeIds)
|
|
864
|
+
: undefined;
|
|
865
|
+
|
|
821
866
|
const systemPrompt = buildGraphExtractionSystemPrompt(
|
|
822
867
|
candidateNodes.map((n) => ({ id: n.id, type: n.type, content: n.content })),
|
|
823
868
|
identityContext,
|
|
869
|
+
activeSet,
|
|
824
870
|
);
|
|
825
871
|
|
|
826
872
|
// 5. Resolve conversation timestamp before the LLM call so we can include
|
|
@@ -904,7 +950,7 @@ export async function runGraphExtraction(
|
|
|
904
950
|
}
|
|
905
951
|
|
|
906
952
|
// 8. Apply the diff
|
|
907
|
-
const result = applyDiff(diff);
|
|
953
|
+
const result = applyDiff(diff, { conversationId });
|
|
908
954
|
|
|
909
955
|
// 9. Apply deferred edges and triggers using the created node IDs
|
|
910
956
|
const createdNodeIds = result.createdNodeIds;
|
|
@@ -1164,11 +1210,8 @@ export function loadTranscriptWithImages(
|
|
|
1164
1210
|
for (let i = 0; i < parsed.length; i++) {
|
|
1165
1211
|
const block = parsed[i];
|
|
1166
1212
|
if (block?.type === "text") {
|
|
1167
|
-
const rawText =
|
|
1168
|
-
|
|
1169
|
-
const text = prefixAdded
|
|
1170
|
-
? rawText
|
|
1171
|
-
: `[${row.role}]: ${rawText}`;
|
|
1213
|
+
const rawText = typeof block.text === "string" ? block.text : "";
|
|
1214
|
+
const text = prefixAdded ? rawText : `[${row.role}]: ${rawText}`;
|
|
1172
1215
|
prefixAdded = true;
|
|
1173
1216
|
totalTextLength += text.length;
|
|
1174
1217
|
contentBlocks.push({ type: "text", text });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { getDb } from "../db.js";
|
|
4
|
+
import { conversationGraphMemoryState } from "../schema.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persist graph memory state for a conversation (upsert).
|
|
8
|
+
*/
|
|
9
|
+
export function saveGraphMemoryState(
|
|
10
|
+
conversationId: string,
|
|
11
|
+
stateJson: string,
|
|
12
|
+
): void {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
db.insert(conversationGraphMemoryState)
|
|
16
|
+
.values({ conversationId, stateJson, createdAt: now, updatedAt: now })
|
|
17
|
+
.onConflictDoUpdate({
|
|
18
|
+
target: conversationGraphMemoryState.conversationId,
|
|
19
|
+
set: { stateJson, updatedAt: now },
|
|
20
|
+
})
|
|
21
|
+
.run();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load graph memory state for a conversation, or null if none exists.
|
|
26
|
+
*/
|
|
27
|
+
export function loadGraphMemoryState(
|
|
28
|
+
conversationId: string,
|
|
29
|
+
): string | null {
|
|
30
|
+
const db = getDb();
|
|
31
|
+
const row = db
|
|
32
|
+
.select({ stateJson: conversationGraphMemoryState.stateJson })
|
|
33
|
+
.from(conversationGraphMemoryState)
|
|
34
|
+
.where(eq(conversationGraphMemoryState.conversationId, conversationId))
|
|
35
|
+
.get();
|
|
36
|
+
return row?.stateJson ?? null;
|
|
37
|
+
}
|