@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.
- package/dist/access-admin-ops-surface.js +2 -2
- package/dist/access-authorization-probe.js +3 -3
- package/dist/access-boundary.js +2 -2
- package/dist/access-cli.js +21 -21
- package/dist/access-extraction-force-flush.js +2 -2
- package/dist/access-http-briefing.js +3 -3
- package/dist/access-http-query.js +2 -2
- package/dist/access-http.js +14 -14
- package/dist/access-identity-continuity-surface.js +2 -2
- package/dist/access-lcm-surface.js +2 -2
- package/dist/access-mcp.js +9 -9
- package/dist/access-namespace-preflight.js +2 -2
- package/dist/access-observe-write-surface.js +2 -2
- package/dist/access-operations-batch.js +3 -3
- package/dist/access-operations.js +8 -8
- package/dist/access-recall-concurrency.js +2 -2
- package/dist/access-recall-response.js +2 -2
- package/dist/access-recall-surface.js +2 -2
- package/dist/access-service.js +2 -2
- package/dist/access-surface-catalog.js +4 -4
- package/dist/buffer.js +2 -2
- package/dist/{chunk-3LSTL6EY.js → chunk-3H25IRJJ.js} +2 -2
- package/dist/{chunk-HRRAOTOQ.js → chunk-3NJJQAL3.js} +8 -8
- package/dist/{chunk-FKGLJJVS.js → chunk-A5FOXI3B.js} +2 -2
- package/dist/{chunk-JPY3RMG2.js → chunk-E6VQJ75T.js} +2 -2
- package/dist/{chunk-MRF5V5CD.js → chunk-F32JRMX2.js} +2 -2
- package/dist/{chunk-N4T5FW6U.js → chunk-FMYAU5MP.js} +2 -2
- package/dist/{chunk-OVL5AD3N.js → chunk-IPCSZLLF.js} +2 -2
- package/dist/{chunk-7JYK7NNY.js → chunk-JYXFIILI.js} +2 -2
- package/dist/{chunk-HOR4RMUM.js → chunk-KFPJIKUW.js} +5 -5
- package/dist/{chunk-KQJUEQ2E.js → chunk-PL75KQGF.js} +3 -3
- package/dist/{chunk-KNMSD2MQ.js → chunk-QAN5ZDRS.js} +2 -2
- package/dist/{chunk-6D3IWYNY.js → chunk-QNZPYH36.js} +6 -6
- package/dist/chunk-QNZPYH36.js.map +1 -0
- package/dist/{chunk-GRAUQXQB.js → chunk-RAU25Q6J.js} +2 -2
- package/dist/{chunk-HHL4MX3W.js → chunk-SH6ZYUTX.js} +2 -2
- package/dist/{chunk-T22FBDG3.js → chunk-T763ADEK.js} +2 -2
- package/dist/{chunk-UH6ROXFJ.js → chunk-VC65GNUS.js} +2 -2
- package/dist/{chunk-YJQJRXUI.js → chunk-VC7GKR2V.js} +2 -2
- package/dist/{chunk-R7WYAXPO.js → chunk-VHSQU3RO.js} +81 -42
- package/dist/chunk-VHSQU3RO.js.map +1 -0
- package/dist/{chunk-JL5HXL25.js → chunk-VSZNPAGL.js} +2 -2
- package/dist/{chunk-2ZSRACNK.js → chunk-XMBDI725.js} +15 -38
- package/dist/chunk-XMBDI725.js.map +1 -0
- package/dist/{chunk-P2EM5LKH.js → chunk-ZGHPELZ6.js} +2 -2
- package/dist/cli.js +16 -16
- package/dist/external-wiki-access.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/index.js +21 -21
- package/dist/memory-browse-operations.js +3 -3
- package/dist/orchestration/buffer-drain-runner.js +21 -21
- package/dist/orchestration/extraction-persist-deps.d.ts +0 -2
- package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts +10 -4
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/extraction-run-helpers.d.ts +3 -2
- package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +3 -6
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +2 -17
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +21 -21
- package/dist/recall-navigation-operations.js +3 -3
- package/dist/recall-navigation.js +2 -2
- package/dist/source-agent-qualifier.js +2 -2
- package/dist/support-passport/index.js +5 -5
- package/dist/threading.d.ts +21 -4
- package/dist/threading.d.ts.map +1 -1
- package/dist/threading.js +1 -1
- package/package.json +2 -2
- package/src/access-observe-replay-durability.test.ts +1 -1
- package/src/connector-provenance-lifecycle.test.ts +5 -8
- package/src/faithfulness-graph-guard.test.ts +11 -6
- package/src/harmonic-construction.test.ts +16 -10
- package/src/orchestration/extraction-persist-deps.ts +0 -2
- package/src/orchestration/extraction-persist.ts +18 -10
- package/src/orchestration/extraction-run-helpers.ts +5 -4
- package/src/orchestration/extraction-run.test.ts +1 -2
- package/src/orchestration/extraction-run.ts +6 -6
- package/src/orchestrator.ts +6 -22
- package/src/testing/orchestrator-lite.ts +1 -1
- package/src/threading.test.ts +153 -0
- package/src/threading.ts +95 -45
- package/dist/chunk-2ZSRACNK.js.map +0 -1
- package/dist/chunk-6D3IWYNY.js.map +0 -1
- package/dist/chunk-R7WYAXPO.js.map +0 -1
- /package/dist/{chunk-3LSTL6EY.js.map → chunk-3H25IRJJ.js.map} +0 -0
- /package/dist/{chunk-HRRAOTOQ.js.map → chunk-3NJJQAL3.js.map} +0 -0
- /package/dist/{chunk-FKGLJJVS.js.map → chunk-A5FOXI3B.js.map} +0 -0
- /package/dist/{chunk-JPY3RMG2.js.map → chunk-E6VQJ75T.js.map} +0 -0
- /package/dist/{chunk-MRF5V5CD.js.map → chunk-F32JRMX2.js.map} +0 -0
- /package/dist/{chunk-N4T5FW6U.js.map → chunk-FMYAU5MP.js.map} +0 -0
- /package/dist/{chunk-OVL5AD3N.js.map → chunk-IPCSZLLF.js.map} +0 -0
- /package/dist/{chunk-7JYK7NNY.js.map → chunk-JYXFIILI.js.map} +0 -0
- /package/dist/{chunk-HOR4RMUM.js.map → chunk-KFPJIKUW.js.map} +0 -0
- /package/dist/{chunk-KQJUEQ2E.js.map → chunk-PL75KQGF.js.map} +0 -0
- /package/dist/{chunk-KNMSD2MQ.js.map → chunk-QAN5ZDRS.js.map} +0 -0
- /package/dist/{chunk-GRAUQXQB.js.map → chunk-RAU25Q6J.js.map} +0 -0
- /package/dist/{chunk-HHL4MX3W.js.map → chunk-SH6ZYUTX.js.map} +0 -0
- /package/dist/{chunk-T22FBDG3.js.map → chunk-T763ADEK.js.map} +0 -0
- /package/dist/{chunk-UH6ROXFJ.js.map → chunk-VC65GNUS.js.map} +0 -0
- /package/dist/{chunk-YJQJRXUI.js.map → chunk-VC7GKR2V.js.map} +0 -0
- /package/dist/{chunk-JL5HXL25.js.map → chunk-VSZNPAGL.js.map} +0 -0
- /package/dist/{chunk-P2EM5LKH.js.map → chunk-ZGHPELZ6.js.map} +0 -0
|
@@ -127,6 +127,16 @@ import {
|
|
|
127
127
|
normalizeStoredHashSource as normalizeStoredHashSourceContent,
|
|
128
128
|
} from "./extraction-persist-citation.js";
|
|
129
129
|
|
|
130
|
+
/** One extraction run's persist outcome: per-run values, no shared side channels (#1526). */
|
|
131
|
+
export interface ExtractionPersistResult {
|
|
132
|
+
persistedIds: string[];
|
|
133
|
+
memoryPathById: Map<string, string>;
|
|
134
|
+
/** Facts the judge deferred — drives this run's buffer retention (#562). */
|
|
135
|
+
deferredCount: number;
|
|
136
|
+
/** Persisted pending_review ids, excluded from the thread episode set (#1635). */
|
|
137
|
+
pendingReviewIds: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
export class ExtractionPersistCoordinator {
|
|
131
141
|
constructor(
|
|
132
142
|
private readonly deps: ExtractionPersistDeps,
|
|
@@ -149,7 +159,7 @@ export class ExtractionPersistCoordinator {
|
|
|
149
159
|
sourceText?: string,
|
|
150
160
|
graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.deps.config),
|
|
151
161
|
lifecycleCaps: MemoryLifecycleCapabilitySet = resolveMemoryLifecycleCapabilities(this.deps.config),
|
|
152
|
-
): Promise<
|
|
162
|
+
): Promise<ExtractionPersistResult> {
|
|
153
163
|
const citationEnabled = resolvePipelineProcessingCapabilities(this.deps.config).inlineSourceAttribution === true;
|
|
154
164
|
const citationTemplate = this.deps.config.inlineSourceAttributionFormat;
|
|
155
165
|
const factDedupEnabled = resolveRecallAuxiliaryCapabilities(this.deps.config).factDeduplication;
|
|
@@ -190,6 +200,10 @@ export class ExtractionPersistCoordinator {
|
|
|
190
200
|
validAt && validAt.length > 0 ? validAt : new Date().toISOString();
|
|
191
201
|
// #1635: pending_review persisted ids, excluded from the thread episode set below.
|
|
192
202
|
const pendingReviewPersistedIds: string[] = [];
|
|
203
|
+
// This run's judge-deferred count, returned to the caller (issue #562 PR 2)
|
|
204
|
+
// instead of shared side-channel state, so concurrent extractions cannot
|
|
205
|
+
// read each other's retention decision.
|
|
206
|
+
let deferredCount = 0;
|
|
193
207
|
const persistedIdsByStorage = new Map<string, { storage: StorageManager; ids: string[] }>();
|
|
194
208
|
const trackPersistedId = (
|
|
195
209
|
targetStorage: StorageManager,
|
|
@@ -1071,7 +1085,7 @@ export class ExtractionPersistCoordinator {
|
|
|
1071
1085
|
"persistExtraction: result or result.facts is invalid, skipping",
|
|
1072
1086
|
{ resultType: typeof result, factsType: typeof result?.facts },
|
|
1073
1087
|
);
|
|
1074
|
-
return { persistedIds, memoryPathById };
|
|
1088
|
+
return { persistedIds, memoryPathById, deferredCount, pendingReviewIds: pendingReviewPersistedIds };
|
|
1075
1089
|
}
|
|
1076
1090
|
|
|
1077
1091
|
// Chunking config from plugin settings
|
|
@@ -1243,10 +1257,6 @@ export class ExtractionPersistCoordinator {
|
|
|
1243
1257
|
// be persisted, not the raw extraction-time category.
|
|
1244
1258
|
let judgeVerdictsByFactIndex: Map<number, import("../extraction-judge.js").JudgeVerdict> | null = null;
|
|
1245
1259
|
let judgeGatedCount = 0;
|
|
1246
|
-
// Reset the side-channel defer count at the start of every
|
|
1247
|
-
// persistExtraction call so stale state from a prior call cannot leak
|
|
1248
|
-
// into the caller's buffer-retention decision.
|
|
1249
|
-
this.deps.setLastPersistExtractionDeferredCount(0);
|
|
1250
1260
|
if (lifecycleCaps.extractionJudge) {
|
|
1251
1261
|
// #1669 P1 + #1713 Item 1: pre-filter redacted facts from judge candidates
|
|
1252
1262
|
// so never-store content is not persisted as judge training data.
|
|
@@ -1433,7 +1443,7 @@ export class ExtractionPersistCoordinator {
|
|
|
1433
1443
|
);
|
|
1434
1444
|
// Expose defer count to the caller (issue #562 PR 2) so it can decide
|
|
1435
1445
|
// whether to retain buffer turns for the next extraction pass.
|
|
1436
|
-
|
|
1446
|
+
deferredCount = judgeResult.deferred;
|
|
1437
1447
|
} catch (err) {
|
|
1438
1448
|
// Fail-open: if the entire judge pipeline errors, proceed without filtering
|
|
1439
1449
|
log.warn(
|
|
@@ -2978,9 +2988,7 @@ export class ExtractionPersistCoordinator {
|
|
|
2978
2988
|
`persisted: ${facts.length - dedupedCount - importanceGatedCount - judgeGatedCount - redactionGatedCount - semanticMergedCount} facts${dedupSuffix}${gatedSuffix}${judgeSuffix}${redactionSuffix}, ${entities.length} entities, ${questions.length} questions, ${profileUpdates.length} profile updates`,
|
|
2979
2989
|
);
|
|
2980
2990
|
|
|
2981
|
-
// #1635: surface pending_review ids so the thread episode set excludes them.
|
|
2982
|
-
this.deps.setLastPersistExtractionPendingReviewIds(pendingReviewPersistedIds);
|
|
2983
2991
|
// Return the persisted fact IDs for threading
|
|
2984
|
-
return { persistedIds, memoryPathById };
|
|
2992
|
+
return { persistedIds, memoryPathById, deferredCount, pendingReviewIds: pendingReviewPersistedIds };
|
|
2985
2993
|
}
|
|
2986
2994
|
}
|
|
@@ -177,7 +177,7 @@ export async function runExtractionPostPersistBestEffort(
|
|
|
177
177
|
export interface ExtractionDeferRetentionOptions {
|
|
178
178
|
clearBufferAfterExtraction: boolean;
|
|
179
179
|
extractionJudgeShadow: boolean;
|
|
180
|
-
|
|
180
|
+
deferredCount: number;
|
|
181
181
|
normalizedTurns: BufferTurn[];
|
|
182
182
|
bufferKey: string;
|
|
183
183
|
retainDeferredTurns: (bufferKey: string, turns: BufferTurn[], max: number) => Promise<void>;
|
|
@@ -193,7 +193,7 @@ export async function runExtractionDeferRetention(
|
|
|
193
193
|
): Promise<void> {
|
|
194
194
|
await runPostPersistBestEffort("during_defer_retention", async () => {
|
|
195
195
|
if (options.clearBufferAfterExtraction && !options.extractionJudgeShadow) {
|
|
196
|
-
const deferredCount = options.
|
|
196
|
+
const deferredCount = options.deferredCount;
|
|
197
197
|
if (deferredCount > 0 && options.normalizedTurns.length > 0) {
|
|
198
198
|
await options.retainDeferredTurns(options.bufferKey, options.normalizedTurns, 10);
|
|
199
199
|
} else {
|
|
@@ -229,7 +229,8 @@ export interface ExtractionPostPersistOptions {
|
|
|
229
229
|
options?: { ignoreAbort?: boolean; ignoreDeadline?: boolean; propagateErrors?: boolean },
|
|
230
230
|
) => Promise<void>;
|
|
231
231
|
extractionJudgeShadow: boolean;
|
|
232
|
-
|
|
232
|
+
/** Facts this run's persist deferred to the judge — drives retention (#1523). */
|
|
233
|
+
deferredCount: number;
|
|
233
234
|
normalizedTurns: BufferTurn[];
|
|
234
235
|
bufferKey: string;
|
|
235
236
|
retainDeferredTurns: (bufferKey: string, turns: BufferTurn[], max: number) => Promise<void>;
|
|
@@ -283,7 +284,7 @@ export async function runExtractionPostPersist(
|
|
|
283
284
|
await runExtractionDeferRetention(runBestEffort, {
|
|
284
285
|
clearBufferAfterExtraction: options.clearBufferAfterExtraction,
|
|
285
286
|
extractionJudgeShadow: options.extractionJudgeShadow,
|
|
286
|
-
|
|
287
|
+
deferredCount: options.deferredCount,
|
|
287
288
|
normalizedTurns: options.normalizedTurns,
|
|
288
289
|
bufferKey: options.bufferKey,
|
|
289
290
|
retainDeferredTurns: options.retainDeferredTurns,
|
|
@@ -175,7 +175,7 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
|
|
|
175
175
|
persistExtraction: async () => {
|
|
176
176
|
persistCalls += 1;
|
|
177
177
|
await persistHandler();
|
|
178
|
-
return { persistedIds: ["memory-1"], memoryPathById: new Map() };
|
|
178
|
+
return { persistedIds: ["memory-1"], memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
179
179
|
},
|
|
180
180
|
maybeCapturePassiveCorrections: async (_turns, options) => {
|
|
181
181
|
await passiveCaptureHandler();
|
|
@@ -193,7 +193,6 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
|
|
|
193
193
|
maybeScheduleConsolidation: () => {},
|
|
194
194
|
requestQmdMaintenance: () => {},
|
|
195
195
|
runTierMigrationCycle: async () => migrationSummary,
|
|
196
|
-
getLastPersistExtractionDeferredCount: () => 0,
|
|
197
196
|
recordProcessedExtractionFingerprint: async (storage, fingerprint, meta) => {
|
|
198
197
|
recordedProcessedCount += 1;
|
|
199
198
|
await fingerprintRecorder(storage, fingerprint, meta);
|
|
@@ -71,7 +71,7 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
71
71
|
sourceText?: string,
|
|
72
72
|
graphCaps?: GraphConstructionCapabilitySet,
|
|
73
73
|
lifecycleCaps?: MemoryLifecycleCapabilitySet
|
|
74
|
-
) => Promise<
|
|
74
|
+
) => Promise<ExtractionPersistResult>;
|
|
75
75
|
|
|
76
76
|
maybeCapturePassiveCorrections: (
|
|
77
77
|
turns: readonly BufferTurn[],
|
|
@@ -89,7 +89,7 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
89
89
|
getCodingContextForSession: (sessionKey: string) => CodingContext | null;
|
|
90
90
|
applyCodingNamespaceOverlay: (sessionKey: string, namespace: string) => string;
|
|
91
91
|
boxBuilderFor: (storage: StorageManager) => Pick<BoxBuilder, "onExtraction">;
|
|
92
|
-
appendPersistedThreadEpisodes: (threadId: string, ids: string[]) => Promise<void>;
|
|
92
|
+
appendPersistedThreadEpisodes: (threadId: string, ids: string[], pendingReviewIds: string[]) => Promise<void>;
|
|
93
93
|
maybeScheduleConsolidation: (nonZero: boolean) => void;
|
|
94
94
|
requestQmdMaintenance: () => void;
|
|
95
95
|
runTierMigrationCycle: (
|
|
@@ -101,7 +101,6 @@ export interface ExtractionRunCoordinatorDeps {
|
|
|
101
101
|
force?: boolean;
|
|
102
102
|
}
|
|
103
103
|
) => Promise<TierMigrationCycleSummary>;
|
|
104
|
-
getLastPersistExtractionDeferredCount: () => number;
|
|
105
104
|
recordProcessedExtractionFingerprint: (
|
|
106
105
|
storage: StorageManager,
|
|
107
106
|
fingerprint: string,
|
|
@@ -125,6 +124,7 @@ import {
|
|
|
125
124
|
EXTRACTION_RETRY_STATE_MAX_ENTRIES,
|
|
126
125
|
} from "./extraction-run-helpers.js";
|
|
127
126
|
import type { ExtractionResilienceStatus, ExtractionRetryStateEntry } from "./extraction-run-helpers.js";
|
|
127
|
+
import type { ExtractionPersistResult } from "./extraction-persist.js";
|
|
128
128
|
|
|
129
129
|
export {
|
|
130
130
|
capExtractionRetryStateEntries,
|
|
@@ -1069,7 +1069,7 @@ export class ExtractionRunCoordinator {
|
|
|
1069
1069
|
throwIfDeadlineExceeded("before_persist");
|
|
1070
1070
|
throwIfAborted("before_persist");
|
|
1071
1071
|
|
|
1072
|
-
const { persistedIds } = await this.deps.persistExtraction(
|
|
1072
|
+
const { persistedIds, deferredCount, pendingReviewIds } = await this.deps.persistExtraction(
|
|
1073
1073
|
result,
|
|
1074
1074
|
storage,
|
|
1075
1075
|
threadIdForExtraction,
|
|
@@ -1106,7 +1106,7 @@ export class ExtractionRunCoordinator {
|
|
|
1106
1106
|
clearBufferAfterExtraction,
|
|
1107
1107
|
runPostPersistBestEffort,
|
|
1108
1108
|
extractionJudgeShadow: this.config.extractionJudgeShadow,
|
|
1109
|
-
|
|
1109
|
+
deferredCount,
|
|
1110
1110
|
normalizedTurns: normalizedTurns as BufferTurn[],
|
|
1111
1111
|
bufferKey,
|
|
1112
1112
|
retainDeferredTurns: (key, retainedTurns, max) =>
|
|
@@ -1155,7 +1155,7 @@ export class ExtractionRunCoordinator {
|
|
|
1155
1155
|
// always attached to the thread. The helper excludes pending_review ids (#1635).
|
|
1156
1156
|
if (resolvePresentationCapabilities(this.config).threading && threadIdForExtraction && persistedIds.length > 0) {
|
|
1157
1157
|
await runPostPersistBestEffort("during_thread_episode_append", () =>
|
|
1158
|
-
this.deps.appendPersistedThreadEpisodes(threadIdForExtraction, persistedIds),
|
|
1158
|
+
this.deps.appendPersistedThreadEpisodes(threadIdForExtraction, persistedIds, pendingReviewIds),
|
|
1159
1159
|
);
|
|
1160
1160
|
}
|
|
1161
1161
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -123,7 +123,7 @@ import {
|
|
|
123
123
|
type ExtractionResilienceStatus,
|
|
124
124
|
} from "./orchestration/extraction-run.js";
|
|
125
125
|
import { ConsolidationRunCoordinator } from "./orchestration/consolidation-run.js";
|
|
126
|
-
import { ExtractionPersistCoordinator } from "./orchestration/extraction-persist.js";
|
|
126
|
+
import { ExtractionPersistCoordinator, type ExtractionPersistResult } from "./orchestration/extraction-persist.js";
|
|
127
127
|
import type { ExtractionSourceContext } from "./orchestration/extraction-origin-context.js";
|
|
128
128
|
import { drainExtractionAndShutdownDependencyPropagation } from "./orchestration/extraction-shutdown.js";
|
|
129
129
|
import {
|
|
@@ -614,19 +614,6 @@ export class Orchestrator {
|
|
|
614
614
|
* (rule 11) — these hang off the orchestrator instance.
|
|
615
615
|
*/
|
|
616
616
|
private faithfulnessCounters: FaithfulnessGateCounters = createFaithfulnessCounters();
|
|
617
|
-
/**
|
|
618
|
-
* Side-channel: number of facts deferred in the most recent
|
|
619
|
-
* `persistExtraction` call (issue #562, PR 2). The caller reads this after
|
|
620
|
-
* `persistExtraction` returns to decide whether to retain buffer turns for
|
|
621
|
-
* the next extraction pass. Not part of the return signature because many
|
|
622
|
-
* callers already destructure `persistedIds` by position.
|
|
623
|
-
*/
|
|
624
|
-
private lastPersistExtractionDeferredCount: number = 0;
|
|
625
|
-
/**
|
|
626
|
-
* Side-channel (#1635): pending_review persisted ids from the last
|
|
627
|
-
* persistExtraction; runExtraction excludes them from the thread episode set.
|
|
628
|
-
*/
|
|
629
|
-
private lastPersistExtractionPendingReviewIds: string[] = [];
|
|
630
617
|
private readonly _fastGatewayLlm: FallbackLlmClient | null;
|
|
631
618
|
|
|
632
619
|
get fastGatewayLlm(): FallbackLlmClient | null {
|
|
@@ -785,11 +772,11 @@ export class Orchestrator {
|
|
|
785
772
|
getCodingContextForSession: (sessionKey) => this.getCodingContextForSession(sessionKey),
|
|
786
773
|
applyCodingNamespaceOverlay: (sessionKey, namespace) => this.applyCodingNamespaceOverlay(sessionKey, namespace),
|
|
787
774
|
boxBuilderFor: (storage) => this.boxBuilderFor(storage),
|
|
788
|
-
appendPersistedThreadEpisodes: (threadId, ids) =>
|
|
775
|
+
appendPersistedThreadEpisodes: (threadId, ids, pendingReviewIds) =>
|
|
776
|
+
this.appendPersistedThreadEpisodes(threadId, ids, pendingReviewIds),
|
|
789
777
|
maybeScheduleConsolidation: (nonZero) => this.maybeScheduleConsolidation(nonZero),
|
|
790
778
|
requestQmdMaintenance: () => this.requestQmdMaintenance(),
|
|
791
779
|
runTierMigrationCycle: (storage, trigger, options) => this.runTierMigrationCycle(storage, trigger, options),
|
|
792
|
-
getLastPersistExtractionDeferredCount: () => this.lastPersistExtractionDeferredCount,
|
|
793
780
|
recordProcessedExtractionFingerprint: (storage, fingerprint, preloadedMeta) =>
|
|
794
781
|
this.recordProcessedExtractionFingerprint(storage, fingerprint, preloadedMeta),
|
|
795
782
|
});
|
|
@@ -841,8 +828,6 @@ export class Orchestrator {
|
|
|
841
828
|
getJudgeDeferCounts: () => this.judgeDeferCounts,
|
|
842
829
|
getFaithfulnessCounters: () => this.faithfulnessCounters,
|
|
843
830
|
getEmbeddingFallback: () => this.embeddingFallback,
|
|
844
|
-
setLastPersistExtractionDeferredCount: (v) => { this.lastPersistExtractionDeferredCount = v; },
|
|
845
|
-
setLastPersistExtractionPendingReviewIds: (ids) => { this.lastPersistExtractionPendingReviewIds = ids; },
|
|
846
831
|
addContentHashDedup: (targetStorage, content) => this.addContentHashDedup(targetStorage, content),
|
|
847
832
|
hasContentHashDedup: (targetStorage, content) => this.hasContentHashDedup(targetStorage, content),
|
|
848
833
|
backfillTemporalBoundsOnDedupHit: (targetStorage, dedupContent, bounds, entityRef, sourceConnector) =>
|
|
@@ -3197,8 +3182,8 @@ export class Orchestrator {
|
|
|
3197
3182
|
sourceText?: string,
|
|
3198
3183
|
graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
|
|
3199
3184
|
lifecycleCaps: MemoryLifecycleCapabilitySet = resolveMemoryLifecycleCapabilities(this.config),
|
|
3200
|
-
): Promise<
|
|
3201
|
-
|
|
3185
|
+
): Promise<ExtractionPersistResult> {
|
|
3186
|
+
return await this.extractionPersistCoordinator.persistExtraction(
|
|
3202
3187
|
result,
|
|
3203
3188
|
storage,
|
|
3204
3189
|
threadIdForExtraction,
|
|
@@ -3209,7 +3194,6 @@ export class Orchestrator {
|
|
|
3209
3194
|
graphCaps,
|
|
3210
3195
|
lifecycleCaps,
|
|
3211
3196
|
);
|
|
3212
|
-
return { persistedIds, memoryPathById };
|
|
3213
3197
|
}
|
|
3214
3198
|
/**
|
|
3215
3199
|
* Append persisted ids to the thread episode set, excluding pending_review
|
|
@@ -3219,8 +3203,8 @@ export class Orchestrator {
|
|
|
3219
3203
|
private async appendPersistedThreadEpisodes(
|
|
3220
3204
|
threadId: string,
|
|
3221
3205
|
persistedIds: string[],
|
|
3206
|
+
pendingReviewIds: string[],
|
|
3222
3207
|
): Promise<void> {
|
|
3223
|
-
const pendingReviewIds = this.lastPersistExtractionPendingReviewIds ?? [];
|
|
3224
3208
|
const episodeIds =
|
|
3225
3209
|
pendingReviewIds.length > 0
|
|
3226
3210
|
? persistedIds.filter((id) => !pendingReviewIds.includes(id))
|
|
@@ -166,7 +166,7 @@ export function stubPersistExtraction(
|
|
|
166
166
|
const impl: PersistExtractionFn = async (...args) => {
|
|
167
167
|
calls.push(args);
|
|
168
168
|
const persistedIds = factory ? await factory(args, calls.length) : [];
|
|
169
|
-
return { persistedIds, memoryPathById: new Map() };
|
|
169
|
+
return { persistedIds, memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
170
170
|
};
|
|
171
171
|
seam.persistExtraction = impl;
|
|
172
172
|
return calls;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { ThreadingManager } from "./threading.js";
|
|
7
|
+
import type { BufferTurn, ConversationThread } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function turn(sessionKey: string, content: string): BufferTurn {
|
|
10
|
+
return { role: "user", content, timestamp: "2026-11-04T10:00:00.000Z", sessionKey };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function makeThreading(): Promise<{ threading: ThreadingManager; cleanup: () => Promise<void> }> {
|
|
14
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-threads-"));
|
|
15
|
+
return { threading: new ThreadingManager(dir), cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("the state being inserted is never evicted, even with every older session in flight", async () => {
|
|
19
|
+
const { threading, cleanup } = await makeThreading();
|
|
20
|
+
try {
|
|
21
|
+
const realSave = threading.saveThread.bind(threading);
|
|
22
|
+
const floodGate = Promise.withResolvers<void>();
|
|
23
|
+
const allEntered = Promise.withResolvers<void>();
|
|
24
|
+
let entered = 0;
|
|
25
|
+
threading.saveThread = async (thread: ConversationThread): Promise<void> => {
|
|
26
|
+
if (thread.sessionKey?.startsWith("flood-")) {
|
|
27
|
+
entered += 1;
|
|
28
|
+
if (entered === 512) allEntered.resolve();
|
|
29
|
+
await floodGate.promise;
|
|
30
|
+
}
|
|
31
|
+
await realSave(thread);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Park all 512 flood sessions inside saveThread so every one of their run
|
|
35
|
+
// states is in-flight while the next session inserts its own.
|
|
36
|
+
const floodRuns: Promise<ConversationThread["id"]>[] = [];
|
|
37
|
+
for (let i = 0; i < 512; i++) {
|
|
38
|
+
floodRuns.push(threading.processTurn(turn(`flood-${i}`, `flood ${i}`), []));
|
|
39
|
+
}
|
|
40
|
+
await allEntered.promise;
|
|
41
|
+
|
|
42
|
+
// The new session's state is the map's last entry and not yet registered
|
|
43
|
+
// in-flight when runStateFor's eviction loop runs; eviction must skip it
|
|
44
|
+
// or this turn's state is orphaned and the follow-up turn forks a new
|
|
45
|
+
// thread (PRRT_kwDORJXyws6ztlO2).
|
|
46
|
+
const idN1 = await threading.processTurn(turn("session-new", "n1"), []);
|
|
47
|
+
const idN2 = await threading.processTurn(turn("session-new", "n2"), []);
|
|
48
|
+
assert.equal(idN2, idN1, "follow-up turn must append to the same thread");
|
|
49
|
+
|
|
50
|
+
floodGate.resolve();
|
|
51
|
+
await Promise.all(floodRuns);
|
|
52
|
+
} finally {
|
|
53
|
+
await cleanup();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a recently used session is not evicted FIFO when the cap overflows", async () => {
|
|
58
|
+
const { threading, cleanup } = await makeThreading();
|
|
59
|
+
try {
|
|
60
|
+
const threadA = await threading.processTurn(turn("session-a", "a1"), []);
|
|
61
|
+
// Fill the map to exactly the cap with inactive sessions (session-a is
|
|
62
|
+
// now the OLDEST entry).
|
|
63
|
+
for (let i = 0; i < 511; i++) {
|
|
64
|
+
await threading.processTurn(turn(`flood-${i}`, `flood ${i}`), []);
|
|
65
|
+
}
|
|
66
|
+
// Touch session-a: under LRU this makes it the most recent entry; under
|
|
67
|
+
// FIFO it stays oldest and the next insertion evicts it.
|
|
68
|
+
await threading.processTurn(turn("session-a", "a2"), ["episode-a2"]);
|
|
69
|
+
await threading.processTurn(turn("flood-new", "the straw"), []);
|
|
70
|
+
|
|
71
|
+
const idA3 = await threading.processTurn(turn("session-a", "a3"), ["episode-a3"]);
|
|
72
|
+
assert.equal(idA3, threadA, "a just-used session must not be evicted before inactive ones");
|
|
73
|
+
assert.ok((await threading.loadThread(threadA))?.episodeIds.includes("episode-a2"));
|
|
74
|
+
} finally {
|
|
75
|
+
await cleanup();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("concurrent turns for two sessions never share thread run state", async () => {
|
|
80
|
+
const { threading, cleanup } = await makeThreading();
|
|
81
|
+
try {
|
|
82
|
+
const realSave = threading.saveThread.bind(threading);
|
|
83
|
+
const aSaveEntered = Promise.withResolvers<void>();
|
|
84
|
+
const aGate = Promise.withResolvers<void>();
|
|
85
|
+
let holdArmed = false;
|
|
86
|
+
// Hold session A's save open, and only let it resume after session B has
|
|
87
|
+
// run a full processTurn to completion through the shared manager — the
|
|
88
|
+
// exact interleaving a shared run-state slot corrupts.
|
|
89
|
+
threading.saveThread = async (thread: ConversationThread): Promise<void> => {
|
|
90
|
+
if (holdArmed && thread.sessionKey === "session-a") {
|
|
91
|
+
aSaveEntered.resolve();
|
|
92
|
+
await aGate.promise;
|
|
93
|
+
}
|
|
94
|
+
await realSave(thread);
|
|
95
|
+
};
|
|
96
|
+
holdArmed = true;
|
|
97
|
+
const pendingA = threading.processTurn(turn("session-a", "a1"), []);
|
|
98
|
+
await aSaveEntered.promise;
|
|
99
|
+
const idB = await threading.processTurn(turn("session-b", "b1"), []);
|
|
100
|
+
aGate.resolve();
|
|
101
|
+
const idA = await pendingA;
|
|
102
|
+
|
|
103
|
+
assert.notEqual(idA, idB);
|
|
104
|
+
assert.equal((await threading.loadThread(idA))?.sessionKey, "session-a");
|
|
105
|
+
assert.equal((await threading.loadThread(idB))?.sessionKey, "session-b");
|
|
106
|
+
|
|
107
|
+
// Each session's follow-up turn must append to its OWN thread. With the
|
|
108
|
+
// shared slot, A resumed last and left its session key in the global
|
|
109
|
+
// state, so B's next turn sees "session-a" and starts a fresh thread.
|
|
110
|
+
assert.equal(await threading.processTurn(turn("session-a", "a2"), []), idA);
|
|
111
|
+
assert.equal(await threading.processTurn(turn("session-b", "b2"), []), idB);
|
|
112
|
+
} finally {
|
|
113
|
+
await cleanup();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("an in-flight session survives a 512-session flood arriving mid-await", async () => {
|
|
118
|
+
const { threading, cleanup } = await makeThreading();
|
|
119
|
+
try {
|
|
120
|
+
const threadA = await threading.processTurn(turn("session-a", "a1"), []);
|
|
121
|
+
|
|
122
|
+
const realSave = threading.saveThread.bind(threading);
|
|
123
|
+
const gate = Promise.withResolvers<void>();
|
|
124
|
+
const a2SaveEntered = Promise.withResolvers<void>();
|
|
125
|
+
let floodArmed = false;
|
|
126
|
+
threading.saveThread = async (thread: ConversationThread): Promise<void> => {
|
|
127
|
+
if (floodArmed && thread.id === threadA) {
|
|
128
|
+
a2SaveEntered.resolve();
|
|
129
|
+
await gate.promise;
|
|
130
|
+
}
|
|
131
|
+
await realSave(thread);
|
|
132
|
+
};
|
|
133
|
+
floodArmed = true;
|
|
134
|
+
const secondTurn = threading.processTurn(turn("session-a", "a2"), ["episode-a2"]);
|
|
135
|
+
await a2SaveEntered.promise;
|
|
136
|
+
|
|
137
|
+
// Session A is parked inside saveThread. 512 other sessions insert their
|
|
138
|
+
// run state while it waits; eviction must not take an in-flight session's
|
|
139
|
+
// state, or A's thread id is lost and its next turn starts a new thread.
|
|
140
|
+
for (let i = 0; i < 512; i++) {
|
|
141
|
+
await threading.processTurn(turn(`flood-${i}`, `flood ${i}`), []);
|
|
142
|
+
}
|
|
143
|
+
gate.resolve();
|
|
144
|
+
|
|
145
|
+
const idA2 = await secondTurn;
|
|
146
|
+
assert.equal(idA2, threadA, "resumed turn must still own its session's thread");
|
|
147
|
+
const idA3 = await threading.processTurn(turn("session-a", "a3"), ["episode-a3"]);
|
|
148
|
+
assert.equal(idA3, threadA);
|
|
149
|
+
assert.ok((await threading.loadThread(threadA))?.episodeIds.includes("episode-a2"));
|
|
150
|
+
} finally {
|
|
151
|
+
await cleanup();
|
|
152
|
+
}
|
|
153
|
+
});
|
package/src/threading.ts
CHANGED
|
@@ -85,10 +85,56 @@ function parseConversationThread(raw: string): ConversationThread | null {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** One session's thread-run state: its current thread and last turn time. */
|
|
89
|
+
interface ThreadingRunState {
|
|
90
|
+
currentThreadId: string | null;
|
|
91
|
+
lastTurnTimestamp: number | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
export class ThreadingManager {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Per-session thread-run state. One global slot let two concurrent
|
|
97
|
+
* extractions (bounded-parallel queue) interleave sessions and attach
|
|
98
|
+
* turns to the wrong thread, so the run state is keyed by session.
|
|
99
|
+
* Bounded: a long-lived daemon sees one entry per active session.
|
|
100
|
+
*/
|
|
101
|
+
private readonly runStateBySession = new Map<string, ThreadingRunState>();
|
|
102
|
+
/**
|
|
103
|
+
* Run states with a processTurn awaiting inside them. A session parked on
|
|
104
|
+
* `saveThread` can watch 512 other sessions insert and evict; dropping its
|
|
105
|
+
* state mid-await loses its thread id and forks its thread, so eviction
|
|
106
|
+
* never takes an in-flight state (the cap may be exceeded briefly by the
|
|
107
|
+
* number of in-flight extractions, which is bounded by the queue).
|
|
108
|
+
*/
|
|
109
|
+
private readonly inFlightRunStates = new Set<ThreadingRunState>();
|
|
110
|
+
private static readonly RUN_STATE_MAX_SESSIONS = 512;
|
|
111
|
+
|
|
112
|
+
private runStateFor(turn: BufferTurn): ThreadingRunState {
|
|
113
|
+
const key = turn.sessionKey ?? "";
|
|
114
|
+
let state = this.runStateBySession.get(key);
|
|
115
|
+
if (state) {
|
|
116
|
+
// Refresh recency: Map iteration is insertion-ordered, so re-putting a
|
|
117
|
+
// touched session makes eviction true LRU instead of first-in-first-out —
|
|
118
|
+
// an active session must not be the oldest entry just because it started
|
|
119
|
+
// first.
|
|
120
|
+
this.runStateBySession.delete(key);
|
|
121
|
+
this.runStateBySession.set(key, state);
|
|
122
|
+
} else {
|
|
123
|
+
state = { currentThreadId: null, lastTurnTimestamp: null };
|
|
124
|
+
this.runStateBySession.set(key, state);
|
|
125
|
+
if (this.runStateBySession.size > ThreadingManager.RUN_STATE_MAX_SESSIONS) {
|
|
126
|
+
for (const [oldestKey, oldest] of this.runStateBySession) {
|
|
127
|
+
// Never evict the state being inserted: before the caller registers
|
|
128
|
+
// it in-flight, every older entry can be in flight and the loop
|
|
129
|
+
// would otherwise delete this turn's own state and fork the thread.
|
|
130
|
+
if (oldest === state || this.inFlightRunStates.has(oldest)) continue;
|
|
131
|
+
this.runStateBySession.delete(oldestKey);
|
|
132
|
+
if (this.runStateBySession.size <= ThreadingManager.RUN_STATE_MAX_SESSIONS) break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
92
138
|
|
|
93
139
|
constructor(
|
|
94
140
|
private readonly threadsDir: string,
|
|
@@ -164,18 +210,16 @@ export class ThreadingManager {
|
|
|
164
210
|
|
|
165
211
|
/**
|
|
166
212
|
* Check if we should start a new thread based on session key and time gap.
|
|
213
|
+
* State is per session, so the cross-session check the shared slot needed
|
|
214
|
+
* is gone: different sessions never share run state at all.
|
|
167
215
|
*/
|
|
168
216
|
shouldStartNewThread(turn: BufferTurn): boolean {
|
|
169
217
|
const turnTime = new Date(turn.timestamp).getTime();
|
|
170
|
-
|
|
171
|
-
// Different session key = new thread
|
|
172
|
-
if (turn.sessionKey && this.lastSessionKey && turn.sessionKey !== this.lastSessionKey) {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
218
|
+
const state = this.runStateFor(turn);
|
|
175
219
|
|
|
176
220
|
// Time gap > threshold = new thread
|
|
177
|
-
if (
|
|
178
|
-
const gapMs = turnTime -
|
|
221
|
+
if (state.lastTurnTimestamp) {
|
|
222
|
+
const gapMs = turnTime - state.lastTurnTimestamp;
|
|
179
223
|
const gapMinutes = gapMs / (1000 * 60);
|
|
180
224
|
if (gapMinutes > this.gapMinutes) {
|
|
181
225
|
return true;
|
|
@@ -183,7 +227,7 @@ export class ThreadingManager {
|
|
|
183
227
|
}
|
|
184
228
|
|
|
185
229
|
// No current thread = new thread
|
|
186
|
-
if (!
|
|
230
|
+
if (!state.currentThreadId) {
|
|
187
231
|
return true;
|
|
188
232
|
}
|
|
189
233
|
|
|
@@ -198,41 +242,45 @@ export class ThreadingManager {
|
|
|
198
242
|
await this.ensureDirectory();
|
|
199
243
|
|
|
200
244
|
const turnTime = new Date(turn.timestamp).getTime();
|
|
245
|
+
const state = this.runStateFor(turn);
|
|
246
|
+
this.inFlightRunStates.add(state);
|
|
247
|
+
try {
|
|
248
|
+
if (this.shouldStartNewThread(turn)) {
|
|
249
|
+
// Create new thread
|
|
250
|
+
const threadId = `thread-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
251
|
+
const thread: ConversationThread = {
|
|
252
|
+
id: threadId,
|
|
253
|
+
title: "New Thread", // Will be updated later with keywords
|
|
254
|
+
createdAt: turn.timestamp,
|
|
255
|
+
updatedAt: turn.timestamp,
|
|
256
|
+
sessionKey: turn.sessionKey,
|
|
257
|
+
episodeIds: [...episodeIds],
|
|
258
|
+
linkedThreadIds: [],
|
|
259
|
+
};
|
|
201
260
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
await this.saveThread(thread);
|
|
216
|
-
this.currentThreadId = threadId;
|
|
217
|
-
log.debug(`created new thread ${threadId}`);
|
|
218
|
-
} else if (this.currentThreadId) {
|
|
219
|
-
// Add episodes to current thread
|
|
220
|
-
const thread = await this.loadThread(this.currentThreadId);
|
|
221
|
-
if (thread) {
|
|
222
|
-
for (const id of episodeIds) {
|
|
223
|
-
if (!thread.episodeIds.includes(id)) {
|
|
224
|
-
thread.episodeIds.push(id);
|
|
261
|
+
await this.saveThread(thread);
|
|
262
|
+
state.currentThreadId = threadId;
|
|
263
|
+
log.debug(`created new thread ${threadId}`);
|
|
264
|
+
} else if (state.currentThreadId) {
|
|
265
|
+
// Add episodes to current thread
|
|
266
|
+
const thread = await this.loadThread(state.currentThreadId);
|
|
267
|
+
if (thread) {
|
|
268
|
+
for (const id of episodeIds) {
|
|
269
|
+
if (!thread.episodeIds.includes(id)) {
|
|
270
|
+
thread.episodeIds.push(id);
|
|
271
|
+
}
|
|
225
272
|
}
|
|
273
|
+
thread.updatedAt = turn.timestamp;
|
|
274
|
+
await this.saveThread(thread);
|
|
226
275
|
}
|
|
227
|
-
thread.updatedAt = turn.timestamp;
|
|
228
|
-
await this.saveThread(thread);
|
|
229
276
|
}
|
|
230
|
-
}
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
this.lastSessionKey = turn.sessionKey ?? null;
|
|
278
|
+
state.lastTurnTimestamp = turnTime;
|
|
234
279
|
|
|
235
|
-
|
|
280
|
+
return state.currentThreadId!;
|
|
281
|
+
} finally {
|
|
282
|
+
this.inFlightRunStates.delete(state);
|
|
283
|
+
}
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
/**
|
|
@@ -378,18 +426,20 @@ export class ThreadingManager {
|
|
|
378
426
|
}
|
|
379
427
|
|
|
380
428
|
/**
|
|
381
|
-
* Get the current thread ID (if any).
|
|
429
|
+
* Get the current thread ID (if any). With per-session run state there is
|
|
430
|
+
* no single current thread; null unless exactly one session is tracked.
|
|
382
431
|
*/
|
|
383
432
|
getCurrentThreadId(): string | null {
|
|
384
|
-
|
|
433
|
+
if (this.runStateBySession.size === 1) {
|
|
434
|
+
return this.runStateBySession.values().next().value?.currentThreadId ?? null;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
385
437
|
}
|
|
386
438
|
|
|
387
439
|
/**
|
|
388
440
|
* Reset threading state (e.g., on plugin restart).
|
|
389
441
|
*/
|
|
390
442
|
reset(): void {
|
|
391
|
-
this.
|
|
392
|
-
this.lastTurnTimestamp = null;
|
|
393
|
-
this.lastSessionKey = null;
|
|
443
|
+
this.runStateBySession.clear();
|
|
394
444
|
}
|
|
395
445
|
}
|