@remnic/core 10.0.94 → 10.0.95

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 (36) hide show
  1. package/dist/access-admin-ops-surface.js +1 -1
  2. package/dist/access-cli.js +1 -1
  3. package/dist/access-extraction-force-flush.js +1 -1
  4. package/dist/access-http-briefing.js +1 -1
  5. package/dist/access-http-query.js +1 -1
  6. package/dist/access-http.js +1 -1
  7. package/dist/access-identity-continuity-surface.js +1 -1
  8. package/dist/access-lcm-surface.js +1 -1
  9. package/dist/access-mcp.js +1 -1
  10. package/dist/access-observe-write-surface.js +1 -1
  11. package/dist/access-operations-batch.js +1 -1
  12. package/dist/access-operations.js +1 -1
  13. package/dist/access-recall-concurrency.js +1 -1
  14. package/dist/access-recall-response.js +1 -1
  15. package/dist/access-recall-surface.js +1 -1
  16. package/dist/access-service.js +1 -1
  17. package/dist/{chunk-D5S6RL32.js → chunk-DDATG2SF.js} +40 -20
  18. package/dist/{chunk-D5S6RL32.js.map → chunk-DDATG2SF.js.map} +1 -1
  19. package/dist/{chunk-RUTN5MMC.js → chunk-LS7PT3YD.js} +2 -2
  20. package/dist/cli.js +1 -1
  21. package/dist/explicit-capture.js +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/orchestration/backlog-scheduler.js +2 -2
  24. package/dist/orchestration/buffer-drain-runner.js +2 -2
  25. package/dist/orchestration/extraction-persist-failure.d.ts +25 -1
  26. package/dist/orchestration/extraction-persist-failure.d.ts.map +1 -1
  27. package/dist/orchestration/extraction-persist.d.ts +3 -6
  28. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  29. package/dist/orchestrator.js +1 -1
  30. package/dist/recall-navigation.js +1 -1
  31. package/package.json +2 -2
  32. package/src/orchestration/extraction-persist-failure.test.ts +90 -0
  33. package/src/orchestration/extraction-persist-failure.ts +39 -1
  34. package/src/orchestration/extraction-persist.ts +29 -21
  35. package/src/orchestrator-persist-branch-failure.test.ts +271 -0
  36. /package/dist/{chunk-RUTN5MMC.js.map → chunk-LS7PT3YD.js.map} +0 -0
@@ -2,7 +2,7 @@ import {
2
2
  ExtractionDrain,
3
3
  TurnIngestionCoordinator,
4
4
  shouldSkipImplicitExtraction
5
- } from "./chunk-D5S6RL32.js";
5
+ } from "./chunk-DDATG2SF.js";
6
6
  import {
7
7
  headerConnector,
8
8
  renderExtractionConversation,
@@ -447,4 +447,4 @@ export {
447
447
  ClaimBufferStorage,
448
448
  BufferDrainRunner
449
449
  };
450
- //# sourceMappingURL=chunk-RUTN5MMC.js.map
450
+ //# sourceMappingURL=chunk-LS7PT3YD.js.map
package/dist/cli.js CHANGED
@@ -91,7 +91,7 @@ import {
91
91
  runWorkProductStatusCliCommand,
92
92
  runWorkProjectCliCommand,
93
93
  runWorkTaskCliCommand
94
- } from "./chunk-D5S6RL32.js";
94
+ } from "./chunk-DDATG2SF.js";
95
95
  import "./chunk-B3DR2JBZ.js";
96
96
  import "./chunk-6P7U3HWG.js";
97
97
  import "./chunk-H4CKS5B2.js";
@@ -8,7 +8,7 @@ import {
8
8
  shouldSkipImplicitExtraction,
9
9
  stripInlineExplicitCaptureNotes,
10
10
  validateExplicitCaptureInput
11
- } from "./chunk-D5S6RL32.js";
11
+ } from "./chunk-DDATG2SF.js";
12
12
  import "./chunk-B3DR2JBZ.js";
13
13
  import "./chunk-6P7U3HWG.js";
14
14
  import "./chunk-H4CKS5B2.js";
package/dist/index.js CHANGED
@@ -259,7 +259,7 @@ import {
259
259
  writeManifest,
260
260
  writeMeetingEpisodeMemory,
261
261
  writeTimelineState
262
- } from "./chunk-D5S6RL32.js";
262
+ } from "./chunk-DDATG2SF.js";
263
263
  import {
264
264
  WEARABLE_SOURCE_PREFIX,
265
265
  WearablesInputError,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BufferDrainRunner
3
- } from "../chunk-RUTN5MMC.js";
4
- import "../chunk-D5S6RL32.js";
3
+ } from "../chunk-LS7PT3YD.js";
4
+ import "../chunk-DDATG2SF.js";
5
5
  import "../chunk-B3DR2JBZ.js";
6
6
  import "../chunk-6P7U3HWG.js";
7
7
  import "../chunk-H4CKS5B2.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  BufferDrainRunner,
3
3
  ClaimBufferStorage
4
- } from "../chunk-RUTN5MMC.js";
5
- import "../chunk-D5S6RL32.js";
4
+ } from "../chunk-LS7PT3YD.js";
5
+ import "../chunk-DDATG2SF.js";
6
6
  import "../chunk-B3DR2JBZ.js";
7
7
  import "../chunk-6P7U3HWG.js";
8
8
  import "../chunk-H4CKS5B2.js";
@@ -1,3 +1,4 @@
1
+ type FinalizeCommittedWrites = (scheduleQmd: boolean) => Promise<void>;
1
2
  /**
2
3
  * Fail a persistence pass after a durable write rejected (#3227, same
3
4
  * contract as the entity write path from #3228). Earlier writes in the batch
@@ -5,5 +6,28 @@
5
6
  * original error is then rethrown so the run leaves its input retryable
6
7
  * instead of recording the fingerprint and clearing the source turns.
7
8
  */
8
- export declare function rethrowAfterCommittedWriteFinalization(finalizeCommittedWrites: (scheduleQmd: boolean) => Promise<void>, cause: unknown, outputKind: string): Promise<never>;
9
+ export declare function rethrowAfterCommittedWriteFinalization(finalizeCommittedWrites: FinalizeCommittedWrites, cause: unknown, outputKind: string): Promise<never>;
10
+ /**
11
+ * Batch-end finalization for one persistExtraction pass (#3288).
12
+ *
13
+ * `bind` wraps the pass's finalizer so it runs at most once. The success
14
+ * tail, the per-branch catch sites and the pass boundary can all reach it,
15
+ * and a second run would append the same behavior signals again.
16
+ *
17
+ * `run` is the pass boundary. A throw from a branch with no catch site of its
18
+ * own (a chunked-parent post-write step, the semantic-merge post effects, the
19
+ * profile or question write) still finalizes the writes already committed
20
+ * before the original error propagates. When a catch site or the success tail
21
+ * already ran the finalizer, the boundary only rethrows. Nothing is durable
22
+ * before the finalizer is bound, so an earlier throw propagates as is.
23
+ */
24
+ export declare class CommittedWriteFinalization {
25
+ private finalizer;
26
+ private started;
27
+ bind(finalize: FinalizeCommittedWrites): FinalizeCommittedWrites;
28
+ run<T>(pass: () => Promise<T>): Promise<T>;
29
+ }
30
+ /** A pass method's parameter list without its leading CommittedWriteFinalization. */
31
+ export type ArgsAfterFinalization<Params> = Params extends [CommittedWriteFinalization, ...infer Rest] ? Rest : never;
32
+ export {};
9
33
  //# sourceMappingURL=extraction-persist-failure.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extraction-persist-failure.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist-failure.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAsB,sCAAsC,CAC1D,uBAAuB,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,EAChE,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,KAAK,CAAC,CAKhB"}
1
+ {"version":3,"file":"extraction-persist-failure.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist-failure.ts"],"names":[],"mappings":"AAEA,KAAK,uBAAuB,GAAG,CAAC,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE;;;;;;GAMG;AACH,wBAAsB,sCAAsC,CAC1D,uBAAuB,EAAE,uBAAuB,EAChD,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,KAAK,CAAC,CAKhB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,0BAA0B;IACrC,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,OAAO,CAA4B;IAE3C,IAAI,CAAC,QAAQ,EAAE,uBAAuB,GAAG,uBAAuB;IAK1D,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAQjD;AAED,qFAAqF;AACrF,MAAM,MAAM,qBAAqB,CAAC,MAAM,IAAI,MAAM,SAAS,CAAC,0BAA0B,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC"}
@@ -1,8 +1,4 @@
1
- import { type ExtractionSourceContext } from "./extraction-origin-context.js";
2
- import { type StorageManager } from "../index.js";
3
- import { type GraphConstructionCapabilitySet, type MemoryLifecycleCapabilitySet } from "../capabilities.js";
4
- import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
5
- import type { ExtractionResult } from "../types.js";
1
+ import { type ArgsAfterFinalization } from "./extraction-persist-failure.js";
6
2
  import type { ExtractionPersistDeps } from "./extraction-persist-deps.js";
7
3
  /** One extraction run's persist outcome: per-run values, no shared side channels (#1526). */
8
4
  export interface ExtractionPersistResult {
@@ -18,6 +14,7 @@ export declare class ExtractionPersistCoordinator {
18
14
  constructor(deps: ExtractionPersistDeps);
19
15
  private get config();
20
16
  private get dependencyPropagationDelivery();
21
- persistExtraction(result: ExtractionResult, storage: StorageManager, threadIdForExtraction?: string | null, sourceContext?: ExtractionSourceContext, baseNamespace?: string, scopeProfileWritePlan?: ResolvedScopeProfilePlan | null, sourceText?: string, graphCaps?: GraphConstructionCapabilitySet, lifecycleCaps?: MemoryLifecycleCapabilitySet): Promise<ExtractionPersistResult>;
17
+ persistExtraction(...args: ArgsAfterFinalization<Parameters<ExtractionPersistCoordinator["persistExtractionPass"]>>): Promise<ExtractionPersistResult>;
18
+ private persistExtractionPass;
22
19
  }
23
20
  //# sourceMappingURL=extraction-persist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extraction-persist.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAkBxG,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7F,OAAO,EAUgE,KAAK,8BAA8B,EACxG,KAAK,4BAA4B,EAClC,MAAM,oBAAoB,CAAC;AAqD5B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF,OAAO,KAAK,EAEV,gBAAgB,EAQjB,MAAM,aAAa,CAAC;AAWrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAmB1E,6FAA6F;AAC7F,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAE9C,OAAO,KAAK,MAAM,GAEjB;IACD,OAAO,KAAK,6BAA6B,GAGxC;IAEK,iBAAiB,CACrB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,cAAc,EACvB,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,EACrC,aAAa,CAAC,EAAE,uBAAuB,EACvC,aAAa,CAAC,EAAE,MAAM,EACtB,qBAAqB,CAAC,EAAE,wBAAwB,GAAG,IAAI,EACvD,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,GAAE,8BAAuF,EAClG,aAAa,GAAE,4BAAmF,GACjG,OAAO,CAAC,uBAAuB,CAAC;CAswFpC"}
1
+ {"version":3,"file":"extraction-persist.d.ts","sourceRoot":"","sources":["../../src/orchestration/extraction-persist.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,qBAAqB,EAAsE,MAAM,iCAAiC,CAAC;AA8GjJ,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAmB1E,6FAA6F;AAC7F,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAE9C,OAAO,KAAK,MAAM,GAEjB;IACD,OAAO,KAAK,6BAA6B,GAGxC;IAED,iBAAiB,CAAC,GAAG,IAAI,EAAE,qBAAqB,CAAC,UAAU,CAAC,4BAA4B,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,uBAAuB,CAAC;YAKxI,qBAAqB;CAmxFpC"}
@@ -6,7 +6,7 @@ import {
6
6
  mergeGraphExpandedResults,
7
7
  qmdStartupCollectionChecksWithTimeout,
8
8
  resolveEffectiveIdentityInjectionMode
9
- } from "./chunk-D5S6RL32.js";
9
+ } from "./chunk-DDATG2SF.js";
10
10
  import "./chunk-B3DR2JBZ.js";
11
11
  import "./chunk-6P7U3HWG.js";
12
12
  import "./chunk-H4CKS5B2.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runRecallNavigation
3
- } from "./chunk-D5S6RL32.js";
3
+ } from "./chunk-DDATG2SF.js";
4
4
  import "./chunk-B3DR2JBZ.js";
5
5
  import "./chunk-6P7U3HWG.js";
6
6
  import "./chunk-H4CKS5B2.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "10.0.94",
3
+ "version": "10.0.95",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3386,7 +3386,7 @@
3386
3386
  "core"
3387
3387
  ],
3388
3388
  "peerDependencies": {
3389
- "@remnic/coding-graph": "^10.0.94"
3389
+ "@remnic/coding-graph": "^10.0.95"
3390
3390
  },
3391
3391
  "peerDependenciesMeta": {
3392
3392
  "@remnic/coding-graph": {
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { CommittedWriteFinalization } from "./extraction-persist-failure.js";
5
+
6
+ function recordingFinalizer() {
7
+ const calls: boolean[] = [];
8
+ return {
9
+ calls,
10
+ finalize: async (scheduleQmd: boolean) => {
11
+ calls.push(scheduleQmd);
12
+ },
13
+ };
14
+ }
15
+
16
+ test("#3288: the bound finalizer runs once across the success tail and catch sites", async () => {
17
+ const finalization = new CommittedWriteFinalization();
18
+ const { calls, finalize } = recordingFinalizer();
19
+ const once = finalization.bind(finalize);
20
+
21
+ const value = await finalization.run(async () => {
22
+ await once(true);
23
+ await once(false);
24
+ return "persisted";
25
+ });
26
+
27
+ assert.equal(value, "persisted");
28
+ assert.deepEqual(calls, [true], "a second caller reuses the first run instead of finalizing again");
29
+ });
30
+
31
+ test("#3288: the pass boundary finalizes with QMD scheduling, then rethrows the original error", async () => {
32
+ const finalization = new CommittedWriteFinalization();
33
+ const { calls, finalize } = recordingFinalizer();
34
+ const cause = new Error("branch without its own catch site");
35
+
36
+ await assert.rejects(
37
+ finalization.run(async () => {
38
+ finalization.bind(finalize);
39
+ throw cause;
40
+ }),
41
+ (error: Error) => error === cause,
42
+ );
43
+ assert.deepEqual(calls, [true]);
44
+ });
45
+
46
+ test("#3288: the pass boundary does not finalize again after a catch site already did", async () => {
47
+ const finalization = new CommittedWriteFinalization();
48
+ const { calls, finalize } = recordingFinalizer();
49
+ const cause = new Error("catch site rethrow");
50
+
51
+ await assert.rejects(
52
+ finalization.run(async () => {
53
+ const once = finalization.bind(finalize);
54
+ await once(true);
55
+ throw cause;
56
+ }),
57
+ (error: Error) => error === cause,
58
+ );
59
+ assert.deepEqual(calls, [true]);
60
+ });
61
+
62
+ test("#3288: a throw before the finalizer is bound propagates without finalizing", async () => {
63
+ const finalization = new CommittedWriteFinalization();
64
+ const cause = new Error("failed before any write");
65
+
66
+ await assert.rejects(
67
+ finalization.run(async () => {
68
+ throw cause;
69
+ }),
70
+ (error: Error) => error === cause,
71
+ );
72
+ });
73
+
74
+ test("#3288: a failing finalizer does not mask the original error", async () => {
75
+ const finalization = new CommittedWriteFinalization();
76
+ const cause = new Error("branch failure");
77
+ let attempts = 0;
78
+
79
+ await assert.rejects(
80
+ finalization.run(async () => {
81
+ finalization.bind(async () => {
82
+ attempts += 1;
83
+ throw new Error("finalization failure");
84
+ });
85
+ throw cause;
86
+ }),
87
+ (error: Error) => error === cause,
88
+ );
89
+ assert.equal(attempts, 1);
90
+ });
@@ -1,5 +1,7 @@
1
1
  import { log } from "../logger.js";
2
2
 
3
+ type FinalizeCommittedWrites = (scheduleQmd: boolean) => Promise<void>;
4
+
3
5
  /**
4
6
  * Fail a persistence pass after a durable write rejected (#3227, same
5
7
  * contract as the entity write path from #3228). Earlier writes in the batch
@@ -8,7 +10,7 @@ import { log } from "../logger.js";
8
10
  * instead of recording the fingerprint and clearing the source turns.
9
11
  */
10
12
  export async function rethrowAfterCommittedWriteFinalization(
11
- finalizeCommittedWrites: (scheduleQmd: boolean) => Promise<void>,
13
+ finalizeCommittedWrites: FinalizeCommittedWrites,
12
14
  cause: unknown,
13
15
  outputKind: string,
14
16
  ): Promise<never> {
@@ -17,3 +19,39 @@ export async function rethrowAfterCommittedWriteFinalization(
17
19
  );
18
20
  throw cause;
19
21
  }
22
+
23
+ /**
24
+ * Batch-end finalization for one persistExtraction pass (#3288).
25
+ *
26
+ * `bind` wraps the pass's finalizer so it runs at most once. The success
27
+ * tail, the per-branch catch sites and the pass boundary can all reach it,
28
+ * and a second run would append the same behavior signals again.
29
+ *
30
+ * `run` is the pass boundary. A throw from a branch with no catch site of its
31
+ * own (a chunked-parent post-write step, the semantic-merge post effects, the
32
+ * profile or question write) still finalizes the writes already committed
33
+ * before the original error propagates. When a catch site or the success tail
34
+ * already ran the finalizer, the boundary only rethrows. Nothing is durable
35
+ * before the finalizer is bound, so an earlier throw propagates as is.
36
+ */
37
+ export class CommittedWriteFinalization {
38
+ private finalizer: FinalizeCommittedWrites | undefined;
39
+ private started: Promise<void> | undefined;
40
+
41
+ bind(finalize: FinalizeCommittedWrites): FinalizeCommittedWrites {
42
+ this.finalizer = (scheduleQmd) => (this.started ??= finalize(scheduleQmd));
43
+ return this.finalizer;
44
+ }
45
+
46
+ async run<T>(pass: () => Promise<T>): Promise<T> {
47
+ try {
48
+ return await pass();
49
+ } catch (err) {
50
+ if (!this.finalizer || this.started) throw err;
51
+ return rethrowAfterCommittedWriteFinalization(this.finalizer, err, "persist pass");
52
+ }
53
+ }
54
+ }
55
+
56
+ /** A pass method's parameter list without its leading CommittedWriteFinalization. */
57
+ export type ArgsAfterFinalization<Params> = Params extends [CommittedWriteFinalization, ...infer Rest] ? Rest : never;
@@ -1,7 +1,7 @@
1
1
  import { classifyExtractionOrigin, type ExtractionSourceContext } from "./extraction-origin-context.js";
2
2
  import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./fact-dedup-window.js";
3
3
  import { writePromotionUnderFactDedupWindow } from "./promotion-dedup-window.js";
4
- import { rethrowAfterCommittedWriteFinalization } from "./extraction-persist-failure.js";
4
+ import { type ArgsAfterFinalization, CommittedWriteFinalization, rethrowAfterCommittedWriteFinalization } from "./extraction-persist-failure.js";
5
5
  import { evaluateInjectionScreen, screenEntityForIndex, screenPersistStrings } from "./extraction-injection-gate.js";
6
6
  /**
7
7
  * Extraction persistence coordinator (issue #1526, seam 16).
@@ -152,7 +152,13 @@ export class ExtractionPersistCoordinator {
152
152
  return this.deps.getDependencyPropagationDelivery();
153
153
  }
154
154
 
155
- async persistExtraction(
155
+ persistExtraction(...args: ArgsAfterFinalization<Parameters<ExtractionPersistCoordinator["persistExtractionPass"]>>): Promise<ExtractionPersistResult> {
156
+ const finalization = new CommittedWriteFinalization(); // #3288: one batch-end finalization boundary per pass
157
+ return finalization.run(() => this.persistExtractionPass(finalization, ...args));
158
+ }
159
+
160
+ private async persistExtractionPass(
161
+ finalization: CommittedWriteFinalization,
156
162
  result: ExtractionResult,
157
163
  storage: StorageManager,
158
164
  threadIdForExtraction?: string | null,
@@ -1125,7 +1131,7 @@ export class ExtractionPersistCoordinator {
1125
1131
  0,
1126
1132
  this.deps.config.extractionMaxProfileUpdatesPerRun,
1127
1133
  );
1128
- const finalizeCommittedWrites = async (scheduleQmd: boolean): Promise<void> => {
1134
+ const finalizeCommittedWrites = finalization.bind(async (scheduleQmd: boolean): Promise<void> => {
1129
1135
  await finalizeExtractionCommittedWrites({
1130
1136
  saveContentHashIndexes: () => this.deps.saveContentHashIndexes(),
1131
1137
  behaviorSignalsByStorage: behaviorSignalsByStorage.values(),
@@ -1150,7 +1156,7 @@ export class ExtractionPersistCoordinator {
1150
1156
  requestQmdMaintenance: () => this.deps.requestQmdMaintenance(),
1151
1157
  scheduleQmd,
1152
1158
  });
1153
- };
1159
+ });
1154
1160
 
1155
1161
  if (
1156
1162
  facts.length < result.facts.length ||
@@ -2211,6 +2217,24 @@ export class ExtractionPersistCoordinator {
2211
2217
  const tombstoneBlocked = parentWrite.tombstoneBlocked;
2212
2218
  const postWriteGuard =
2213
2219
  faithfulnessEnforceStatus === "pending_review" || tombstoneBlocked;
2220
+ // #3288: the parent is durable now. Track it before any post-write step
2221
+ // that can throw (non-chunked rule, #3231); a retry exact-dedupes it.
2222
+ trackBehaviorSignals(
2223
+ targetStorage,
2224
+ buildBehaviorSignalsForMemory({
2225
+ memoryId: parentId,
2226
+ category: writeCategory,
2227
+ content: fact.content,
2228
+ namespace: this.deps.storageDirNamespace(targetStorage.dir),
2229
+ confidence: fact.confidence,
2230
+ source: "extraction",
2231
+ }),
2232
+ );
2233
+ trackPersistedId(targetStorage, parentId, {
2234
+ pendingReview: postWriteGuard,
2235
+ category: writeCategory,
2236
+ harmonicFact,
2237
+ });
2214
2238
  // #1645: defer contradiction auto-resolve until tombstone status is
2215
2239
  // known (see applyDeferredContradictionResolve).
2216
2240
  const contradictionOutcome = await this.deps.applyDeferredContradictionResolve(
@@ -2306,11 +2330,6 @@ export class ExtractionPersistCoordinator {
2306
2330
  log.debug(
2307
2331
  `chunked memory ${parentId} into ${chunkResult.chunks.length} chunks`,
2308
2332
  );
2309
- trackPersistedId(targetStorage, parentId, {
2310
- pendingReview: postWriteGuard,
2311
- category: writeCategory,
2312
- harmonicFact,
2313
- });
2314
2333
  await anchorSnapshots.replace(targetStorage, parentId, writeCategory, memoryPathById);
2315
2334
  // #1576 (cursor Medium): keep pending_review ids out of threadEpisodeIdsForGraph — else later active facts build thread-predecessor edges to an unfaithful memory.
2316
2335
  if (
@@ -2490,17 +2509,6 @@ export class ExtractionPersistCoordinator {
2490
2509
  } finally {
2491
2510
  // #1522: the catalog write touch lives in the storage chokepoint.
2492
2511
  }
2493
- trackBehaviorSignals(
2494
- targetStorage,
2495
- buildBehaviorSignalsForMemory({
2496
- memoryId: parentId,
2497
- category: writeCategory,
2498
- content: fact.content,
2499
- namespace: this.deps.storageDirNamespace(targetStorage.dir),
2500
- confidence: fact.confidence,
2501
- source: "extraction",
2502
- }),
2503
- );
2504
2512
  continue; // Skip the normal write below
2505
2513
  }
2506
2514
  }
@@ -2967,7 +2975,7 @@ export class ExtractionPersistCoordinator {
2967
2975
  }
2968
2976
 
2969
2977
  // Shared batch-end finalization (hash, behavior, harmonic, temporal).
2970
- // Failure paths call the same helper with scheduleQmd=true (#3231).
2978
+ // Failure paths call it with scheduleQmd=true (#3231); it runs once per pass (#3288).
2971
2979
  await finalizeCommittedWrites(false);
2972
2980
 
2973
2981
  const dedupSuffix = `${dedupedCount > 0 ? ` (${dedupedCount} deduped)` : ""}${semanticMergedCount > 0 ? ` (${semanticMergedCount} merged)` : ""}`;