@remnic/core 10.0.66 → 10.0.67
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 +16 -16
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +60 -60
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-briefing.js +17 -17
- package/dist/access-http-query.js +18 -18
- package/dist/access-http.js +33 -33
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.js +26 -26
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.js +24 -24
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.js +18 -18
- package/dist/active-recall.js +3 -3
- package/dist/briefing.js +3 -3
- package/dist/buffer.js +2 -2
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-KISOMGY2.js → chunk-3RHU5MD7.js} +2 -2
- package/dist/{chunk-3H25IRJJ.js → chunk-4MPAKYZU.js} +2 -2
- package/dist/{chunk-IPCSZLLF.js → chunk-4NV7O76Q.js} +2 -2
- package/dist/{chunk-BH7NVSEC.js → chunk-4SOWMA55.js} +3 -3
- package/dist/{chunk-GROHOWR5.js → chunk-5MGKGEL7.js} +2 -2
- package/dist/{chunk-ZPLQ27MY.js → chunk-5UM4H64E.js} +2 -2
- package/dist/{chunk-2DNFRLLL.js → chunk-5Y6VA6X2.js} +2 -2
- package/dist/{chunk-RAU25Q6J.js → chunk-6WKWL53V.js} +13 -13
- package/dist/{chunk-KFPJIKUW.js → chunk-75DCOS6I.js} +23 -23
- package/dist/{chunk-25QK3LR2.js → chunk-7O2MC42R.js} +2 -2
- package/dist/{chunk-7ETWIBTO.js → chunk-ADYP7HXE.js} +3 -3
- package/dist/{chunk-FUDKGJDN.js → chunk-AVGDYYMA.js} +2 -2
- package/dist/{chunk-ECF627KC.js → chunk-AYJQ644F.js} +2 -2
- package/dist/{chunk-T763ADEK.js → chunk-B3MWELZ3.js} +2 -2
- package/dist/{chunk-DRTSPRWL.js → chunk-B4DXREWC.js} +2 -2
- package/dist/{chunk-5UEGL5BF.js → chunk-CI3MAAIY.js} +2 -2
- package/dist/{chunk-JYXFIILI.js → chunk-CTH2CQCB.js} +2 -2
- package/dist/{chunk-UVHXLVFJ.js → chunk-EURPSF4Y.js} +2 -2
- package/dist/{chunk-VPEA63FC.js → chunk-EWJZZDRG.js} +2 -2
- package/dist/{chunk-BOZQAUAJ.js → chunk-EXQJMFRF.js} +2 -2
- package/dist/{chunk-VC65GNUS.js → chunk-GGD2YDS6.js} +2 -2
- package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
- package/dist/{chunk-FMYAU5MP.js → chunk-GJ4HU6LK.js} +2 -2
- package/dist/{chunk-3NJJQAL3.js → chunk-H5DIRCZP.js} +12 -12
- package/dist/{chunk-434E4BTO.js → chunk-HH73M65H.js} +3 -3
- package/dist/{chunk-LIGZO7XN.js → chunk-HPB5TF55.js} +2 -2
- package/dist/{chunk-E4QIX2OB.js → chunk-IAGVM7EY.js} +2 -2
- package/dist/{chunk-VC7GKR2V.js → chunk-IG3EZ3TF.js} +2 -2
- package/dist/{chunk-QAN5ZDRS.js → chunk-IVUVQPTQ.js} +2 -2
- package/dist/{chunk-XOFIJGB2.js → chunk-IZBIDPMY.js} +2 -2
- package/dist/{chunk-HFEGRMFG.js → chunk-JLGBJ4AX.js} +7 -7
- package/dist/{chunk-BBHKN2CN.js → chunk-JLMOH5JA.js} +2 -2
- package/dist/{chunk-TPCRDPUY.js → chunk-KZSGBWOJ.js} +2 -2
- package/dist/{chunk-SHHZWSSE.js → chunk-KZZ5EZ7O.js} +2 -2
- package/dist/{chunk-XMBDI725.js → chunk-LFYEQK45.js} +674 -518
- package/dist/chunk-LFYEQK45.js.map +1 -0
- package/dist/{chunk-A5FOXI3B.js → chunk-LN4X2NDA.js} +2 -2
- package/dist/{chunk-5ZHFPUBC.js → chunk-MOIJWWNA.js} +8 -8
- package/dist/{chunk-KO224R36.js → chunk-NCZGGGWI.js} +2 -2
- package/dist/{chunk-XVQVF2TK.js → chunk-NHWDTOBN.js} +269 -230
- package/dist/chunk-NHWDTOBN.js.map +1 -0
- package/dist/{chunk-7ZTGMEXF.js → chunk-O5ZINWW3.js} +2 -2
- package/dist/{chunk-WWH7GJ42.js → chunk-OCK4D5PC.js} +2 -2
- package/dist/{chunk-G6L3REL3.js → chunk-OPFVXM2D.js} +2 -2
- package/dist/{chunk-FR7PK7ED.js → chunk-P4TV3TQ2.js} +2 -2
- package/dist/{chunk-ZGHPELZ6.js → chunk-PMDOGXA2.js} +2 -2
- package/dist/{chunk-NM2II57L.js → chunk-PRKNAIGZ.js} +2 -1
- package/dist/chunk-PRKNAIGZ.js.map +1 -0
- package/dist/{chunk-F32JRMX2.js → chunk-QGFR73SN.js} +2 -2
- package/dist/{chunk-QNZPYH36.js → chunk-R2BIVU2Z.js} +352 -290
- package/dist/chunk-R2BIVU2Z.js.map +1 -0
- package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
- package/dist/{chunk-SH6ZYUTX.js → chunk-SXTVZ6JL.js} +2 -2
- package/dist/{chunk-CLSKGTAT.js → chunk-TECK5IVY.js} +2 -2
- package/dist/{chunk-SNS3DWG6.js → chunk-TVE7W67N.js} +2 -2
- package/dist/{chunk-KYSSUY66.js → chunk-U2CAGMUD.js} +2 -2
- package/dist/{chunk-XLCEFUAU.js → chunk-UVVFPCPX.js} +2 -2
- package/dist/{chunk-PL75KQGF.js → chunk-UW6LBFYN.js} +5 -5
- package/dist/{chunk-SEA56G6S.js → chunk-V4XE263I.js} +1 -1
- package/dist/chunk-V4XE263I.js.map +1 -0
- package/dist/{chunk-VSZNPAGL.js → chunk-V6SBA5O7.js} +3 -3
- package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
- package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
- package/dist/{chunk-E6VQJ75T.js → chunk-ZORS5BG5.js} +4 -4
- package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
- package/dist/cli.js +46 -46
- package/dist/coding/export-okf-codegraph.js +2 -2
- package/dist/compounding/engine.js +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/convergence-refresh.js +5 -5
- package/dist/corpus-watermark.js +5 -5
- package/dist/entity-retrieval.js +3 -3
- package/dist/external-wiki-access.js +17 -17
- package/dist/extraction.js +6 -6
- package/dist/harmonic-retrieval.js +3 -3
- package/dist/index.js +60 -60
- package/dist/location/index.js +5 -5
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/memory-browse-operations.js +17 -17
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-doctor-corpus.js +6 -6
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/buffer-drain-runner.js +60 -60
- package/dist/orchestration/consolidation-run.d.ts.map +1 -1
- package/dist/orchestration/extraction-drain.d.ts +4 -4
- package/dist/orchestration/extraction-drain.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/extraction-queue-coordinator.d.ts +67 -33
- package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +1 -8
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/fact-dedup-window.d.ts +49 -0
- package/dist/orchestration/fact-dedup-window.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +13 -13
- package/dist/orchestration/provider-breaker.d.ts +58 -0
- package/dist/orchestration/provider-breaker.d.ts.map +1 -0
- package/dist/orchestrator.js +60 -60
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +17 -17
- package/dist/recall-navigation.js +16 -16
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +4 -4
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/source-agent-qualifier.js +5 -5
- package/dist/standup/build.js +3 -3
- package/dist/storage/profile-mutation.d.ts +25 -0
- package/dist/storage/profile-mutation.d.ts.map +1 -0
- package/dist/storage.d.ts +4 -6
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -2
- package/dist/support-passport/index.js +19 -19
- package/dist/transfer/export-okf.js +3 -3
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +3 -3
- package/dist/who-knows-cli.js +4 -4
- package/dist/who-knows.js +3 -3
- package/package.json +2 -2
- package/src/buffer-drain-handover.test.ts +3 -3
- package/src/buffer-drop-extraction-chunk.test.ts +0 -1
- package/src/config.test.ts +12 -0
- package/src/config.ts +2 -0
- package/src/fact-dedup-window.test.ts +116 -0
- package/src/orchestration/consolidation-profile-race.test.ts +178 -0
- package/src/orchestration/consolidation-run.ts +19 -7
- package/src/orchestration/extraction-drain.ts +10 -3
- package/src/orchestration/extraction-persist.ts +141 -138
- package/src/orchestration/extraction-queue-coordinator.ts +167 -56
- package/src/orchestration/extraction-run.ts +50 -93
- package/src/orchestration/fact-dedup-window.ts +122 -0
- package/src/orchestration/provider-breaker.test.ts +89 -0
- package/src/orchestration/provider-breaker.ts +158 -0
- package/src/orchestration/turn-ingestion.ts +5 -5
- package/src/orchestrator-extraction-queue.test.ts +179 -8
- package/src/orchestrator.ts +1 -1
- package/src/storage/profile-mutation.ts +119 -0
- package/src/storage-append-profile.test.ts +114 -0
- package/src/storage.ts +17 -48
- package/src/types.ts +2 -0
- package/dist/chunk-NM2II57L.js.map +0 -1
- package/dist/chunk-QNZPYH36.js.map +0 -1
- package/dist/chunk-SEA56G6S.js.map +0 -1
- package/dist/chunk-XMBDI725.js.map +0 -1
- package/dist/chunk-XVQVF2TK.js.map +0 -1
- /package/dist/{chunk-KISOMGY2.js.map → chunk-3RHU5MD7.js.map} +0 -0
- /package/dist/{chunk-3H25IRJJ.js.map → chunk-4MPAKYZU.js.map} +0 -0
- /package/dist/{chunk-IPCSZLLF.js.map → chunk-4NV7O76Q.js.map} +0 -0
- /package/dist/{chunk-BH7NVSEC.js.map → chunk-4SOWMA55.js.map} +0 -0
- /package/dist/{chunk-GROHOWR5.js.map → chunk-5MGKGEL7.js.map} +0 -0
- /package/dist/{chunk-ZPLQ27MY.js.map → chunk-5UM4H64E.js.map} +0 -0
- /package/dist/{chunk-2DNFRLLL.js.map → chunk-5Y6VA6X2.js.map} +0 -0
- /package/dist/{chunk-RAU25Q6J.js.map → chunk-6WKWL53V.js.map} +0 -0
- /package/dist/{chunk-KFPJIKUW.js.map → chunk-75DCOS6I.js.map} +0 -0
- /package/dist/{chunk-25QK3LR2.js.map → chunk-7O2MC42R.js.map} +0 -0
- /package/dist/{chunk-7ETWIBTO.js.map → chunk-ADYP7HXE.js.map} +0 -0
- /package/dist/{chunk-FUDKGJDN.js.map → chunk-AVGDYYMA.js.map} +0 -0
- /package/dist/{chunk-ECF627KC.js.map → chunk-AYJQ644F.js.map} +0 -0
- /package/dist/{chunk-T763ADEK.js.map → chunk-B3MWELZ3.js.map} +0 -0
- /package/dist/{chunk-DRTSPRWL.js.map → chunk-B4DXREWC.js.map} +0 -0
- /package/dist/{chunk-5UEGL5BF.js.map → chunk-CI3MAAIY.js.map} +0 -0
- /package/dist/{chunk-JYXFIILI.js.map → chunk-CTH2CQCB.js.map} +0 -0
- /package/dist/{chunk-UVHXLVFJ.js.map → chunk-EURPSF4Y.js.map} +0 -0
- /package/dist/{chunk-VPEA63FC.js.map → chunk-EWJZZDRG.js.map} +0 -0
- /package/dist/{chunk-BOZQAUAJ.js.map → chunk-EXQJMFRF.js.map} +0 -0
- /package/dist/{chunk-VC65GNUS.js.map → chunk-GGD2YDS6.js.map} +0 -0
- /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
- /package/dist/{chunk-FMYAU5MP.js.map → chunk-GJ4HU6LK.js.map} +0 -0
- /package/dist/{chunk-3NJJQAL3.js.map → chunk-H5DIRCZP.js.map} +0 -0
- /package/dist/{chunk-434E4BTO.js.map → chunk-HH73M65H.js.map} +0 -0
- /package/dist/{chunk-LIGZO7XN.js.map → chunk-HPB5TF55.js.map} +0 -0
- /package/dist/{chunk-E4QIX2OB.js.map → chunk-IAGVM7EY.js.map} +0 -0
- /package/dist/{chunk-VC7GKR2V.js.map → chunk-IG3EZ3TF.js.map} +0 -0
- /package/dist/{chunk-QAN5ZDRS.js.map → chunk-IVUVQPTQ.js.map} +0 -0
- /package/dist/{chunk-XOFIJGB2.js.map → chunk-IZBIDPMY.js.map} +0 -0
- /package/dist/{chunk-HFEGRMFG.js.map → chunk-JLGBJ4AX.js.map} +0 -0
- /package/dist/{chunk-BBHKN2CN.js.map → chunk-JLMOH5JA.js.map} +0 -0
- /package/dist/{chunk-TPCRDPUY.js.map → chunk-KZSGBWOJ.js.map} +0 -0
- /package/dist/{chunk-SHHZWSSE.js.map → chunk-KZZ5EZ7O.js.map} +0 -0
- /package/dist/{chunk-A5FOXI3B.js.map → chunk-LN4X2NDA.js.map} +0 -0
- /package/dist/{chunk-5ZHFPUBC.js.map → chunk-MOIJWWNA.js.map} +0 -0
- /package/dist/{chunk-KO224R36.js.map → chunk-NCZGGGWI.js.map} +0 -0
- /package/dist/{chunk-7ZTGMEXF.js.map → chunk-O5ZINWW3.js.map} +0 -0
- /package/dist/{chunk-WWH7GJ42.js.map → chunk-OCK4D5PC.js.map} +0 -0
- /package/dist/{chunk-G6L3REL3.js.map → chunk-OPFVXM2D.js.map} +0 -0
- /package/dist/{chunk-FR7PK7ED.js.map → chunk-P4TV3TQ2.js.map} +0 -0
- /package/dist/{chunk-ZGHPELZ6.js.map → chunk-PMDOGXA2.js.map} +0 -0
- /package/dist/{chunk-F32JRMX2.js.map → chunk-QGFR73SN.js.map} +0 -0
- /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
- /package/dist/{chunk-SH6ZYUTX.js.map → chunk-SXTVZ6JL.js.map} +0 -0
- /package/dist/{chunk-CLSKGTAT.js.map → chunk-TECK5IVY.js.map} +0 -0
- /package/dist/{chunk-SNS3DWG6.js.map → chunk-TVE7W67N.js.map} +0 -0
- /package/dist/{chunk-KYSSUY66.js.map → chunk-U2CAGMUD.js.map} +0 -0
- /package/dist/{chunk-XLCEFUAU.js.map → chunk-UVVFPCPX.js.map} +0 -0
- /package/dist/{chunk-PL75KQGF.js.map → chunk-UW6LBFYN.js.map} +0 -0
- /package/dist/{chunk-VSZNPAGL.js.map → chunk-V6SBA5O7.js.map} +0 -0
- /package/dist/{chunk-B7VD36TI.js.map → chunk-WHROVQ5Q.js.map} +0 -0
- /package/dist/{chunk-LDHR465K.js.map → chunk-Y3XPSGNF.js.map} +0 -0
- /package/dist/{chunk-E6VQJ75T.js.map → chunk-ZORS5BG5.js.map} +0 -0
- /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
|
|
4
|
+
import { ExtractionQueueCoordinator, type ExtractionPreemptHandle, type ExtractionQueueTask } from "./orchestration/extraction-queue-coordinator.js";
|
|
5
5
|
import { initLogger, type LoggerBackend } from "./logger.js";
|
|
6
6
|
import { abortError } from "./abort-error.js";
|
|
7
7
|
|
|
@@ -227,17 +227,17 @@ test("a front enqueue aborts the running automatic task and runs before queued w
|
|
|
227
227
|
const coordinator = createCoordinator();
|
|
228
228
|
const order: string[] = [];
|
|
229
229
|
const started = Promise.withResolvers<void>();
|
|
230
|
-
coordinator.enqueue(async () => {
|
|
231
|
-
const
|
|
232
|
-
|
|
230
|
+
coordinator.enqueue(async (preempt) => {
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
preempt?.arm(() => controller.abort());
|
|
233
233
|
started.resolve();
|
|
234
234
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const { promise, reject } = Promise.withResolvers<void>();
|
|
236
|
+
controller.signal.addEventListener("abort", () => reject(abortError("preempted")), { once: true });
|
|
237
|
+
await promise;
|
|
238
238
|
order.push("automatic-finished");
|
|
239
239
|
} finally {
|
|
240
|
-
|
|
240
|
+
preempt?.disarm();
|
|
241
241
|
}
|
|
242
242
|
});
|
|
243
243
|
await started.promise;
|
|
@@ -270,3 +270,174 @@ test("a front enqueue does not abort a task that did not register as preemptable
|
|
|
270
270
|
assert.deepEqual(order, ["protected", "flush"]);
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
|
|
274
|
+
// ── Bounded-parallel queue (extractionQueueConcurrency) ─────────────────────
|
|
275
|
+
|
|
276
|
+
function createParallelCoordinator(limit: number): ExtractionQueueCoordinator {
|
|
277
|
+
return new ExtractionQueueCoordinator(limit);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A parked queue task with deterministic start/abort/release signals. */
|
|
281
|
+
function makeParkedTask(preemptable: boolean) {
|
|
282
|
+
const started = Promise.withResolvers<void>();
|
|
283
|
+
const release = Promise.withResolvers<void>();
|
|
284
|
+
let didStart = false;
|
|
285
|
+
let outcome: "parked" | "finished" | "aborted" = "parked";
|
|
286
|
+
const task: ExtractionQueueTask = async (preempt?: ExtractionPreemptHandle) => {
|
|
287
|
+
didStart = true;
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
if (preemptable) {
|
|
290
|
+
preempt?.arm(() => controller.abort(abortError("preempted")));
|
|
291
|
+
controller.signal.addEventListener("abort", () => {
|
|
292
|
+
outcome = "aborted";
|
|
293
|
+
release.resolve();
|
|
294
|
+
}, { once: true });
|
|
295
|
+
}
|
|
296
|
+
started.resolve();
|
|
297
|
+
await release.promise;
|
|
298
|
+
if (outcome === "parked") outcome = "finished";
|
|
299
|
+
preempt?.disarm();
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
task,
|
|
303
|
+
startedPromise: started.promise,
|
|
304
|
+
release: () => release.resolve(),
|
|
305
|
+
get started() {
|
|
306
|
+
return didStart;
|
|
307
|
+
},
|
|
308
|
+
get outcome() {
|
|
309
|
+
return outcome;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
test("independent keys run concurrently up to the configured limit", async () => {
|
|
315
|
+
const coordinator = createParallelCoordinator(2);
|
|
316
|
+
const first = makeParkedTask(false);
|
|
317
|
+
const second = makeParkedTask(false);
|
|
318
|
+
const third = makeParkedTask(false);
|
|
319
|
+
coordinator.enqueue(first.task, { key: "session-a" });
|
|
320
|
+
coordinator.enqueue(second.task, { key: "session-b" });
|
|
321
|
+
coordinator.enqueue(third.task, { key: "session-c" });
|
|
322
|
+
await first.startedPromise;
|
|
323
|
+
await second.startedPromise;
|
|
324
|
+
assert.equal(third.started, false, "the third task must still be queued, not running: both slots are taken");
|
|
325
|
+
first.release();
|
|
326
|
+
await third.startedPromise;
|
|
327
|
+
second.release();
|
|
328
|
+
third.release();
|
|
329
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
330
|
+
assert.equal(first.outcome, "finished");
|
|
331
|
+
assert.equal(second.outcome, "finished");
|
|
332
|
+
assert.equal(third.outcome, "finished");
|
|
333
|
+
assert.equal(coordinator.isProcessing, false);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("tasks sharing a buffer key never run concurrently even with free capacity", async () => {
|
|
337
|
+
const coordinator = createParallelCoordinator(2);
|
|
338
|
+
const busy = makeParkedTask(false);
|
|
339
|
+
const queuedSameKey = makeParkedTask(false);
|
|
340
|
+
const otherKey = makeParkedTask(false);
|
|
341
|
+
coordinator.enqueue(busy.task, { key: "s1" });
|
|
342
|
+
await busy.startedPromise;
|
|
343
|
+
coordinator.enqueue(queuedSameKey.task, { key: "s1" });
|
|
344
|
+
coordinator.enqueue(otherKey.task, { key: "s2" });
|
|
345
|
+
await otherKey.startedPromise;
|
|
346
|
+
assert.equal(queuedSameKey.started, false, "the busy session's next task must not have started: its own key is still busy");
|
|
347
|
+
busy.release();
|
|
348
|
+
await queuedSameKey.startedPromise;
|
|
349
|
+
queuedSameKey.release();
|
|
350
|
+
otherKey.release();
|
|
351
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
352
|
+
assert.equal(queuedSameKey.outcome, "finished");
|
|
353
|
+
assert.equal(otherKey.outcome, "finished");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("the default limit stays serial", async () => {
|
|
357
|
+
const coordinator = createCoordinator();
|
|
358
|
+
assert.equal(coordinator.maxConcurrency, 1);
|
|
359
|
+
const first = makeParkedTask(false);
|
|
360
|
+
const second = makeParkedTask(false);
|
|
361
|
+
coordinator.enqueue(first.task, { key: "s1" });
|
|
362
|
+
coordinator.enqueue(second.task, { key: "s2" });
|
|
363
|
+
await first.startedPromise;
|
|
364
|
+
assert.equal(second.started, false, "limit 1 never starts a second task concurrently");
|
|
365
|
+
first.release();
|
|
366
|
+
await second.startedPromise;
|
|
367
|
+
second.release();
|
|
368
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
369
|
+
assert.equal(second.outcome, "finished");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a failing task does not stall the queue", async () => {
|
|
373
|
+
const { entries } = installCapturingLogger();
|
|
374
|
+
const coordinator = createParallelCoordinator(2);
|
|
375
|
+
const parked = makeParkedTask(false);
|
|
376
|
+
coordinator.enqueue(async () => {
|
|
377
|
+
throw new Error("extraction exploded");
|
|
378
|
+
}, { key: "s1" });
|
|
379
|
+
coordinator.enqueue(parked.task, { key: "s2" });
|
|
380
|
+
await parked.startedPromise;
|
|
381
|
+
parked.release();
|
|
382
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
383
|
+
assert.equal(parked.outcome, "finished");
|
|
384
|
+
assert.ok(entries.some((entry) => entry.level === "error" && /background extraction task failed/.test(entry.message)));
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("a front enqueue aborts exactly one armed running task when two run", async () => {
|
|
388
|
+
const coordinator = createParallelCoordinator(2);
|
|
389
|
+
const first = makeParkedTask(true);
|
|
390
|
+
const second = makeParkedTask(true);
|
|
391
|
+
let flushRan = false;
|
|
392
|
+
coordinator.enqueue(first.task, { key: "s1" });
|
|
393
|
+
coordinator.enqueue(second.task, { key: "s2" });
|
|
394
|
+
await first.startedPromise;
|
|
395
|
+
await second.startedPromise;
|
|
396
|
+
coordinator.enqueue(async () => {
|
|
397
|
+
flushRan = true;
|
|
398
|
+
}, { front: true });
|
|
399
|
+
assert.equal(second.outcome, "aborted", "the most recently armed task is the one preempted");
|
|
400
|
+
assert.equal(first.outcome, "parked", "the unrelated running extraction is not cancelled");
|
|
401
|
+
first.release();
|
|
402
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
403
|
+
assert.equal(flushRan, true);
|
|
404
|
+
assert.equal(first.outcome, "finished");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a front enqueue prefers the requested key's own running task", async () => {
|
|
408
|
+
const coordinator = createParallelCoordinator(2);
|
|
409
|
+
const other = makeParkedTask(true);
|
|
410
|
+
const mine = makeParkedTask(true);
|
|
411
|
+
let flushRan = false;
|
|
412
|
+
coordinator.enqueue(other.task, { key: "s-other" });
|
|
413
|
+
coordinator.enqueue(mine.task, { key: "s-mine" });
|
|
414
|
+
await other.startedPromise;
|
|
415
|
+
await mine.startedPromise;
|
|
416
|
+
coordinator.enqueue(async () => {
|
|
417
|
+
flushRan = true;
|
|
418
|
+
}, { front: true, key: "s-mine" });
|
|
419
|
+
assert.equal(mine.outcome, "aborted", "the flush preempts its own session's running extraction");
|
|
420
|
+
assert.equal(other.outcome, "parked", "the unrelated running extraction keeps going");
|
|
421
|
+
other.release();
|
|
422
|
+
assert.equal(await coordinator.waitForIdle(5_000), true);
|
|
423
|
+
assert.equal(flushRan, true);
|
|
424
|
+
assert.equal(other.outcome, "finished");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("pauseAndDrain waits for concurrent accepted work and rejects new producers", async () => {
|
|
428
|
+
const coordinator = createParallelCoordinator(2);
|
|
429
|
+
const first = makeParkedTask(false);
|
|
430
|
+
const second = makeParkedTask(false);
|
|
431
|
+
coordinator.enqueue(first.task, { key: "s1" });
|
|
432
|
+
coordinator.enqueue(second.task, { key: "s2" });
|
|
433
|
+
await first.startedPromise;
|
|
434
|
+
await second.startedPromise;
|
|
435
|
+
const drain = coordinator.pauseAndDrain(5_000);
|
|
436
|
+
assert.equal(coordinator.enqueue(async () => {}, { key: "s3" }), false);
|
|
437
|
+
first.release();
|
|
438
|
+
second.release();
|
|
439
|
+
assert.equal(await drain, true);
|
|
440
|
+
assert.equal(first.outcome, "finished");
|
|
441
|
+
assert.equal(second.outcome, "finished");
|
|
442
|
+
assert.equal(coordinator.isProcessing, false);
|
|
443
|
+
});
|
package/src/orchestrator.ts
CHANGED
|
@@ -1513,7 +1513,7 @@ export class Orchestrator {
|
|
|
1513
1513
|
this.storageRouter.onStorageCreated = bindStorage;
|
|
1514
1514
|
this.storageRouter.forEachCachedStorage(bindStorage);
|
|
1515
1515
|
// Issue #1526: background extraction queue lives on its own coordinator.
|
|
1516
|
-
this.extractionQueueCoordinator = new ExtractionQueueCoordinator();
|
|
1516
|
+
this.extractionQueueCoordinator = new ExtractionQueueCoordinator(config.extractionQueueConcurrency);
|
|
1517
1517
|
|
|
1518
1518
|
this.recallResultFormatter = new RecallResultFormatter(this.config);
|
|
1519
1519
|
const conversationIndexRuntime = createConversationIndexRuntime(config, {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ports over the StorageManager surface the profile mutation domain needs.
|
|
6
|
+
* Kept as ports so this module stays out of the storage.ts god-file (issue
|
|
7
|
+
* #1533/#1995) while sharing ONE serialization domain per baseDir: the
|
|
8
|
+
* `profile:<baseDir>` key is the in-process FIFO and `profile.md.lock` is the
|
|
9
|
+
* cross-process gate, matching updateMetaSerialized's meta.json.lock shape
|
|
10
|
+
* (bounded, fail-closed BEFORE any read/write, no LLM inside).
|
|
11
|
+
*/
|
|
12
|
+
export interface ProfileMutationPorts {
|
|
13
|
+
readonly baseDir: string;
|
|
14
|
+
readProfile(): Promise<string>;
|
|
15
|
+
writeProfile(content: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ProfileCommitOutcome = "committed" | "conflict";
|
|
19
|
+
|
|
20
|
+
function normalizeForDedup(s: string): string {
|
|
21
|
+
return s
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
24
|
+
.replace(/\s+/g, " ")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a new bullet is a fuzzy duplicate of any existing bullet.
|
|
30
|
+
* Returns true if the new bullet should be skipped.
|
|
31
|
+
*/
|
|
32
|
+
function isFuzzyDuplicate(newNorm: string, existingNorms: string[]): boolean {
|
|
33
|
+
for (const existing of existingNorms) {
|
|
34
|
+
// Exact normalized match
|
|
35
|
+
if (newNorm === existing) return true;
|
|
36
|
+
|
|
37
|
+
// Containment check: shorter must be >60% length of longer
|
|
38
|
+
const shorter = newNorm.length <= existing.length ? newNorm : existing;
|
|
39
|
+
const longer = newNorm.length > existing.length ? newNorm : existing;
|
|
40
|
+
if (shorter.length > 20 && shorter.length / longer.length > 0.6 && longer.includes(shorter)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function appendToProfileUnlocked(ports: ProfileMutationPorts, updates: string[]): Promise<void> {
|
|
48
|
+
// Filter out empty entries the LLM may return (persist screens strings already).
|
|
49
|
+
updates = updates.filter((u) => u.trim().length > 0);
|
|
50
|
+
if (updates.length === 0) return;
|
|
51
|
+
const existing = await ports.readProfile();
|
|
52
|
+
|
|
53
|
+
const existingNorms = (existing ? existing.split("\n") : [])
|
|
54
|
+
.filter((l) => l.startsWith("- "))
|
|
55
|
+
.map((l) => normalizeForDedup(l.slice(2).trim()));
|
|
56
|
+
const newBullets = updates.filter((u) => {
|
|
57
|
+
const norm = normalizeForDedup(u);
|
|
58
|
+
return !isFuzzyDuplicate(norm, existingNorms);
|
|
59
|
+
});
|
|
60
|
+
if (newBullets.length === 0) return;
|
|
61
|
+
|
|
62
|
+
await ports.writeProfile(
|
|
63
|
+
existing
|
|
64
|
+
? existing.trimEnd() + "\n" + newBullets.map((b) => `- ${b}`).join("\n") + "\n"
|
|
65
|
+
: ["# Behavioral Profile", "", ...newBullets.map((b) => `- ${b}`), ""].join("\n"),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cross-process profile mutation domain: fail-closed `profile.md.lock` for the
|
|
71
|
+
* brief mutation window only. Lock contention throws (`profile update skipped:
|
|
72
|
+
* profile.md.lock not acquired`) — never a silent unlocked RMW; callers treat
|
|
73
|
+
* it like a meta lock skip (retain and retry).
|
|
74
|
+
*/
|
|
75
|
+
async function withProfileMutationLock<T>(ports: ProfileMutationPorts, task: () => Promise<T>): Promise<T> {
|
|
76
|
+
return withHeldFileLock(
|
|
77
|
+
path.join(ports.baseDir, "profile.md.lock"),
|
|
78
|
+
// Bounded: a profile RMW is a short read-merge-write, never a scan.
|
|
79
|
+
{ staleMs: 5_000, maxWaitMs: 2_000, pollMs: 50 },
|
|
80
|
+
async (acquired) => {
|
|
81
|
+
if (!acquired) {
|
|
82
|
+
throw new Error(`profile update skipped: profile.md.lock not acquired (${ports.baseDir})`);
|
|
83
|
+
}
|
|
84
|
+
return task();
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function appendToProfileSerialized(ports: ProfileMutationPorts, updates: string[]): Promise<void> {
|
|
90
|
+
// Per-dir in-process chain, then the cross-process lock around the whole
|
|
91
|
+
// RMW: serialized, the second append re-reads after the first's write.
|
|
92
|
+
// Errors propagate: the extraction retry path retains the accepted turns.
|
|
93
|
+
await serializeMutations(`profile:${ports.baseDir}`, () =>
|
|
94
|
+
withProfileMutationLock(ports, () => appendToProfileUnlocked(ports, updates)),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Commit an LLM profile consolidation only if the profile is unchanged since
|
|
100
|
+
* `baseline` was read; when a concurrent append landed during the LLM call,
|
|
101
|
+
* return "conflict" and leave the file untouched (the append survives and the
|
|
102
|
+
* line cap re-triggers consolidation on fresh data next pass). The LLM call
|
|
103
|
+
* itself stays OUTSIDE this serialization domain; only this short
|
|
104
|
+
* read-compare-write is serialized + locked.
|
|
105
|
+
*/
|
|
106
|
+
export async function commitConsolidatedProfileSerialized(
|
|
107
|
+
ports: ProfileMutationPorts,
|
|
108
|
+
baseline: string,
|
|
109
|
+
consolidated: string,
|
|
110
|
+
): Promise<ProfileCommitOutcome> {
|
|
111
|
+
return serializeMutations(`profile:${ports.baseDir}`, () =>
|
|
112
|
+
withProfileMutationLock(ports, async () => {
|
|
113
|
+
const current = await ports.readProfile();
|
|
114
|
+
if (current !== baseline) return "conflict";
|
|
115
|
+
await ports.writeProfile(consolidated);
|
|
116
|
+
return "committed";
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { StorageManager } from "./storage.js";
|
|
7
|
+
import { withHeldFileLock } from "./utils/serialize-mutations.js";
|
|
8
|
+
|
|
9
|
+
async function makeStorage(): Promise<{ storage: StorageManager; cleanup: () => Promise<void> }> {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-profile-append-"));
|
|
11
|
+
return { storage: new StorageManager(root), cleanup: () => rm(root, { recursive: true, force: true }) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("concurrent appendToProfile bullets both survive the shared profile read-modify-write", async () => {
|
|
15
|
+
const { storage, cleanup } = await makeStorage();
|
|
16
|
+
try {
|
|
17
|
+
// Hold the FIRST write open while the second append runs a full RMW. Under
|
|
18
|
+
// a shared unlocked RMW both reads see the same (missing) profile and the
|
|
19
|
+
// second write silently drops the first task's bullet.
|
|
20
|
+
const realWrite = storage.writeProfile.bind(storage);
|
|
21
|
+
const firstWriteEntered = Promise.withResolvers<void>();
|
|
22
|
+
const firstWriteGate = Promise.withResolvers<void>();
|
|
23
|
+
let writes = 0;
|
|
24
|
+
// SAFETY: the test drives only readProfile/writeProfile on the real manager;
|
|
25
|
+
// instance overrides keep the RMW interception local to this test.
|
|
26
|
+
storage.writeProfile = async (content: string) => {
|
|
27
|
+
writes += 1;
|
|
28
|
+
if (writes === 1) {
|
|
29
|
+
firstWriteEntered.resolve();
|
|
30
|
+
await firstWriteGate.promise;
|
|
31
|
+
}
|
|
32
|
+
await realWrite(content);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const first = storage.appendToProfile(["bullet-a"]);
|
|
36
|
+
await firstWriteEntered.promise;
|
|
37
|
+
const second = storage.appendToProfile(["bullet-b"]);
|
|
38
|
+
firstWriteGate.resolve();
|
|
39
|
+
await Promise.all([first, second]);
|
|
40
|
+
|
|
41
|
+
const profile = await storage.readProfile();
|
|
42
|
+
assert.match(profile ?? "", /bullet-a/, "first task's bullet must survive");
|
|
43
|
+
assert.match(profile ?? "", /bullet-b/, "second task's bullet must survive");
|
|
44
|
+
} finally {
|
|
45
|
+
await cleanup();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("appendToProfile fails closed when a cross-process peer holds profile.md.lock", async () => {
|
|
50
|
+
const { storage, cleanup } = await makeStorage();
|
|
51
|
+
const peer = new StorageManager(storage["baseDir"]);
|
|
52
|
+
try {
|
|
53
|
+
const holderEntered = Promise.withResolvers<void>();
|
|
54
|
+
const holderRelease = Promise.withResolvers<void>();
|
|
55
|
+
// Foreign holder (simulated external drain process) owns the lock file.
|
|
56
|
+
const holder = withHeldFileLock(
|
|
57
|
+
path.join(storage["baseDir"], "profile.md.lock"),
|
|
58
|
+
{ staleMs: 60_000 },
|
|
59
|
+
async (acquired) => {
|
|
60
|
+
assert.ok(acquired, "test holder must acquire the lock");
|
|
61
|
+
holderEntered.resolve();
|
|
62
|
+
await holderRelease.promise;
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
await holderEntered.promise;
|
|
66
|
+
|
|
67
|
+
// The peer must NOT fall back to an unlocked RMW: fail closed, file untouched.
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
peer.appendToProfile(["lost bullet"]),
|
|
70
|
+
/profile\.md\.lock not acquired/,
|
|
71
|
+
);
|
|
72
|
+
const before = await storage.readProfile();
|
|
73
|
+
assert.doesNotMatch(before, /lost bullet/, "locked-out append must not write");
|
|
74
|
+
|
|
75
|
+
holderRelease.resolve();
|
|
76
|
+
await holder;
|
|
77
|
+
|
|
78
|
+
// Once the peer releases, the append goes through normally.
|
|
79
|
+
await peer.appendToProfile(["late bullet"]);
|
|
80
|
+
const after = await storage.readProfile();
|
|
81
|
+
assert.match(after, /late bullet/, "append after contention window must succeed");
|
|
82
|
+
} finally {
|
|
83
|
+
await cleanup();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("commitConsolidatedProfile commits unchanged baseline and conflicts on a stale one", async () => {
|
|
88
|
+
const { storage, cleanup } = await makeStorage();
|
|
89
|
+
try {
|
|
90
|
+
await storage.appendToProfile(["trait one", "trait two"]);
|
|
91
|
+
const baseline = await storage.readProfile();
|
|
92
|
+
|
|
93
|
+
// Unchanged profile → commit takes the consolidated rewrite.
|
|
94
|
+
const committed = await storage.commitConsolidatedProfile(
|
|
95
|
+
baseline,
|
|
96
|
+
"# Behavioral Profile\n\n- consolidated summary\n",
|
|
97
|
+
);
|
|
98
|
+
assert.equal(committed, "committed");
|
|
99
|
+
assert.match(await storage.readProfile(), /consolidated summary/);
|
|
100
|
+
|
|
101
|
+
// Stale baseline (profile moved on since the LLM saw it) → conflict, file untouched.
|
|
102
|
+
await storage.appendToProfile(["fresh concurrent trait"]);
|
|
103
|
+
const current = await storage.readProfile();
|
|
104
|
+
const conflict = await storage.commitConsolidatedProfile(
|
|
105
|
+
baseline,
|
|
106
|
+
"# Behavioral Profile\n\n- older stale rewrite\n",
|
|
107
|
+
);
|
|
108
|
+
assert.equal(conflict, "conflict");
|
|
109
|
+
assert.equal(await storage.readProfile(), current, "conflicting commit must not touch the file");
|
|
110
|
+
assert.match(await storage.readProfile(), /fresh concurrent trait/);
|
|
111
|
+
} finally {
|
|
112
|
+
await cleanup();
|
|
113
|
+
}
|
|
114
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -28,13 +28,14 @@ import { createTombstoneMigrationSourceContents } from "./storage/tombstone-migr
|
|
|
28
28
|
import { MemoryReadStore, readWindowedMemories, type WindowedMemoryReadOptions, type WindowedMemoryReadResult } from "./storage/memory-read-store.js";
|
|
29
29
|
import { runCommittedInvalidation } from "./storage/committed-invalidation.js";
|
|
30
30
|
import { renderProfileWithLastUpdated, stripOkfProfileFrontmatter, withOkfProfileFrontmatter } from "./storage/profile-header.js";
|
|
31
|
+
import { appendToProfileSerialized, commitConsolidatedProfileSerialized, type ProfileMutationPorts } from "./storage/profile-mutation.js";
|
|
31
32
|
import { parseQuestionFile as parseQuestionFileText } from "./storage/questions-file.js";
|
|
32
33
|
import { assertNotOkfReservedBasename, OKF_QUESTION_TYPE, okfTypeForEntityKind, okfTypeForMemory } from "./okf/type-mapping.js";
|
|
33
34
|
import { appendContextTransformRecords, readContextTransformRecords } from "./storage/context-transform-ledger.js";
|
|
34
35
|
import type { ContextTransformTelemetryRecord } from "./active-context-transform.js";
|
|
35
36
|
import { readMaybeEncryptedLines, readMemoryActionEventRowsFromLines } from "./storage/secure-line-reader.js";
|
|
36
37
|
import { parseOfflineSyncDigestCache, type OfflineSyncDigestCacheEntry } from "./offline-sync-digest-cache.js";
|
|
37
|
-
import { updateMetaSerialized
|
|
38
|
+
import { updateMetaSerialized } from "./utils/serialize-mutations.js";
|
|
38
39
|
import {
|
|
39
40
|
appendLifecycleEventsSerialized,
|
|
40
41
|
type DrainPendingLifecycleForSyncResult,
|
|
@@ -4296,57 +4297,25 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4296
4297
|
log.debug("updated profile.md");
|
|
4297
4298
|
}
|
|
4298
4299
|
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
return s
|
|
4302
|
-
.toLowerCase()
|
|
4303
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
4304
|
-
.replace(/\s+/g, " ")
|
|
4305
|
-
.trim();
|
|
4300
|
+
async appendToProfile(updates: string[]): Promise<void> {
|
|
4301
|
+
await appendToProfileSerialized(this.profileMutationPorts(), updates);
|
|
4306
4302
|
}
|
|
4307
4303
|
|
|
4308
|
-
/**
|
|
4309
|
-
*
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
if (newNorm === existing) return true;
|
|
4316
|
-
|
|
4317
|
-
// Containment check: shorter must be >60% length of longer
|
|
4318
|
-
const shorter = newNorm.length <= existing.length ? newNorm : existing;
|
|
4319
|
-
const longer = newNorm.length > existing.length ? newNorm : existing;
|
|
4320
|
-
if (shorter.length > 20 && shorter.length / longer.length > 0.6 && longer.includes(shorter)) {
|
|
4321
|
-
return true;
|
|
4322
|
-
}
|
|
4323
|
-
}
|
|
4324
|
-
return false;
|
|
4304
|
+
/** Commit an LLM profile consolidation only if the profile is unchanged
|
|
4305
|
+
* since `baseline` was read; see profile-mutation.ts for the race domain. */
|
|
4306
|
+
async commitConsolidatedProfile(
|
|
4307
|
+
baseline: string,
|
|
4308
|
+
consolidated: string,
|
|
4309
|
+
): Promise<"committed" | "conflict"> {
|
|
4310
|
+
return commitConsolidatedProfileSerialized(this.profileMutationPorts(), baseline, consolidated);
|
|
4325
4311
|
}
|
|
4326
4312
|
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
const lines = existing ? existing.split("\n") : [];
|
|
4334
|
-
const existingBulletRaw = lines.filter((l) => l.startsWith("- ")).map((l) => l.slice(2).trim());
|
|
4335
|
-
const existingNorms = existingBulletRaw.map(StorageManager.normalizeForDedup);
|
|
4336
|
-
|
|
4337
|
-
const newBullets = updates.filter((u) => {
|
|
4338
|
-
const norm = StorageManager.normalizeForDedup(u);
|
|
4339
|
-
return !StorageManager.isFuzzyDuplicate(norm, existingNorms);
|
|
4340
|
-
});
|
|
4341
|
-
if (newBullets.length === 0) return;
|
|
4342
|
-
|
|
4343
|
-
if (!existing) {
|
|
4344
|
-
const content = ["# Behavioral Profile", "", ...newBullets.map((b) => `- ${b}`), ""].join("\n");
|
|
4345
|
-
await this.writeProfile(content);
|
|
4346
|
-
} else {
|
|
4347
|
-
const withBullets = existing.trimEnd() + "\n" + newBullets.map((b) => `- ${b}`).join("\n") + "\n";
|
|
4348
|
-
await this.writeProfile(withBullets);
|
|
4349
|
-
}
|
|
4313
|
+
private profileMutationPorts(): ProfileMutationPorts {
|
|
4314
|
+
return {
|
|
4315
|
+
baseDir: this.baseDir,
|
|
4316
|
+
readProfile: () => this.readProfile(),
|
|
4317
|
+
writeProfile: (content) => this.writeProfile(content),
|
|
4318
|
+
};
|
|
4350
4319
|
}
|
|
4351
4320
|
|
|
4352
4321
|
/** Check if profile.md exceeds the max line cap and needs LLM consolidation */
|
package/src/types.ts
CHANGED
|
@@ -722,6 +722,8 @@ export interface PluginConfig
|
|
|
722
722
|
/** Distinct sessions the extraction buffer admits before backpressure (#3303). */
|
|
723
723
|
bufferMaxSessions: number;
|
|
724
724
|
bufferMaxMinutes: number;
|
|
725
|
+
/** Queue depth cap; default 1 = serial queue, same-buffer tasks never overlap. Applies at daemon start. */
|
|
726
|
+
extractionQueueConcurrency: number;
|
|
725
727
|
/**
|
|
726
728
|
* Debounce window (ms) for persisting the smart buffer to `state/buffer.json`
|
|
727
729
|
* (issue #1909). Steady-state buffering coalesces the whole-state serialize
|