@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
|
@@ -1,18 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
import { getConfig } from "../config/loader.js";
|
|
2
5
|
import type { Speed } from "../config/schemas/inference.js";
|
|
3
6
|
import type { HeartbeatAlert } from "../daemon/message-protocol.js";
|
|
4
7
|
import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
|
|
8
|
+
import { isTemplateContent } from "../prompts/system-prompt.js";
|
|
5
9
|
import { readTextFileSync } from "../util/fs.js";
|
|
6
10
|
import { getLogger } from "../util/logger.js";
|
|
7
|
-
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
11
|
+
import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
|
|
8
12
|
import { stripCommentLines } from "../util/strip-comment-lines.js";
|
|
9
13
|
|
|
10
14
|
const log = getLogger("heartbeat-check");
|
|
11
15
|
|
|
12
16
|
const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still accurate? Update it if anything has changed.
|
|
13
17
|
- Think about your user. Is there anything from recent conversations you should follow up on? Anything you noticed that you should bring up?
|
|
18
|
+
- Have a thought. Think about something your user would find interesting or worth talking about. A follow-up, a connection you made, something you came across. Give them a reason to open a conversation.
|
|
14
19
|
- Check if there's anything on the horizon — events, deadlines, things they mentioned wanting to do.
|
|
15
|
-
- If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right
|
|
20
|
+
- If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right.
|
|
21
|
+
- If something has happened since your last journal entry, write one. Even a few sentences. The journal is how future-you stays connected.`;
|
|
22
|
+
|
|
23
|
+
const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
|
|
24
|
+
const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
25
|
+
|
|
26
|
+
/** @internal Exported for testing. */
|
|
27
|
+
export function isShallowProfile(): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const identityPath = getWorkspacePromptPath("IDENTITY.md");
|
|
30
|
+
const userPath = getWorkspacePromptPath("USER.md");
|
|
31
|
+
const rawIdentity = readTextFileSync(identityPath);
|
|
32
|
+
const rawUser = readTextFileSync(userPath);
|
|
33
|
+
const identity = rawIdentity != null ? stripCommentLines(rawIdentity) : null;
|
|
34
|
+
const user = rawUser != null ? stripCommentLines(rawUser) : null;
|
|
35
|
+
return (
|
|
36
|
+
isTemplateContent(identity, "IDENTITY.md") &&
|
|
37
|
+
isTemplateContent(user, "USER.md")
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getReengagementTimestampPath(): string {
|
|
45
|
+
return join(getWorkspaceDir(), ".reengagement-ts");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isReengagementCooldownElapsed(): boolean {
|
|
49
|
+
const tsPath = getReengagementTimestampPath();
|
|
50
|
+
if (!existsSync(tsPath)) return true;
|
|
51
|
+
try {
|
|
52
|
+
const lastTs = parseInt(readFileSync(tsPath, "utf-8").trim(), 10);
|
|
53
|
+
if (isNaN(lastTs)) return true;
|
|
54
|
+
return Date.now() - lastTs >= REENGAGEMENT_COOLDOWN_MS;
|
|
55
|
+
} catch {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recordReengagementTimestamp(): void {
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(getReengagementTimestampPath(), Date.now().toString());
|
|
63
|
+
} catch {
|
|
64
|
+
// Best-effort; don't block the heartbeat.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
16
67
|
|
|
17
68
|
export interface HeartbeatDeps {
|
|
18
69
|
processMessage: (
|
|
@@ -141,6 +192,7 @@ export class HeartbeatService {
|
|
|
141
192
|
},
|
|
142
193
|
"Outside active hours, skipping",
|
|
143
194
|
);
|
|
195
|
+
this.scheduleNextRun(config.intervalMs);
|
|
144
196
|
return false;
|
|
145
197
|
}
|
|
146
198
|
}
|
|
@@ -153,10 +205,34 @@ export class HeartbeatService {
|
|
|
153
205
|
|
|
154
206
|
const run = this.executeRun();
|
|
155
207
|
this.activeRun = run;
|
|
208
|
+
// Clear activeRun once executeRun finishes. On timeout, runOnce releases
|
|
209
|
+
// activeRun separately (see catch block below) so future runs aren't
|
|
210
|
+
// permanently blocked. The .finally() handler still serves as the
|
|
211
|
+
// normal-completion cleanup path and uses an identity guard to avoid
|
|
212
|
+
// clearing a different run's activeRun.
|
|
213
|
+
run.finally(() => {
|
|
214
|
+
if (this.activeRun === run) {
|
|
215
|
+
this.activeRun = null;
|
|
216
|
+
}
|
|
217
|
+
}).catch(() => {}); // Suppress unhandled rejection if executeRun rejects
|
|
218
|
+
|
|
219
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
156
220
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
221
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
222
|
+
timerId = setTimeout(
|
|
223
|
+
() => reject(new Error("Heartbeat execution timed out")),
|
|
224
|
+
HEARTBEAT_TIMEOUT_MS,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
timeout.catch(() => {}); // Prevent unhandled rejection if run resolves first
|
|
228
|
+
await Promise.race([run, timeout]);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
log.warn({ err }, "Heartbeat run timed out");
|
|
231
|
+
// Release activeRun so the overlap guard doesn't permanently block
|
|
232
|
+
// future heartbeat runs when executeRun hangs past the timeout.
|
|
159
233
|
this.activeRun = null;
|
|
234
|
+
} finally {
|
|
235
|
+
clearTimeout(timerId);
|
|
160
236
|
this._lastRunAt = Date.now();
|
|
161
237
|
this.scheduleNextRun(getConfig().heartbeat.intervalMs);
|
|
162
238
|
}
|
|
@@ -173,7 +249,7 @@ export class HeartbeatService {
|
|
|
173
249
|
try {
|
|
174
250
|
const config = getConfig().heartbeat;
|
|
175
251
|
const checklist = this.readChecklist();
|
|
176
|
-
const prompt = this.buildPrompt(checklist);
|
|
252
|
+
const { prompt, includedReengagement } = this.buildPrompt(checklist);
|
|
177
253
|
|
|
178
254
|
const conversation = bootstrapConversation({
|
|
179
255
|
conversationType: "background",
|
|
@@ -191,6 +267,11 @@ export class HeartbeatService {
|
|
|
191
267
|
await this.deps.processMessage(conversation.id, prompt, {
|
|
192
268
|
speed: config.speed,
|
|
193
269
|
});
|
|
270
|
+
|
|
271
|
+
if (includedReengagement) {
|
|
272
|
+
recordReengagementTimestamp();
|
|
273
|
+
}
|
|
274
|
+
|
|
194
275
|
log.info({ conversationId: conversation.id }, "Heartbeat completed");
|
|
195
276
|
} catch (err) {
|
|
196
277
|
log.error({ err }, "Heartbeat failed");
|
|
@@ -214,8 +295,8 @@ export class HeartbeatService {
|
|
|
214
295
|
}
|
|
215
296
|
|
|
216
297
|
/** @internal Exposed for testing. */
|
|
217
|
-
buildPrompt(checklist: string): string {
|
|
218
|
-
|
|
298
|
+
buildPrompt(checklist: string): { prompt: string; includedReengagement: boolean } {
|
|
299
|
+
let prompt = `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
|
|
219
300
|
|
|
220
301
|
<heartbeat-checklist>
|
|
221
302
|
${checklist}
|
|
@@ -226,6 +307,14 @@ After completing your review, end your response with one of:
|
|
|
226
307
|
- HEARTBEAT_OK — if everything looks good, no action needed
|
|
227
308
|
- HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
|
|
228
309
|
</heartbeat-disposition>`;
|
|
310
|
+
|
|
311
|
+
let includedReengagement = false;
|
|
312
|
+
if (isShallowProfile() && isReengagementCooldownElapsed()) {
|
|
313
|
+
includedReengagement = true;
|
|
314
|
+
prompt += `\n\n<relationship-depth>\nYou don't know much about this person yet — their profile is still sparse. If the moment feels right during this beat, gently invite them to share something about themselves. Not an interrogation — something natural like "I realized I don't actually know much about what you do. Fill me in sometime?" Only do this occasionally, not every beat. If they engage, save what you learn.\n</relationship-depth>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { prompt, includedReengagement };
|
|
229
318
|
}
|
|
230
319
|
}
|
|
231
320
|
|
package/src/mcp/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
5
5
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
6
|
|
|
7
7
|
import type { McpTransport } from "../config/schemas/mcp.js";
|
|
8
|
+
import { shouldUsePlatformCallbacks } from "../inbound/platform-callback-registration.js";
|
|
8
9
|
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
9
10
|
import { getLogger } from "../util/logger.js";
|
|
10
11
|
import { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
@@ -63,9 +64,14 @@ export class McpClient {
|
|
|
63
64
|
`mcp:${this.serverId}:tokens`,
|
|
64
65
|
);
|
|
65
66
|
if (cachedTokens) {
|
|
67
|
+
const callbackTransport = shouldUsePlatformCallbacks()
|
|
68
|
+
? "gateway"
|
|
69
|
+
: "loopback";
|
|
66
70
|
this.oauthProvider = new McpOAuthProvider(
|
|
67
71
|
this.serverId,
|
|
68
72
|
transportConfig.url,
|
|
73
|
+
/* interactive */ false,
|
|
74
|
+
callbackTransport,
|
|
69
75
|
);
|
|
70
76
|
}
|
|
71
77
|
}
|
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* OAuthClientProvider implementation for MCP servers.
|
|
3
3
|
*
|
|
4
4
|
* Uses secure-keys (credential store) for persistent credential storage
|
|
5
|
-
* and a loopback HTTP server
|
|
5
|
+
* and either a loopback HTTP server or the gateway callback registry
|
|
6
|
+
* for the browser callback.
|
|
7
|
+
*
|
|
8
|
+
* Two callback transports:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Loopback** (default) — starts a temporary HTTP server on localhost.
|
|
11
|
+
* Works when the daemon runs on the user's machine (desktop app).
|
|
12
|
+
*
|
|
13
|
+
* 2. **Gateway** — routes callbacks through the platform's public ingress
|
|
14
|
+
* URL and the in-memory oauth-callback-registry. Used when the daemon
|
|
15
|
+
* runs inside Docker/platform where localhost is unreachable from the
|
|
16
|
+
* user's browser.
|
|
6
17
|
*/
|
|
7
18
|
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
8
20
|
import { createServer, type Server } from "node:http";
|
|
9
21
|
|
|
10
22
|
import type {
|
|
@@ -22,8 +34,8 @@ import {
|
|
|
22
34
|
getSecureKeyAsync,
|
|
23
35
|
setSecureKeyAsync,
|
|
24
36
|
} from "../security/secure-keys.js";
|
|
37
|
+
import { openInHostBrowser } from "../util/browser.js";
|
|
25
38
|
import { getLogger } from "../util/logger.js";
|
|
26
|
-
import { isLinux, isMacOS } from "../util/platform.js";
|
|
27
39
|
|
|
28
40
|
const log = getLogger("mcp-oauth");
|
|
29
41
|
|
|
@@ -46,24 +58,41 @@ export interface McpOAuthCallbackResult {
|
|
|
46
58
|
codePromise: Promise<string>;
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/** Which callback transport to use for receiving the OAuth redirect. */
|
|
62
|
+
export type McpOAuthCallbackTransport = "loopback" | "gateway";
|
|
63
|
+
|
|
49
64
|
export class McpOAuthProvider implements OAuthClientProvider {
|
|
50
65
|
private readonly serverId: string;
|
|
51
66
|
private readonly serverUrl: string;
|
|
52
67
|
private readonly interactive: boolean;
|
|
68
|
+
private readonly callbackTransport: McpOAuthCallbackTransport;
|
|
53
69
|
private _codeVerifier: string | undefined;
|
|
70
|
+
private _state: string | undefined;
|
|
54
71
|
private _redirectUrl: string | undefined;
|
|
55
72
|
private _codePromise: Promise<string> | null = null;
|
|
56
73
|
private callbackServer: Server | null = null;
|
|
57
74
|
private callbackTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
75
|
+
/** Deferred resolver/rejector for the gateway code promise. */
|
|
76
|
+
private _gatewayCodeResolve: ((code: string) => void) | undefined;
|
|
77
|
+
private _gatewayCodeReject: ((err: Error) => void) | undefined;
|
|
58
78
|
|
|
59
79
|
/**
|
|
60
80
|
* @param interactive When true (e.g. `mcp auth` CLI), opens browser for OAuth.
|
|
61
81
|
* When false (daemon), logs a message instead.
|
|
82
|
+
* @param callbackTransport Which transport to use for the OAuth redirect.
|
|
83
|
+
* - `"loopback"` (default): localhost HTTP server — for desktop clients.
|
|
84
|
+
* - `"gateway"`: platform ingress + callback registry — for Docker/platform.
|
|
62
85
|
*/
|
|
63
|
-
constructor(
|
|
86
|
+
constructor(
|
|
87
|
+
serverId: string,
|
|
88
|
+
serverUrl: string,
|
|
89
|
+
interactive = false,
|
|
90
|
+
callbackTransport: McpOAuthCallbackTransport = "loopback",
|
|
91
|
+
) {
|
|
64
92
|
this.serverId = serverId;
|
|
65
93
|
this.serverUrl = serverUrl;
|
|
66
94
|
this.interactive = interactive;
|
|
95
|
+
this.callbackTransport = callbackTransport;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
// --- redirectUrl ---
|
|
@@ -161,6 +190,26 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
161
190
|
return this._codeVerifier;
|
|
162
191
|
}
|
|
163
192
|
|
|
193
|
+
// --- State (CSRF token for OAuth) ---
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return a `state` value for the authorization URL.
|
|
197
|
+
*
|
|
198
|
+
* The MCP SDK calls `provider.state?.()` and, when the return value is
|
|
199
|
+
* truthy, appends it as the `state` query parameter. For the **gateway**
|
|
200
|
+
* transport the state is mandatory because it is the key used by the
|
|
201
|
+
* `oauth-callback-registry` to route the redirect back to this flow.
|
|
202
|
+
* For the **loopback** transport the state is optional (the loopback
|
|
203
|
+
* server matches on the callback URL itself), but we generate one anyway
|
|
204
|
+
* for defense-in-depth.
|
|
205
|
+
*/
|
|
206
|
+
async state(): Promise<string> {
|
|
207
|
+
if (!this._state) {
|
|
208
|
+
this._state = randomBytes(16).toString("hex");
|
|
209
|
+
}
|
|
210
|
+
return this._state;
|
|
211
|
+
}
|
|
212
|
+
|
|
164
213
|
// --- Discovery State ---
|
|
165
214
|
|
|
166
215
|
async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
|
|
@@ -191,6 +240,35 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
191
240
|
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
192
241
|
const url = authorizationUrl.toString();
|
|
193
242
|
|
|
243
|
+
// For gateway transport, extract the SDK-generated `state` from the
|
|
244
|
+
// authorization URL and register it with the callback registry now.
|
|
245
|
+
if (
|
|
246
|
+
this.callbackTransport === "gateway" &&
|
|
247
|
+
this._gatewayCodeResolve &&
|
|
248
|
+
this._gatewayCodeReject
|
|
249
|
+
) {
|
|
250
|
+
const sdkState = authorizationUrl.searchParams.get("state");
|
|
251
|
+
if (sdkState) {
|
|
252
|
+
// Dynamic import to avoid circular deps
|
|
253
|
+
const { registerPendingCallback } =
|
|
254
|
+
await import("../security/oauth-callback-registry.js");
|
|
255
|
+
registerPendingCallback(
|
|
256
|
+
sdkState,
|
|
257
|
+
this._gatewayCodeResolve,
|
|
258
|
+
this._gatewayCodeReject,
|
|
259
|
+
);
|
|
260
|
+
log.info(
|
|
261
|
+
{ serverId: this.serverId, state: sdkState },
|
|
262
|
+
"MCP OAuth gateway callback registered with SDK state",
|
|
263
|
+
);
|
|
264
|
+
} else {
|
|
265
|
+
log.warn(
|
|
266
|
+
{ serverId: this.serverId },
|
|
267
|
+
"Authorization URL missing state parameter — gateway callback may not resolve",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
194
272
|
if (!this.interactive) {
|
|
195
273
|
// Daemon mode — don't open browser, just log guidance
|
|
196
274
|
log.info(
|
|
@@ -208,28 +286,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
208
286
|
`[MCP] Opening browser for OAuth authorization of "${this.serverId}"...`,
|
|
209
287
|
);
|
|
210
288
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const onError = (err: Error | null) => {
|
|
214
|
-
if (err) {
|
|
215
|
-
log.warn({ err }, "Failed to open browser");
|
|
216
|
-
console.log(`[MCP] Please open this URL in your browser:\n${url}`);
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
if (isMacOS()) {
|
|
220
|
-
execFile("open", [url], onError);
|
|
221
|
-
} else if (isLinux()) {
|
|
222
|
-
execFile("xdg-open", [url], onError);
|
|
223
|
-
} else {
|
|
224
|
-
log.warn(
|
|
225
|
-
"Unsupported platform for browser open — please visit the URL manually",
|
|
226
|
-
);
|
|
227
|
-
console.log(`[MCP] Please open this URL in your browser:\n${url}`);
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
log.warn({ err }, "Failed to open browser");
|
|
231
|
-
console.log(`[MCP] Please open this URL in your browser:\n${url}`);
|
|
232
|
-
}
|
|
289
|
+
await openInHostBrowser(url);
|
|
290
|
+
console.log(`[MCP] If the browser did not open, visit this URL:\n${url}`);
|
|
233
291
|
}
|
|
234
292
|
|
|
235
293
|
// --- Invalidate Credentials ---
|
|
@@ -272,6 +330,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
272
330
|
}
|
|
273
331
|
if (scope === "all" || scope === "verifier") {
|
|
274
332
|
this._codeVerifier = undefined;
|
|
333
|
+
this._state = undefined;
|
|
275
334
|
}
|
|
276
335
|
if (scope === "all" || scope === "discovery") {
|
|
277
336
|
const result = await deleteSecureKeyAsync(discoveryKey(this.serverId));
|
|
@@ -292,10 +351,64 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
292
351
|
// --- Callback Server ---
|
|
293
352
|
|
|
294
353
|
/**
|
|
295
|
-
* Start
|
|
296
|
-
*
|
|
354
|
+
* Start listening for the OAuth callback.
|
|
355
|
+
*
|
|
356
|
+
* - **Loopback transport**: starts a temporary HTTP server on localhost.
|
|
357
|
+
* - **Gateway transport**: registers a pending callback with the
|
|
358
|
+
* oauth-callback-registry and resolves a public redirect URL via
|
|
359
|
+
* the platform callback registration system.
|
|
360
|
+
*
|
|
361
|
+
* Returns a promise that resolves with the authorization code promise.
|
|
297
362
|
*/
|
|
298
363
|
startCallbackServer(): Promise<McpOAuthCallbackResult> {
|
|
364
|
+
if (this.callbackTransport === "gateway") {
|
|
365
|
+
return this.startGatewayCallback();
|
|
366
|
+
}
|
|
367
|
+
return this.startLoopbackServer();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Gateway transport: resolve the public redirect URL and create a
|
|
372
|
+
* deferred code promise. The actual `registerPendingCallback` call
|
|
373
|
+
* is deferred until `redirectToAuthorization` where we can extract
|
|
374
|
+
* the SDK-generated `state` parameter from the authorization URL.
|
|
375
|
+
*/
|
|
376
|
+
private async startGatewayCallback(): Promise<McpOAuthCallbackResult> {
|
|
377
|
+
const { resolveCallbackUrl } =
|
|
378
|
+
await import("../inbound/platform-callback-registration.js");
|
|
379
|
+
const { getOAuthCallbackUrl } =
|
|
380
|
+
await import("../inbound/public-ingress-urls.js");
|
|
381
|
+
const { loadConfig } = await import("../config/loader.js");
|
|
382
|
+
|
|
383
|
+
const appConfig = loadConfig();
|
|
384
|
+
const redirectUrl = await resolveCallbackUrl(
|
|
385
|
+
() => getOAuthCallbackUrl(appConfig),
|
|
386
|
+
"webhooks/oauth/callback",
|
|
387
|
+
"mcp_oauth",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
this._redirectUrl = redirectUrl;
|
|
391
|
+
|
|
392
|
+
// Create a deferred promise — it will be wired to the callback
|
|
393
|
+
// registry in redirectToAuthorization() once we know the SDK's state.
|
|
394
|
+
const codePromise = new Promise<string>((resolve, reject) => {
|
|
395
|
+
this._gatewayCodeResolve = resolve;
|
|
396
|
+
this._gatewayCodeReject = reject;
|
|
397
|
+
});
|
|
398
|
+
this._codePromise = codePromise;
|
|
399
|
+
|
|
400
|
+
log.info(
|
|
401
|
+
{ serverId: this.serverId, redirectUrl },
|
|
402
|
+
"MCP OAuth gateway callback prepared (awaiting state from auth URL)",
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return { codePromise };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Loopback transport: start a temporary HTTP server on localhost.
|
|
410
|
+
*/
|
|
411
|
+
private startLoopbackServer(): Promise<McpOAuthCallbackResult> {
|
|
299
412
|
return new Promise((resolveSetup, rejectSetup) => {
|
|
300
413
|
let settled = false;
|
|
301
414
|
let listening = false;
|
|
@@ -423,6 +536,15 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
423
536
|
this.callbackServer.close();
|
|
424
537
|
this.callbackServer = null;
|
|
425
538
|
}
|
|
539
|
+
// Gateway transport cleanup — reject the deferred promise so callers
|
|
540
|
+
// awaiting codePromise don't hang indefinitely.
|
|
541
|
+
if (this._gatewayCodeReject) {
|
|
542
|
+
this._gatewayCodeReject(
|
|
543
|
+
new Error("MCP OAuth gateway callback cancelled"),
|
|
544
|
+
);
|
|
545
|
+
this._gatewayCodeResolve = undefined;
|
|
546
|
+
this._gatewayCodeReject = undefined;
|
|
547
|
+
}
|
|
426
548
|
}
|
|
427
549
|
}
|
|
428
550
|
|
package/src/memory/admin.ts
CHANGED
|
@@ -7,14 +7,20 @@ import { getConversationMemoryScopeId } from "./conversation-crud.js";
|
|
|
7
7
|
import { getDb, rawGet } from "./db.js";
|
|
8
8
|
import { getMemoryBackendStatus } from "./embedding-backend.js";
|
|
9
9
|
import { handleRecall, type RecallResult } from "./graph/tool-handlers.js";
|
|
10
|
-
import { enqueueBackfillJob, enqueueRebuildIndexJob, MIN_SEGMENT_CHARS } from "./indexer.js";
|
|
11
10
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
enqueueBackfillJob,
|
|
12
|
+
enqueueRebuildIndexJob,
|
|
13
|
+
MIN_SEGMENT_CHARS,
|
|
14
|
+
} from "./indexer.js";
|
|
15
|
+
import { enqueueMemoryJob, getMemoryJobCounts } from "./jobs-store.js";
|
|
15
16
|
import { withQdrantBreaker } from "./qdrant-circuit-breaker.js";
|
|
16
17
|
import { getQdrantClient } from "./qdrant-client.js";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
conversations,
|
|
20
|
+
memorySegments,
|
|
21
|
+
memorySummaries,
|
|
22
|
+
messages,
|
|
23
|
+
} from "./schema.js";
|
|
18
24
|
|
|
19
25
|
const log = getLogger("memory-admin");
|
|
20
26
|
|
|
@@ -69,10 +75,6 @@ export function requestMemoryRebuildIndex(): string {
|
|
|
69
75
|
return id;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
export function requestMemoryCleanup(_retentionMs?: number): void {
|
|
73
|
-
log.info("Memory cleanup requested (legacy items table dropped — no-op)");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
78
|
export async function queryMemory(
|
|
77
79
|
query: string,
|
|
78
80
|
_conversationId: string,
|
|
@@ -94,9 +96,9 @@ export interface CleanupShortSegmentsResult {
|
|
|
94
96
|
* These short fragments waste embedding budget, retrieval slots, and
|
|
95
97
|
* injection tokens.
|
|
96
98
|
*/
|
|
97
|
-
export async function cleanupShortSegments(
|
|
98
|
-
|
|
99
|
-
): Promise<CleanupShortSegmentsResult> {
|
|
99
|
+
export async function cleanupShortSegments(opts?: {
|
|
100
|
+
dryRun?: boolean;
|
|
101
|
+
}): Promise<CleanupShortSegmentsResult> {
|
|
100
102
|
const db = getDb();
|
|
101
103
|
|
|
102
104
|
const shortSegments = db
|
|
@@ -117,18 +119,22 @@ export async function cleanupShortSegments(
|
|
|
117
119
|
await withQdrantBreaker(() => qdrant.deleteByTarget("segment", row.id));
|
|
118
120
|
} catch (err) {
|
|
119
121
|
// Keep the SQLite row so the target ID is preserved for retry
|
|
120
|
-
log.warn(
|
|
122
|
+
log.warn(
|
|
123
|
+
{ segmentId: row.id, err },
|
|
124
|
+
"Qdrant deletion failed — skipping SQLite deletion to preserve target ID",
|
|
125
|
+
);
|
|
121
126
|
failed++;
|
|
122
127
|
continue;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
db.delete(memorySegments)
|
|
126
|
-
.where(eq(memorySegments.id, row.id))
|
|
127
|
-
.run();
|
|
130
|
+
db.delete(memorySegments).where(eq(memorySegments.id, row.id)).run();
|
|
128
131
|
removed++;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
|
-
log.info(
|
|
134
|
+
log.info(
|
|
135
|
+
{ removed, failed, threshold: MIN_SEGMENT_CHARS },
|
|
136
|
+
"Cleaned up short segments",
|
|
137
|
+
);
|
|
132
138
|
return { removed, failed };
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -160,7 +166,7 @@ export function findReextractTargets(limit: number): ReextractTarget[] {
|
|
|
160
166
|
.from(conversations)
|
|
161
167
|
.leftJoin(messages, eq(messages.conversationId, conversations.id))
|
|
162
168
|
.where(
|
|
163
|
-
sql`${conversations.conversationType} NOT IN ('background', 'private')`,
|
|
169
|
+
sql`${conversations.conversationType} NOT IN ('background', 'private', 'scheduled')`,
|
|
164
170
|
)
|
|
165
171
|
.groupBy(conversations.id)
|
|
166
172
|
.orderBy(desc(sql`count(${messages.id})`))
|
|
@@ -204,25 +210,21 @@ export function findReextractTarget(
|
|
|
204
210
|
/**
|
|
205
211
|
* Queue re-extraction for a set of conversations.
|
|
206
212
|
* Resets extraction checkpoints and clears extraction summaries so the
|
|
207
|
-
*
|
|
213
|
+
* graph extraction handler processes all messages from scratch with
|
|
208
214
|
* expanded supersession context.
|
|
209
215
|
*/
|
|
210
|
-
export function requestReextract(
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
export function requestReextract(targets: ReextractTarget[]): {
|
|
217
|
+
jobIds: string[];
|
|
218
|
+
} {
|
|
213
219
|
const db = getDb();
|
|
214
220
|
const jobIds: string[] = [];
|
|
215
221
|
|
|
216
222
|
for (const target of targets) {
|
|
217
223
|
const { conversationId } = target;
|
|
218
224
|
|
|
219
|
-
// Reset
|
|
220
|
-
deleteMemoryCheckpoint(
|
|
221
|
-
|
|
222
|
-
);
|
|
223
|
-
deleteMemoryCheckpoint(
|
|
224
|
-
`batch_extract:${conversationId}:pending_count`,
|
|
225
|
-
);
|
|
225
|
+
// Reset graph extraction checkpoints
|
|
226
|
+
deleteMemoryCheckpoint(`graph_extract:${conversationId}:last_ts`);
|
|
227
|
+
deleteMemoryCheckpoint(`graph_extract:${conversationId}:pending_count`);
|
|
226
228
|
|
|
227
229
|
// Clear the extraction summary so it starts fresh
|
|
228
230
|
db.delete(memorySummaries)
|
|
@@ -234,12 +236,11 @@ export function requestReextract(
|
|
|
234
236
|
)
|
|
235
237
|
.run();
|
|
236
238
|
|
|
237
|
-
// Resolve scope and enqueue
|
|
239
|
+
// Resolve scope and enqueue re-extraction
|
|
238
240
|
const scopeId = getConversationMemoryScopeId(conversationId);
|
|
239
|
-
const jobId = enqueueMemoryJob("
|
|
241
|
+
const jobId = enqueueMemoryJob("graph_extract", {
|
|
240
242
|
conversationId,
|
|
241
243
|
scopeId,
|
|
242
|
-
fullReextract: true,
|
|
243
244
|
});
|
|
244
245
|
jobIds.push(jobId);
|
|
245
246
|
|
package/src/memory/app-store.ts
CHANGED
|
@@ -216,6 +216,8 @@ export function generateAppDirName(
|
|
|
216
216
|
|
|
217
217
|
/** Cache of id -> dirName mappings to avoid repeated filesystem scans. */
|
|
218
218
|
const idToDirNameCache = new Map<string, string>();
|
|
219
|
+
/** Reverse cache: dirName -> id. */
|
|
220
|
+
const dirNameToIdCache = new Map<string, string>();
|
|
219
221
|
|
|
220
222
|
/**
|
|
221
223
|
* Resolve an app's directory name and path from its ID.
|
|
@@ -265,12 +267,79 @@ export function getAppDirPath(appId: string): string {
|
|
|
265
267
|
return resolveAppDir(appId).appDir;
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Resolve an app ID from its directory name (slug).
|
|
272
|
+
* Checks caches first, then reads the JSON definition file directly.
|
|
273
|
+
*/
|
|
274
|
+
export function resolveAppIdByDirName(dirName: string): string | null {
|
|
275
|
+
const cached = dirNameToIdCache.get(dirName);
|
|
276
|
+
if (cached) return cached;
|
|
277
|
+
|
|
278
|
+
// Check forward cache (reverse iteration)
|
|
279
|
+
for (const [id, dn] of idToDirNameCache) {
|
|
280
|
+
if (dn === dirName) {
|
|
281
|
+
dirNameToIdCache.set(dirName, id);
|
|
282
|
+
return id;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Read the JSON definition file directly
|
|
287
|
+
const dir = getAppsDir();
|
|
288
|
+
const jsonPath = join(dir, `${dirName}.json`);
|
|
289
|
+
if (existsSync(jsonPath)) {
|
|
290
|
+
try {
|
|
291
|
+
const raw = readFileSync(jsonPath, "utf-8");
|
|
292
|
+
const parsed = JSON.parse(raw) as { id?: string; dirName?: string };
|
|
293
|
+
if (parsed.id) {
|
|
294
|
+
dirNameToIdCache.set(dirName, parsed.id);
|
|
295
|
+
idToDirNameCache.set(parsed.id, dirName);
|
|
296
|
+
return parsed.id;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// skip malformed files
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract app ID from an absolute file path if it falls within the apps
|
|
308
|
+
* directory and targets a source file (not records/ or dist/).
|
|
309
|
+
*/
|
|
310
|
+
export function resolveAppIdFromPath(filePath: string): string | null {
|
|
311
|
+
let appsDir: string;
|
|
312
|
+
try {
|
|
313
|
+
appsDir = getAppsDir();
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (!filePath.startsWith(appsDir + "/")) return null;
|
|
318
|
+
|
|
319
|
+
const relPath = filePath.slice(appsDir.length + 1);
|
|
320
|
+
const slashIdx = relPath.indexOf("/");
|
|
321
|
+
if (slashIdx === -1) return null; // file directly in apps/ (e.g. the .json definition)
|
|
322
|
+
|
|
323
|
+
const dirName = relPath.slice(0, slashIdx);
|
|
324
|
+
const innerPath = relPath.slice(slashIdx + 1);
|
|
325
|
+
|
|
326
|
+
// Skip non-source directories
|
|
327
|
+
if (innerPath.startsWith("records/") || innerPath.startsWith("dist/")) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return resolveAppIdByDirName(dirName);
|
|
332
|
+
}
|
|
333
|
+
|
|
268
334
|
/** Invalidate the id->dirName cache for a specific app or all apps. */
|
|
269
335
|
function invalidateDirNameCache(appId?: string): void {
|
|
270
336
|
if (appId) {
|
|
337
|
+
const dirName = idToDirNameCache.get(appId);
|
|
271
338
|
idToDirNameCache.delete(appId);
|
|
339
|
+
if (dirName) dirNameToIdCache.delete(dirName);
|
|
272
340
|
} else {
|
|
273
341
|
idToDirNameCache.clear();
|
|
342
|
+
dirNameToIdCache.clear();
|
|
274
343
|
}
|
|
275
344
|
}
|
|
276
345
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./conversation-title-service.js";
|
|
7
7
|
|
|
8
8
|
export interface BootstrapConversationOptions {
|
|
9
|
-
conversationType?: "standard" | "private" | "background";
|
|
9
|
+
conversationType?: "standard" | "private" | "background" | "scheduled";
|
|
10
10
|
source?: string;
|
|
11
11
|
origin: TitleOrigin;
|
|
12
12
|
systemHint: string;
|