@remnic/core 9.63.0 → 9.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +1 -1
- package/dist/access-admin-ops-surface.js +13 -13
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +14 -14
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +13 -13
- package/dist/access-cli.js +43 -43
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +13 -13
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +13 -13
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +22 -22
- package/dist/access-identity-continuity-surface.js +13 -13
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +13 -13
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +18 -18
- package/dist/access-namespace-preflight.js +13 -13
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +13 -13
- package/dist/access-operations-batch.js +14 -14
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +17 -17
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +13 -13
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +13 -13
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +13 -13
- package/dist/access-schema.d.ts +88 -88
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +13 -13
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +15 -15
- package/dist/briefing.js +5 -5
- package/dist/buffer.js +3 -3
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-CH627RRV.js → chunk-2AZBPQLK.js} +3 -3
- package/dist/{chunk-WFLBP5GC.js → chunk-4NZHHGVL.js} +25 -6
- package/dist/chunk-4NZHHGVL.js.map +1 -0
- package/dist/{chunk-LTJTP5YQ.js → chunk-4WW5FMUD.js} +2 -2
- package/dist/{chunk-HSBQJRV2.js → chunk-5LVSFKQB.js} +2 -2
- package/dist/{chunk-RFH7U5LT.js → chunk-5S7Y7AC6.js} +2 -2
- package/dist/{chunk-7BJYTX6Q.js → chunk-6VX2ZZH2.js} +2 -2
- package/dist/{chunk-4FFROSFG.js → chunk-7CJO52ET.js} +8 -8
- package/dist/{chunk-JFLKSD5V.js → chunk-A5BYPCDI.js} +2 -2
- package/dist/{chunk-EFWHCTYJ.js → chunk-ALKRPLWB.js} +2 -2
- package/dist/{chunk-JDI7G6EA.js → chunk-AUIAHNO7.js} +2 -2
- package/dist/{chunk-BHO6RHE5.js → chunk-BE5SVRFC.js} +2 -2
- package/dist/{chunk-QCECHYZL.js → chunk-E2GYBTSP.js} +24 -4
- package/dist/chunk-E2GYBTSP.js.map +1 -0
- package/dist/{chunk-3MA7JVXN.js → chunk-EJYAE2U7.js} +4 -4
- package/dist/{chunk-IZRNVNPV.js → chunk-F2FZEVDE.js} +2 -2
- package/dist/{chunk-AYIHDC5S.js → chunk-FNYIMSKI.js} +2 -2
- package/dist/{chunk-ZPGHA7EO.js → chunk-FU7OICXE.js} +7 -7
- package/dist/{chunk-4Y2RHVCV.js → chunk-GL4RHVHX.js} +2 -2
- package/dist/{chunk-HVKMJAEF.js → chunk-GUBT77YC.js} +2 -2
- package/dist/{chunk-4IZ2IT7D.js → chunk-J5AFG5JN.js} +3 -3
- package/dist/{chunk-QCI73Z7C.js → chunk-KP36WQBJ.js} +2 -2
- package/dist/{chunk-H23GIXJC.js → chunk-LAX6S5WS.js} +2 -2
- package/dist/{chunk-DTLF2SLZ.js → chunk-MMM7SDOJ.js} +2 -2
- package/dist/{chunk-KPML4Q3M.js → chunk-MQFFR6QB.js} +2 -2
- package/dist/{chunk-BBHFAVMP.js → chunk-NVSTZ2PN.js} +7 -7
- package/dist/{chunk-CALURYWR.js → chunk-OF6KQJ35.js} +2 -2
- package/dist/{chunk-JCJGYNIS.js → chunk-ORB56L3Y.js} +3 -3
- package/dist/{chunk-YIIC5IDU.js → chunk-P2BT5ALE.js} +1 -1
- package/dist/{chunk-BP6WMYLP.js → chunk-R7R4FSV2.js} +2 -2
- package/dist/{chunk-CLJ5QS6J.js → chunk-RBBYKXXK.js} +30 -24
- package/dist/{chunk-CLJ5QS6J.js.map → chunk-RBBYKXXK.js.map} +1 -1
- package/dist/{chunk-FF6E6HOT.js → chunk-RBJKE5LV.js} +16 -16
- package/dist/{chunk-PSNN25DW.js → chunk-S4MF77NV.js} +2 -2
- package/dist/{chunk-4HO3542O.js → chunk-S52C2DEM.js} +2 -2
- package/dist/{chunk-WMOJIOIG.js → chunk-SHSJP4D6.js} +2 -2
- package/dist/{chunk-CK3NQUSQ.js → chunk-U4CCRHN6.js} +2 -2
- package/dist/{chunk-TGSMU3F2.js → chunk-U5LYM2NS.js} +2 -2
- package/dist/{chunk-X6E7TT3A.js → chunk-UDL4DNBM.js} +2 -2
- package/dist/{chunk-WR2YKUIW.js → chunk-UKBIB3FP.js} +2 -2
- package/dist/{chunk-347ZR6D4.js → chunk-UM7A4CPT.js} +2 -2
- package/dist/{chunk-TIRJV2LW.js → chunk-USKANB65.js} +2 -2
- package/dist/{chunk-UDTME32W.js → chunk-UW74LP76.js} +2 -2
- package/dist/{chunk-YSR3DXGC.js → chunk-WLITCQAC.js} +2 -2
- package/dist/{chunk-LLGDVG52.js → chunk-ZSH6GJ2V.js} +3 -3
- package/dist/{chunk-DROAPP6N.js → chunk-ZXMNT75M.js} +3 -3
- package/dist/{cli-Bw9z-KqX.d.ts → cli-DJwNNDL1.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +32 -32
- package/dist/compounding/engine.js +5 -5
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/index.js +5 -5
- package/dist/convergence-refresh.js +7 -7
- package/dist/corpus-watermark.js +7 -7
- package/dist/entity-retrieval.js +5 -5
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +14 -14
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/extraction.js +9 -9
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +681 -681
- package/dist/index.js +43 -43
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +5 -5
- package/dist/operator-doctor-corpus.js +8 -8
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/maintenance.js +10 -10
- package/dist/orchestrator.js +43 -43
- package/dist/{public-http-DK-QwaBu.d.ts → public-http-43z1YT-k.d.ts} +1 -1
- package/dist/{qmd-D_62bYNl.d.ts → qmd-CBSxevyb.d.ts} +12 -1
- package/dist/qmd-preflight.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/qmd.js +3 -1
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/schemas.d.ts +117 -434
- package/dist/schemas.js +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/storage.js +4 -4
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +16 -16
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/orchestration/turn-ingestion.ts +24 -1
- package/src/orchestrator-flush.test.ts +142 -1
- package/src/qmd-process-group.test.ts +125 -0
- package/src/qmd.ts +30 -2
- package/src/schemas.test.ts +158 -0
- package/src/schemas.ts +45 -22
- package/dist/chunk-QCECHYZL.js.map +0 -1
- package/dist/chunk-WFLBP5GC.js.map +0 -1
- package/dist/{access-service-M4LgO141.d.ts → access-service-DBpEnlzA.d.ts} +64 -64
- /package/dist/{chunk-CH627RRV.js.map → chunk-2AZBPQLK.js.map} +0 -0
- /package/dist/{chunk-LTJTP5YQ.js.map → chunk-4WW5FMUD.js.map} +0 -0
- /package/dist/{chunk-HSBQJRV2.js.map → chunk-5LVSFKQB.js.map} +0 -0
- /package/dist/{chunk-RFH7U5LT.js.map → chunk-5S7Y7AC6.js.map} +0 -0
- /package/dist/{chunk-7BJYTX6Q.js.map → chunk-6VX2ZZH2.js.map} +0 -0
- /package/dist/{chunk-4FFROSFG.js.map → chunk-7CJO52ET.js.map} +0 -0
- /package/dist/{chunk-JFLKSD5V.js.map → chunk-A5BYPCDI.js.map} +0 -0
- /package/dist/{chunk-EFWHCTYJ.js.map → chunk-ALKRPLWB.js.map} +0 -0
- /package/dist/{chunk-JDI7G6EA.js.map → chunk-AUIAHNO7.js.map} +0 -0
- /package/dist/{chunk-BHO6RHE5.js.map → chunk-BE5SVRFC.js.map} +0 -0
- /package/dist/{chunk-3MA7JVXN.js.map → chunk-EJYAE2U7.js.map} +0 -0
- /package/dist/{chunk-IZRNVNPV.js.map → chunk-F2FZEVDE.js.map} +0 -0
- /package/dist/{chunk-AYIHDC5S.js.map → chunk-FNYIMSKI.js.map} +0 -0
- /package/dist/{chunk-ZPGHA7EO.js.map → chunk-FU7OICXE.js.map} +0 -0
- /package/dist/{chunk-4Y2RHVCV.js.map → chunk-GL4RHVHX.js.map} +0 -0
- /package/dist/{chunk-HVKMJAEF.js.map → chunk-GUBT77YC.js.map} +0 -0
- /package/dist/{chunk-4IZ2IT7D.js.map → chunk-J5AFG5JN.js.map} +0 -0
- /package/dist/{chunk-QCI73Z7C.js.map → chunk-KP36WQBJ.js.map} +0 -0
- /package/dist/{chunk-H23GIXJC.js.map → chunk-LAX6S5WS.js.map} +0 -0
- /package/dist/{chunk-DTLF2SLZ.js.map → chunk-MMM7SDOJ.js.map} +0 -0
- /package/dist/{chunk-KPML4Q3M.js.map → chunk-MQFFR6QB.js.map} +0 -0
- /package/dist/{chunk-BBHFAVMP.js.map → chunk-NVSTZ2PN.js.map} +0 -0
- /package/dist/{chunk-CALURYWR.js.map → chunk-OF6KQJ35.js.map} +0 -0
- /package/dist/{chunk-JCJGYNIS.js.map → chunk-ORB56L3Y.js.map} +0 -0
- /package/dist/{chunk-YIIC5IDU.js.map → chunk-P2BT5ALE.js.map} +0 -0
- /package/dist/{chunk-BP6WMYLP.js.map → chunk-R7R4FSV2.js.map} +0 -0
- /package/dist/{chunk-FF6E6HOT.js.map → chunk-RBJKE5LV.js.map} +0 -0
- /package/dist/{chunk-PSNN25DW.js.map → chunk-S4MF77NV.js.map} +0 -0
- /package/dist/{chunk-4HO3542O.js.map → chunk-S52C2DEM.js.map} +0 -0
- /package/dist/{chunk-WMOJIOIG.js.map → chunk-SHSJP4D6.js.map} +0 -0
- /package/dist/{chunk-CK3NQUSQ.js.map → chunk-U4CCRHN6.js.map} +0 -0
- /package/dist/{chunk-TGSMU3F2.js.map → chunk-U5LYM2NS.js.map} +0 -0
- /package/dist/{chunk-X6E7TT3A.js.map → chunk-UDL4DNBM.js.map} +0 -0
- /package/dist/{chunk-WR2YKUIW.js.map → chunk-UKBIB3FP.js.map} +0 -0
- /package/dist/{chunk-347ZR6D4.js.map → chunk-UM7A4CPT.js.map} +0 -0
- /package/dist/{chunk-TIRJV2LW.js.map → chunk-USKANB65.js.map} +0 -0
- /package/dist/{chunk-UDTME32W.js.map → chunk-UW74LP76.js.map} +0 -0
- /package/dist/{chunk-YSR3DXGC.js.map → chunk-WLITCQAC.js.map} +0 -0
- /package/dist/{chunk-LLGDVG52.js.map → chunk-ZSH6GJ2V.js.map} +0 -0
- /package/dist/{chunk-DROAPP6N.js.map → chunk-ZXMNT75M.js.map} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { runCommandWithTimeoutForTest } from "./qmd.js";
|
|
8
|
+
|
|
9
|
+
// Regression for issue #2456: on timeout/abort the bounded qmd subprocess
|
|
10
|
+
// launcher must be killed together with its descendants. The launcher runs
|
|
11
|
+
// detached (own process group) so a negative-pid signal reaches grandchildren
|
|
12
|
+
// the old child.kill("SIGKILL") left behind. POSIX only — Windows falls back
|
|
13
|
+
// to a direct child kill by design.
|
|
14
|
+
//
|
|
15
|
+
// Real timers by design: this suite kills real OS processes, so the timeout
|
|
16
|
+
// trigger and the exit polls run against the platform clock. Fake timers
|
|
17
|
+
// cannot deliver signals to a spawned `sleep`. Every poll awaits a condition
|
|
18
|
+
// (pid file exists, process gone), never a fixed sleep.
|
|
19
|
+
const isPosix = process.platform !== "win32";
|
|
20
|
+
|
|
21
|
+
function isAlive(pid: number): boolean {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function waitForExit(pid: number, deadlineMs = 10_000): Promise<boolean> {
|
|
31
|
+
const deadline = Date.now() + deadlineMs;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
if (!isAlive(pid)) return true;
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
35
|
+
}
|
|
36
|
+
return !isAlive(pid);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function setupFakeLauncher(): Promise<{ script: string; pidFile: string; dir: string }> {
|
|
40
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remnic-qmd-kill-"));
|
|
41
|
+
const script = path.join(dir, "fake-qmd");
|
|
42
|
+
const pidFile = path.join(dir, "grandchild.pid");
|
|
43
|
+
// Launcher spawns a grandchild that outlives it, then hangs until killed.
|
|
44
|
+
await fs.promises.writeFile(
|
|
45
|
+
script,
|
|
46
|
+
`#!/bin/sh\nsleep 30 &\necho $! > "${pidFile}"\nsleep 30\n`,
|
|
47
|
+
{ mode: 0o755 },
|
|
48
|
+
);
|
|
49
|
+
return { script, pidFile, dir };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readPidFile(pidFile: string, deadlineMs = 5_000): Promise<number> {
|
|
53
|
+
const deadline = Date.now() + deadlineMs;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
const text = await fs.promises.readFile(pidFile, "utf8");
|
|
57
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
58
|
+
assert.ok(Number.isInteger(pid) && pid > 0, `bad grandchild pid: ${text}`);
|
|
59
|
+
return pid;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (Date.now() >= deadline) throw err;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function cleanupSurvivors(pids: Array<number | undefined>, dir?: string): Promise<void> {
|
|
68
|
+
for (const pid of pids) {
|
|
69
|
+
if (pid === undefined || !isAlive(pid)) continue;
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, "SIGKILL");
|
|
72
|
+
} catch {
|
|
73
|
+
// Already gone.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (dir) await fs.promises.rm(dir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test(
|
|
80
|
+
"timeout kills qmd launcher process group including grandchildren",
|
|
81
|
+
{ skip: isPosix ? false : "POSIX process groups do not exist on Windows" },
|
|
82
|
+
async () => {
|
|
83
|
+
const { script, pidFile, dir } = await setupFakeLauncher();
|
|
84
|
+
let grandchildPid: number | undefined;
|
|
85
|
+
try {
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
runCommandWithTimeoutForTest(script, [], { timeoutMs: 500, label: "qmd fake" }),
|
|
88
|
+
(err: Error & { timedOut?: boolean }) => err.timedOut === true,
|
|
89
|
+
);
|
|
90
|
+
grandchildPid = await readPidFile(pidFile);
|
|
91
|
+
assert.ok(
|
|
92
|
+
await waitForExit(grandchildPid),
|
|
93
|
+
`grandchild ${grandchildPid} survived the group kill`,
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
await cleanupSurvivors([grandchildPid], dir);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
test(
|
|
102
|
+
"abort kills qmd launcher process group including grandchildren",
|
|
103
|
+
{ skip: isPosix ? false : "POSIX process groups do not exist on Windows" },
|
|
104
|
+
async () => {
|
|
105
|
+
const { script, pidFile, dir } = await setupFakeLauncher();
|
|
106
|
+
let grandchildPid: number | undefined;
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
try {
|
|
109
|
+
const run = runCommandWithTimeoutForTest(script, [], {
|
|
110
|
+
timeoutMs: 60_000,
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
label: "qmd fake",
|
|
113
|
+
});
|
|
114
|
+
setTimeout(() => controller.abort(), 300);
|
|
115
|
+
await assert.rejects(run, /aborted/);
|
|
116
|
+
grandchildPid = await readPidFile(pidFile);
|
|
117
|
+
assert.ok(
|
|
118
|
+
await waitForExit(grandchildPid),
|
|
119
|
+
`grandchild ${grandchildPid} survived the group kill`,
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
await cleanupSurvivors([grandchildPid], dir);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
package/src/qmd.ts
CHANGED
|
@@ -562,6 +562,11 @@ function runCommandWithTimeout(
|
|
|
562
562
|
const child = launchProcess(command, args, {
|
|
563
563
|
env: mergeEnv({ NO_COLOR: "1", ...options.env }),
|
|
564
564
|
stdio: ["ignore", "pipe", "pipe"],
|
|
565
|
+
// POSIX: own process group so a timeout/abort kill can take down qmd's
|
|
566
|
+
// grandchildren too (launcher scripts spawn workers that outlive the
|
|
567
|
+
// launcher itself). Issue #2456. Windows ignores this option here —
|
|
568
|
+
// kill falls back to a direct child.kill.
|
|
569
|
+
detached: process.platform !== "win32",
|
|
565
570
|
});
|
|
566
571
|
if (!child.stdout || !child.stderr) {
|
|
567
572
|
reject(new Error(`${label} failed to open stdio pipes`));
|
|
@@ -575,7 +580,7 @@ function runCommandWithTimeout(
|
|
|
575
580
|
const timer = setTimeout(() => {
|
|
576
581
|
settled = true;
|
|
577
582
|
cleanup();
|
|
578
|
-
child
|
|
583
|
+
killBoundedChild(child);
|
|
579
584
|
// Flag the deadline breach so classifyProbeFailure can tell a genuine
|
|
580
585
|
// timeout from a non-zero-exit error whose message merely embeds stderr.
|
|
581
586
|
reject(
|
|
@@ -589,7 +594,7 @@ function runCommandWithTimeout(
|
|
|
589
594
|
settled = true;
|
|
590
595
|
clearTimeout(timer);
|
|
591
596
|
cleanup();
|
|
592
|
-
child
|
|
597
|
+
killBoundedChild(child);
|
|
593
598
|
reject(abortError(`${label} aborted`));
|
|
594
599
|
};
|
|
595
600
|
const cleanup = () => {
|
|
@@ -636,6 +641,29 @@ function runProcessCommand(
|
|
|
636
641
|
});
|
|
637
642
|
}
|
|
638
643
|
|
|
644
|
+
// Kill a bounded subprocess and its descendants. On POSIX the child runs
|
|
645
|
+
// detached (own process group), so a negative-pid signal reaches the whole
|
|
646
|
+
// tree. Fall back to a direct kill on Windows, missing pid, or when the
|
|
647
|
+
// group is already gone. Never throws — callers use it on best-effort paths.
|
|
648
|
+
function killBoundedChild(child: CommandChildProcess): void {
|
|
649
|
+
const pid = child.pid;
|
|
650
|
+
if (pid !== undefined && process.platform !== "win32") {
|
|
651
|
+
try {
|
|
652
|
+
process.kill(-pid, "SIGKILL");
|
|
653
|
+
return;
|
|
654
|
+
} catch {
|
|
655
|
+
// Group already reaped or never became a leader — fall through.
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
child.kill("SIGKILL");
|
|
660
|
+
} catch {
|
|
661
|
+
// Already exited between the check and the kill.
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export { runCommandWithTimeout as runCommandWithTimeoutForTest };
|
|
666
|
+
|
|
639
667
|
// ---------------------------------------------------------------------------
|
|
640
668
|
// QMD Stdio Daemon Session (MCP over stdio child process)
|
|
641
669
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { ConsolidationResultSchema, ExtractionResultSchema } from "./schemas.js";
|
|
5
|
+
|
|
6
|
+
// Synthetic fixtures only — no real memory content (public repo).
|
|
7
|
+
const validFact = {
|
|
8
|
+
category: "fact",
|
|
9
|
+
content: "The API rate limit is 1000 requests per minute.",
|
|
10
|
+
confidence: 0.8,
|
|
11
|
+
tags: ["api"],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const validEntity = {
|
|
15
|
+
name: "acme-corp",
|
|
16
|
+
type: "company",
|
|
17
|
+
facts: ["Uses PostgreSQL for the main database."],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const validQuestion = {
|
|
21
|
+
question: "Which service enforces the rate limit?",
|
|
22
|
+
context: "The limit shape suggests an upstream gateway.",
|
|
23
|
+
priority: 0.4,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const validRelationship = {
|
|
27
|
+
source: "person-jane-doe",
|
|
28
|
+
target: "acme-corp",
|
|
29
|
+
label: "works at",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function baseResult() {
|
|
33
|
+
return {
|
|
34
|
+
facts: [validFact],
|
|
35
|
+
profileUpdates: ["Prefers concise status updates."],
|
|
36
|
+
entities: [validEntity],
|
|
37
|
+
questions: [validQuestion],
|
|
38
|
+
relationships: [validRelationship],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("ExtractionResultSchema salvages valid facts from a mixed-invalid array (#2455)", () => {
|
|
43
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
44
|
+
...baseResult(),
|
|
45
|
+
facts: [
|
|
46
|
+
validFact,
|
|
47
|
+
{ category: "not-a-category", content: "x", confidence: 0.5, tags: [] },
|
|
48
|
+
{ category: "fact", content: "missing confidence", tags: [] },
|
|
49
|
+
// procedure facts without steps fail the superRefine — salvage must
|
|
50
|
+
// respect refinement issues, not just base-shape issues
|
|
51
|
+
{ category: "procedure", content: "Deploy the service.", confidence: 0.9, tags: ["deploy"] },
|
|
52
|
+
"bare string is not a fact",
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(parsed.success, true);
|
|
57
|
+
assert.deepEqual(parsed.data?.facts, [validFact]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("ExtractionResultSchema fails when a non-empty facts array is wholly invalid (#2455)", () => {
|
|
61
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
62
|
+
...baseResult(),
|
|
63
|
+
facts: [
|
|
64
|
+
{ category: "not-a-category", content: "x", confidence: 0.5, tags: [] },
|
|
65
|
+
42,
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.equal(parsed.success, false);
|
|
70
|
+
assert.ok(
|
|
71
|
+
parsed.error.issues.some((issue) => issue.message.includes("no items matching the schema")),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("ExtractionResultSchema keeps explicitly empty arrays valid (#2455)", () => {
|
|
76
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
77
|
+
facts: [],
|
|
78
|
+
profileUpdates: [],
|
|
79
|
+
entities: [],
|
|
80
|
+
questions: [],
|
|
81
|
+
relationships: [],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(parsed.success, true);
|
|
85
|
+
assert.deepEqual(parsed.data?.facts, []);
|
|
86
|
+
assert.deepEqual(parsed.data?.questions, []);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("ExtractionResultSchema salvages valid entities from a mixed-invalid array (#2455)", () => {
|
|
90
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
91
|
+
...baseResult(),
|
|
92
|
+
entities: [
|
|
93
|
+
validEntity,
|
|
94
|
+
{ name: "bad-type", type: "organization", facts: ["nope"] },
|
|
95
|
+
{ name: "bad-facts", type: "person", facts: [17] },
|
|
96
|
+
null,
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(parsed.success, true);
|
|
101
|
+
assert.deepEqual(parsed.data?.entities, [validEntity]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("ExtractionResultSchema fails when a non-empty entities array is wholly invalid (#2455)", () => {
|
|
105
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
106
|
+
...baseResult(),
|
|
107
|
+
entities: [{ name: "bad-type", type: "organization", facts: ["nope"] }],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.equal(parsed.success, false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("ExtractionResultSchema salvages valid questions from a mixed-invalid array (#2455)", () => {
|
|
114
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
115
|
+
...baseResult(),
|
|
116
|
+
questions: [
|
|
117
|
+
validQuestion,
|
|
118
|
+
{ question: "no context", priority: 0.3 },
|
|
119
|
+
{ question: "bad priority", context: "out of range", priority: 5 },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.equal(parsed.success, true);
|
|
124
|
+
assert.deepEqual(parsed.data?.questions, [validQuestion]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("ExtractionResultSchema salvages valid profileUpdates and relationships (#2455)", () => {
|
|
128
|
+
const parsed = ExtractionResultSchema.safeParse({
|
|
129
|
+
...baseResult(),
|
|
130
|
+
profileUpdates: ["Prefers concise status updates.", 42, null],
|
|
131
|
+
relationships: [validRelationship, { source: "jane-doe", label: "missing target" }],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.equal(parsed.success, true);
|
|
135
|
+
assert.deepEqual(parsed.data?.profileUpdates, ["Prefers concise status updates."]);
|
|
136
|
+
assert.deepEqual(parsed.data?.relationships, [validRelationship]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("ExtractionResultSchema still accepts null/omitted relationships (#2455)", () => {
|
|
140
|
+
const nullParsed = ExtractionResultSchema.safeParse({ ...baseResult(), relationships: null });
|
|
141
|
+
assert.equal(nullParsed.success, true);
|
|
142
|
+
|
|
143
|
+
const omitted: Record<string, unknown> = { ...baseResult() };
|
|
144
|
+
delete omitted.relationships;
|
|
145
|
+
const omittedParsed = ExtractionResultSchema.safeParse(omitted);
|
|
146
|
+
assert.equal(omittedParsed.success, true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("ConsolidationResultSchema stays strict on mixed-invalid arrays (#2455)", () => {
|
|
150
|
+
const validItem = { existingId: "fact-1", action: "SKIP", reason: "still accurate" };
|
|
151
|
+
const parsed = ConsolidationResultSchema.safeParse({
|
|
152
|
+
items: [validItem, { existingId: "fact-2", action: "DESTROY", reason: "bad enum" }],
|
|
153
|
+
profileUpdates: [],
|
|
154
|
+
entityUpdates: [],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
assert.equal(parsed.success, false);
|
|
158
|
+
});
|
package/src/schemas.ts
CHANGED
|
@@ -278,27 +278,51 @@ export const ProactiveExtractionResultSchema = z.object({
|
|
|
278
278
|
),
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Salvage array for model-produced extraction output (issue #2455).
|
|
283
|
+
*
|
|
284
|
+
* Mixed valid+invalid items parse to the valid subset so one malformed
|
|
285
|
+
* element no longer discards the whole extraction. A non-empty array with
|
|
286
|
+
* zero valid items still fails, so bounded retry stays effective and wholly
|
|
287
|
+
* malformed output is never silently accepted as empty. Empty arrays stay
|
|
288
|
+
* valid. Consolidation schemas intentionally stay strict — do not use this
|
|
289
|
+
* for them.
|
|
290
|
+
*/
|
|
291
|
+
function salvageArray<T extends z.ZodTypeAny>(itemSchema: T) {
|
|
292
|
+
return z
|
|
293
|
+
.array(z.unknown())
|
|
294
|
+
.transform((items, ctx) => {
|
|
295
|
+
const valid: z.infer<T>[] = [];
|
|
296
|
+
for (const raw of items) {
|
|
297
|
+
const parsed = itemSchema.safeParse(raw);
|
|
298
|
+
if (parsed.success) {
|
|
299
|
+
valid.push(parsed.data);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (items.length > 0 && valid.length === 0) {
|
|
303
|
+
ctx.addIssue({
|
|
304
|
+
code: z.ZodIssueCode.custom,
|
|
305
|
+
message: "array contains no items matching the schema",
|
|
306
|
+
});
|
|
307
|
+
return z.NEVER;
|
|
308
|
+
}
|
|
309
|
+
return valid;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
export const ExtractionResultSchema = z.object({
|
|
282
|
-
facts:
|
|
283
|
-
.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.describe(
|
|
295
|
-
"Entities mentioned in the conversation with new facts about them.",
|
|
296
|
-
),
|
|
297
|
-
questions: z
|
|
298
|
-
.array(ExtractedQuestionSchema)
|
|
299
|
-
.describe(
|
|
300
|
-
"Zero to three source-grounded questions useful in future sessions. Return an empty array when the conversation supports none.",
|
|
301
|
-
),
|
|
314
|
+
facts: salvageArray(ExtractedFactSchema).describe(
|
|
315
|
+
"Extracted memories from the conversation. Include facts, preferences, corrections, and decisions. Only extract genuinely new, durable information — skip transient task state.",
|
|
316
|
+
),
|
|
317
|
+
profileUpdates: salvageArray(z.string()).describe(
|
|
318
|
+
"Updates to the user's behavioral profile. Each string is a standalone statement about the user's preferences, habits, or personality. Only include genuinely new insights.",
|
|
319
|
+
),
|
|
320
|
+
entities: salvageArray(EntityMentionSchema).describe(
|
|
321
|
+
"Entities mentioned in the conversation with new facts about them.",
|
|
322
|
+
),
|
|
323
|
+
questions: salvageArray(ExtractedQuestionSchema).describe(
|
|
324
|
+
"Zero to three source-grounded questions useful in future sessions. Return an empty array when the conversation supports none.",
|
|
325
|
+
),
|
|
302
326
|
identityReflection: z
|
|
303
327
|
.string()
|
|
304
328
|
.optional()
|
|
@@ -311,8 +335,7 @@ export const ExtractionResultSchema = z.object({
|
|
|
311
335
|
.optional()
|
|
312
336
|
.nullable()
|
|
313
337
|
.describe("A six-to-eight word title for this conversation segment."),
|
|
314
|
-
relationships:
|
|
315
|
-
.array(ExtractedRelationshipSchema)
|
|
338
|
+
relationships: salvageArray(ExtractedRelationshipSchema)
|
|
316
339
|
.optional()
|
|
317
340
|
.nullable()
|
|
318
341
|
.describe(
|