@gmickel/gno 2.5.1 → 2.7.0

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 (160) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +80 -4
  4. package/assets/skill/cli-reference.md +132 -2
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +54 -1
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +449 -35
  17. package/spec/mcp.md +234 -4
  18. package/spec/output-schemas/ask.schema.json +1 -1
  19. package/spec/output-schemas/capture-receipt.schema.json +4 -1
  20. package/spec/output-schemas/doctor.schema.json +88 -0
  21. package/spec/output-schemas/error.schema.json +11 -2
  22. package/spec/output-schemas/get.schema.json +1 -1
  23. package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
  24. package/spec/output-schemas/memory-remember.schema.json +10 -4
  25. package/spec/output-schemas/multi-get.schema.json +4 -1
  26. package/spec/output-schemas/peek.schema.json +2 -9
  27. package/spec/output-schemas/request-status.schema.json +113 -0
  28. package/spec/output-schemas/resident-status.schema.json +22 -0
  29. package/spec/output-schemas/search-result.schema.json +1 -1
  30. package/spec/output-schemas/search-results.schema.json +1 -1
  31. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  32. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  33. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  34. package/spec/output-schemas/sessions-status.schema.json +432 -0
  35. package/spec/output-schemas/status.schema.json +98 -0
  36. package/src/cli/commands/ask.ts +14 -2
  37. package/src/cli/commands/capture.ts +55 -96
  38. package/src/cli/commands/daemon.ts +41 -0
  39. package/src/cli/commands/doctor.ts +54 -20
  40. package/src/cli/commands/embed.ts +41 -3
  41. package/src/cli/commands/ls.ts +3 -0
  42. package/src/cli/commands/memory.ts +12 -3
  43. package/src/cli/commands/query.ts +5 -0
  44. package/src/cli/commands/request-status.ts +59 -0
  45. package/src/cli/commands/reset.ts +39 -5
  46. package/src/cli/commands/sessions.ts +713 -0
  47. package/src/cli/commands/shared.ts +14 -1
  48. package/src/cli/commands/status.ts +63 -5
  49. package/src/cli/commands/vec.ts +54 -0
  50. package/src/cli/detach.ts +29 -1
  51. package/src/cli/errors.ts +13 -9
  52. package/src/cli/program.ts +441 -2
  53. package/src/cli/session-binding.ts +49 -0
  54. package/src/config/types.ts +8 -0
  55. package/src/core/capture-publish.ts +239 -0
  56. package/src/core/capture-sync.ts +12 -2
  57. package/src/core/host-paths.ts +31 -0
  58. package/src/core/memory-remember.ts +234 -122
  59. package/src/core/memory-types.ts +11 -0
  60. package/src/core/network-boundary-inventory.ts +8 -0
  61. package/src/core/request-receipts.ts +671 -0
  62. package/src/core/shutdown-budget.ts +6 -0
  63. package/src/core/vector-partition-status.ts +52 -0
  64. package/src/embed/backlog.ts +124 -18
  65. package/src/embed/fingerprint.ts +6 -3
  66. package/src/embed/retry.ts +66 -27
  67. package/src/embed/variant-backlog.ts +15 -10
  68. package/src/embed/variant-retry.ts +31 -22
  69. package/src/index.ts +30 -2
  70. package/src/llm/native-worker/dispatcher.ts +2 -0
  71. package/src/llm/native-worker/embedding-identity.ts +42 -0
  72. package/src/llm/native-worker/protocol.ts +1 -0
  73. package/src/llm/types.ts +3 -0
  74. package/src/mcp/context.ts +17 -0
  75. package/src/mcp/http-egress.ts +4 -0
  76. package/src/mcp/http-transport.ts +2 -0
  77. package/src/mcp/resources/index.ts +6 -5
  78. package/src/mcp/tool-descriptions-core.ts +1 -1
  79. package/src/mcp/tools/capture.ts +87 -85
  80. package/src/mcp/tools/index.ts +77 -4
  81. package/src/mcp/tools/memory-remember.ts +8 -1
  82. package/src/mcp/tools/memory-shared.ts +7 -1
  83. package/src/mcp/tools/request-status.ts +73 -0
  84. package/src/mcp/tools/sessions.ts +208 -0
  85. package/src/mcp/tools/status.ts +4 -0
  86. package/src/pipeline/hybrid.ts +37 -7
  87. package/src/pipeline/vsearch.ts +14 -2
  88. package/src/sdk/client.ts +180 -84
  89. package/src/sdk/index.ts +6 -0
  90. package/src/sdk/types.ts +54 -2
  91. package/src/serve/capture-service.ts +98 -32
  92. package/src/serve/config-sync.ts +3 -2
  93. package/src/serve/embed-scheduler.ts +133 -19
  94. package/src/serve/host-path-redaction.ts +79 -0
  95. package/src/serve/public/app.tsx +4 -1
  96. package/src/serve/public/components/CaptureModal.tsx +26 -8
  97. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  98. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  99. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  100. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  101. package/src/serve/public/components/sessions/api.ts +40 -0
  102. package/src/serve/public/globals.built.css +1 -1
  103. package/src/serve/public/hooks/use-api.ts +26 -3
  104. package/src/serve/public/lib/request-intent.ts +77 -0
  105. package/src/serve/public/lib/snippet.tsx +52 -0
  106. package/src/serve/public/lib/workspace-actions.ts +12 -1
  107. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  108. package/src/serve/public/pages/Dashboard.tsx +22 -9
  109. package/src/serve/public/pages/DocView.tsx +25 -6
  110. package/src/serve/public/pages/DocumentEditor.tsx +224 -104
  111. package/src/serve/public/pages/Search.tsx +1 -41
  112. package/src/serve/public/pages/Sessions.tsx +350 -0
  113. package/src/serve/resident-runtime.ts +69 -4
  114. package/src/serve/resident-status.ts +13 -1
  115. package/src/serve/routes/api.ts +476 -147
  116. package/src/serve/routes/sessions.ts +766 -0
  117. package/src/serve/security.ts +9 -0
  118. package/src/serve/server.ts +215 -10
  119. package/src/serve/session-automation.ts +146 -0
  120. package/src/serve/status-model.ts +16 -0
  121. package/src/serve/status.ts +2 -0
  122. package/src/serve/watch-reconciliation-shared.ts +3 -0
  123. package/src/serve/watch-service-events.ts +3 -2
  124. package/src/serve/watch-service-run-flush.ts +35 -2
  125. package/src/serve/watch-service.ts +5 -0
  126. package/src/sessions/archive.ts +348 -0
  127. package/src/sessions/automation-state.ts +444 -0
  128. package/src/sessions/automation-status.ts +239 -0
  129. package/src/sessions/automation.ts +1169 -0
  130. package/src/sessions/binding.ts +105 -0
  131. package/src/sessions/claude-hook.ts +240 -0
  132. package/src/sessions/config.ts +176 -0
  133. package/src/sessions/format.ts +191 -0
  134. package/src/sessions/import-child-env.ts +8 -0
  135. package/src/sessions/import-child.ts +152 -0
  136. package/src/sessions/parsers/claude-code.ts +259 -0
  137. package/src/sessions/parsers/codex.ts +303 -0
  138. package/src/sessions/parsers/hermes.ts +248 -0
  139. package/src/sessions/parsers/openclaw.ts +496 -0
  140. package/src/sessions/parsers/shared.ts +184 -0
  141. package/src/sessions/sanitize.ts +222 -0
  142. package/src/sessions/service.ts +1533 -0
  143. package/src/sessions/setup.ts +477 -0
  144. package/src/sessions/sources.ts +518 -0
  145. package/src/sessions/state.ts +118 -0
  146. package/src/sessions/types.ts +457 -0
  147. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  148. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  149. package/src/store/migrations/index.ts +4 -0
  150. package/src/store/sqlite/adapter.ts +76 -16
  151. package/src/store/sqlite/scoped-index.ts +9 -0
  152. package/src/store/types.ts +11 -1
  153. package/src/store/vector/lazy.ts +46 -43
  154. package/src/store/vector/runtime-compat.ts +651 -0
  155. package/src/store/vector/sqlite-vec.ts +20 -2
  156. package/src/store/vector/status.ts +276 -35
  157. package/src/store/vector/types.ts +2 -0
  158. package/src/store/vector/variant-search.ts +71 -23
  159. package/src/store/vector/variants.ts +49 -14
  160. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -0,0 +1,73 @@
1
+ /**
2
+ * MCP request receipts: the caller's namespace, error rethrow, and the
3
+ * bounded `gno_request_status` lookup.
4
+ *
5
+ * @module src/mcp/tools/request-status
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ import type { ToolContext } from "../server";
11
+
12
+ import {
13
+ formatRequestStatus,
14
+ LOCAL_OWNER_NAMESPACE,
15
+ readRequestStatus,
16
+ RequestReceiptError,
17
+ requestLedgerPath,
18
+ } from "../../core/request-receipts";
19
+ import { runTool, type ToolResult } from "./index";
20
+
21
+ export const REQUEST_STATUS_MCP_ANNOTATIONS = {
22
+ readOnlyHint: true,
23
+ destructiveHint: false,
24
+ idempotentHint: true,
25
+ openWorldHint: false,
26
+ } as const;
27
+
28
+ export const requestIdInputSchema = z
29
+ .string()
30
+ .describe(
31
+ "Optional caller request ID (1-128 of A-Z a-z 0-9 . _ : -). Reuse the same ID when retrying the same write after a lost response; a new intent needs a new ID"
32
+ );
33
+
34
+ export const requestStatusInputSchema = z.object({
35
+ requestId: z
36
+ .string()
37
+ .describe("Request ID previously sent with gno_capture or gno_remember"),
38
+ });
39
+
40
+ /** HTTP callers get their authorized identity's namespace; stdio is the local owner. */
41
+ export function mcpRequestNamespace(ctx: ToolContext): string {
42
+ return ctx.getRequestNamespace?.() ?? LOCAL_OWNER_NAMESPACE;
43
+ }
44
+
45
+ /** Re-throw a request receipt error in the `CODE: message` shape runTool parses. */
46
+ export function rethrowRequestError(error: unknown): never {
47
+ if (error instanceof RequestReceiptError) {
48
+ throw new Error(`${error.code}: ${error.message}`);
49
+ }
50
+ throw error;
51
+ }
52
+
53
+ export function handleRequestStatus(
54
+ args: z.infer<typeof requestStatusInputSchema>,
55
+ ctx: ToolContext
56
+ ): Promise<ToolResult> {
57
+ return runTool(
58
+ ctx,
59
+ "gno_request_status",
60
+ async () => {
61
+ try {
62
+ return await readRequestStatus({
63
+ ledgerPath: requestLedgerPath(ctx.store.getDbPath()),
64
+ namespace: mcpRequestNamespace(ctx),
65
+ requestId: args.requestId,
66
+ });
67
+ } catch (error) {
68
+ return rethrowRequestError(error);
69
+ }
70
+ },
71
+ formatRequestStatus
72
+ );
73
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * MCP session-archive tools: bounded status (including opt-in automation),
3
+ * import of owner-registered sources, and a run request for a configured
4
+ * automation profile. Thin adapters over the core sessions services. Hooks,
5
+ * schedules and source access are managed only by the local owner.
6
+ *
7
+ * Remote callers never discover host directories and never name paths:
8
+ * import accepts a registered source ID only. Import is registered only with
9
+ * `--enable-write`, and neither tool is part of the core profile.
10
+ *
11
+ * @module src/mcp/tools/sessions
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ import type { ToolContext } from "../server";
17
+
18
+ import { runAutomationProfile } from "../../sessions/automation";
19
+ import {
20
+ formatAutomationRunText,
21
+ formatImportReceiptText,
22
+ formatStatusText,
23
+ } from "../../sessions/format";
24
+ import { importInChildProcess } from "../../sessions/import-child";
25
+ import { SessionsService } from "../../sessions/service";
26
+ import {
27
+ MAX_IMPORT_LIMIT,
28
+ type SessionAutomationRunResult,
29
+ type SessionImportReceipt,
30
+ remoteSafeSessionsError,
31
+ } from "../../sessions/types";
32
+ import { runTool, type ToolResult } from "./index";
33
+
34
+ export const sessionsStatusInputSchema = z.object({});
35
+
36
+ export const sessionsImportInputSchema = z
37
+ .object({
38
+ sourceId: z
39
+ .string()
40
+ .trim()
41
+ .min(1)
42
+ .max(64)
43
+ .describe(
44
+ "ID of an owner-registered session source (see gno_sessions_status)"
45
+ ),
46
+ dryRun: z
47
+ .boolean()
48
+ .optional()
49
+ .describe("Parse and report without writing archive or index state"),
50
+ limit: z
51
+ .number()
52
+ .int()
53
+ .min(1)
54
+ .max(MAX_IMPORT_LIMIT)
55
+ .optional()
56
+ .describe(
57
+ "Maximum changed units processed this call; the rest are deferred"
58
+ ),
59
+ })
60
+ .strict();
61
+
62
+ export type SessionsImportToolInput = z.infer<typeof sessionsImportInputSchema>;
63
+
64
+ export const sessionsAutomationRunInputSchema = z
65
+ .object({
66
+ profileId: z
67
+ .string()
68
+ .trim()
69
+ .min(1)
70
+ .max(64)
71
+ .describe(
72
+ "ID of an owner-configured automation profile (see gno_sessions_status)"
73
+ ),
74
+ })
75
+ .strict();
76
+
77
+ export type SessionsAutomationRunToolInput = z.infer<
78
+ typeof sessionsAutomationRunInputSchema
79
+ >;
80
+
81
+ export const SESSIONS_AUTOMATION_RUN_MCP_ANNOTATIONS = {
82
+ readOnlyHint: false,
83
+ destructiveHint: false,
84
+ idempotentHint: true,
85
+ openWorldHint: false,
86
+ } as const;
87
+
88
+ export const SESSIONS_STATUS_MCP_ANNOTATIONS = {
89
+ readOnlyHint: true,
90
+ destructiveHint: false,
91
+ idempotentHint: true,
92
+ openWorldHint: false,
93
+ } as const;
94
+
95
+ export const SESSIONS_IMPORT_MCP_ANNOTATIONS = {
96
+ readOnlyHint: false,
97
+ destructiveHint: false,
98
+ idempotentHint: true,
99
+ openWorldHint: false,
100
+ } as const;
101
+
102
+ function service(ctx: ToolContext): SessionsService {
103
+ return new SessionsService({
104
+ config: ctx.config,
105
+ configPath: ctx.actualConfigPath,
106
+ indexName: ctx.indexName,
107
+ store: ctx.store,
108
+ });
109
+ }
110
+
111
+ /** Re-throw as `CODE: message` (the shape runTool parses), never with host paths. */
112
+ function rethrowSessionsError(error: unknown): never {
113
+ const typed = remoteSafeSessionsError(error);
114
+ throw new Error(`${typed.code}: ${typed.message}`);
115
+ }
116
+
117
+ export function handleSessionsStatus(ctx: ToolContext): Promise<ToolResult> {
118
+ return runTool(
119
+ ctx,
120
+ "gno_sessions_status",
121
+ async () => {
122
+ try {
123
+ return await service(ctx).status();
124
+ } catch (error) {
125
+ return rethrowSessionsError(error);
126
+ }
127
+ },
128
+ formatStatusText
129
+ );
130
+ }
131
+
132
+ export function handleSessionsImport(
133
+ args: SessionsImportToolInput,
134
+ ctx: ToolContext
135
+ ): Promise<ToolResult> {
136
+ return runTool(
137
+ ctx,
138
+ "gno_sessions_import",
139
+ async () => {
140
+ if (!ctx.enableWrite) {
141
+ throw new Error(
142
+ "WRITE_DISABLED: gno_sessions_import requires --enable-write or GNO_MCP_ENABLE_WRITE=1"
143
+ );
144
+ }
145
+ let receipt: SessionImportReceipt;
146
+ try {
147
+ // A child process keeps this server answering during a long import.
148
+ receipt = await importInChildProcess({
149
+ config: ctx.config,
150
+ configPath: ctx.actualConfigPath,
151
+ indexName: ctx.indexName,
152
+ sourceId: args.sourceId,
153
+ dryRun: args.dryRun === true,
154
+ limit: args.limit,
155
+ });
156
+ } catch (error) {
157
+ return rethrowSessionsError(error);
158
+ }
159
+ if (!receipt.dryRun && receipt.lexical.collections.length > 0) {
160
+ ctx.markContentMutation?.();
161
+ ctx.markIndexMutation?.();
162
+ }
163
+ return receipt;
164
+ },
165
+ formatImportReceiptText
166
+ );
167
+ }
168
+
169
+ export function handleSessionsAutomationRun(
170
+ args: SessionsAutomationRunToolInput,
171
+ ctx: ToolContext
172
+ ): Promise<ToolResult> {
173
+ return runTool(
174
+ ctx,
175
+ "gno_sessions_automation_run",
176
+ async () => {
177
+ if (!ctx.enableWrite) {
178
+ throw new Error(
179
+ "WRITE_DISABLED: gno_sessions_automation_run requires --enable-write or GNO_MCP_ENABLE_WRITE=1"
180
+ );
181
+ }
182
+ let result: SessionAutomationRunResult;
183
+ try {
184
+ result = await runAutomationProfile(
185
+ {
186
+ configPath: ctx.actualConfigPath,
187
+ indexName: ctx.indexName,
188
+ store: ctx.store,
189
+ // A child process keeps this server answering during the run.
190
+ inChildProcess: true,
191
+ },
192
+ args.profileId,
193
+ { trigger: "manual" }
194
+ );
195
+ } catch (error) {
196
+ return rethrowSessionsError(error);
197
+ }
198
+ if (
199
+ result.receipts.some((receipt) => receipt.lexical.collections.length)
200
+ ) {
201
+ ctx.markContentMutation?.();
202
+ ctx.markIndexMutation?.();
203
+ }
204
+ return result;
205
+ },
206
+ formatAutomationRunText
207
+ );
208
+ }
@@ -9,6 +9,7 @@ import type { ToolContext } from "../server";
9
9
 
10
10
  import { buildContentTypeBoostStatus } from "../../config/content-types";
11
11
  import { formatChunkingStatus } from "../../core/chunking-status";
12
+ import { formatVectorPartitionLines } from "../../core/vector-partition-status";
12
13
  import { resolveModelUri } from "../../llm/registry";
13
14
  import { createStandaloneResidentStatus } from "../../serve/resident-status";
14
15
  import { runTool, type ToolResult } from "./index";
@@ -59,6 +60,9 @@ function formatStatus(status: IndexStatus): string {
59
60
  if (status.embeddingBacklog > 0) {
60
61
  lines.push(`Embedding backlog: ${status.embeddingBacklog} chunks`);
61
62
  }
63
+ lines.push(
64
+ ...formatVectorPartitionLines(status.vectorPartitions, status.vectorRuntime)
65
+ );
62
66
 
63
67
  const chunking = formatChunkingStatus(status.chunking);
64
68
  if (chunking) lines.push(chunking);
@@ -33,7 +33,11 @@ import {
33
33
  withInferenceScope,
34
34
  } from "../llm/inference-scope";
35
35
  import { err, ok } from "../store/types";
36
- import { resolveVectorSearchIdentity } from "../store/vector/variant-search";
36
+ import {
37
+ lexicalFallbackNotice,
38
+ resolveVectorSearchIdentity,
39
+ VECTOR_RUNTIME_INCOMPATIBLE,
40
+ } from "../store/vector/variant-search";
37
41
  import { createChunkLookup } from "./chunk-lookup";
38
42
  import {
39
43
  attachAuxiliaryScoreMetadata,
@@ -284,7 +288,10 @@ async function searchVectorChunks(
284
288
  allowedMirrorHashes?: string[];
285
289
  eligibility?: VectorSearchOptions["eligibility"];
286
290
  }
287
- ): Promise<{ ok: true; chunks: ChunkId[] } | { ok: false; reason: string }> {
291
+ ): Promise<
292
+ | { ok: true; chunks: ChunkId[] }
293
+ | { ok: false; reason: string; notice?: string }
294
+ > {
288
295
  if (!vectorIndex.searchAvailable) {
289
296
  return { ok: false, reason: "vector_unavailable" };
290
297
  }
@@ -298,12 +305,19 @@ async function searchVectorChunks(
298
305
  return { ok: false, reason: "vector_embed_error" };
299
306
  }
300
307
 
308
+ const partition = await resolveVectorSearchIdentity(embedPort, vectorIndex);
309
+ if (partition.unavailable)
310
+ return {
311
+ ok: false,
312
+ reason: VECTOR_RUNTIME_INCOMPATIBLE,
313
+ notice: lexicalFallbackNotice(partition.unavailable),
314
+ };
301
315
  const queryEmbedding = new Float32Array(embedResult.value);
302
316
  const searchResult = await vectorIndex.searchNearest(
303
317
  queryEmbedding,
304
318
  options.limit,
305
319
  {
306
- embeddingIdentity: resolveVectorSearchIdentity(embedPort),
320
+ embeddingIdentity: partition.identity,
307
321
  minScore: options.minScore,
308
322
  allowedMirrorHashes: options.allowedMirrorHashes,
309
323
  eligibility: options.eligibility,
@@ -630,6 +644,7 @@ async function searchHybridWithHydration(
630
644
  // Vector search
631
645
  let vecCount = 0;
632
646
  let vectorsUsed = false;
647
+ let vectorNotice: string | undefined;
633
648
  const vectorAvailable =
634
649
  (vectorIndex?.searchAvailable && embedPort !== null) ?? false;
635
650
  if (!vectorAvailable) {
@@ -662,8 +677,10 @@ async function searchHybridWithHydration(
662
677
  }
663
678
  );
664
679
 
665
- if (!vectorResult.ok) counters.fallbackEvents.push(vectorResult.reason);
666
- else vectorsUsed = true;
680
+ if (!vectorResult.ok) {
681
+ counters.fallbackEvents.push(vectorResult.reason);
682
+ vectorNotice = vectorResult.notice;
683
+ } else vectorsUsed = true;
667
684
  const vecChunks = vectorResult.ok ? vectorResult.chunks : [];
668
685
  vecCount = vecChunks.length;
669
686
  vectorTraceChunks.push(...vecChunks);
@@ -691,8 +708,14 @@ async function searchHybridWithHydration(
691
708
  );
692
709
 
693
710
  assertInferenceResult(embedResult);
711
+ const partition = embedResult.ok
712
+ ? await resolveVectorSearchIdentity(embedPort, vectorIndex)
713
+ : undefined;
694
714
  if (!embedResult.ok) {
695
715
  counters.fallbackEvents.push("vector_embed_error");
716
+ } else if (partition?.unavailable) {
717
+ counters.fallbackEvents.push(VECTOR_RUNTIME_INCOMPATIBLE);
718
+ vectorNotice = lexicalFallbackNotice(partition.unavailable);
696
719
  } else {
697
720
  if (embedResult.value.batchFailed) {
698
721
  counters.fallbackEvents.push("vector_embed_batch_fallback");
@@ -709,7 +732,7 @@ async function searchHybridWithHydration(
709
732
  new Float32Array(embedding),
710
733
  variant.limit,
711
734
  {
712
- embeddingIdentity: resolveVectorSearchIdentity(embedPort),
735
+ embeddingIdentity: partition?.identity,
713
736
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
714
737
  eligibility: vectorEligibility,
715
738
  }
@@ -1390,6 +1413,11 @@ async function searchHybridWithHydration(
1390
1413
  trace: diagnoseTrace,
1391
1414
  },
1392
1415
  };
1416
+ if (vectorNotice)
1417
+ output.meta.warnings = [
1418
+ ...(output.meta.warnings ?? []),
1419
+ { code: VECTOR_RUNTIME_INCOMPATIBLE, message: vectorNotice },
1420
+ ];
1393
1421
  const fallbackCodes = [...new Set(counters.fallbackEvents)].sort();
1394
1422
  const capabilityOutcomes = [
1395
1423
  { capability: "lexical_search", status: "used" as const },
@@ -1400,7 +1428,9 @@ async function searchHybridWithHydration(
1400
1428
  status: "failed" as const,
1401
1429
  reasonCode: fallbackCodes.includes("vector_embed_error")
1402
1430
  ? "vector_embed_error"
1403
- : "vector_search_error",
1431
+ : vectorNotice
1432
+ ? VECTOR_RUNTIME_INCOMPATIBLE
1433
+ : "vector_search_error",
1404
1434
  }
1405
1435
  : { capability: "semantic_search", status: "used" as const }
1406
1436
  : {
@@ -21,7 +21,10 @@ import {
21
21
  } from "../llm/inference-scope";
22
22
  import { getContentBatch } from "../store/content-batch";
23
23
  import { err, ok } from "../store/types";
24
- import { resolveVectorSearchIdentity } from "../store/vector/variant-search";
24
+ import {
25
+ resolveVectorSearchIdentity,
26
+ vectorSearchUnavailableMessage,
27
+ } from "../store/vector/variant-search";
25
28
  import { createChunkLookup } from "./chunk-lookup";
26
29
  import {
27
30
  applyContentTypeBoost,
@@ -144,7 +147,16 @@ async function searchVectorWithEmbeddingOwned(
144
147
 
145
148
  let embeddingIdentity;
146
149
  try {
147
- embeddingIdentity = resolveVectorSearchIdentity(deps.embedPort);
150
+ const partition = await resolveVectorSearchIdentity(
151
+ deps.embedPort,
152
+ vectorIndex
153
+ );
154
+ if (partition.unavailable)
155
+ return err(
156
+ "VEC_SEARCH_UNAVAILABLE",
157
+ vectorSearchUnavailableMessage(partition.unavailable)
158
+ );
159
+ embeddingIdentity = partition.identity;
148
160
  } catch (cause) {
149
161
  return err(
150
162
  "QUERY_FAILED",