@remnic/core 10.0.65 → 10.0.66

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 (104) hide show
  1. package/dist/access-admin-ops-surface.js +2 -2
  2. package/dist/access-authorization-probe.js +3 -3
  3. package/dist/access-boundary.js +2 -2
  4. package/dist/access-cli.js +21 -21
  5. package/dist/access-extraction-force-flush.js +2 -2
  6. package/dist/access-http-briefing.js +3 -3
  7. package/dist/access-http-query.js +2 -2
  8. package/dist/access-http.js +14 -14
  9. package/dist/access-identity-continuity-surface.js +2 -2
  10. package/dist/access-lcm-surface.js +2 -2
  11. package/dist/access-mcp.js +9 -9
  12. package/dist/access-namespace-preflight.js +2 -2
  13. package/dist/access-observe-write-surface.js +2 -2
  14. package/dist/access-operations-batch.js +3 -3
  15. package/dist/access-operations.js +8 -8
  16. package/dist/access-recall-concurrency.js +2 -2
  17. package/dist/access-recall-response.js +2 -2
  18. package/dist/access-recall-surface.js +2 -2
  19. package/dist/access-service.js +2 -2
  20. package/dist/access-surface-catalog.js +4 -4
  21. package/dist/buffer.js +2 -2
  22. package/dist/{chunk-3LSTL6EY.js → chunk-3H25IRJJ.js} +2 -2
  23. package/dist/{chunk-HRRAOTOQ.js → chunk-3NJJQAL3.js} +8 -8
  24. package/dist/{chunk-FKGLJJVS.js → chunk-A5FOXI3B.js} +2 -2
  25. package/dist/{chunk-JPY3RMG2.js → chunk-E6VQJ75T.js} +2 -2
  26. package/dist/{chunk-MRF5V5CD.js → chunk-F32JRMX2.js} +2 -2
  27. package/dist/{chunk-N4T5FW6U.js → chunk-FMYAU5MP.js} +2 -2
  28. package/dist/{chunk-OVL5AD3N.js → chunk-IPCSZLLF.js} +2 -2
  29. package/dist/{chunk-7JYK7NNY.js → chunk-JYXFIILI.js} +2 -2
  30. package/dist/{chunk-HOR4RMUM.js → chunk-KFPJIKUW.js} +5 -5
  31. package/dist/{chunk-KQJUEQ2E.js → chunk-PL75KQGF.js} +3 -3
  32. package/dist/{chunk-KNMSD2MQ.js → chunk-QAN5ZDRS.js} +2 -2
  33. package/dist/{chunk-6D3IWYNY.js → chunk-QNZPYH36.js} +6 -6
  34. package/dist/chunk-QNZPYH36.js.map +1 -0
  35. package/dist/{chunk-GRAUQXQB.js → chunk-RAU25Q6J.js} +2 -2
  36. package/dist/{chunk-HHL4MX3W.js → chunk-SH6ZYUTX.js} +2 -2
  37. package/dist/{chunk-T22FBDG3.js → chunk-T763ADEK.js} +2 -2
  38. package/dist/{chunk-UH6ROXFJ.js → chunk-VC65GNUS.js} +2 -2
  39. package/dist/{chunk-YJQJRXUI.js → chunk-VC7GKR2V.js} +2 -2
  40. package/dist/{chunk-R7WYAXPO.js → chunk-VHSQU3RO.js} +81 -42
  41. package/dist/chunk-VHSQU3RO.js.map +1 -0
  42. package/dist/{chunk-JL5HXL25.js → chunk-VSZNPAGL.js} +2 -2
  43. package/dist/{chunk-2ZSRACNK.js → chunk-XMBDI725.js} +15 -38
  44. package/dist/chunk-XMBDI725.js.map +1 -0
  45. package/dist/{chunk-P2EM5LKH.js → chunk-ZGHPELZ6.js} +2 -2
  46. package/dist/cli.js +16 -16
  47. package/dist/external-wiki-access.js +3 -3
  48. package/dist/extraction.js +3 -3
  49. package/dist/index.js +21 -21
  50. package/dist/memory-browse-operations.js +3 -3
  51. package/dist/orchestration/buffer-drain-runner.js +21 -21
  52. package/dist/orchestration/extraction-persist-deps.d.ts +0 -2
  53. package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
  54. package/dist/orchestration/extraction-persist.d.ts +10 -4
  55. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  56. package/dist/orchestration/extraction-run-helpers.d.ts +3 -2
  57. package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
  58. package/dist/orchestration/extraction-run.d.ts +3 -6
  59. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  60. package/dist/orchestrator.d.ts +2 -17
  61. package/dist/orchestrator.d.ts.map +1 -1
  62. package/dist/orchestrator.js +21 -21
  63. package/dist/recall-navigation-operations.js +3 -3
  64. package/dist/recall-navigation.js +2 -2
  65. package/dist/source-agent-qualifier.js +2 -2
  66. package/dist/support-passport/index.js +5 -5
  67. package/dist/threading.d.ts +21 -4
  68. package/dist/threading.d.ts.map +1 -1
  69. package/dist/threading.js +1 -1
  70. package/package.json +2 -2
  71. package/src/access-observe-replay-durability.test.ts +1 -1
  72. package/src/connector-provenance-lifecycle.test.ts +5 -8
  73. package/src/faithfulness-graph-guard.test.ts +11 -6
  74. package/src/harmonic-construction.test.ts +16 -10
  75. package/src/orchestration/extraction-persist-deps.ts +0 -2
  76. package/src/orchestration/extraction-persist.ts +18 -10
  77. package/src/orchestration/extraction-run-helpers.ts +5 -4
  78. package/src/orchestration/extraction-run.test.ts +1 -2
  79. package/src/orchestration/extraction-run.ts +6 -6
  80. package/src/orchestrator.ts +6 -22
  81. package/src/testing/orchestrator-lite.ts +1 -1
  82. package/src/threading.test.ts +153 -0
  83. package/src/threading.ts +95 -45
  84. package/dist/chunk-2ZSRACNK.js.map +0 -1
  85. package/dist/chunk-6D3IWYNY.js.map +0 -1
  86. package/dist/chunk-R7WYAXPO.js.map +0 -1
  87. /package/dist/{chunk-3LSTL6EY.js.map → chunk-3H25IRJJ.js.map} +0 -0
  88. /package/dist/{chunk-HRRAOTOQ.js.map → chunk-3NJJQAL3.js.map} +0 -0
  89. /package/dist/{chunk-FKGLJJVS.js.map → chunk-A5FOXI3B.js.map} +0 -0
  90. /package/dist/{chunk-JPY3RMG2.js.map → chunk-E6VQJ75T.js.map} +0 -0
  91. /package/dist/{chunk-MRF5V5CD.js.map → chunk-F32JRMX2.js.map} +0 -0
  92. /package/dist/{chunk-N4T5FW6U.js.map → chunk-FMYAU5MP.js.map} +0 -0
  93. /package/dist/{chunk-OVL5AD3N.js.map → chunk-IPCSZLLF.js.map} +0 -0
  94. /package/dist/{chunk-7JYK7NNY.js.map → chunk-JYXFIILI.js.map} +0 -0
  95. /package/dist/{chunk-HOR4RMUM.js.map → chunk-KFPJIKUW.js.map} +0 -0
  96. /package/dist/{chunk-KQJUEQ2E.js.map → chunk-PL75KQGF.js.map} +0 -0
  97. /package/dist/{chunk-KNMSD2MQ.js.map → chunk-QAN5ZDRS.js.map} +0 -0
  98. /package/dist/{chunk-GRAUQXQB.js.map → chunk-RAU25Q6J.js.map} +0 -0
  99. /package/dist/{chunk-HHL4MX3W.js.map → chunk-SH6ZYUTX.js.map} +0 -0
  100. /package/dist/{chunk-T22FBDG3.js.map → chunk-T763ADEK.js.map} +0 -0
  101. /package/dist/{chunk-UH6ROXFJ.js.map → chunk-VC65GNUS.js.map} +0 -0
  102. /package/dist/{chunk-YJQJRXUI.js.map → chunk-VC7GKR2V.js.map} +0 -0
  103. /package/dist/{chunk-JL5HXL25.js.map → chunk-VSZNPAGL.js.map} +0 -0
  104. /package/dist/{chunk-P2EM5LKH.js.map → chunk-ZGHPELZ6.js.map} +0 -0
@@ -119,16 +119,48 @@ function parseConversationThread(raw) {
119
119
  return null;
120
120
  }
121
121
  }
122
- var ThreadingManager = class {
122
+ var ThreadingManager = class _ThreadingManager {
123
123
  constructor(threadsDir, gapMinutes = 30) {
124
124
  this.threadsDir = threadsDir;
125
125
  this.gapMinutes = gapMinutes;
126
126
  }
127
127
  threadsDir;
128
128
  gapMinutes;
129
- currentThreadId = null;
130
- lastTurnTimestamp = null;
131
- lastSessionKey = null;
129
+ /**
130
+ * Per-session thread-run state. One global slot let two concurrent
131
+ * extractions (bounded-parallel queue) interleave sessions and attach
132
+ * turns to the wrong thread, so the run state is keyed by session.
133
+ * Bounded: a long-lived daemon sees one entry per active session.
134
+ */
135
+ runStateBySession = /* @__PURE__ */ new Map();
136
+ /**
137
+ * Run states with a processTurn awaiting inside them. A session parked on
138
+ * `saveThread` can watch 512 other sessions insert and evict; dropping its
139
+ * state mid-await loses its thread id and forks its thread, so eviction
140
+ * never takes an in-flight state (the cap may be exceeded briefly by the
141
+ * number of in-flight extractions, which is bounded by the queue).
142
+ */
143
+ inFlightRunStates = /* @__PURE__ */ new Set();
144
+ static RUN_STATE_MAX_SESSIONS = 512;
145
+ runStateFor(turn) {
146
+ const key = turn.sessionKey ?? "";
147
+ let state = this.runStateBySession.get(key);
148
+ if (state) {
149
+ this.runStateBySession.delete(key);
150
+ this.runStateBySession.set(key, state);
151
+ } else {
152
+ state = { currentThreadId: null, lastTurnTimestamp: null };
153
+ this.runStateBySession.set(key, state);
154
+ if (this.runStateBySession.size > _ThreadingManager.RUN_STATE_MAX_SESSIONS) {
155
+ for (const [oldestKey, oldest] of this.runStateBySession) {
156
+ if (oldest === state || this.inFlightRunStates.has(oldest)) continue;
157
+ this.runStateBySession.delete(oldestKey);
158
+ if (this.runStateBySession.size <= _ThreadingManager.RUN_STATE_MAX_SESSIONS) break;
159
+ }
160
+ }
161
+ }
162
+ return state;
163
+ }
132
164
  async ensureDirectory() {
133
165
  await mkdir(this.threadsDir, { recursive: true });
134
166
  }
@@ -182,20 +214,20 @@ var ThreadingManager = class {
182
214
  }
183
215
  /**
184
216
  * Check if we should start a new thread based on session key and time gap.
217
+ * State is per session, so the cross-session check the shared slot needed
218
+ * is gone: different sessions never share run state at all.
185
219
  */
186
220
  shouldStartNewThread(turn) {
187
221
  const turnTime = new Date(turn.timestamp).getTime();
188
- if (turn.sessionKey && this.lastSessionKey && turn.sessionKey !== this.lastSessionKey) {
189
- return true;
190
- }
191
- if (this.lastTurnTimestamp) {
192
- const gapMs = turnTime - this.lastTurnTimestamp;
222
+ const state = this.runStateFor(turn);
223
+ if (state.lastTurnTimestamp) {
224
+ const gapMs = turnTime - state.lastTurnTimestamp;
193
225
  const gapMinutes = gapMs / (1e3 * 60);
194
226
  if (gapMinutes > this.gapMinutes) {
195
227
  return true;
196
228
  }
197
229
  }
198
- if (!this.currentThreadId) {
230
+ if (!state.currentThreadId) {
199
231
  return true;
200
232
  }
201
233
  return false;
@@ -207,36 +239,41 @@ var ThreadingManager = class {
207
239
  async processTurn(turn, episodeIds) {
208
240
  await this.ensureDirectory();
209
241
  const turnTime = new Date(turn.timestamp).getTime();
210
- if (this.shouldStartNewThread(turn)) {
211
- const threadId = `thread-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
212
- const thread = {
213
- id: threadId,
214
- title: "New Thread",
215
- // Will be updated later with keywords
216
- createdAt: turn.timestamp,
217
- updatedAt: turn.timestamp,
218
- sessionKey: turn.sessionKey,
219
- episodeIds: [...episodeIds],
220
- linkedThreadIds: []
221
- };
222
- await this.saveThread(thread);
223
- this.currentThreadId = threadId;
224
- log.debug(`created new thread ${threadId}`);
225
- } else if (this.currentThreadId) {
226
- const thread = await this.loadThread(this.currentThreadId);
227
- if (thread) {
228
- for (const id of episodeIds) {
229
- if (!thread.episodeIds.includes(id)) {
230
- thread.episodeIds.push(id);
242
+ const state = this.runStateFor(turn);
243
+ this.inFlightRunStates.add(state);
244
+ try {
245
+ if (this.shouldStartNewThread(turn)) {
246
+ const threadId = `thread-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
247
+ const thread = {
248
+ id: threadId,
249
+ title: "New Thread",
250
+ // Will be updated later with keywords
251
+ createdAt: turn.timestamp,
252
+ updatedAt: turn.timestamp,
253
+ sessionKey: turn.sessionKey,
254
+ episodeIds: [...episodeIds],
255
+ linkedThreadIds: []
256
+ };
257
+ await this.saveThread(thread);
258
+ state.currentThreadId = threadId;
259
+ log.debug(`created new thread ${threadId}`);
260
+ } else if (state.currentThreadId) {
261
+ const thread = await this.loadThread(state.currentThreadId);
262
+ if (thread) {
263
+ for (const id of episodeIds) {
264
+ if (!thread.episodeIds.includes(id)) {
265
+ thread.episodeIds.push(id);
266
+ }
231
267
  }
268
+ thread.updatedAt = turn.timestamp;
269
+ await this.saveThread(thread);
232
270
  }
233
- thread.updatedAt = turn.timestamp;
234
- await this.saveThread(thread);
235
271
  }
272
+ state.lastTurnTimestamp = turnTime;
273
+ return state.currentThreadId;
274
+ } finally {
275
+ this.inFlightRunStates.delete(state);
236
276
  }
237
- this.lastTurnTimestamp = turnTime;
238
- this.lastSessionKey = turn.sessionKey ?? null;
239
- return this.currentThreadId;
240
277
  }
241
278
  /**
242
279
  * Append persisted memory IDs to an existing thread.
@@ -351,22 +388,24 @@ var ThreadingManager = class {
351
388
  return true;
352
389
  }
353
390
  /**
354
- * Get the current thread ID (if any).
391
+ * Get the current thread ID (if any). With per-session run state there is
392
+ * no single current thread; null unless exactly one session is tracked.
355
393
  */
356
394
  getCurrentThreadId() {
357
- return this.currentThreadId;
395
+ if (this.runStateBySession.size === 1) {
396
+ return this.runStateBySession.values().next().value?.currentThreadId ?? null;
397
+ }
398
+ return null;
358
399
  }
359
400
  /**
360
401
  * Reset threading state (e.g., on plugin restart).
361
402
  */
362
403
  reset() {
363
- this.currentThreadId = null;
364
- this.lastTurnTimestamp = null;
365
- this.lastSessionKey = null;
404
+ this.runStateBySession.clear();
366
405
  }
367
406
  };
368
407
 
369
408
  export {
370
409
  ThreadingManager
371
410
  };
372
- //# sourceMappingURL=chunk-R7WYAXPO.js.map
411
+ //# sourceMappingURL=chunk-VHSQU3RO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/threading.ts"],"sourcesContent":["/**\n * Conversation Threading (Phase 3B)\n *\n * Groups related memories into conversation threads with auto-generated titles.\n * Thread boundary detection: new session key OR time gap > threshold.\n */\n\nimport { readdir, readFile, writeFile, mkdir, stat } from \"node:fs/promises\";\nimport { log } from \"./logger.js\";\nimport type { BufferTurn, ConversationThread } from \"./types.js\";\nimport {\n encodeStoragePathSegment,\n encodeStoragePathSegmentWithHash,\n isSafeLegacyPathSegment,\n resolveSafeStoragePath,\n} from \"./storage-paths.js\";\n\n/** Stop words for title extraction */\nconst STOP_WORDS = new Set([\n \"a\", \"an\", \"the\", \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\", \"would\", \"could\",\n \"should\", \"may\", \"might\", \"must\", \"shall\", \"can\", \"need\", \"to\", \"of\",\n \"in\", \"for\", \"on\", \"with\", \"at\", \"by\", \"from\", \"as\", \"into\", \"through\",\n \"during\", \"before\", \"after\", \"above\", \"below\", \"between\", \"this\", \"that\",\n \"these\", \"those\", \"i\", \"me\", \"my\", \"we\", \"our\", \"you\", \"your\", \"he\",\n \"she\", \"it\", \"they\", \"them\", \"their\", \"what\", \"which\", \"who\", \"how\",\n \"when\", \"where\", \"why\", \"and\", \"but\", \"or\", \"if\", \"because\", \"so\",\n \"just\", \"about\", \"like\", \"also\", \"very\", \"really\", \"here\", \"there\",\n]);\n\n/**\n * Extract top keywords from content for thread title.\n * Simple TF approach (no IDF needed for single thread).\n */\nfunction extractKeywords(content: string, maxKeywords: number = 3): string[] {\n const words = content\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length >= 4 && !STOP_WORDS.has(w));\n\n // Count frequencies\n const freq = new Map<string, number>();\n for (const word of words) {\n freq.set(word, (freq.get(word) ?? 0) + 1);\n }\n\n // Sort by frequency, take top N\n return [...freq.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, maxKeywords)\n .map(([word]) => word);\n}\n\n/**\n * Generate a thread title from keywords.\n */\nfunction generateTitle(keywords: string[]): string {\n if (keywords.length === 0) return \"Untitled Thread\";\n // Capitalize and join\n return keywords\n .map((k) => k.charAt(0).toUpperCase() + k.slice(1))\n .join(\", \");\n}\n\nfunction isConversationThread(value: unknown): value is ConversationThread {\n if (value === null || typeof value !== \"object\") return false;\n const candidate = value as Partial<ConversationThread>;\n return (\n typeof candidate.id === \"string\" &&\n typeof candidate.title === \"string\" &&\n typeof candidate.createdAt === \"string\" &&\n typeof candidate.updatedAt === \"string\" &&\n Array.isArray(candidate.episodeIds) &&\n Array.isArray(candidate.linkedThreadIds)\n );\n}\n\nfunction parseConversationThread(raw: string): ConversationThread | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n return isConversationThread(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/** One session's thread-run state: its current thread and last turn time. */\ninterface ThreadingRunState {\n currentThreadId: string | null;\n lastTurnTimestamp: number | null;\n}\n\nexport class ThreadingManager {\n /**\n * Per-session thread-run state. One global slot let two concurrent\n * extractions (bounded-parallel queue) interleave sessions and attach\n * turns to the wrong thread, so the run state is keyed by session.\n * Bounded: a long-lived daemon sees one entry per active session.\n */\n private readonly runStateBySession = new Map<string, ThreadingRunState>();\n /**\n * Run states with a processTurn awaiting inside them. A session parked on\n * `saveThread` can watch 512 other sessions insert and evict; dropping its\n * state mid-await loses its thread id and forks its thread, so eviction\n * never takes an in-flight state (the cap may be exceeded briefly by the\n * number of in-flight extractions, which is bounded by the queue).\n */\n private readonly inFlightRunStates = new Set<ThreadingRunState>();\n private static readonly RUN_STATE_MAX_SESSIONS = 512;\n\n private runStateFor(turn: BufferTurn): ThreadingRunState {\n const key = turn.sessionKey ?? \"\";\n let state = this.runStateBySession.get(key);\n if (state) {\n // Refresh recency: Map iteration is insertion-ordered, so re-putting a\n // touched session makes eviction true LRU instead of first-in-first-out —\n // an active session must not be the oldest entry just because it started\n // first.\n this.runStateBySession.delete(key);\n this.runStateBySession.set(key, state);\n } else {\n state = { currentThreadId: null, lastTurnTimestamp: null };\n this.runStateBySession.set(key, state);\n if (this.runStateBySession.size > ThreadingManager.RUN_STATE_MAX_SESSIONS) {\n for (const [oldestKey, oldest] of this.runStateBySession) {\n // Never evict the state being inserted: before the caller registers\n // it in-flight, every older entry can be in flight and the loop\n // would otherwise delete this turn's own state and fork the thread.\n if (oldest === state || this.inFlightRunStates.has(oldest)) continue;\n this.runStateBySession.delete(oldestKey);\n if (this.runStateBySession.size <= ThreadingManager.RUN_STATE_MAX_SESSIONS) break;\n }\n }\n }\n return state;\n }\n\n constructor(\n private readonly threadsDir: string,\n private readonly gapMinutes: number = 30,\n ) {}\n\n async ensureDirectory(): Promise<void> {\n await mkdir(this.threadsDir, { recursive: true });\n }\n\n private encodedThreadFileName(threadId: string): string {\n return `${encodeStoragePathSegment(threadId)}.json`;\n }\n\n private collisionSafeThreadFileName(threadId: string): string {\n return `${encodeStoragePathSegmentWithHash(threadId)}.json`;\n }\n\n private legacyThreadFileName(threadId: string): string | undefined {\n if (!isSafeLegacyPathSegment(threadId)) return undefined;\n const legacyFile = `${threadId}.json`;\n return legacyFile === this.encodedThreadFileName(threadId) ? undefined : legacyFile;\n }\n\n private async threadCandidateStatus(\n filePath: string,\n threadId: string,\n ): Promise<\"missing\" | \"matches\" | \"invalid\" | \"mismatches\"> {\n try {\n if (!(await stat(filePath)).isFile()) return \"mismatches\";\n const thread = parseConversationThread(await readFile(filePath, \"utf-8\"));\n if (!thread) return \"invalid\";\n return thread.id === threadId ? \"matches\" : \"mismatches\";\n } catch (err) {\n const code =\n err && typeof err === \"object\" && \"code\" in err\n ? (err as { code?: string }).code\n : undefined;\n if (code === \"ENOENT\") return \"missing\";\n throw err;\n }\n }\n\n private async threadFilePathForWrite(threadId: string): Promise<string> {\n const encodedPath = await resolveSafeStoragePath(\n this.threadsDir,\n this.encodedThreadFileName(threadId),\n );\n const encodedStatus = await this.threadCandidateStatus(encodedPath, threadId);\n if (encodedStatus === \"matches\") return encodedPath;\n\n const legacyFile = this.legacyThreadFileName(threadId);\n if (legacyFile) {\n const legacyPath = await resolveSafeStoragePath(this.threadsDir, legacyFile);\n if ((await this.threadCandidateStatus(legacyPath, threadId)) === \"matches\") {\n return legacyPath;\n }\n }\n\n if (encodedStatus === \"missing\") return encodedPath;\n\n const collisionSafePath = await resolveSafeStoragePath(\n this.threadsDir,\n this.collisionSafeThreadFileName(threadId),\n );\n const collisionSafeStatus = await this.threadCandidateStatus(collisionSafePath, threadId);\n if (collisionSafeStatus === \"mismatches\") {\n throw new Error(`thread storage path collision for thread id: ${threadId}`);\n }\n\n return collisionSafePath;\n }\n\n /**\n * Check if we should start a new thread based on session key and time gap.\n * State is per session, so the cross-session check the shared slot needed\n * is gone: different sessions never share run state at all.\n */\n shouldStartNewThread(turn: BufferTurn): boolean {\n const turnTime = new Date(turn.timestamp).getTime();\n const state = this.runStateFor(turn);\n\n // Time gap > threshold = new thread\n if (state.lastTurnTimestamp) {\n const gapMs = turnTime - state.lastTurnTimestamp;\n const gapMinutes = gapMs / (1000 * 60);\n if (gapMinutes > this.gapMinutes) {\n return true;\n }\n }\n\n // No current thread = new thread\n if (!state.currentThreadId) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Process a turn and return the thread ID it belongs to.\n * Creates a new thread if needed.\n */\n async processTurn(turn: BufferTurn, episodeIds: string[]): Promise<string> {\n await this.ensureDirectory();\n\n const turnTime = new Date(turn.timestamp).getTime();\n const state = this.runStateFor(turn);\n this.inFlightRunStates.add(state);\n try {\n if (this.shouldStartNewThread(turn)) {\n // Create new thread\n const threadId = `thread-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;\n const thread: ConversationThread = {\n id: threadId,\n title: \"New Thread\", // Will be updated later with keywords\n createdAt: turn.timestamp,\n updatedAt: turn.timestamp,\n sessionKey: turn.sessionKey,\n episodeIds: [...episodeIds],\n linkedThreadIds: [],\n };\n\n await this.saveThread(thread);\n state.currentThreadId = threadId;\n log.debug(`created new thread ${threadId}`);\n } else if (state.currentThreadId) {\n // Add episodes to current thread\n const thread = await this.loadThread(state.currentThreadId);\n if (thread) {\n for (const id of episodeIds) {\n if (!thread.episodeIds.includes(id)) {\n thread.episodeIds.push(id);\n }\n }\n thread.updatedAt = turn.timestamp;\n await this.saveThread(thread);\n }\n }\n\n state.lastTurnTimestamp = turnTime;\n\n return state.currentThreadId!;\n } finally {\n this.inFlightRunStates.delete(state);\n }\n }\n\n /**\n * Append persisted memory IDs to an existing thread.\n * Keeps IDs unique and updates the thread timestamp.\n */\n async appendEpisodeIds(threadId: string, episodeIds: string[]): Promise<void> {\n if (episodeIds.length === 0) return;\n const thread = await this.loadThread(threadId);\n if (!thread) return;\n\n let changed = false;\n for (const id of episodeIds) {\n if (!thread.episodeIds.includes(id)) {\n thread.episodeIds.push(id);\n changed = true;\n }\n }\n if (!changed) return;\n\n thread.updatedAt = new Date().toISOString();\n await this.saveThread(thread);\n }\n\n /**\n * Update thread title based on accumulated content.\n */\n async updateThreadTitle(threadId: string, content: string): Promise<void> {\n const thread = await this.loadThread(threadId);\n if (!thread) return;\n\n const keywords = extractKeywords(content);\n const title = generateTitle(keywords);\n\n if (title !== thread.title) {\n thread.title = title;\n thread.updatedAt = new Date().toISOString();\n await this.saveThread(thread);\n log.debug(`updated thread ${threadId} title: ${title}`);\n }\n }\n\n /**\n * Get all threads, sorted by updatedAt desc.\n */\n async getAllThreads(): Promise<ConversationThread[]> {\n try {\n const files = await readdir(this.threadsDir);\n const threadsById = new Map<\n string,\n { thread: ConversationThread; storagePriority: number }\n >();\n\n for (const file of files) {\n if (!file.endsWith(\".json\")) continue;\n try {\n const raw = await readFile(\n await resolveSafeStoragePath(this.threadsDir, file),\n \"utf-8\",\n );\n const thread = parseConversationThread(raw);\n if (!thread) continue;\n\n const legacyFile = this.legacyThreadFileName(thread.id);\n const storagePriority =\n file === this.encodedThreadFileName(thread.id)\n ? 3\n : file === this.collisionSafeThreadFileName(thread.id)\n ? 2\n : legacyFile && file === legacyFile\n ? 1\n : 0;\n const current = threadsById.get(thread.id);\n if (!current || storagePriority > current.storagePriority) {\n threadsById.set(thread.id, { thread, storagePriority });\n }\n } catch {\n // skip unreadable or malformed thread files\n }\n }\n\n return [...threadsById.values()].map(({ thread }) => thread).sort(\n (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n } catch {\n return [];\n }\n }\n\n /**\n * Get a thread by ID.\n */\n async loadThread(threadId: string): Promise<ConversationThread | null> {\n const candidates = [\n this.encodedThreadFileName(threadId),\n this.legacyThreadFileName(threadId),\n this.collisionSafeThreadFileName(threadId),\n ];\n for (const file of candidates) {\n if (!file) continue;\n try {\n const filePath = await resolveSafeStoragePath(this.threadsDir, file);\n const raw = await readFile(filePath, \"utf-8\");\n const thread = parseConversationThread(raw);\n if (thread?.id === threadId) return thread;\n } catch {\n // try the next candidate\n }\n }\n return null;\n }\n\n /**\n * Save a thread.\n */\n async saveThread(thread: ConversationThread): Promise<void> {\n await this.ensureDirectory();\n const filePath = await this.threadFilePathForWrite(thread.id);\n await writeFile(filePath, JSON.stringify(thread, null, 2), \"utf-8\");\n }\n\n /**\n * Link two threads together.\n */\n async linkThreads(threadId1: string, threadId2: string): Promise<boolean> {\n const thread1 = await this.loadThread(threadId1);\n const thread2 = await this.loadThread(threadId2);\n\n if (!thread1 || !thread2) return false;\n\n // Add bidirectional links\n if (!thread1.linkedThreadIds.includes(threadId2)) {\n thread1.linkedThreadIds.push(threadId2);\n await this.saveThread(thread1);\n }\n\n if (!thread2.linkedThreadIds.includes(threadId1)) {\n thread2.linkedThreadIds.push(threadId1);\n await this.saveThread(thread2);\n }\n\n return true;\n }\n\n /**\n * Get the current thread ID (if any). With per-session run state there is\n * no single current thread; null unless exactly one session is tracked.\n */\n getCurrentThreadId(): string | null {\n if (this.runStateBySession.size === 1) {\n return this.runStateBySession.values().next().value?.currentThreadId ?? null;\n }\n return null;\n }\n\n /**\n * Reset threading state (e.g., on plugin restart).\n */\n reset(): void {\n this.runStateBySession.clear();\n }\n}\n"],"mappings":"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","names":[]}
@@ -21,7 +21,7 @@ import {
21
21
  defineOperation,
22
22
  parseNamespacePreflightWriteOp,
23
23
  resolveAuthorizedNamespaceWritablePreflight
24
- } from "./chunk-GRAUQXQB.js";
24
+ } from "./chunk-RAU25Q6J.js";
25
25
  import {
26
26
  parseSharedWriteOutputControls
27
27
  } from "./chunk-IFBSZGW3.js";
@@ -618,4 +618,4 @@ defineOperation({ name: "graph_events", description: "SSE graph events.", schema
618
618
  defineOperation({ name: "chat_message", description: "Chat message.", schema: strictSchema({ message: S.str, chatSessionId: S.str }), handler: async (_i, _ctx) => ({ result: { ok: true } }) });
619
619
  defineOperation({ name: "chat_events", description: "SSE chat events.", schema: strictSchema({}), handler: async (_i, _ctx) => ({ result: { stream: "sse" } }) });
620
620
  defineOperation({ name: "adapters_status", description: "Adapter registry metadata (issue #1850 round 5). Marker op: the HTTP transport builds the response from its own adapterRegistry, but the op still must be registered so it appears in listRegisteredOperations and enforceTokenOp can gate it for scoped tokens.", schema: strictSchema({}), handler: async (_i, _ctx) => ({ result: { ok: true } }) });
621
- //# sourceMappingURL=chunk-JL5HXL25.js.map
621
+ //# sourceMappingURL=chunk-VSZNPAGL.js.map
@@ -175,7 +175,7 @@ import {
175
175
  } from "./chunk-VA3GNAWQ.js";
176
176
  import {
177
177
  ExtractionEngine
178
- } from "./chunk-FKGLJJVS.js";
178
+ } from "./chunk-A5FOXI3B.js";
179
179
  import {
180
180
  parseMemoryActionEligibilityContext
181
181
  } from "./chunk-JHU4EYDV.js";
@@ -234,7 +234,7 @@ import {
234
234
  runTimelinePublishCli,
235
235
  screenCandidateFact,
236
236
  summarizeVaultPublish
237
- } from "./chunk-HOR4RMUM.js";
237
+ } from "./chunk-KFPJIKUW.js";
238
238
  import {
239
239
  normalizeReplaySessionKey
240
240
  } from "./chunk-AJVLDJU6.js";
@@ -247,7 +247,7 @@ import {
247
247
  } from "./chunk-SHHZWSSE.js";
248
248
  import {
249
249
  ThreadingManager
250
- } from "./chunk-R7WYAXPO.js";
250
+ } from "./chunk-VHSQU3RO.js";
251
251
  import {
252
252
  compareVerifiedSemanticRuleResults,
253
253
  searchVerifiedSemanticRules
@@ -299,7 +299,7 @@ import {
299
299
  } from "./chunk-ECF627KC.js";
300
300
  import {
301
301
  SmartBuffer
302
- } from "./chunk-3LSTL6EY.js";
302
+ } from "./chunk-3H25IRJJ.js";
303
303
  import {
304
304
  semanticChunkContent
305
305
  } from "./chunk-2GBFKHBZ.js";
@@ -373,7 +373,7 @@ import {
373
373
  } from "./chunk-QW27DOHG.js";
374
374
  import {
375
375
  MeetingsInputError
376
- } from "./chunk-HRRAOTOQ.js";
376
+ } from "./chunk-3NJJQAL3.js";
377
377
  import {
378
378
  WearablesInputError,
379
379
  describeErrorForOperator
@@ -400,7 +400,7 @@ import {
400
400
  detectPassiveCorrections,
401
401
  renderSessionExperiencePreview,
402
402
  scoreSessionExperienceForPrompt
403
- } from "./chunk-GRAUQXQB.js";
403
+ } from "./chunk-RAU25Q6J.js";
404
404
  import {
405
405
  importCapsule
406
406
  } from "./chunk-QDZWQBLU.js";
@@ -480,7 +480,7 @@ import {
480
480
  ExtractionDeadlineError,
481
481
  ExtractionRunCoordinator,
482
482
  classifyExtractionOrigin
483
- } from "./chunk-6D3IWYNY.js";
483
+ } from "./chunk-QNZPYH36.js";
484
484
  import {
485
485
  ARTIFACT_STATUS_CACHE_TTL_MS,
486
486
  BulkImportBatchPartialFailureError,
@@ -6454,6 +6454,7 @@ var ExtractionPersistCoordinator = class {
6454
6454
  });
6455
6455
  const supersessionOrderingAt = (validAt) => validAt && validAt.length > 0 ? validAt : (/* @__PURE__ */ new Date()).toISOString();
6456
6456
  const pendingReviewPersistedIds = [];
6457
+ let deferredCount = 0;
6457
6458
  const persistedIdsByStorage = /* @__PURE__ */ new Map();
6458
6459
  const trackPersistedId = (targetStorage, id, options = {}) => {
6459
6460
  if (options.includeReturnedIds !== false) {
@@ -6960,7 +6961,7 @@ var ExtractionPersistCoordinator = class {
6960
6961
  "persistExtraction: result or result.facts is invalid, skipping",
6961
6962
  { resultType: typeof result, factsType: typeof result?.facts }
6962
6963
  );
6963
- return { persistedIds, memoryPathById };
6964
+ return { persistedIds, memoryPathById, deferredCount, pendingReviewIds: pendingReviewPersistedIds };
6964
6965
  }
6965
6966
  const chunkingConfig = {
6966
6967
  targetTokens: this.deps.config.chunkingTargetTokens,
@@ -7067,7 +7068,6 @@ var ExtractionPersistCoordinator = class {
7067
7068
  }
7068
7069
  let judgeVerdictsByFactIndex = null;
7069
7070
  let judgeGatedCount = 0;
7070
- this.deps.setLastPersistExtractionDeferredCount(0);
7071
7071
  if (lifecycleCaps.extractionJudge) {
7072
7072
  let preJudgeRedactionRules = [];
7073
7073
  try {
@@ -7195,7 +7195,7 @@ var ExtractionPersistCoordinator = class {
7195
7195
  log.info(
7196
7196
  `extraction-judge: ${judgeResult.verdicts.size}/${judgeCandidates.length} facts evaluated, ${judgeResult.cached} cached, ${judgeResult.judged} judged, ${judgeResult.deferred} deferred` + (judgeResult.deferredCappedToReject > 0 ? ` (${judgeResult.deferredCappedToReject} cap-rejected)` : "") + `, ${judgeResult.elapsed}ms`
7197
7197
  );
7198
- this.deps.setLastPersistExtractionDeferredCount(judgeResult.deferred);
7198
+ deferredCount = judgeResult.deferred;
7199
7199
  } catch (err) {
7200
7200
  log.warn(
7201
7201
  `extraction-judge: pipeline error, proceeding without filtering (fail-open): ${err instanceof Error ? err.message : String(err)}`
@@ -8226,8 +8226,7 @@ ${q.context ?? ""}`], entityScreenOn, entityScreenProfile);
8226
8226
  log.info(
8227
8227
  `persisted: ${facts.length - dedupedCount - importanceGatedCount - judgeGatedCount - redactionGatedCount - semanticMergedCount} facts${dedupSuffix}${gatedSuffix}${judgeSuffix}${redactionSuffix}, ${entities.length} entities, ${questions.length} questions, ${profileUpdates.length} profile updates`
8228
8228
  );
8229
- this.deps.setLastPersistExtractionPendingReviewIds(pendingReviewPersistedIds);
8230
- return { persistedIds, memoryPathById };
8229
+ return { persistedIds, memoryPathById, deferredCount, pendingReviewIds: pendingReviewPersistedIds };
8231
8230
  }
8232
8231
  };
8233
8232
 
@@ -21874,19 +21873,6 @@ var Orchestrator = class {
21874
21873
  * (rule 11) — these hang off the orchestrator instance.
21875
21874
  */
21876
21875
  faithfulnessCounters = createFaithfulnessCounters();
21877
- /**
21878
- * Side-channel: number of facts deferred in the most recent
21879
- * `persistExtraction` call (issue #562, PR 2). The caller reads this after
21880
- * `persistExtraction` returns to decide whether to retain buffer turns for
21881
- * the next extraction pass. Not part of the return signature because many
21882
- * callers already destructure `persistedIds` by position.
21883
- */
21884
- lastPersistExtractionDeferredCount = 0;
21885
- /**
21886
- * Side-channel (#1635): pending_review persisted ids from the last
21887
- * persistExtraction; runExtraction excludes them from the thread episode set.
21888
- */
21889
- lastPersistExtractionPendingReviewIds = [];
21890
21876
  _fastGatewayLlm;
21891
21877
  get fastGatewayLlm() {
21892
21878
  return this._fastGatewayLlm;
@@ -22014,11 +22000,10 @@ var Orchestrator = class {
22014
22000
  getCodingContextForSession: (sessionKey) => this.getCodingContextForSession(sessionKey),
22015
22001
  applyCodingNamespaceOverlay: (sessionKey, namespace) => this.applyCodingNamespaceOverlay(sessionKey, namespace),
22016
22002
  boxBuilderFor: (storage) => this.boxBuilderFor(storage),
22017
- appendPersistedThreadEpisodes: (threadId, ids) => this.appendPersistedThreadEpisodes(threadId, ids),
22003
+ appendPersistedThreadEpisodes: (threadId, ids, pendingReviewIds) => this.appendPersistedThreadEpisodes(threadId, ids, pendingReviewIds),
22018
22004
  maybeScheduleConsolidation: (nonZero) => this.maybeScheduleConsolidation(nonZero),
22019
22005
  requestQmdMaintenance: () => this.requestQmdMaintenance(),
22020
22006
  runTierMigrationCycle: (storage, trigger, options) => this.runTierMigrationCycle(storage, trigger, options),
22021
- getLastPersistExtractionDeferredCount: () => this.lastPersistExtractionDeferredCount,
22022
22007
  recordProcessedExtractionFingerprint: (storage, fingerprint, preloadedMeta) => this.recordProcessedExtractionFingerprint(storage, fingerprint, preloadedMeta)
22023
22008
  });
22024
22009
  }
@@ -22067,12 +22052,6 @@ var Orchestrator = class {
22067
22052
  getJudgeDeferCounts: () => this.judgeDeferCounts,
22068
22053
  getFaithfulnessCounters: () => this.faithfulnessCounters,
22069
22054
  getEmbeddingFallback: () => this.embeddingFallback,
22070
- setLastPersistExtractionDeferredCount: (v) => {
22071
- this.lastPersistExtractionDeferredCount = v;
22072
- },
22073
- setLastPersistExtractionPendingReviewIds: (ids) => {
22074
- this.lastPersistExtractionPendingReviewIds = ids;
22075
- },
22076
22055
  addContentHashDedup: (targetStorage, content) => this.addContentHashDedup(targetStorage, content),
22077
22056
  hasContentHashDedup: (targetStorage, content) => this.hasContentHashDedup(targetStorage, content),
22078
22057
  backfillTemporalBoundsOnDedupHit: (targetStorage, dedupContent, bounds, entityRef, sourceConnector) => this.backfillTemporalBoundsOnDedupHit(targetStorage, dedupContent, bounds, entityRef, sourceConnector),
@@ -23712,7 +23691,7 @@ ${doc.content}` : doc.content,
23712
23691
  this.maintenanceScheduler.requestQmdMaintenanceForTool(reason);
23713
23692
  }
23714
23693
  async persistExtraction(result, storage, threadIdForExtraction, sourceContext, baseNamespace, scopeProfileWritePlan, sourceText, graphCaps = resolveGraphConstructionCapabilities(this.config), lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config)) {
23715
- const { persistedIds, memoryPathById } = await this.extractionPersistCoordinator.persistExtraction(
23694
+ return await this.extractionPersistCoordinator.persistExtraction(
23716
23695
  result,
23717
23696
  storage,
23718
23697
  threadIdForExtraction,
@@ -23723,15 +23702,13 @@ ${doc.content}` : doc.content,
23723
23702
  graphCaps,
23724
23703
  lifecycleCaps
23725
23704
  );
23726
- return { persistedIds, memoryPathById };
23727
23705
  }
23728
23706
  /**
23729
23707
  * Append persisted ids to the thread episode set, excluding pending_review
23730
23708
  * fact ids (#1635) so they don't re-seed predecessor edges. Fail-open like
23731
23709
  * the raw appendEpisodeIds call it replaces.
23732
23710
  */
23733
- async appendPersistedThreadEpisodes(threadId, persistedIds) {
23734
- const pendingReviewIds = this.lastPersistExtractionPendingReviewIds ?? [];
23711
+ async appendPersistedThreadEpisodes(threadId, persistedIds, pendingReviewIds) {
23735
23712
  const episodeIds = pendingReviewIds.length > 0 ? persistedIds.filter((id) => !pendingReviewIds.includes(id)) : persistedIds;
23736
23713
  if (episodeIds.length === 0) return;
23737
23714
  try {
@@ -31644,4 +31621,4 @@ export {
31644
31621
  qmdStartupCollectionChecksWithTimeout,
31645
31622
  Orchestrator
31646
31623
  };
31647
- //# sourceMappingURL=chunk-2ZSRACNK.js.map
31624
+ //# sourceMappingURL=chunk-XMBDI725.js.map