@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memory v3 — `memory_v3_maintain` job handler.
|
|
3
3
|
*
|
|
4
|
-
* A flag-gated, best-effort self-maintenance pass over the v3
|
|
5
|
-
*
|
|
4
|
+
* A flag-gated, best-effort self-maintenance pass over the v3 section dense
|
|
5
|
+
* store and the in-memory lanes. It runs three independent stages, in order:
|
|
6
6
|
*
|
|
7
|
-
* 1. **
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
7
|
+
* 1. **Section re-embed** — diff the page index by `modifiedAt` against the
|
|
8
|
+
* last successful pass (the high-water mark below), and for every page that
|
|
9
|
+
* is new or edited since then, re-chunk it into sections
|
|
10
|
+
* (`buildSectionIndex`) and refresh its dense points
|
|
11
|
+
* (`deleteSectionsForArticle` + `upsertSections`). This keeps the
|
|
12
|
+
* section-grain Qdrant collection in sync with on-disk page edits so the
|
|
13
|
+
* dense lane retrieves against current content. The high-water mark is
|
|
14
|
+
* advanced only after the pass completes with zero page failures (and is
|
|
15
|
+
* captured before any potential mtime bumps) so a page is not re-embedded
|
|
16
|
+
* forever, yet a page whose embed failed (and whose sections were therefore
|
|
17
|
+
* left deleted) stays above the mark and is retried next pass.
|
|
18
|
+
* 2. **Deleted-page prune** — diff the dense store's stored articles
|
|
19
|
+
* (`listSectionArticles`) against the live page-index slugs and
|
|
20
|
+
* `deleteSectionsForArticle` for any article that is no longer in the
|
|
21
|
+
* index. A deleted page's slug never reaches the re-embed delta selector
|
|
22
|
+
* (it only names live pages), so without this its section points would
|
|
23
|
+
* linger in Qdrant and the dense lane could still surface the deleted page.
|
|
24
|
+
* Synthetic capability rows are in the page index, so they are never pruned.
|
|
25
|
+
* 3. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
|
|
26
|
+
* in-memory section index, needle, and edge graph from the freshly-updated
|
|
27
|
+
* pages.
|
|
18
28
|
*
|
|
19
29
|
* Best-effort by construction: each stage is wrapped so a failure in one is
|
|
20
|
-
* logged and recorded in the outcome but does NOT abort the others.
|
|
21
|
-
*
|
|
30
|
+
* logged and recorded in the outcome but does NOT abort the others. A single
|
|
31
|
+
* page whose embed fails does not abort the rest of the re-embed stage, and a
|
|
32
|
+
* single prune delete that throws does not abort the rest of the prune stage.
|
|
33
|
+
* The job is a no-op (returns a disabled outcome) unless `memory-v3-shadow` OR
|
|
22
34
|
* `memory-v3-live` is enabled — the same flags that gate the v3 plugin.
|
|
23
35
|
*
|
|
24
|
-
* Dependency-injectable: `deps` lets tests substitute the
|
|
25
|
-
*
|
|
36
|
+
* Dependency-injectable: `deps` lets tests substitute the page-index reader,
|
|
37
|
+
* section builder, dense-store ops (including the prune-stage
|
|
38
|
+
* `listSectionArticles`/`listIndexedSlugs` collaborators), and `invalidateLanes`
|
|
39
|
+
* without process-global module mocks.
|
|
26
40
|
*/
|
|
27
41
|
|
|
28
42
|
import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
|
|
@@ -33,186 +47,404 @@ import {
|
|
|
33
47
|
} from "../../../memory/checkpoints.js";
|
|
34
48
|
import type { MemoryJob } from "../../../memory/jobs-store.js";
|
|
35
49
|
import { getPageIndex } from "../../../memory/v2/page-index.js";
|
|
36
|
-
import {
|
|
37
|
-
listPages,
|
|
38
|
-
readPage,
|
|
39
|
-
writePage,
|
|
40
|
-
} from "../../../memory/v2/page-store.js";
|
|
50
|
+
import { readPage } from "../../../memory/v2/page-store.js";
|
|
41
51
|
import { getLogger } from "../../../util/logger.js";
|
|
42
52
|
import { getWorkspaceDir } from "../../../util/platform.js";
|
|
43
|
-
import {
|
|
53
|
+
import { capabilityOrDiskBody } from "./capabilities.js";
|
|
54
|
+
import {
|
|
55
|
+
deleteSectionsForArticle as realDeleteSectionsForArticle,
|
|
56
|
+
ensureSectionCollection as realEnsureSectionCollection,
|
|
57
|
+
listSectionArticles as realListSectionArticles,
|
|
58
|
+
upsertSections as realUpsertSections,
|
|
59
|
+
} from "./section-dense-store.js";
|
|
60
|
+
import { buildSectionIndex as realBuildSectionIndex } from "./sections.js";
|
|
44
61
|
import { invalidateLanes as realInvalidateLanes } from "./shadow-plugin.js";
|
|
45
|
-
import {
|
|
46
|
-
import type { LeafPath, LeafTree, Slug } from "./types.js";
|
|
62
|
+
import type { Slug } from "./types.js";
|
|
47
63
|
|
|
48
64
|
const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
|
|
49
65
|
const MEMORY_V3_LIVE = "memory-v3-live" as const;
|
|
50
66
|
|
|
51
67
|
/**
|
|
52
68
|
* Durable checkpoint holding the epoch-ms high-water mark of the last successful
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
69
|
+
* re-embed pass. Pages whose mtime is past this mark are re-chunked + re-embedded
|
|
70
|
+
* into the section dense store; the mark is advanced only after a pass completes,
|
|
71
|
+
* captured before the pass so a transient embed write does not re-trigger itself.
|
|
72
|
+
*
|
|
73
|
+
* This is a FRESH key, distinct from the tree-era `enriched_through_ms` that the
|
|
74
|
+
* old classify-union maintainer advanced. On upgrade the new key is absent, so
|
|
75
|
+
* `computeChangedPages` sees a high-water of 0 and re-embeds EVERY page on the
|
|
76
|
+
* first run — seeding the otherwise-empty `memory_v3_sections` collection.
|
|
77
|
+
* Reusing the old (already-advanced) key would skip all historical pages and
|
|
78
|
+
* leave dense retrieval blind to existing memory until each page was next edited.
|
|
79
|
+
*
|
|
80
|
+
* Distinct too from `memory_v3_maintain_last_run` (the enqueue-cadence checkpoint
|
|
81
|
+
* in `jobs-worker.ts`), which advances on every backstop enqueue rather than on
|
|
82
|
+
* an actual maintenance run.
|
|
59
83
|
*/
|
|
60
|
-
const
|
|
61
|
-
"memory_v3_maintain:
|
|
84
|
+
const MAINTAIN_EMBED_HIGH_WATER_KEY =
|
|
85
|
+
"memory_v3_maintain:sections_embedded_through_ms" as const;
|
|
62
86
|
|
|
63
87
|
const log = getLogger("memory-v3-maintain");
|
|
64
88
|
|
|
89
|
+
/** Page-index projection the changed-page selector reads. */
|
|
90
|
+
export interface ChangedPageCandidate {
|
|
91
|
+
slug: Slug;
|
|
92
|
+
/** File mtime in epoch ms; 0 for synthetic skill/CLI rows (excluded). */
|
|
93
|
+
modifiedAt: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
/** Injectable collaborators; defaults wire the real implementations. */
|
|
66
97
|
export interface MaintainJobDeps {
|
|
67
|
-
/** Load the v3 leaf tree (page→leaf membership resolved from frontmatter). */
|
|
68
|
-
loadTree: () => Promise<LeafTree>;
|
|
69
|
-
/** Classify-union pass over pages. */
|
|
70
|
-
assignPages: typeof realAssignPages;
|
|
71
98
|
/**
|
|
72
|
-
* The slugs to
|
|
73
|
-
* the last successful pass. See {@link
|
|
99
|
+
* The slugs whose sections to re-embed this pass: pages new or edited since
|
|
100
|
+
* the last successful pass. See {@link computeChangedPages}.
|
|
101
|
+
*/
|
|
102
|
+
selectChangedPages: () => Promise<Slug[]>;
|
|
103
|
+
/** Re-chunk pages into the flat section index (pure; reads page bodies). */
|
|
104
|
+
buildSectionIndex: typeof realBuildSectionIndex;
|
|
105
|
+
/** Read a page's frontmatter-stripped body for `buildSectionIndex`. */
|
|
106
|
+
readPageBody: (slug: Slug) => Promise<string>;
|
|
107
|
+
/** Clear an article's stale section points before re-upserting. */
|
|
108
|
+
deleteSectionsForArticle: typeof realDeleteSectionsForArticle;
|
|
109
|
+
/** Embed + upsert an article's current sections into the dense store. */
|
|
110
|
+
upsertSections: typeof realUpsertSections;
|
|
111
|
+
/**
|
|
112
|
+
* Persist the high-water mark after a re-embed pass with zero failures. The
|
|
113
|
+
* value is captured before the pass's writes (see the key docstring); the
|
|
114
|
+
* caller skips this when any page failed so failed pages retry next pass.
|
|
74
115
|
*/
|
|
75
|
-
|
|
116
|
+
commitEmbedHighWater: (highWaterMs: number) => void;
|
|
76
117
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
118
|
+
* Every distinct `article` slug that currently has section points in the
|
|
119
|
+
* dense store. The prune stage diffs this against the live page-index slugs
|
|
120
|
+
* to find articles whose points linger after the page was deleted.
|
|
79
121
|
*/
|
|
80
|
-
|
|
81
|
-
/**
|
|
122
|
+
listSectionArticles: () => Promise<string[]>;
|
|
123
|
+
/**
|
|
124
|
+
* The slugs currently in the page index (the live set the lanes are rebuilt
|
|
125
|
+
* from). The prune stage keeps these and deletes any section article absent
|
|
126
|
+
* from this set. Includes synthetic capability rows so they are never pruned.
|
|
127
|
+
*/
|
|
128
|
+
listIndexedSlugs: () => Promise<Slug[]>;
|
|
129
|
+
/** Drop the memoized v3 lanes so the next turn rebuilds them. */
|
|
82
130
|
invalidateLanes: () => void;
|
|
83
|
-
/**
|
|
84
|
-
|
|
131
|
+
/** Active assistant config (for the dense-store/embedding calls). */
|
|
132
|
+
config: AssistantConfig;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Injectable collaborators for the one-time {@link backfillAllSections} pass.
|
|
137
|
+
* Distinct from {@link MaintainJobDeps}: the backfill embeds EVERY page
|
|
138
|
+
* (including synthetic capability rows the incremental selector excludes), so it
|
|
139
|
+
* uses an all-pages selector, a capability-aware body reader, and an explicit
|
|
140
|
+
* collection-ensure step rather than the change-delta machinery.
|
|
141
|
+
*/
|
|
142
|
+
export interface BackfillJobDeps {
|
|
143
|
+
/** All page-index slugs, including synthetic skill/CLI capability rows. */
|
|
144
|
+
selectAllPages: () => Promise<Slug[]>;
|
|
145
|
+
/** Create the section collection if absent before the first upsert. */
|
|
146
|
+
ensureSectionCollection: typeof realEnsureSectionCollection;
|
|
147
|
+
/** Re-chunk pages into the flat section index (pure; reads page bodies). */
|
|
148
|
+
buildSectionIndex: typeof realBuildSectionIndex;
|
|
149
|
+
/** Read a page's body; capability slugs resolve their rendered content. */
|
|
150
|
+
readPageBody: (slug: Slug) => Promise<string>;
|
|
151
|
+
/** Clear an article's stale section points before re-upserting. */
|
|
152
|
+
deleteSectionsForArticle: typeof realDeleteSectionsForArticle;
|
|
153
|
+
/** Embed + upsert an article's current sections into the dense store. */
|
|
154
|
+
upsertSections: typeof realUpsertSections;
|
|
155
|
+
/**
|
|
156
|
+
* Persist the high-water mark after the backfill completes with zero
|
|
157
|
+
* failures. Skipped when any page failed so failed pages retry next pass.
|
|
158
|
+
*/
|
|
159
|
+
commitEmbedHighWater: (highWaterMs: number) => void;
|
|
160
|
+
/** Epoch-ms stamped as the new high-water mark; injectable for tests. */
|
|
161
|
+
nowMs: () => number;
|
|
162
|
+
/** Active assistant config (for the dense-store/embedding calls). */
|
|
163
|
+
config: AssistantConfig;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Counts surfaced by {@link backfillAllSections}. */
|
|
167
|
+
export interface BackfillOutcome {
|
|
168
|
+
/** Pages whose sections were (re-)embedded this pass. */
|
|
169
|
+
articles: number;
|
|
170
|
+
/** Total section points upserted across all articles. */
|
|
171
|
+
sections: number;
|
|
172
|
+
/** Pages whose embed threw (and was contained). */
|
|
173
|
+
failures: number;
|
|
85
174
|
}
|
|
86
175
|
|
|
87
176
|
/** Per-stage outcome surfaced in the structured log line. */
|
|
88
177
|
export interface MaintainOutcome {
|
|
89
178
|
/** True when both v3 flags were off and the job no-opped. */
|
|
90
179
|
disabled: boolean;
|
|
91
|
-
/** Pages
|
|
92
|
-
|
|
93
|
-
/** Pages whose
|
|
180
|
+
/** Pages whose sections were re-chunked + re-embedded this pass. */
|
|
181
|
+
reembedded: number;
|
|
182
|
+
/** Pages whose re-embed threw (and was contained). */
|
|
183
|
+
reembedFailures: number;
|
|
184
|
+
/**
|
|
185
|
+
* Deleted-page articles whose lingering section points were pruned this pass
|
|
186
|
+
* (present in the dense store but absent from the page index).
|
|
187
|
+
*/
|
|
94
188
|
pruned: number;
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
/** Whether the
|
|
189
|
+
/** Prune deletes that threw (and were contained). */
|
|
190
|
+
pruneFailures: number;
|
|
191
|
+
/** Whether the in-memory lanes were invalidated. */
|
|
98
192
|
invalidated: boolean;
|
|
99
193
|
/** Stages that threw (and were contained). */
|
|
100
194
|
failures: string[];
|
|
101
195
|
}
|
|
102
196
|
|
|
103
197
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
198
|
+
* Pick the pages the re-embed stage should refresh this pass: every page whose
|
|
199
|
+
* mtime is past `prevHighWaterMs` (new or edited since the last successful run).
|
|
200
|
+
* Synthetic skill/CLI rows (`modifiedAt === 0`) are excluded — they are
|
|
201
|
+
* capability entries with no on-disk page to chunk. On the first run
|
|
202
|
+
* (`prevHighWaterMs` null) every real page is selected so a fresh or
|
|
203
|
+
* freshly-backfilled install seeds the section dense store once.
|
|
107
204
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const pageLeaves = new Map<Slug, LeafPath[]>();
|
|
111
|
-
for (const entry of pageIndex.entries) {
|
|
112
|
-
pageLeaves.set(entry.slug, entry.leaves);
|
|
113
|
-
}
|
|
114
|
-
return realLoadLeafTree(resolveDataDir(), pageLeaves);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Page-index projection the classify-target selector reads. */
|
|
118
|
-
export interface ClassifyCandidate {
|
|
119
|
-
slug: Slug;
|
|
120
|
-
/** File mtime in epoch ms; 0 for synthetic skill/CLI rows (excluded). */
|
|
121
|
-
modifiedAt: number;
|
|
122
|
-
/** Leaf assignments from frontmatter; `[]` when unassigned. */
|
|
123
|
-
leaves: LeafPath[];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Pick the pages the classify-union stage should run this pass:
|
|
128
|
-
*
|
|
129
|
-
* - **Unassigned** pages (empty `leaves:`) — always classified so a page is
|
|
130
|
-
* never left permanently unrouted (the pre-enrichment behavior).
|
|
131
|
-
* - **Recently edited** pages (mtime past `prevHighWaterMs`) — even when
|
|
132
|
-
* already assigned, so the classifier can enrich the assistant's own
|
|
133
|
-
* in-consolidation labels with leaves it missed. `assignPages` UNIONs, so a
|
|
134
|
-
* page's existing picks are never dropped — only added to.
|
|
135
|
-
*
|
|
136
|
-
* Synthetic skill/CLI rows (`modifiedAt === 0`) are excluded: they are
|
|
137
|
-
* capability entries, not topic-tree pages. On the first run (`prevHighWaterMs`
|
|
138
|
-
* null) the recency arm is disabled, so a fresh or freshly-backfilled install
|
|
139
|
-
* does a single unassigned-only pass instead of re-classifying the whole corpus.
|
|
140
|
-
*/
|
|
141
|
-
export function computeClassifyTargets(
|
|
142
|
-
pages: ReadonlyArray<ClassifyCandidate>,
|
|
205
|
+
export function computeChangedPages(
|
|
206
|
+
pages: ReadonlyArray<ChangedPageCandidate>,
|
|
143
207
|
prevHighWaterMs: number | null,
|
|
144
208
|
): Slug[] {
|
|
145
|
-
const
|
|
209
|
+
const changed: Slug[] = [];
|
|
146
210
|
for (const page of pages) {
|
|
147
211
|
if (page.modifiedAt <= 0) continue; // synthetic capability row
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (unassigned || recentlyEdited) targets.push(page.slug);
|
|
212
|
+
if (prevHighWaterMs === null || page.modifiedAt > prevHighWaterMs) {
|
|
213
|
+
changed.push(page.slug);
|
|
214
|
+
}
|
|
152
215
|
}
|
|
153
|
-
return
|
|
216
|
+
return changed;
|
|
154
217
|
}
|
|
155
218
|
|
|
156
219
|
/** Read the persisted high-water mark, treating missing/garbage as first-run. */
|
|
157
|
-
function
|
|
158
|
-
const raw = getMemoryCheckpoint(
|
|
220
|
+
function readEmbedHighWater(): number | null {
|
|
221
|
+
const raw = getMemoryCheckpoint(MAINTAIN_EMBED_HIGH_WATER_KEY);
|
|
159
222
|
if (raw === null) return null;
|
|
160
223
|
const parsed = Number.parseInt(raw, 10);
|
|
161
224
|
return Number.isNaN(parsed) ? null : parsed;
|
|
162
225
|
}
|
|
163
226
|
|
|
164
|
-
/** Default
|
|
165
|
-
async function
|
|
227
|
+
/** Default selector: page index → {@link computeChangedPages}. */
|
|
228
|
+
async function selectChangedPagesFromWorkspace(
|
|
166
229
|
workspaceDir: string,
|
|
167
230
|
): Promise<Slug[]> {
|
|
168
231
|
const index = await getPageIndex(workspaceDir);
|
|
169
|
-
return
|
|
232
|
+
return computeChangedPages(index.entries, readEmbedHighWater());
|
|
170
233
|
}
|
|
171
234
|
|
|
172
|
-
function
|
|
173
|
-
setMemoryCheckpoint(
|
|
235
|
+
function commitEmbedHighWater(highWaterMs: number): void {
|
|
236
|
+
setMemoryCheckpoint(MAINTAIN_EMBED_HIGH_WATER_KEY, String(highWaterMs));
|
|
174
237
|
}
|
|
175
238
|
|
|
176
|
-
|
|
239
|
+
/** Read a page's frontmatter-stripped body; missing/failed reads degrade to "". */
|
|
240
|
+
async function readPageBodyFromWorkspace(
|
|
241
|
+
workspaceDir: string,
|
|
242
|
+
slug: Slug,
|
|
243
|
+
): Promise<string> {
|
|
244
|
+
try {
|
|
245
|
+
const page = await readPage(workspaceDir, slug);
|
|
246
|
+
return page?.body ?? "";
|
|
247
|
+
} catch {
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Capability-aware page-body reader for the full backfill: synthetic skill/CLI
|
|
254
|
+
* slugs have no on-disk page, so they contribute their rendered capability
|
|
255
|
+
* content (exactly what `page-content.ts` injects for them) while real pages
|
|
256
|
+
* read from disk. Shares the capability-or-disk dispatch with `shadow-plugin.ts`
|
|
257
|
+
* `initLanes`' `pageBody` via {@link capabilityOrDiskBody}.
|
|
258
|
+
*/
|
|
259
|
+
async function backfillPageBodyFromWorkspace(
|
|
260
|
+
workspaceDir: string,
|
|
261
|
+
slug: Slug,
|
|
262
|
+
): Promise<string> {
|
|
263
|
+
return capabilityOrDiskBody(slug, (s) =>
|
|
264
|
+
readPageBodyFromWorkspace(workspaceDir, s),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** All page-index slugs, including synthetic skill/CLI capability rows. */
|
|
269
|
+
async function selectAllPagesFromWorkspace(
|
|
270
|
+
workspaceDir: string,
|
|
271
|
+
): Promise<Slug[]> {
|
|
272
|
+
const index = await getPageIndex(workspaceDir);
|
|
273
|
+
return index.entries.map((entry) => entry.slug);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function defaultDeps(config: AssistantConfig): MaintainJobDeps {
|
|
177
277
|
const workspaceDir = getWorkspaceDir();
|
|
178
278
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
279
|
+
selectChangedPages: () => selectChangedPagesFromWorkspace(workspaceDir),
|
|
280
|
+
buildSectionIndex: realBuildSectionIndex,
|
|
281
|
+
readPageBody: (slug) => readPageBodyFromWorkspace(workspaceDir, slug),
|
|
282
|
+
deleteSectionsForArticle: realDeleteSectionsForArticle,
|
|
283
|
+
upsertSections: realUpsertSections,
|
|
284
|
+
commitEmbedHighWater,
|
|
285
|
+
listSectionArticles: () => realListSectionArticles(config),
|
|
286
|
+
listIndexedSlugs: () => selectAllPagesFromWorkspace(workspaceDir),
|
|
184
287
|
invalidateLanes: realInvalidateLanes,
|
|
185
|
-
|
|
288
|
+
config,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function defaultBackfillDeps(config: AssistantConfig): BackfillJobDeps {
|
|
293
|
+
const workspaceDir = getWorkspaceDir();
|
|
294
|
+
return {
|
|
295
|
+
selectAllPages: () => selectAllPagesFromWorkspace(workspaceDir),
|
|
296
|
+
ensureSectionCollection: realEnsureSectionCollection,
|
|
297
|
+
buildSectionIndex: realBuildSectionIndex,
|
|
298
|
+
readPageBody: (slug) => backfillPageBodyFromWorkspace(workspaceDir, slug),
|
|
299
|
+
deleteSectionsForArticle: realDeleteSectionsForArticle,
|
|
300
|
+
upsertSections: realUpsertSections,
|
|
301
|
+
commitEmbedHighWater,
|
|
302
|
+
nowMs: () => Date.now(),
|
|
303
|
+
config,
|
|
186
304
|
};
|
|
187
305
|
}
|
|
188
306
|
|
|
189
307
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
308
|
+
* Re-chunk + re-embed each changed page into the section dense store. Each page
|
|
309
|
+
* is refreshed independently: a single page whose delete/build/upsert throws is
|
|
310
|
+
* logged and counted in `reembedFailures` without aborting the rest. Returns the
|
|
311
|
+
* counts of successfully refreshed and failed pages.
|
|
193
312
|
*/
|
|
194
|
-
async function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
): Promise<{
|
|
198
|
-
|
|
199
|
-
|
|
313
|
+
async function reembedChangedPages(
|
|
314
|
+
slugs: Slug[],
|
|
315
|
+
deps: MaintainJobDeps,
|
|
316
|
+
): Promise<{ reembedded: number; reembedFailures: number }> {
|
|
317
|
+
let reembedded = 0;
|
|
318
|
+
let reembedFailures = 0;
|
|
319
|
+
for (const slug of slugs) {
|
|
320
|
+
try {
|
|
321
|
+
const index = await deps.buildSectionIndex([slug], deps.readPageBody);
|
|
322
|
+
await deps.deleteSectionsForArticle(deps.config, slug);
|
|
323
|
+
await deps.upsertSections(deps.config, index.sections);
|
|
324
|
+
reembedded += 1;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
reembedFailures += 1;
|
|
327
|
+
log.warn(
|
|
328
|
+
{ slug, err: err instanceof Error ? err.message : String(err) },
|
|
329
|
+
"memory-v3 maintain: page re-embed failed (non-fatal)",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { reembedded, reembedFailures };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Prune section points for articles that no longer exist in the page index. A
|
|
338
|
+
* deleted concept page leaves `getPageIndex` (so the needle and edge lanes both
|
|
339
|
+
* drop it on the next rebuild) but its section points linger in Qdrant, where
|
|
340
|
+
* the dense lane could still surface it. The incremental selector
|
|
341
|
+
* ({@link computeChangedPages}) never names a slug that is gone, so a deleted
|
|
342
|
+
* page's stale points are only reachable by reading the collection back and
|
|
343
|
+
* diffing against the live slug set.
|
|
344
|
+
*
|
|
345
|
+
* Each deletion is independent: a single `deleteSectionsForArticle` throw is
|
|
346
|
+
* logged and counted in `pruneFailures` without aborting the rest. Synthetic
|
|
347
|
+
* capability rows are in the page index, so they are never pruned.
|
|
348
|
+
*/
|
|
349
|
+
async function pruneDeletedPages(
|
|
350
|
+
deps: MaintainJobDeps,
|
|
351
|
+
): Promise<{ pruned: number; pruneFailures: number }> {
|
|
352
|
+
const [storedArticles, indexedSlugs] = await Promise.all([
|
|
353
|
+
deps.listSectionArticles(),
|
|
354
|
+
deps.listIndexedSlugs(),
|
|
355
|
+
]);
|
|
356
|
+
const live = new Set(indexedSlugs);
|
|
357
|
+
const deleted = storedArticles.filter((article) => !live.has(article));
|
|
358
|
+
|
|
200
359
|
let pruned = 0;
|
|
201
|
-
let
|
|
360
|
+
let pruneFailures = 0;
|
|
361
|
+
for (const article of deleted) {
|
|
362
|
+
try {
|
|
363
|
+
await deps.deleteSectionsForArticle(deps.config, article);
|
|
364
|
+
pruned += 1;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
pruneFailures += 1;
|
|
367
|
+
log.warn(
|
|
368
|
+
{ article, err: err instanceof Error ? err.message : String(err) },
|
|
369
|
+
"memory-v3 maintain: deleted-page section prune failed (non-fatal)",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return { pruned, pruneFailures };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* One-time full backfill of the section dense store. Unlike {@link maintainJob}
|
|
378
|
+
* (which only re-embeds pages changed since the high-water mark and EXCLUDES
|
|
379
|
+
* synthetic capability rows), this embeds EVERY page in the index — including
|
|
380
|
+
* synthetic skill/CLI rows, whose `modifiedAt` is 0 and which the incremental
|
|
381
|
+
* selector therefore never picks. It exists for the operator transition before
|
|
382
|
+
* A/B/cutover: the `memory_v3_sections` collection starts empty, so on an
|
|
383
|
+
* existing install most pages were never embedded into the dense lane.
|
|
384
|
+
*
|
|
385
|
+
* Each article is refreshed independently (delete its stale points, then
|
|
386
|
+
* re-upsert its current sections); a single article whose build/delete/upsert
|
|
387
|
+
* throws is logged and counted in `failures` without aborting the rest. When the
|
|
388
|
+
* pass completes with zero failures the high-water mark is advanced to the
|
|
389
|
+
* timestamp captured BEFORE the pass, so the next incremental maintain run
|
|
390
|
+
* deltas from here instead of re-embedding the whole corpus again. The timestamp
|
|
391
|
+
* is captured up-front (as in `maintainJob`) so an embed write that bumps a
|
|
392
|
+
* page's mtime does not re-trigger it next pass. If any page failed the
|
|
393
|
+
* checkpoint is held (not advanced): a failed page was delete-then-upsert'd and
|
|
394
|
+
* so has no sections, and advancing past its mtime would hide it from the
|
|
395
|
+
* incremental selector forever — holding the mark lets the next pass re-embed it.
|
|
396
|
+
*/
|
|
397
|
+
export async function backfillAllSections(
|
|
398
|
+
config: AssistantConfig,
|
|
399
|
+
deps: BackfillJobDeps = defaultBackfillDeps(config),
|
|
400
|
+
): Promise<BackfillOutcome> {
|
|
401
|
+
// Capture the high-water stamp before any writes (see the function docstring).
|
|
402
|
+
const startedAtMs = deps.nowMs();
|
|
403
|
+
|
|
404
|
+
const slugs = await deps.selectAllPages();
|
|
405
|
+
await deps.ensureSectionCollection(deps.config);
|
|
406
|
+
|
|
407
|
+
let articles = 0;
|
|
408
|
+
let sections = 0;
|
|
409
|
+
let failures = 0;
|
|
202
410
|
for (const slug of slugs) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
411
|
+
try {
|
|
412
|
+
const index = await deps.buildSectionIndex([slug], deps.readPageBody);
|
|
413
|
+
await deps.deleteSectionsForArticle(deps.config, slug);
|
|
414
|
+
await deps.upsertSections(deps.config, index.sections);
|
|
415
|
+
articles += 1;
|
|
416
|
+
sections += index.sections.length;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
failures += 1;
|
|
419
|
+
log.warn(
|
|
420
|
+
{ slug, err: err instanceof Error ? err.message : String(err) },
|
|
421
|
+
"memory-v3 backfill: page embed failed (non-fatal)",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Advance the maintain high-water mark only when every page embedded cleanly,
|
|
427
|
+
// so the next incremental pass deltas from here rather than re-embedding
|
|
428
|
+
// everything. A failed page was delete-then-upsert'd, so it now has no
|
|
429
|
+
// sections; advancing past its mtime would hide it from `computeChangedPages`
|
|
430
|
+
// forever. Holding the checkpoint keeps failed pages above the mark so the
|
|
431
|
+
// next pass re-embeds them — re-embedding the succeeded pages too is
|
|
432
|
+
// idempotent and cheap.
|
|
433
|
+
if (failures === 0) {
|
|
434
|
+
deps.commitEmbedHighWater(startedAtMs);
|
|
435
|
+
} else {
|
|
436
|
+
log.info(
|
|
437
|
+
{ articles, sections, failures },
|
|
438
|
+
"memory-v3 backfill: embed checkpoint held (page failures) — failed pages retry next pass",
|
|
439
|
+
);
|
|
214
440
|
}
|
|
215
|
-
|
|
441
|
+
|
|
442
|
+
log.info(
|
|
443
|
+
{ articles, sections, failures },
|
|
444
|
+
"memory-v3 section backfill complete",
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
return { articles, sections, failures };
|
|
216
448
|
}
|
|
217
449
|
|
|
218
450
|
/**
|
|
@@ -223,13 +455,14 @@ async function prunePages(
|
|
|
223
455
|
export async function maintainJob(
|
|
224
456
|
_job: MemoryJob,
|
|
225
457
|
config: AssistantConfig,
|
|
226
|
-
deps: MaintainJobDeps = defaultDeps(),
|
|
458
|
+
deps: MaintainJobDeps = defaultDeps(config),
|
|
227
459
|
): Promise<MaintainOutcome> {
|
|
228
460
|
const outcome: MaintainOutcome = {
|
|
229
461
|
disabled: false,
|
|
230
|
-
|
|
462
|
+
reembedded: 0,
|
|
463
|
+
reembedFailures: 0,
|
|
231
464
|
pruned: 0,
|
|
232
|
-
|
|
465
|
+
pruneFailures: 0,
|
|
233
466
|
invalidated: false,
|
|
234
467
|
failures: [],
|
|
235
468
|
};
|
|
@@ -242,59 +475,59 @@ export async function maintainJob(
|
|
|
242
475
|
return outcome;
|
|
243
476
|
}
|
|
244
477
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
let tree: LeafTree | null = null;
|
|
478
|
+
// Stage 1: re-chunk + re-embed pages changed since the last pass. Capture the
|
|
479
|
+
// high-water mark BEFORE the pass so an embed write that bumps a page's mtime
|
|
480
|
+
// does not re-trigger it next pass; advance it only when every page succeeded.
|
|
249
481
|
try {
|
|
250
|
-
|
|
482
|
+
const startedAtMs = Date.now();
|
|
483
|
+
const changed = await deps.selectChangedPages();
|
|
484
|
+
const { reembedded, reembedFailures } = await reembedChangedPages(
|
|
485
|
+
changed,
|
|
486
|
+
deps,
|
|
487
|
+
);
|
|
488
|
+
outcome.reembedded = reembedded;
|
|
489
|
+
outcome.reembedFailures = reembedFailures;
|
|
490
|
+
// Only advance the high-water mark when nothing failed. A failed page is
|
|
491
|
+
// processed as delete-then-upsert, so a transient embed/Qdrant error leaves
|
|
492
|
+
// its sections deleted; if we advanced past its mtime, `computeChangedPages`
|
|
493
|
+
// would never re-select it (mtime <= high-water) and it would stay missing.
|
|
494
|
+
// Holding the checkpoint keeps every failed page above the mark so it
|
|
495
|
+
// retries next pass — the handful of already-succeeded pages re-embedding
|
|
496
|
+
// again is idempotent and cheap.
|
|
497
|
+
if (reembedFailures === 0) {
|
|
498
|
+
deps.commitEmbedHighWater(startedAtMs);
|
|
499
|
+
} else {
|
|
500
|
+
log.info(
|
|
501
|
+
{ reembedded, reembedFailures },
|
|
502
|
+
"memory-v3 maintain: embed checkpoint held (page failures) — failed pages retry next pass",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
251
505
|
} catch (err) {
|
|
252
|
-
outcome.failures.push("
|
|
506
|
+
outcome.failures.push("reembed");
|
|
253
507
|
log.warn(
|
|
254
508
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
255
|
-
"memory-v3 maintain:
|
|
509
|
+
"memory-v3 maintain: section re-embed failed (non-fatal)",
|
|
256
510
|
);
|
|
257
511
|
}
|
|
258
512
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
deps.commitClassifyHighWater(Date.now());
|
|
275
|
-
} catch (err) {
|
|
276
|
-
outcome.failures.push("assign");
|
|
277
|
-
log.warn(
|
|
278
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
279
|
-
"memory-v3 maintain: classify-union failed (non-fatal)",
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Stage 2: prune dangling refs.
|
|
284
|
-
try {
|
|
285
|
-
const { pruned, prunedRefs } = await prunePages(tree, deps.workspaceDir);
|
|
286
|
-
outcome.pruned = pruned;
|
|
287
|
-
outcome.prunedRefs = prunedRefs;
|
|
288
|
-
} catch (err) {
|
|
289
|
-
outcome.failures.push("prune");
|
|
290
|
-
log.warn(
|
|
291
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
292
|
-
"memory-v3 maintain: prune failed (non-fatal)",
|
|
293
|
-
);
|
|
294
|
-
}
|
|
513
|
+
// Stage 2: prune section points for pages deleted from the index. A deleted
|
|
514
|
+
// page's slug never appears in `selectChangedPages` (the delta selector only
|
|
515
|
+
// names live pages), so its lingering points are cleared by diffing the dense
|
|
516
|
+
// store's stored articles against the live page-index slugs. Contained: a
|
|
517
|
+
// failure is logged + recorded in `failures` without aborting invalidation.
|
|
518
|
+
try {
|
|
519
|
+
const { pruned, pruneFailures } = await pruneDeletedPages(deps);
|
|
520
|
+
outcome.pruned = pruned;
|
|
521
|
+
outcome.pruneFailures = pruneFailures;
|
|
522
|
+
} catch (err) {
|
|
523
|
+
outcome.failures.push("prune");
|
|
524
|
+
log.warn(
|
|
525
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
526
|
+
"memory-v3 maintain: deleted-page prune failed (non-fatal)",
|
|
527
|
+
);
|
|
295
528
|
}
|
|
296
529
|
|
|
297
|
-
// Stage 3: rebuild
|
|
530
|
+
// Stage 3: rebuild section index + needle + edge graph on the next turn.
|
|
298
531
|
try {
|
|
299
532
|
deps.invalidateLanes();
|
|
300
533
|
outcome.invalidated = true;
|
|
@@ -308,9 +541,10 @@ export async function maintainJob(
|
|
|
308
541
|
|
|
309
542
|
log.info(
|
|
310
543
|
{
|
|
311
|
-
|
|
544
|
+
reembedded: outcome.reembedded,
|
|
545
|
+
reembedFailures: outcome.reembedFailures,
|
|
312
546
|
pruned: outcome.pruned,
|
|
313
|
-
|
|
547
|
+
pruneFailures: outcome.pruneFailures,
|
|
314
548
|
invalidated: outcome.invalidated,
|
|
315
549
|
failures: outcome.failures,
|
|
316
550
|
},
|