@anuma/sdk 3.11.6 → 3.12.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ getLogger
3
+ } from "./chunk-ILAWDQDE.mjs";
4
+
1
5
  // src/lib/pii/patterns.ts
2
6
  function luhnCheck(digits) {
3
7
  let sum = 0;
@@ -637,10 +641,24 @@ function createStreamingDeAnonymizer(redactor, emit) {
637
641
  };
638
642
  }
639
643
 
644
+ // src/lib/memory/consolidationFallback.ts
645
+ function notifyConsolidationFallback(reason, onFallback, detail) {
646
+ if (detail !== void 0) {
647
+ getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
648
+ } else {
649
+ getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
650
+ }
651
+ try {
652
+ onFallback?.(reason);
653
+ } catch {
654
+ }
655
+ }
656
+
640
657
  export {
641
658
  PII_PATTERNS,
642
659
  PiiRedactor,
643
660
  isPiiRedactor,
644
661
  resolvePiiRedactor,
645
- createStreamingDeAnonymizer
662
+ createStreamingDeAnonymizer,
663
+ notifyConsolidationFallback
646
664
  };
@@ -1,9 +1,9 @@
1
1
  import {
2
+ notifyConsolidationFallback,
2
3
  resolvePiiRedactor
3
- } from "./chunk-Y7XT2VNQ.mjs";
4
+ } from "./chunk-WV7C7VHF.mjs";
4
5
  import {
5
- callPortalJsonCompletion,
6
- getLogger
6
+ callPortalJsonCompletion
7
7
  } from "./chunk-ILAWDQDE.mjs";
8
8
  import "./chunk-GYKJOWEO.mjs";
9
9
 
@@ -102,15 +102,7 @@ ${candidateText}`;
102
102
  return result;
103
103
  }
104
104
  function degrade(reason, fallback, options, detail) {
105
- if (detail !== void 0) {
106
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
107
- } else {
108
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
109
- }
110
- try {
111
- options.onFallback?.(reason);
112
- } catch {
113
- }
105
+ notifyConsolidationFallback(reason, options.onFallback, detail);
114
106
  return { ...fallback, fallbackReason: reason };
115
107
  }
116
108
  function validate(parsed, newContent, validIds) {
@@ -8170,6 +8170,25 @@ var init_searchTool = __esm({
8170
8170
  }
8171
8171
  });
8172
8172
 
8173
+ // src/lib/memory/consolidationFallback.ts
8174
+ function notifyConsolidationFallback(reason, onFallback, detail) {
8175
+ if (detail !== void 0) {
8176
+ getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
8177
+ } else {
8178
+ getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
8179
+ }
8180
+ try {
8181
+ onFallback?.(reason);
8182
+ } catch {
8183
+ }
8184
+ }
8185
+ var init_consolidationFallback = __esm({
8186
+ "src/lib/memory/consolidationFallback.ts"() {
8187
+ "use strict";
8188
+ init_logger();
8189
+ }
8190
+ });
8191
+
8173
8192
  // src/lib/memory/consolidate.ts
8174
8193
  var consolidate_exports = {};
8175
8194
  __export(consolidate_exports, {
@@ -8234,15 +8253,7 @@ ${candidateText}`;
8234
8253
  return result;
8235
8254
  }
8236
8255
  function degrade(reason, fallback, options, detail) {
8237
- if (detail !== void 0) {
8238
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
8239
- } else {
8240
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
8241
- }
8242
- try {
8243
- options.onFallback?.(reason);
8244
- } catch {
8245
- }
8256
+ notifyConsolidationFallback(reason, options.onFallback, detail);
8246
8257
  return { ...fallback, fallbackReason: reason };
8247
8258
  }
8248
8259
  function validate2(parsed, newContent, validIds) {
@@ -8282,8 +8293,8 @@ var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDA
8282
8293
  var init_consolidate = __esm({
8283
8294
  "src/lib/memory/consolidate.ts"() {
8284
8295
  "use strict";
8285
- init_logger();
8286
8296
  init_redactor();
8297
+ init_consolidationFallback();
8287
8298
  init_portalLlm();
8288
8299
  DEFAULT_CONSOLIDATION_MODEL = "inclusionai/ling-2.6-flash";
8289
8300
  DEFAULT_CONSOLIDATE_ATTEMPTS = 3;
@@ -11786,13 +11797,18 @@ Executing tool: ${toolInfo}
11786
11797
  }
11787
11798
  const truncatedToNothing = (acc) => acc.finishReason === "length" && acc.toolCalls.size === 0 && acc.content.trim() === "";
11788
11799
  const TRUNCATION_ERROR = "Model response was truncated at the output-token limit before it produced any usable content or tool call. Retry with a higher `maxOutputTokens`, or prompt for fewer/smaller tool calls per turn.";
11800
+ const terminalStateOf = (acc) => ({
11801
+ ...acc.finishReason !== void 0 && { finishReason: acc.finishReason },
11802
+ finalToolCallCount: acc.toolCalls.size
11803
+ });
11789
11804
  if (toolIteration > 0) {
11790
11805
  if (truncatedToNothing(currentAccumulator)) {
11791
11806
  await fireRunError({ error: TRUNCATION_ERROR, stage: "model" });
11792
11807
  return {
11793
11808
  data: buildResponseFinal(currentAccumulator),
11794
11809
  error: TRUNCATION_ERROR,
11795
- toolsChecksum: currentAccumulator.toolsChecksum
11810
+ toolsChecksum: currentAccumulator.toolsChecksum,
11811
+ terminalState: terminalStateOf(currentAccumulator)
11796
11812
  };
11797
11813
  }
11798
11814
  const finalResponse = buildResponseFinal(currentAccumulator);
@@ -11805,7 +11821,8 @@ Executing tool: ${toolInfo}
11805
11821
  data: finalResponse,
11806
11822
  error: null,
11807
11823
  toolsChecksum: currentAccumulator.toolsChecksum,
11808
- autoExecutedToolResults: accumulatedToolResults
11824
+ autoExecutedToolResults: accumulatedToolResults,
11825
+ terminalState: terminalStateOf(currentAccumulator)
11809
11826
  };
11810
11827
  }
11811
11828
  if (truncatedToNothing(accumulator)) {
@@ -11813,7 +11830,8 @@ Executing tool: ${toolInfo}
11813
11830
  return {
11814
11831
  data: response,
11815
11832
  error: TRUNCATION_ERROR,
11816
- toolsChecksum: accumulator.toolsChecksum
11833
+ toolsChecksum: accumulator.toolsChecksum,
11834
+ terminalState: terminalStateOf(accumulator)
11817
11835
  };
11818
11836
  }
11819
11837
  if (onFinish) onFinish(response);
@@ -11824,7 +11842,8 @@ Executing tool: ${toolInfo}
11824
11842
  return {
11825
11843
  data: response,
11826
11844
  error: null,
11827
- toolsChecksum: accumulator.toolsChecksum
11845
+ toolsChecksum: accumulator.toolsChecksum,
11846
+ terminalState: terminalStateOf(accumulator)
11828
11847
  };
11829
11848
  } catch (err) {
11830
11849
  if (isAbortError(err)) {
@@ -14840,6 +14859,7 @@ init_constants();
14840
14859
  init_embeddings();
14841
14860
  init_vector();
14842
14861
  init_searchTool();
14862
+ init_consolidationFallback();
14843
14863
  var DEFAULT_AUTO_MERGE_THRESHOLD = 0.8;
14844
14864
  var DEFAULT_SCOPE = "private";
14845
14865
  var DEFAULT_CONSOLIDATE_THRESHOLD = 0.55;
@@ -15019,6 +15039,11 @@ async function retain(content, ctx, options = {}) {
15019
15039
  proofCount: 1
15020
15040
  };
15021
15041
  }
15042
+ notifyConsolidationFallback(
15043
+ "target_vanished",
15044
+ options.consolidateOptions?.onFallback,
15045
+ `supersede: primary target ${primaryTargetId} retired or deleted inside the atomic write`
15046
+ );
15022
15047
  }
15023
15048
  const created = await createVaultMemoryOp(ctx.vaultCtx, createOpts);
15024
15049
  ctx.vaultCache.set(created.uniqueId, Float32Array.from(embedding));
@@ -15070,6 +15095,10 @@ function pickFactTypeUpdate(existing, incoming) {
15070
15095
  function resurrectFields(existing) {
15071
15096
  return typeof existing.archivedAt === "number" ? { restore: true } : { preserveUpdatedAt: true };
15072
15097
  }
15098
+ function abandonToRace(options, detail) {
15099
+ notifyConsolidationFallback("target_vanished", options.consolidateOptions?.onFallback, detail);
15100
+ return null;
15101
+ }
15073
15102
  async function tryConsolidate(trimmed, ctx, options, prepared) {
15074
15103
  const consolidateOptions = options.consolidateOptions;
15075
15104
  if (!consolidateOptions) return null;
@@ -15102,12 +15131,22 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
15102
15131
  const existing = await getVaultMemoryOp(ctx.vaultCtx, id);
15103
15132
  if (existing && !existing.supersededBy) valid.push(id);
15104
15133
  }
15105
- if (valid.length === 0) return null;
15134
+ if (valid.length === 0) {
15135
+ return abandonToRace(
15136
+ options,
15137
+ `supersede: all ${requestedIds.length} target(s) deleted or already superseded`
15138
+ );
15139
+ }
15106
15140
  return { supersede: valid, content: decision.content };
15107
15141
  }
15108
15142
  if (decision.action === "noop" && decision.targetId) {
15109
15143
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
15110
- if (!existing || existing.supersededBy) return null;
15144
+ if (!existing || existing.supersededBy) {
15145
+ return abandonToRace(
15146
+ options,
15147
+ `noop: target ${decision.targetId} ${existing ? "superseded" : "deleted"} before the write`
15148
+ );
15149
+ }
15111
15150
  const mergedSourceIds = unionStrings(
15112
15151
  existing.sourceChunkIds ?? [],
15113
15152
  options.sourceChunkIds ?? []
@@ -15132,7 +15171,10 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
15132
15171
  });
15133
15172
  if (!updated) {
15134
15173
  await assertMergeTargetGoneOrThrow(ctx, decision.targetId);
15135
- return null;
15174
+ return abandonToRace(
15175
+ options,
15176
+ `noop: target ${decision.targetId} deleted mid-write (proof-count bump lost)`
15177
+ );
15136
15178
  }
15137
15179
  return {
15138
15180
  done: {
@@ -15145,7 +15187,12 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
15145
15187
  }
15146
15188
  if (decision.action === "update" && decision.targetId && decision.content) {
15147
15189
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
15148
- if (!existing || existing.supersededBy) return null;
15190
+ if (!existing || existing.supersededBy) {
15191
+ return abandonToRace(
15192
+ options,
15193
+ `update: target ${decision.targetId} ${existing ? "superseded" : "deleted"} before the write`
15194
+ );
15195
+ }
15149
15196
  const mergedSourceIds = unionStrings(
15150
15197
  existing.sourceChunkIds ?? [],
15151
15198
  options.sourceChunkIds ?? []
@@ -15176,7 +15223,10 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
15176
15223
  });
15177
15224
  if (!updated) {
15178
15225
  await assertMergeTargetGoneOrThrow(ctx, decision.targetId);
15179
- return null;
15226
+ return abandonToRace(
15227
+ options,
15228
+ `update: target ${decision.targetId} deleted mid-write (consolidated rewrite lost)`
15229
+ );
15180
15230
  }
15181
15231
  ctx.vaultCache.set(decision.targetId, Float32Array.from(newEmbedding));
15182
15232
  return {
@@ -15188,6 +15238,11 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
15188
15238
  }
15189
15239
  };
15190
15240
  }
15241
+ notifyConsolidationFallback(
15242
+ "invalid_response",
15243
+ options.consolidateOptions?.onFallback,
15244
+ `unapplicable ${decision.action} decision reached the applier \u2014 validate() should have rejected it`
15245
+ );
15191
15246
  return null;
15192
15247
  }
15193
15248
  async function assertMergeTargetGoneOrThrow(ctx, targetId) {
@@ -2429,6 +2429,32 @@ type StepFinishEvent = {
2429
2429
  outputTokens?: number;
2430
2430
  };
2431
2431
  };
2432
+ /**
2433
+ * Terminal state of the LAST model response in the turn, normalized to the
2434
+ * completions vocabulary (`"stop"` | `"length"` | `"tool_calls"` | provider-
2435
+ * specific values). `undefined` when the stream carried no finish reason at all.
2436
+ *
2437
+ * Exposed because neither response shape reliably carries it out to a caller.
2438
+ * The Responses API shape has no field for it — `LlmapiResponseResponse` declares
2439
+ * neither `status` nor `incomplete_details` — so `buildFinalResponse` cannot
2440
+ * surface truncation there even though the loop detected it (`responses.ts`
2441
+ * normalizes it onto the accumulator). Completions does expose
2442
+ * `choices[0].finish_reason`, but omits `tool_calls` entirely when there are
2443
+ * none, so a caller sniffing the response cannot tell "no tool calls" from
2444
+ * "field absent".
2445
+ *
2446
+ * The loop already knows both, unambiguously and identically for either API.
2447
+ * Reporting them here means callers and test harnesses stop reverse-engineering
2448
+ * them from a response shape that cannot answer. See anuma-ai/sdk#805.
2449
+ */
2450
+ type RunTerminalState = {
2451
+ /** Normalized finish reason of the final response. `"length"` means the output
2452
+ * ceiling cut it off; anything else means the model stopped on its own. */
2453
+ finishReason?: string;
2454
+ /** Tool calls the final response carried. `0` is meaningful — combined with
2455
+ * empty content it identifies a turn that ended having produced nothing. */
2456
+ finalToolCallCount: number;
2457
+ };
2432
2458
  type RunToolLoopResult = {
2433
2459
  data: ApiResponse;
2434
2460
  error: null;
@@ -2436,6 +2462,8 @@ type RunToolLoopResult = {
2436
2462
  toolsChecksum?: string;
2437
2463
  /** Results from tools that were auto-executed by the SDK */
2438
2464
  autoExecutedToolResults?: AutoExecutedToolResult[];
2465
+ /** Terminal state of the final model response. See {@link RunTerminalState}. */
2466
+ terminalState?: RunTerminalState;
2439
2467
  } | {
2440
2468
  data: ApiResponse | null;
2441
2469
  error: string;
@@ -2447,6 +2475,18 @@ type RunToolLoopResult = {
2447
2475
  detached?: true;
2448
2476
  /** Non-null only when `resumable` was true AND an inference id was captured before detach. */
2449
2477
  resume?: StreamResumeHandle | null;
2478
+ /**
2479
+ * Terminal state of the final model response, when the failure is one the
2480
+ * loop diagnosed FROM that response — today, the truncated-to-nothing
2481
+ * error. See {@link RunTerminalState}.
2482
+ *
2483
+ * Absent on failures that never got a terminal response to read (transport
2484
+ * errors, detach, an SSE failure mid-stream), which is why it stays
2485
+ * optional here. Present on the truncation error specifically because that
2486
+ * is where it is most diagnostic: a caller can branch on
2487
+ * `finishReason === "length"` instead of matching the error string.
2488
+ */
2489
+ terminalState?: RunTerminalState;
2450
2490
  };
2451
2491
  /** Capability header: tells the portal this client can resume a detached stream. Pinned with zeta-chain/ai-portal#1139. */
2452
2492
  declare const STREAM_RESUMABLE_HEADER = "X-Stream-Resumable";
@@ -5282,8 +5322,26 @@ interface RecallDiagnostics {
5282
5322
  }
5283
5323
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5284
5324
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5285
- /** Why the consolidator fell back to "create" instead of a real decision. */
5286
- type ConsolidationFallbackReason = "llm_error" | "invalid_response";
5325
+ /**
5326
+ * Why a retain fell back to "create" instead of applying a consolidation
5327
+ * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5328
+ * keeping them apart:
5329
+ *
5330
+ * - `llm_error` — the consolidation call never produced a response (network,
5331
+ * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
5332
+ * portal and the auth config.
5333
+ * - `invalid_response` — the model answered, but with something unusable: an
5334
+ * unknown action, a targetId that was not in the candidate set, an `update` or
5335
+ * `supersede` with empty content, a `noop` with no target. Look at the prompt
5336
+ * and the model.
5337
+ * - `target_vanished` — the model returned a decision that was valid when it was
5338
+ * made, and the row it named was deleted or superseded by a concurrent writer
5339
+ * before the write landed. Nothing is broken in the consolidator; a sustained
5340
+ * rate points at write contention (for example an auto-extraction worker and a
5341
+ * manual write racing over the same vault), which quietly costs you the
5342
+ * dedup that decision would have performed.
5343
+ */
5344
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5287
5345
  interface RetainOptions {
5288
5346
  source?: RetainSource;
5289
5347
  sourceChunkIds?: string[];
@@ -5299,7 +5357,8 @@ interface RetainOptions {
5299
5357
  * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5300
5358
  */
5301
5359
  respectTombstones?: boolean;
5302
- /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5360
+ /** Cosine similarity threshold for auto-merge. Default: 0.8
5361
+ * (`DEFAULT_AUTO_MERGE_THRESHOLD` in retain.ts — the source of truth). */
5303
5362
  autoMergeThreshold?: number;
5304
5363
  /**
5305
5364
  * When provided, runs an LLM-based consolidation pass against the top-K
@@ -5327,9 +5386,13 @@ interface RetainOptions {
5327
5386
  */
5328
5387
  piiRedaction?: boolean | PiiRedactor;
5329
5388
  };
5330
- /** Cosine similarity floor for the consolidator candidate set. Default: 0.65. */
5389
+ /** Cosine similarity floor for the consolidator candidate set. Default: 0.55
5390
+ * (`DEFAULT_CONSOLIDATE_THRESHOLD` in retain.ts — the source of truth). */
5331
5391
  consolidateThreshold?: number;
5332
- /** Top-K consolidation candidates to feed the LLM. Default: 5. */
5392
+ /** Top-K consolidation candidates to feed the LLM. Default: 20
5393
+ * (`DEFAULT_CONSOLIDATE_TOP_K` in retain.ts — the source of truth). Widened
5394
+ * from 5 so a value change can find and retire ALL stale duplicates of the
5395
+ * old value in one pass, not just the nearest few. */
5333
5396
  consolidateTopK?: number;
5334
5397
  /**
5335
5398
  * W6 temporal lane — when the event in this fact occurred. Persisted to
@@ -2429,6 +2429,32 @@ type StepFinishEvent = {
2429
2429
  outputTokens?: number;
2430
2430
  };
2431
2431
  };
2432
+ /**
2433
+ * Terminal state of the LAST model response in the turn, normalized to the
2434
+ * completions vocabulary (`"stop"` | `"length"` | `"tool_calls"` | provider-
2435
+ * specific values). `undefined` when the stream carried no finish reason at all.
2436
+ *
2437
+ * Exposed because neither response shape reliably carries it out to a caller.
2438
+ * The Responses API shape has no field for it — `LlmapiResponseResponse` declares
2439
+ * neither `status` nor `incomplete_details` — so `buildFinalResponse` cannot
2440
+ * surface truncation there even though the loop detected it (`responses.ts`
2441
+ * normalizes it onto the accumulator). Completions does expose
2442
+ * `choices[0].finish_reason`, but omits `tool_calls` entirely when there are
2443
+ * none, so a caller sniffing the response cannot tell "no tool calls" from
2444
+ * "field absent".
2445
+ *
2446
+ * The loop already knows both, unambiguously and identically for either API.
2447
+ * Reporting them here means callers and test harnesses stop reverse-engineering
2448
+ * them from a response shape that cannot answer. See anuma-ai/sdk#805.
2449
+ */
2450
+ type RunTerminalState = {
2451
+ /** Normalized finish reason of the final response. `"length"` means the output
2452
+ * ceiling cut it off; anything else means the model stopped on its own. */
2453
+ finishReason?: string;
2454
+ /** Tool calls the final response carried. `0` is meaningful — combined with
2455
+ * empty content it identifies a turn that ended having produced nothing. */
2456
+ finalToolCallCount: number;
2457
+ };
2432
2458
  type RunToolLoopResult = {
2433
2459
  data: ApiResponse;
2434
2460
  error: null;
@@ -2436,6 +2462,8 @@ type RunToolLoopResult = {
2436
2462
  toolsChecksum?: string;
2437
2463
  /** Results from tools that were auto-executed by the SDK */
2438
2464
  autoExecutedToolResults?: AutoExecutedToolResult[];
2465
+ /** Terminal state of the final model response. See {@link RunTerminalState}. */
2466
+ terminalState?: RunTerminalState;
2439
2467
  } | {
2440
2468
  data: ApiResponse | null;
2441
2469
  error: string;
@@ -2447,6 +2475,18 @@ type RunToolLoopResult = {
2447
2475
  detached?: true;
2448
2476
  /** Non-null only when `resumable` was true AND an inference id was captured before detach. */
2449
2477
  resume?: StreamResumeHandle | null;
2478
+ /**
2479
+ * Terminal state of the final model response, when the failure is one the
2480
+ * loop diagnosed FROM that response — today, the truncated-to-nothing
2481
+ * error. See {@link RunTerminalState}.
2482
+ *
2483
+ * Absent on failures that never got a terminal response to read (transport
2484
+ * errors, detach, an SSE failure mid-stream), which is why it stays
2485
+ * optional here. Present on the truncation error specifically because that
2486
+ * is where it is most diagnostic: a caller can branch on
2487
+ * `finishReason === "length"` instead of matching the error string.
2488
+ */
2489
+ terminalState?: RunTerminalState;
2450
2490
  };
2451
2491
  /** Capability header: tells the portal this client can resume a detached stream. Pinned with zeta-chain/ai-portal#1139. */
2452
2492
  declare const STREAM_RESUMABLE_HEADER = "X-Stream-Resumable";
@@ -5282,8 +5322,26 @@ interface RecallDiagnostics {
5282
5322
  }
5283
5323
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5284
5324
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5285
- /** Why the consolidator fell back to "create" instead of a real decision. */
5286
- type ConsolidationFallbackReason = "llm_error" | "invalid_response";
5325
+ /**
5326
+ * Why a retain fell back to "create" instead of applying a consolidation
5327
+ * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5328
+ * keeping them apart:
5329
+ *
5330
+ * - `llm_error` — the consolidation call never produced a response (network,
5331
+ * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
5332
+ * portal and the auth config.
5333
+ * - `invalid_response` — the model answered, but with something unusable: an
5334
+ * unknown action, a targetId that was not in the candidate set, an `update` or
5335
+ * `supersede` with empty content, a `noop` with no target. Look at the prompt
5336
+ * and the model.
5337
+ * - `target_vanished` — the model returned a decision that was valid when it was
5338
+ * made, and the row it named was deleted or superseded by a concurrent writer
5339
+ * before the write landed. Nothing is broken in the consolidator; a sustained
5340
+ * rate points at write contention (for example an auto-extraction worker and a
5341
+ * manual write racing over the same vault), which quietly costs you the
5342
+ * dedup that decision would have performed.
5343
+ */
5344
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5287
5345
  interface RetainOptions {
5288
5346
  source?: RetainSource;
5289
5347
  sourceChunkIds?: string[];
@@ -5299,7 +5357,8 @@ interface RetainOptions {
5299
5357
  * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5300
5358
  */
5301
5359
  respectTombstones?: boolean;
5302
- /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5360
+ /** Cosine similarity threshold for auto-merge. Default: 0.8
5361
+ * (`DEFAULT_AUTO_MERGE_THRESHOLD` in retain.ts — the source of truth). */
5303
5362
  autoMergeThreshold?: number;
5304
5363
  /**
5305
5364
  * When provided, runs an LLM-based consolidation pass against the top-K
@@ -5327,9 +5386,13 @@ interface RetainOptions {
5327
5386
  */
5328
5387
  piiRedaction?: boolean | PiiRedactor;
5329
5388
  };
5330
- /** Cosine similarity floor for the consolidator candidate set. Default: 0.65. */
5389
+ /** Cosine similarity floor for the consolidator candidate set. Default: 0.55
5390
+ * (`DEFAULT_CONSOLIDATE_THRESHOLD` in retain.ts — the source of truth). */
5331
5391
  consolidateThreshold?: number;
5332
- /** Top-K consolidation candidates to feed the LLM. Default: 5. */
5392
+ /** Top-K consolidation candidates to feed the LLM. Default: 20
5393
+ * (`DEFAULT_CONSOLIDATE_TOP_K` in retain.ts — the source of truth). Widened
5394
+ * from 5 so a value change can find and retire ALL stale duplicates of the
5395
+ * old value in one pass, not just the nearest few. */
5333
5396
  consolidateTopK?: number;
5334
5397
  /**
5335
5398
  * W6 temporal lane — when the event in this fact occurred. Persisted to
@@ -120,8 +120,9 @@ import {
120
120
  PiiRedactor,
121
121
  createStreamingDeAnonymizer,
122
122
  isPiiRedactor,
123
+ notifyConsolidationFallback,
123
124
  resolvePiiRedactor
124
- } from "./chunk-Y7XT2VNQ.mjs";
125
+ } from "./chunk-WV7C7VHF.mjs";
125
126
  import {
126
127
  callPortalJsonCompletion,
127
128
  consoleLogger,
@@ -3389,13 +3390,18 @@ Executing tool: ${toolInfo}
3389
3390
  }
3390
3391
  const truncatedToNothing = (acc) => acc.finishReason === "length" && acc.toolCalls.size === 0 && acc.content.trim() === "";
3391
3392
  const TRUNCATION_ERROR = "Model response was truncated at the output-token limit before it produced any usable content or tool call. Retry with a higher `maxOutputTokens`, or prompt for fewer/smaller tool calls per turn.";
3393
+ const terminalStateOf = (acc) => ({
3394
+ ...acc.finishReason !== void 0 && { finishReason: acc.finishReason },
3395
+ finalToolCallCount: acc.toolCalls.size
3396
+ });
3392
3397
  if (toolIteration > 0) {
3393
3398
  if (truncatedToNothing(currentAccumulator)) {
3394
3399
  await fireRunError({ error: TRUNCATION_ERROR, stage: "model" });
3395
3400
  return {
3396
3401
  data: buildResponseFinal(currentAccumulator),
3397
3402
  error: TRUNCATION_ERROR,
3398
- toolsChecksum: currentAccumulator.toolsChecksum
3403
+ toolsChecksum: currentAccumulator.toolsChecksum,
3404
+ terminalState: terminalStateOf(currentAccumulator)
3399
3405
  };
3400
3406
  }
3401
3407
  const finalResponse = buildResponseFinal(currentAccumulator);
@@ -3408,7 +3414,8 @@ Executing tool: ${toolInfo}
3408
3414
  data: finalResponse,
3409
3415
  error: null,
3410
3416
  toolsChecksum: currentAccumulator.toolsChecksum,
3411
- autoExecutedToolResults: accumulatedToolResults
3417
+ autoExecutedToolResults: accumulatedToolResults,
3418
+ terminalState: terminalStateOf(currentAccumulator)
3412
3419
  };
3413
3420
  }
3414
3421
  if (truncatedToNothing(accumulator)) {
@@ -3416,7 +3423,8 @@ Executing tool: ${toolInfo}
3416
3423
  return {
3417
3424
  data: response,
3418
3425
  error: TRUNCATION_ERROR,
3419
- toolsChecksum: accumulator.toolsChecksum
3426
+ toolsChecksum: accumulator.toolsChecksum,
3427
+ terminalState: terminalStateOf(accumulator)
3420
3428
  };
3421
3429
  }
3422
3430
  if (onFinish) onFinish(response);
@@ -3427,7 +3435,8 @@ Executing tool: ${toolInfo}
3427
3435
  return {
3428
3436
  data: response,
3429
3437
  error: null,
3430
- toolsChecksum: accumulator.toolsChecksum
3438
+ toolsChecksum: accumulator.toolsChecksum,
3439
+ terminalState: terminalStateOf(accumulator)
3431
3440
  };
3432
3441
  } catch (err) {
3433
3442
  if (isAbortError(err)) {
@@ -6575,6 +6584,11 @@ async function retain(content, ctx, options = {}) {
6575
6584
  proofCount: 1
6576
6585
  };
6577
6586
  }
6587
+ notifyConsolidationFallback(
6588
+ "target_vanished",
6589
+ options.consolidateOptions?.onFallback,
6590
+ `supersede: primary target ${primaryTargetId} retired or deleted inside the atomic write`
6591
+ );
6578
6592
  }
6579
6593
  const created = await createVaultMemoryOp(ctx.vaultCtx, createOpts);
6580
6594
  ctx.vaultCache.set(created.uniqueId, Float32Array.from(embedding));
@@ -6626,6 +6640,10 @@ function pickFactTypeUpdate(existing, incoming) {
6626
6640
  function resurrectFields(existing) {
6627
6641
  return typeof existing.archivedAt === "number" ? { restore: true } : { preserveUpdatedAt: true };
6628
6642
  }
6643
+ function abandonToRace(options, detail) {
6644
+ notifyConsolidationFallback("target_vanished", options.consolidateOptions?.onFallback, detail);
6645
+ return null;
6646
+ }
6629
6647
  async function tryConsolidate(trimmed, ctx, options, prepared) {
6630
6648
  const consolidateOptions = options.consolidateOptions;
6631
6649
  if (!consolidateOptions) return null;
@@ -6647,7 +6665,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6647
6665
  content: m.content,
6648
6666
  similarity: m.similarity
6649
6667
  }));
6650
- const { consolidateMemory: doConsolidate } = await import("./consolidate-HYIEFPFA.mjs");
6668
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-AURTG64H.mjs");
6651
6669
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6652
6670
  if (decision.action === "create") return null;
6653
6671
  if (decision.action === "supersede" && decision.content) {
@@ -6658,12 +6676,22 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6658
6676
  const existing = await getVaultMemoryOp(ctx.vaultCtx, id);
6659
6677
  if (existing && !existing.supersededBy) valid.push(id);
6660
6678
  }
6661
- if (valid.length === 0) return null;
6679
+ if (valid.length === 0) {
6680
+ return abandonToRace(
6681
+ options,
6682
+ `supersede: all ${requestedIds.length} target(s) deleted or already superseded`
6683
+ );
6684
+ }
6662
6685
  return { supersede: valid, content: decision.content };
6663
6686
  }
6664
6687
  if (decision.action === "noop" && decision.targetId) {
6665
6688
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6666
- if (!existing || existing.supersededBy) return null;
6689
+ if (!existing || existing.supersededBy) {
6690
+ return abandonToRace(
6691
+ options,
6692
+ `noop: target ${decision.targetId} ${existing ? "superseded" : "deleted"} before the write`
6693
+ );
6694
+ }
6667
6695
  const mergedSourceIds = unionStrings(
6668
6696
  existing.sourceChunkIds ?? [],
6669
6697
  options.sourceChunkIds ?? []
@@ -6688,7 +6716,10 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6688
6716
  });
6689
6717
  if (!updated) {
6690
6718
  await assertMergeTargetGoneOrThrow(ctx, decision.targetId);
6691
- return null;
6719
+ return abandonToRace(
6720
+ options,
6721
+ `noop: target ${decision.targetId} deleted mid-write (proof-count bump lost)`
6722
+ );
6692
6723
  }
6693
6724
  return {
6694
6725
  done: {
@@ -6701,7 +6732,12 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6701
6732
  }
6702
6733
  if (decision.action === "update" && decision.targetId && decision.content) {
6703
6734
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6704
- if (!existing || existing.supersededBy) return null;
6735
+ if (!existing || existing.supersededBy) {
6736
+ return abandonToRace(
6737
+ options,
6738
+ `update: target ${decision.targetId} ${existing ? "superseded" : "deleted"} before the write`
6739
+ );
6740
+ }
6705
6741
  const mergedSourceIds = unionStrings(
6706
6742
  existing.sourceChunkIds ?? [],
6707
6743
  options.sourceChunkIds ?? []
@@ -6732,7 +6768,10 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6732
6768
  });
6733
6769
  if (!updated) {
6734
6770
  await assertMergeTargetGoneOrThrow(ctx, decision.targetId);
6735
- return null;
6771
+ return abandonToRace(
6772
+ options,
6773
+ `update: target ${decision.targetId} deleted mid-write (consolidated rewrite lost)`
6774
+ );
6736
6775
  }
6737
6776
  ctx.vaultCache.set(decision.targetId, Float32Array.from(newEmbedding));
6738
6777
  return {
@@ -6744,6 +6783,11 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6744
6783
  }
6745
6784
  };
6746
6785
  }
6786
+ notifyConsolidationFallback(
6787
+ "invalid_response",
6788
+ options.consolidateOptions?.onFallback,
6789
+ `unapplicable ${decision.action} decision reached the applier \u2014 validate() should have rejected it`
6790
+ );
6747
6791
  return null;
6748
6792
  }
6749
6793
  async function assertMergeTargetGoneOrThrow(ctx, targetId) {