@vellumai/assistant 0.11.0-dev.202607301413.bdcf8bc → 0.11.0-dev.202607301449.cefb1f3

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.
Files changed (44) hide show
  1. package/ARCHITECTURE.md +4 -4
  2. package/docs/architecture/memory.md +59 -0
  3. package/openapi.yaml +86 -1
  4. package/package.json +1 -1
  5. package/src/__tests__/conversations-import-system-filter.test.ts +101 -4
  6. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  7. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +1 -0
  8. package/src/cli/commands/memory/__tests__/memory-ingest.test.ts +591 -0
  9. package/src/cli/commands/memory/index.help.ts +55 -1
  10. package/src/cli/commands/memory/index.ts +2 -0
  11. package/src/cli/commands/memory/memory-ingest.ts +273 -0
  12. package/src/config/schemas/memory-v3.ts +7 -7
  13. package/src/home/feed-source-enrichment.test.ts +7 -0
  14. package/src/home/feed-source-enrichment.ts +5 -0
  15. package/src/plugins/defaults/memory/AGENTS.md +10 -7
  16. package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +1 -0
  17. package/src/plugins/defaults/memory/src/__tests__/memory-ingest-routes.test.ts +176 -0
  18. package/src/plugins/defaults/memory/src/memory-ingest-routes.ts +125 -0
  19. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-lock.test.ts +136 -0
  20. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +440 -0
  21. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +129 -2
  22. package/src/plugins/defaults/memory/substrate/__tests__/page-store.test.ts +31 -0
  23. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +10 -218
  24. package/src/plugins/defaults/memory/substrate/consolidation-lock.ts +250 -0
  25. package/src/plugins/defaults/memory/substrate/ingest.ts +360 -0
  26. package/src/plugins/defaults/memory/substrate/page-index.ts +110 -1
  27. package/src/plugins/defaults/memory/substrate/page-store.ts +20 -24
  28. package/src/plugins/defaults/memory/substrate/types.ts +12 -0
  29. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +2 -2
  30. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +1 -0
  31. package/src/plugins/defaults/memory/v3/__tests__/edge.test.ts +9 -1
  32. package/src/plugins/defaults/memory/v3/__tests__/fresh-set.test.ts +39 -5
  33. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +1 -0
  34. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +2 -0
  35. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +1 -0
  36. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +4 -0
  37. package/src/plugins/defaults/memory/v3/card.ts +1 -1
  38. package/src/plugins/defaults/memory/v3/fresh-set.ts +24 -14
  39. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +24 -17
  40. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +6 -0
  41. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +8 -3
  42. package/src/providers/speech-to-text/google-gemini-live-stream.ts +21 -9
  43. package/src/runtime/routes/conversations-import-routes.ts +28 -2
  44. package/src/runtime/routes/index.ts +2 -0
package/ARCHITECTURE.md CHANGED
@@ -624,10 +624,10 @@ Real-time conversation chat message capture on macOS uses a WebSocket-based stre
624
624
 
625
625
  Two provider adapters are supported, each implementing the `StreamingTranscriber` interface from `src/stt/types.ts`:
626
626
 
627
- | Provider | Adapter | Mode | Mechanism |
628
- | ----------------- | ----------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
629
- | **Deepgram** | `src/providers/speech-to-text/deepgram-realtime.ts` | `realtime-ws` | Opens a WebSocket to Deepgram's `/v1/listen` endpoint, forwards raw PCM audio, normalizes Deepgram's `is_final`/`speech_final` semantics into `partial`/`final` events. Uses model `nova-2`. |
630
- | **Google Gemini** | `src/providers/speech-to-text/google-gemini-live-stream.ts` | `realtime-ws` | Opens a bidirectional streaming session against Gemini's Live API (`ai.live.connect`), forwards PCM audio frames, and normalizes `serverContent.inputTranscription` events into `partial`/`final` events. Uses model `gemini-live-2.5-flash-preview`. |
627
+ | Provider | Adapter | Mode | Mechanism |
628
+ | ----------------- | ----------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
629
+ | **Deepgram** | `src/providers/speech-to-text/deepgram-realtime.ts` | `realtime-ws` | Opens a WebSocket to Deepgram's `/v1/listen` endpoint, forwards raw PCM audio, normalizes Deepgram's `is_final`/`speech_final` semantics into `partial`/`final` events. Uses model `nova-2`. |
630
+ | **Google Gemini** | `src/providers/speech-to-text/google-gemini-live-stream.ts` | `realtime-ws` | Opens a bidirectional streaming session against Gemini's Live API (`ai.live.connect`), forwards PCM audio frames, and normalizes `serverContent.inputTranscription` events into `partial`/`final` events. Uses model `gemini-2.5-flash-native-audio-latest`. |
631
631
 
632
632
  **Provider-specific behavior differences:**
633
633
 
@@ -67,6 +67,9 @@ graph LR
67
67
  CONS --> VIEWS["essentials / threads / recent"]
68
68
  CONS --> REEMBED["memory_v2_reembed →<br/>concept-page Qdrant collection"]
69
69
  CONS --> MAINT["memory_v3_maintain<br/>(v3-live follow-up)"]
70
+ ING["POST /v1/memory/ingest<br/>(deterministic batch import)"] --> PAGES
71
+ ING --> REEMBED
72
+ ING --> MAINT
70
73
  ```
71
74
 
72
75
  - `handleRemember` (`graph/tool-handlers.ts`) appends timestamped bullets to
@@ -85,6 +88,62 @@ graph LR
85
88
  failure-backoff-respecting);
86
89
  - manual "Run now" via `POST /v1/consolidation/run-now`.
87
90
  Failed runs enter an exponential backoff (transient vs billing curves).
91
+ - **Ingestion** (`substrate/ingest.ts`, exposed as `POST /v1/memory/ingest`;
92
+ generated HTTP operation id `memory_ingest_post`, IPC method
93
+ `memory_ingest`) is the second sanctioned writer of
94
+ `memory/concepts/`: a deterministic batch import of fully-formed,
95
+ page-shaped markdown (frontmatter + body) that bypasses the buffer
96
+ entirely. The bypass is load-bearing, not a shortcut: buffer timestamps
97
+ have minute precision, so a bulk append lands every entry on one shared
98
+ minute stamp, the chunker's timestamp cutoff cannot split inside that
99
+ minute, and its same-minute burst guard falls back to processing the
100
+ whole buffer in a single oversized run. Ingest writes validated pages
101
+ directly instead. Purely mechanical: each page is validated and reported
102
+ individually, writes hold the consolidation lock so a batch cannot
103
+ interleave with a consolidation pass, and a batch that wrote at least one
104
+ page enqueues the same reindex follow-ups as consolidation
105
+ (`memory_v2_reembed`, `memory_v3_maintain`). Consolidation remains the only
106
+ LLM-driven writer.
107
+
108
+ ### Ingestion tracks and provenance
109
+
110
+ Two user-facing flows feed the ingest route, chosen by the shape of the
111
+ source material rather than its size:
112
+
113
+ - **Distilled import** (`skills/assistant-migration`): memory exported from
114
+ another assistant is parsed, creator-reviewed, and shaped into concept
115
+ pages. The input is already distilled, so everything approved becomes
116
+ pages; assistant exports route here regardless of size because they need
117
+ candidate parsing and creator review.
118
+ - **Large-corpus skim** (`skills/memory-corpus-ingest`): the raw dataset
119
+ (recording archives, document dumps) is cold-stored under a workspace
120
+ imports directory and never enters the memory corpus. Only a small map of
121
+ roughly 10 to 50 skim pages is ingested, each pointing back at the raw
122
+ files, and a drill-in retrieval skill authored alongside searches the cold
123
+ store on demand.
124
+
125
+ Both stage pages on disk and write them through the `assistant memory ingest`
126
+ CLI (`src/cli/commands/memory/memory-ingest.ts`: staging directory, JSON
127
+ manifest, or stdin; dry-run; overwrite; 200-page batches). The one exception
128
+ is a migration yielding only a handful of approved facts: assistant-migration
129
+ saves those through the normal `remember` tool, so they enter via the buffer
130
+ like any organic capture instead of the ingest route.
131
+
132
+ Ingested pages carry provenance frontmatter with distinct consumers:
133
+
134
+ - `source:` marks a page's origin, by convention `import:<provider>`
135
+ (e.g. `import:chatgpt`), so imported pages stay distinguishable from
136
+ consolidation-authored ones, which never carry the key.
137
+ - `origin_date:` (ISO 8601 date or datetime) declares when the page's
138
+ content is from and drives `PageIndexEntry.freshAt`, the effective-recency
139
+ signal: any finite parse is accepted (pre-epoch dates included; an
140
+ offset-less datetime is read as UTC), and an absent or unparseable value
141
+ falls back to file mtime. The v3 fresh lane ranks on `freshAt` and the
142
+ card's `dated` stamp renders it, so a backdated import ranks by what its
143
+ content is about rather than when it reached disk. `modifiedAt` stays raw
144
+ mtime because the maintain job's re-embed delta diffs on it. Synthetic
145
+ entries (skills, CLI commands) carry `freshAt: null` and are skipped by
146
+ recency ranking.
88
147
 
89
148
  ### Read paths
90
149
 
package/openapi.yaml CHANGED
@@ -8264,7 +8264,10 @@ paths:
8264
8264
  post:
8265
8265
  operationId: conversations_import_post
8266
8266
  summary: Import conversations
8267
- description: Import conversations from a standard JSON payload.
8267
+ description:
8268
+ Import conversations from a standard JSON payload. Created conversations record a provenance source of
8269
+ `import:<provider>` derived from the `sourceKey` prefix (e.g. `chatgpt:abc123` -> `import:chatgpt`), or
8270
+ `import:unknown` when no prefix is present.
8268
8271
  tags:
8269
8272
  - conversations
8270
8273
  requestBody:
@@ -17742,6 +17745,88 @@ paths:
17742
17745
  - confident
17743
17746
  - notes
17744
17747
  additionalProperties: false
17748
+ /v1/memory/ingest:
17749
+ post:
17750
+ operationId: memory_ingest_post
17751
+ summary:
17752
+ Batch-ingest fully-formed concept pages directly into memory/concepts/ (bypassing the consolidation buffer) and
17753
+ enqueue reindex jobs
17754
+ tags:
17755
+ - memory
17756
+ requestBody:
17757
+ required: true
17758
+ content:
17759
+ application/json:
17760
+ schema:
17761
+ type: object
17762
+ properties:
17763
+ pages:
17764
+ minItems: 1
17765
+ maxItems: 200
17766
+ type: array
17767
+ items:
17768
+ type: object
17769
+ properties:
17770
+ slug:
17771
+ type: string
17772
+ content:
17773
+ type: string
17774
+ required:
17775
+ - slug
17776
+ - content
17777
+ dryRun:
17778
+ type: boolean
17779
+ overwrite:
17780
+ type: boolean
17781
+ required:
17782
+ - pages
17783
+ responses:
17784
+ "200":
17785
+ description: Successful response
17786
+ content:
17787
+ application/json:
17788
+ schema:
17789
+ type: object
17790
+ properties:
17791
+ results:
17792
+ type: array
17793
+ items:
17794
+ type: object
17795
+ properties:
17796
+ slug:
17797
+ type: string
17798
+ action:
17799
+ type: string
17800
+ enum:
17801
+ - written
17802
+ - skipped_exists
17803
+ - invalid
17804
+ warnings:
17805
+ type: array
17806
+ items:
17807
+ type: string
17808
+ error:
17809
+ type: string
17810
+ required:
17811
+ - slug
17812
+ - action
17813
+ - warnings
17814
+ additionalProperties: false
17815
+ written:
17816
+ type: number
17817
+ skipped:
17818
+ type: number
17819
+ invalid:
17820
+ type: number
17821
+ dryRun:
17822
+ type: boolean
17823
+ required:
17824
+ - results
17825
+ - written
17826
+ - skipped
17827
+ - invalid
17828
+ - dryRun
17829
+ additionalProperties: false
17745
17830
  /v1/memory/remember:
17746
17831
  post:
17747
17832
  operationId: memory_remember_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.0-dev.202607301413.bdcf8bc",
3
+ "version": "0.11.0-dev.202607301449.cefb1f3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Tests that the conversations import route never persists non-renderable
3
- * roles. The messages store is UI-facing (`ConversationMessage`), so an
4
- * imported export carrying agent-context `system` rows must land only its
5
- * `user`/`assistant` turns — the `system` rows are dropped, not persisted.
2
+ * Route-level tests for the conversations import route.
3
+ *
4
+ * Covers two behaviors: non-renderable roles are never persisted (the
5
+ * messages store is UI-facing (`ConversationMessage`), so an imported export
6
+ * carrying agent-context `system` rows must land only its `user`/`assistant`
7
+ * turns; the `system` rows are dropped, not persisted), and imported
8
+ * conversations carry a provenance `source` derived from the `sourceKey`
9
+ * prefix (`import:<provider>`, or `import:unknown` when absent).
6
10
  */
7
11
 
8
12
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -82,3 +86,96 @@ describe("conversations import system-row filtering", () => {
82
86
  ).toBe(false);
83
87
  });
84
88
  });
89
+
90
+ describe("conversations import provenance source", () => {
91
+ beforeEach(resetTables);
92
+
93
+ test("stamps import:<provider> from a prefixed sourceKey and keeps dedup", async () => {
94
+ // GIVEN an export whose sourceKey carries a provider prefix
95
+ const body = {
96
+ conversations: [
97
+ {
98
+ sourceKey: "chatgpt:abc123",
99
+ title: "ChatGPT import",
100
+ messages: [{ role: "user", content: "hello" }],
101
+ },
102
+ ],
103
+ };
104
+
105
+ // WHEN the conversation is imported
106
+ const result = (await importHandler({
107
+ body,
108
+ } as unknown as RouteHandlerArgs)) as { imported: number };
109
+ expect(result.imported).toBe(1);
110
+
111
+ // THEN the created row records the provider-derived provenance source
112
+ const db = getDb();
113
+ const conv = db.select().from(conversations).all()[0];
114
+ expect(conv.source).toBe("import:chatgpt");
115
+
116
+ // AND re-importing the same sourceKey still dedups (no second row)
117
+ const again = (await importHandler({
118
+ body,
119
+ } as unknown as RouteHandlerArgs)) as { imported: number; skipped: number };
120
+ expect(again.imported).toBe(0);
121
+ expect(again.skipped).toBe(1);
122
+ expect(db.select().from(conversations).all()).toHaveLength(1);
123
+ });
124
+
125
+ test("normalizes non-canonical prefixes instead of dropping them", async () => {
126
+ // GIVEN sourceKeys whose prefixes carry uppercase or underscore characters
127
+ const body = {
128
+ conversations: [
129
+ {
130
+ sourceKey: "OpenAI:abc",
131
+ title: "Uppercase prefix",
132
+ messages: [{ role: "user", content: "hello" }],
133
+ },
134
+ {
135
+ sourceKey: "chat_gpt:def",
136
+ title: "Underscore prefix",
137
+ messages: [{ role: "user", content: "hello" }],
138
+ },
139
+ ],
140
+ };
141
+
142
+ // WHEN the conversations are imported
143
+ const result = (await importHandler({
144
+ body,
145
+ } as unknown as RouteHandlerArgs)) as { imported: number };
146
+ expect(result.imported).toBe(2);
147
+
148
+ // THEN each prefix is normalized into import:<provider>, not import:unknown
149
+ const db = getDb();
150
+ const sources = db
151
+ .select()
152
+ .from(conversations)
153
+ .all()
154
+ .map((row) => row.source)
155
+ .sort();
156
+ expect(sources).toEqual(["import:chat-gpt", "import:openai"]);
157
+ });
158
+
159
+ test("falls back to import:unknown when sourceKey is absent", async () => {
160
+ // GIVEN an export entry with no sourceKey at all
161
+ const body = {
162
+ conversations: [
163
+ {
164
+ title: "Prefixless import",
165
+ messages: [{ role: "user", content: "hello" }],
166
+ },
167
+ ],
168
+ };
169
+
170
+ // WHEN the conversation is imported
171
+ const result = (await importHandler({
172
+ body,
173
+ } as unknown as RouteHandlerArgs)) as { imported: number };
174
+ expect(result.imported).toBe(1);
175
+
176
+ // THEN the created row falls back to the unknown-provider source
177
+ const db = getDb();
178
+ const conv = db.select().from(conversations).all()[0];
179
+ expect(conv.source).toBe("import:unknown");
180
+ });
181
+ });
@@ -147,6 +147,7 @@ const BASELINE: Record<string, readonly string[]> = {
147
147
  "../../../../runtime/auth/route-policy.js",
148
148
  "../../../../runtime/background-job-runner.js",
149
149
  "../../../../runtime/routes/errors.js",
150
+ "../../../../runtime/routes/parse-body.js",
150
151
  "../../../../runtime/routes/types.js",
151
152
  "../../../../security/secret-scanner.js",
152
153
  "../../../../skills/catalog-cache.js",
@@ -73,6 +73,7 @@ const BASELINE: Record<string, readonly string[]> = {
73
73
  ],
74
74
  "image-recovery": ["src/daemon/conversation-error.ts"],
75
75
  memory: [
76
+ "src/cli/commands/memory/memory-ingest.ts",
76
77
  "src/cli/commands/memory/memory-retrospective.ts",
77
78
  "src/cli/commands/memory/memory-v2-compare-render.ts",
78
79
  "src/cli/commands/memory/memory-v2.ts",