@jjh32456/dsh-longlive 1.6.7 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -1,375 +0,0 @@
1
- // WP4a — WAL rotation / folding / pruning (DESIGN §3.5, §3.7, §5.4).
2
- //
3
- // These tests drive the WAL directly on the same on-disk layout the service
4
- // uses, plus one end-to-end crash-recovery case through the proposal engine and
5
- // queue rebuild. They assert real state transitions: segment names, chain
6
- // hashes across boundaries, snapshot contents, prune policy and ticket survival.
7
-
8
- import assert from 'node:assert/strict';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import test from 'node:test';
12
-
13
- import { normalizeRouterConfig } from '../../src/router/service/config.mjs';
14
- import { createProposalEngine } from '../../src/router/service/proposals.mjs';
15
- import { createQueueManager } from '../../src/router/service/queue.mjs';
16
- import { createWal, walLineHash } from '../../src/router/service/wal.mjs';
17
- import { readQueueFile } from '../../src/router/service/store.mjs';
18
- import { readJson, tempRoot, writeMemory } from './helpers.mjs';
19
-
20
- const DAY_MS = 24 * 60 * 60 * 1000;
21
-
22
- function walDir(prefix) {
23
- return path.join(tempRoot(prefix), 'vault', 'proposals');
24
- }
25
-
26
- function makeWal(prefix, options = {}) {
27
- const dir = walDir(prefix);
28
- const audit = [];
29
- const wal = createWal({
30
- file: path.join(dir, 'wal.jsonl'),
31
- audit: (kind, data = {}) => audit.push({ kind, ...data }),
32
- ...options,
33
- });
34
- return { dir, wal, audit, file: path.join(dir, 'wal.jsonl'), snapshotFile: path.join(dir, 'wal.snapshot.json') };
35
- }
36
-
37
- function rawLines(file) {
38
- if (!fs.existsSync(file)) return [];
39
- return fs.readFileSync(file, 'utf8').split('\n').filter((line) => line.trim() !== '').map((line) => JSON.parse(line));
40
- }
41
-
42
- function setup(prefix, configExtra = {}) {
43
- const temp = tempRoot(prefix);
44
- const memoryRoot = path.join(temp, 'memory');
45
- fs.mkdirSync(memoryRoot, { recursive: true });
46
- writeMemory(memoryRoot);
47
- const vaultDir = path.join(temp, 'vault');
48
- const config = normalizeRouterConfig({ proposalEnabled: true, applyEnabled: true, ...configExtra });
49
- const audit = [];
50
- const auditFn = (kind, data = {}) => audit.push({ kind, ...data });
51
- const paths = { vaultDir, proposalsDir: path.join(vaultDir, 'proposals'), liveRoot: memoryRoot };
52
- const walFile = path.join(paths.proposalsDir, 'wal.jsonl');
53
- const wal = createWal({ file: walFile, audit: auditFn });
54
- const engine = createProposalEngine({ config, paths, memoryRoot, audit: auditFn, now: () => new Date() });
55
- return {
56
- temp,
57
- memoryRoot,
58
- vaultDir,
59
- config,
60
- paths,
61
- audit,
62
- wal,
63
- engine,
64
- walFile,
65
- snapshotFile: path.join(paths.proposalsDir, 'wal.snapshot.json'),
66
- cleanup: () => fs.rmSync(temp, { recursive: true, force: true }),
67
- };
68
- }
69
-
70
- const TARGETS = Object.freeze({
71
- one: 'journal/2026-11.md',
72
- first: 'journal/2026-11.md',
73
- two: 'topics/two.md',
74
- second: 'topics/second.md',
75
- three: 'journal/2026-12.md',
76
- four: 'topics/four.md',
77
- });
78
-
79
- function propose(engine, content) {
80
- const target = TARGETS[content] ?? `topics/${content}.md`;
81
- return engine.propose({
82
- op: target.startsWith('journal/') ? 'journal.append' : 'topic.append',
83
- // One target per proposal: distinct targets avoid §5.8 small-write merge,
84
- // which would otherwise fold two test proposals into one.
85
- target,
86
- title: `t-${content}`,
87
- content,
88
- }, { caller: { kind: 'root', sessionId: 'root-session' } });
89
- }
90
-
91
- test('WAL-1: the line limit rotates before writing and names segments by first seq', () => {
92
- const env = makeWal('wal-rot-lines-', { maxLines: 3, maxBytes: 1_000_000 });
93
- try {
94
- for (let index = 1; index <= 8; index += 1) {
95
- env.wal.append('queue.reconcile', { reason: `n${index}` });
96
- }
97
- const segments = env.wal.listSegments().map((segment) => segment.name);
98
- assert.deepEqual(segments, ['wal.1.jsonl', 'wal.4.jsonl']);
99
- for (const segment of env.wal.listSegments()) {
100
- const first = rawLines(segment.file)[0];
101
- assert.equal(first.seq, segment.firstSeq, `${segment.name} starts at its seq`);
102
- }
103
- // Each archived segment is at or under the line limit (rotation happens
104
- // before the line that would exceed it).
105
- for (const segment of env.wal.listSegments()) {
106
- assert.ok(rawLines(segment.file).length <= 3, `${segment.name} respects maxLines`);
107
- }
108
- const state = env.wal.read();
109
- assert.equal(state.broken, false);
110
- assert.equal(state.lastSeq, 8);
111
- assert.deepEqual(state.lines.map((line) => line.seq), [7, 8]);
112
- // The whole physical chain still verifies when the snapshot is removed.
113
- fs.rmSync(env.snapshotFile);
114
- const replay = env.wal.read();
115
- assert.equal(replay.broken, false);
116
- assert.deepEqual(replay.lines.map((line) => line.seq), [1, 2, 3, 4, 5, 6, 7, 8]);
117
- for (const line of replay.lines) assert.equal(line.hash, walLineHash(line));
118
- } finally {
119
- fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
120
- }
121
- });
122
-
123
- test('WAL-2: the byte limit rotates and the active file stays bounded', () => {
124
- const env = makeWal('wal-rot-bytes-', { maxLines: 100_000, maxBytes: 400 });
125
- try {
126
- const padding = 'x'.repeat(260);
127
- for (let index = 1; index <= 6; index += 1) {
128
- env.wal.append('queue.reconcile', { reason: `${index}-${padding}` });
129
- }
130
- const segments = env.wal.listSegments();
131
- assert.ok(segments.length >= 2, `expected multiple segments, got ${segments.length}`);
132
- for (const segment of segments) {
133
- const bytes = fs.statSync(segment.file).size;
134
- assert.ok(bytes <= 400 + 512, `${segment.name} stayed near maxBytes (${bytes})`);
135
- }
136
- assert.equal(env.wal.read().broken, false);
137
- assert.equal(env.wal.read().lastSeq, 6);
138
- } finally {
139
- fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
140
- }
141
- });
142
-
143
- test('WAL-3: only the newest three archived segments are retained once older terminal lines are prunable', () => {
144
- const env = makeWal('wal-rot-keep-', { maxLines: 2, maxBytes: 1_000_000, keepSegments: 3 });
145
- try {
146
- const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
147
- for (let index = 1; index <= 10; index += 1) {
148
- env.wal.append('apply.done', { at: old, reason: `n${index}` });
149
- }
150
- const names = env.wal.listSegments().map((segment) => segment.name);
151
- assert.deepEqual(names, ['wal.3.jsonl', 'wal.5.jsonl', 'wal.7.jsonl']);
152
- assert.equal(fs.existsSync(path.join(env.dir, 'wal.1.jsonl')), false);
153
- assert.equal(env.wal.read().lastSeq, 10);
154
- assert.equal(env.wal.read().broken, false);
155
- } finally {
156
- fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
157
- }
158
- });
159
-
160
- test('WAL-4: snapshot folds non-terminal proposals, assign summaries, seqCounter and head', async () => {
161
- const env = setup('wal-rot-snapshot-');
162
- try {
163
- const first = await propose(env.engine, 'first');
164
- const second = await propose(env.engine, 'second');
165
- assert.equal(first.ok, true);
166
- assert.equal(second.ok, true);
167
- const approved = env.engine.approve({ id: first.id }, { caller: { kind: 'root', sessionId: 'root-session' } });
168
- assert.equal(approved.ok, true);
169
- const rotated = env.engine.wal.rotate({ force: true });
170
- assert.equal(rotated.rotated, true);
171
- assert.equal(rotated.snapshotError, null);
172
-
173
- // Read-back: the file on disk parses to the same snapshot the WAL returned.
174
- const snapshot = readJson(env.snapshotFile);
175
- assert.equal(snapshot.v, 1);
176
- assert.deepEqual(rotated.snapshot, snapshot);
177
- assert.equal(snapshot.upToSeq, 3); // 2 assigns + 1 approve
178
- assert.equal(snapshot.lastSeq, 3);
179
- assert.match(snapshot.lastHash, /^sha256:[0-9a-f]{64}$/u);
180
- assert.equal(snapshot.proposals.length, 2);
181
- assert.deepEqual(snapshot.proposals.map((proposal) => proposal.id).sort(), [first.id, second.id].sort());
182
- assert.ok(snapshot.proposals.every((proposal) => proposal.status === 'pending' || proposal.status === 'approved'));
183
- assert.deepEqual(
184
- snapshot.assignments.map((assignment) => [assignment.proposalId, assignment.ticket]).sort(),
185
- [[first.id, first.ticketName], [second.id, second.ticketName]].sort(),
186
- );
187
- assert.equal(snapshot.seqCounter, 3);
188
- const queue = readQueueFile(path.join(env.paths.proposalsDir, 'queue.json'));
189
- assert.deepEqual(snapshot.head, { ticket: queue.entries[0].ticket, proposalId: queue.entries[0].proposalId });
190
-
191
- // Replay after folding: no suffix lines remain, and the boundary hash matches.
192
- const state = env.engine.wal.read();
193
- assert.equal(state.broken, false);
194
- assert.equal(state.lines.length, 0);
195
- assert.equal(state.lastSeq, 3);
196
- assert.equal(state.boundaryLineSeen, true);
197
- // Atomic write leaves no tmp file behind.
198
- assert.equal(fs.readdirSync(env.paths.proposalsDir).filter((name) => name.includes('.tmp')).length, 0);
199
- } finally {
200
- env.cleanup();
201
- }
202
- });
203
-
204
- test('WAL-5: an assignment folded into the snapshot survives queue rebuild and wins over proposal drift', async () => {
205
- const env = setup('wal-rot-survive-');
206
- try {
207
- const first = await propose(env.engine, 'first');
208
- const second = await propose(env.engine, 'second');
209
- assert.equal(env.engine.wal.rotate({ force: true }).rotated, true);
210
-
211
- // Crash/restart shape: no active WAL, no queue.json, only snapshot+segment.
212
- fs.rmSync(env.walFile, { force: true });
213
- fs.rmSync(path.join(env.paths.proposalsDir, 'queue.json'), { force: true });
214
- const restartedWal = createWal({ file: env.walFile, audit: () => {} });
215
- const queue = createQueueManager({
216
- paths: env.paths,
217
- config: env.config,
218
- wal: restartedWal,
219
- audit: (kind, data = {}) => env.audit.push({ kind, ...data }),
220
- now: () => new Date(),
221
- });
222
- const rebuilt = queue.rebuild();
223
- assert.equal(rebuilt.recovered.length, 0);
224
- assert.equal(rebuilt.reconciled.length, 0);
225
- assert.equal(rebuilt.seqCounter, 3);
226
- assert.deepEqual(readQueueFile(path.join(env.paths.proposalsDir, 'queue.json')).entries.map((entry) => entry.ticket).sort(), [first.ticketName, second.ticketName].sort());
227
-
228
- // Propose-side drift: WAL wins, ticket restored, no new recovered assign.
229
- const proposalFile = path.join(env.paths.proposalsDir, `${first.id}.json`);
230
- const drifted = readJson(proposalFile);
231
- drifted.queue.ticket = 'q-0009';
232
- drifted.queue.seq = 9;
233
- fs.writeFileSync(proposalFile, `${JSON.stringify(drifted)}\n`);
234
- const repaired = queue.rebuild();
235
- assert.equal(repaired.recovered.length, 0);
236
- assert.equal(repaired.reconciled.length, 1);
237
- assert.equal(readJson(proposalFile).queue.ticket, first.ticketName);
238
- assert.ok(env.audit.some((row) => row.kind === 'wal.reconcile'));
239
- assert.equal(restartedWal.read().lines.filter((line) => line.ev === 'queue.assign.recovered').length, 0);
240
- assert.equal(second.ticketName, 'q-0002');
241
- } finally {
242
- env.cleanup();
243
- }
244
- });
245
-
246
- test('WAL-6: seven-day terminal events are pruned, incident records are not', () => {
247
- const env = makeWal('wal-rot-prune-', { maxLines: 1000, keepSegments: 0 });
248
- try {
249
- const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
250
- const recent = new Date(Date.now() - 60 * 1000).toISOString();
251
- const doneId = 'p-20261105T000000-aaaaaa';
252
- fs.mkdirSync(env.dir, { recursive: true });
253
- fs.writeFileSync(path.join(env.dir, `${doneId}.json`), JSON.stringify({
254
- v: 1,
255
- id: doneId,
256
- status: 'done',
257
- queue: { ticket: 'q-0001' },
258
- }));
259
- env.wal.append('queue.assign', { ticket: 'q-0001', proposalId: doneId, at: old });
260
- env.wal.append('apply.done', { proposalId: doneId, at: old }); // terminal via proposal status
261
- env.wal.append('lease.expired', { at: old });
262
- env.wal.append('apply.conflict', { at: old });
263
- env.wal.append('rollback.done', { at: old });
264
- env.wal.append('queue.cancel', { at: old, proposalId: doneId });
265
- env.wal.append('lease.release', { at: old });
266
- env.wal.append('queue.reconcile', { at: old });
267
- env.wal.append('apply.done', { at: recent, proposalId: 'other' });
268
- env.wal.rotate({ force: true });
269
- const segment = env.wal.listSegments()[0];
270
- const kept = rawLines(segment.file).map((line) => line.ev);
271
- assert.deepEqual(kept, ['lease.expired', 'apply.conflict', 'rollback.done', 'queue.reconcile', 'apply.done']);
272
- assert.equal(env.audit.filter((row) => row.kind === 'wal.pruned_event').length, 4);
273
- assert.ok(env.audit.every((row) => row.kind !== 'wal.prune_failed'));
274
- // Folded gaps are not a chain break: replay starts from the snapshot.
275
- assert.equal(env.wal.read().broken, false);
276
- } finally {
277
- fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
278
- }
279
- });
280
-
281
- test('WAL-7: without an audit archive nothing is pruned', () => {
282
- const dir = path.join(tempRoot('wal-rot-noaudit-'), 'vault', 'proposals');
283
- try {
284
- const file = path.join(dir, 'wal.jsonl');
285
- const wal = createWal({ file, maxLines: 1000, keepSegments: 0 });
286
- const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
287
- for (let index = 0; index < 4; index += 1) wal.append('apply.done', { at: old, seqNote: index });
288
- const rotated = wal.rotate({ force: true });
289
- assert.equal(rotated.rotated, true);
290
- const segment = wal.listSegments()[0];
291
- assert.equal(rawLines(segment.file).length, 4, 'incident evidence kept without an archive');
292
- assert.equal(fs.existsSync(path.join(dir, 'wal.snapshot.json')), true);
293
- } finally {
294
- fs.rmSync(path.dirname(path.dirname(dir)), { recursive: true, force: true });
295
- }
296
- });
297
-
298
- test('WAL-8: replay verifies the chain continuously across segments and stops at a break', () => {
299
- const dir = path.join(tempRoot('wal-rot-chain-'), 'vault', 'proposals');
300
- try {
301
- const file = path.join(dir, 'wal.jsonl');
302
- const wal = createWal({ file, maxLines: 1000 });
303
- for (let index = 1; index <= 4; index += 1) wal.append('queue.reconcile', { reason: `a${index}` });
304
- assert.equal(wal.rotate({ force: true }).rotated, true);
305
- for (let index = 5; index <= 8; index += 1) wal.append('queue.reconcile', { reason: `b${index}` });
306
- assert.equal(wal.rotate({ force: true }).rotated, true);
307
- assert.deepEqual(wal.listSegments().map((segment) => segment.name), ['wal.1.jsonl', 'wal.5.jsonl']);
308
-
309
- // Missing snapshot: replay both segments + active as one chain.
310
- fs.rmSync(path.join(dir, 'wal.snapshot.json'));
311
- const replay = wal.read();
312
- assert.equal(replay.broken, false);
313
- assert.deepEqual(replay.lines.map((line) => line.seq), [1, 2, 3, 4, 5, 6, 7, 8]);
314
- for (const line of replay.lines) assert.equal(line.hash, walLineHash(line));
315
-
316
- // Break the second line of the oldest segment: stop there, never guess.
317
- const segmentFile = wal.listSegments()[0].file;
318
- const broken = rawLines(segmentFile);
319
- broken[1].seq = 22;
320
- fs.writeFileSync(segmentFile, `${broken.map((line) => JSON.stringify(line)).join('\n')}\n`);
321
- const state = wal.read();
322
- assert.equal(state.broken, true);
323
- assert.equal(state.brokenAt, 1);
324
- assert.deepEqual(state.lines.map((line) => line.seq), [1]);
325
- assert.throws(() => wal.append('queue.reconcile', { reason: 'nope' }), (error) => error.code === 'E_WAL_BROKEN');
326
- } finally {
327
- fs.rmSync(path.dirname(path.dirname(dir)), { recursive: true, force: true });
328
- }
329
- });
330
-
331
- test('WAL-9: crash between rename and snapshot keeps every assignment and the ticket sequence', async () => {
332
- const env = setup('wal-rot-crash-');
333
- try {
334
- const one = await propose(env.engine, 'one');
335
- const two = await propose(env.engine, 'two');
336
- const three = await propose(env.engine, 'three');
337
- assert.equal(env.engine.wal.rotate({ force: true }).rotated, true);
338
- // Fault injection: the segment exists, the snapshot was never written.
339
- fs.rmSync(env.snapshotFile);
340
- assert.deepEqual(env.engine.wal.listSegments().map((segment) => segment.name), ['wal.1.jsonl']);
341
-
342
- const restartedWal = createWal({ file: env.walFile, audit: () => {} });
343
- const restartedEngine = createProposalEngine({
344
- config: env.config,
345
- paths: env.paths,
346
- memoryRoot: env.memoryRoot,
347
- audit: () => {},
348
- now: () => new Date(),
349
- });
350
- const four = await propose(restartedEngine, 'four');
351
- assert.equal(four.ok, true);
352
- assert.equal(four.ticket, 4);
353
- assert.equal(four.ticketName, 'q-0004');
354
- const replay = restartedWal.read();
355
- assert.equal(replay.broken, false);
356
- assert.equal(replay.lastSeq, 4);
357
- assert.equal(replay.lines.filter((line) => line.ev === 'queue.assign').length, 4);
358
-
359
- const queue = createQueueManager({
360
- paths: env.paths,
361
- config: env.config,
362
- wal: restartedWal,
363
- audit: () => {},
364
- now: () => new Date(),
365
- });
366
- const rebuilt = queue.rebuild();
367
- assert.equal(rebuilt.recovered.length, 0);
368
- assert.equal(rebuilt.reconciled.length, 0);
369
- const tickets = readQueueFile(path.join(env.paths.proposalsDir, 'queue.json')).entries.map((entry) => entry.ticket).sort();
370
- assert.deepEqual(tickets, [one.ticketName, two.ticketName, three.ticketName, four.ticketName].sort());
371
- assert.equal(new Set(tickets).size, 4);
372
- } finally {
373
- env.cleanup();
374
- }
375
- });
@@ -1,170 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import {
5
- normalizeRouterConfig,
6
- normalizeRouterConfigFromPlugin,
7
- ROUTER_CONFIG_KEYS,
8
- ROUTER_DEFAULTS,
9
- } from '../../src/router/service/config.mjs';
10
-
11
- const DESIGN_KEYS = [
12
- 'routerEnabled',
13
- 'routerProvider',
14
- 'routerModel',
15
- 'routerTickMs',
16
- 'routerReflectOnTurn',
17
- 'routerReflectCoalesceMs',
18
- 'routerAutonomy',
19
- 'routerAllowAgent',
20
- 'routerMaxTokensPerRun',
21
- 'routerMaxTokensPerDay',
22
- 'routerCallMaxTokens',
23
- 'routerMaxAttempts',
24
- 'routerMaxConsecutiveErrors',
25
- 'routerAnchorPath',
26
- 'routerStopFile',
27
- 'routerAllowMissingAnchor',
28
- 'routerVaultDir',
29
- 'routerAgentTimeoutMs',
30
- 'routerDisposeTimeoutMs',
31
- 'routerFlowSlice',
32
- 'routerSharedBoard',
33
- 'routerHintScope',
34
- 'routerHintTokens',
35
- 'routerReflectHintTokens',
36
- 'routerInjectEveryTurns',
37
- 'routerInjectStateMaxSessions',
38
- 'routerInjectMaxPerSessionPerDay',
39
- 'routerInjectMaxTotalTokens',
40
- 'routerLiveRowsMax',
41
- 'routerBoardMaxSessions',
42
- 'routerProposalEnabled',
43
- 'routerApplyEnabled',
44
- 'routerL4MergeEnabled',
45
- 'routerProposalMaxChars',
46
- 'routerProposalsPerSessionHour',
47
- 'routerMaxProposals',
48
- 'routerAgingSeconds',
49
- 'routerLeaseRenewMax',
50
- 'routerLeaseWaitTimeoutMs',
51
- 'routerBlockedOnUserTtlMs',
52
- 'routerProposalTtlH',
53
- 'routerWakeEnabled',
54
- 'routerWakeCooldownMs',
55
- 'routerWakeDailyQuota',
56
- 'routerWakeLogMaxBytes',
57
- 'routerStarvationAlertMs',
58
- 'routerQueueMaxBytes',
59
- 'routerWalMaxBytes',
60
- 'routerWriterLeaseMs',
61
- 'routerSnapshotKeepDays',
62
- ];
63
-
64
- test('the 50 DESIGN §8.1 keys are all present exactly once', () => {
65
- assert.equal(DESIGN_KEYS.length, 50);
66
- assert.equal(Object.keys(ROUTER_DEFAULTS).length, 50);
67
- assert.deepEqual(Object.values(ROUTER_CONFIG_KEYS).sort(), [...DESIGN_KEYS].sort());
68
- const cfg = normalizeRouterConfig({});
69
- for (const [short, long] of Object.entries(ROUTER_CONFIG_KEYS)) {
70
- assert.ok(short in cfg, `${short} (${long}) missing from normalized config`);
71
- }
72
- for (const long of DESIGN_KEYS) assert.equal(long in cfg, false, `normalized config must not keep ${long}`);
73
- assert.equal('maxQueue' in cfg, false);
74
- assert.equal('routerMaxQueue' in cfg, false);
75
- });
76
-
77
- test('DESIGN defaults for budget, injection and merge are authoritative', () => {
78
- const cfg = normalizeRouterConfig({ routerEnabled: true });
79
- assert.equal(cfg.maxTokensPerRun, 80000);
80
- assert.equal(cfg.maxTokensPerDay, 1000000);
81
- assert.equal(cfg.callMaxTokens, 2048);
82
- assert.equal(cfg.maxAttempts, 2);
83
- assert.equal(cfg.injectMaxPerSessionPerDay, 24);
84
- assert.equal(cfg.injectMaxTotalTokens, 12000);
85
- assert.equal(cfg.l4MergeEnabled, false);
86
- assert.equal(cfg.hintTokens, 700);
87
- assert.equal(cfg.reflectHintTokens, 150);
88
- assert.equal(cfg.flowSlice, 1200);
89
- assert.equal(cfg.snapshotKeepDays, 30);
90
- assert.deepEqual(cfg.warnings, []);
91
- });
92
-
93
- test('range-clamped keys clamp instead of falling back to default', () => {
94
- const high = normalizeRouterConfig({ routerHintTokens: 900, routerReflectHintTokens: 900, routerWriterLeaseMs: 999999 });
95
- assert.equal(high.hintTokens, 700);
96
- assert.equal(high.reflectHintTokens, 150);
97
- assert.equal(high.writerLeaseMs, 120000);
98
- assert.ok(high.warnings.includes('CONFIG_CLAMPED:hintTokens'));
99
- assert.ok(high.warnings.includes('CONFIG_CLAMPED:reflectHintTokens'));
100
- assert.ok(high.warnings.includes('CONFIG_CLAMPED:writerLeaseMs'));
101
-
102
- const low = normalizeRouterConfig({ routerHintTokens: 1, routerReflectHintTokens: 1, routerWriterLeaseMs: 1 });
103
- assert.equal(low.hintTokens, 200);
104
- assert.equal(low.reflectHintTokens, 50);
105
- assert.equal(low.writerLeaseMs, 60000);
106
-
107
- const notNumber = normalizeRouterConfig({ routerWriterLeaseMs: 'later' });
108
- assert.equal(notNumber.writerLeaseMs, 120000);
109
- assert.ok(notNumber.warnings.includes('CONFIG_INVALID:writerLeaseMs'));
110
- });
111
-
112
- test('legacy keys migrate on read, and the normalized object only carries new keys', () => {
113
- const merged = normalizeRouterConfig({ routerMergeEnabled: true });
114
- assert.equal(merged.l4MergeEnabled, true);
115
- assert.ok(merged.warnings.includes('CONFIG_MIGRATED:routerMergeEnabled'));
116
-
117
- const plural = normalizeRouterConfig({ routerProposalsEnabled: true });
118
- assert.equal(plural.proposalEnabled, true);
119
- assert.ok(plural.warnings.includes('CONFIG_MIGRATED:routerProposalsEnabled'));
120
-
121
- const lease = normalizeRouterConfig({ routerLeaseTtlMs: 90000 });
122
- assert.equal(lease.writerLeaseMs, 90000);
123
- assert.ok(lease.warnings.includes('CONFIG_MIGRATED:routerLeaseTtlMs'));
124
-
125
- // 新键优先,旧键不再写回。
126
- const both = normalizeRouterConfig({ routerMergeEnabled: true, routerL4MergeEnabled: false });
127
- assert.equal(both.l4MergeEnabled, false);
128
- assert.equal(both.warnings.some((warning) => warning.includes('MIGRATED')), false);
129
- });
130
-
131
- test('unknown router keys warn; non-router keys and removed routerMaxQueue are handled', () => {
132
- const cfg = normalizeRouterConfig({
133
- routerNope: 1,
134
- routerRuntime: 'off',
135
- routerPolicyProfile: 'x',
136
- unknownKey: 'ignored',
137
- routerMaxQueue: 999,
138
- });
139
- assert.ok(cfg.warnings.includes('CONFIG_UNKNOWN:routerNope'));
140
- assert.equal(cfg.warnings.some((warning) => warning.includes('unknownKey')), false);
141
- assert.equal(cfg.warnings.some((warning) => warning.includes('routerRuntime')), false);
142
- assert.ok(cfg.warnings.includes('CONFIG_DEPRECATED:routerMaxQueue'));
143
- });
144
-
145
- test('illegal values fall back to defaults with a warning', () => {
146
- const cfg = normalizeRouterConfig({
147
- routerFlowSlice: 'x',
148
- routerInjectEveryTurns: 0,
149
- routerHintScope: 'some',
150
- routerProposalMaxChars: 10,
151
- routerSharedBoard: 'yes',
152
- routerProvider: 42,
153
- });
154
- assert.equal(cfg.flowSlice, 1200);
155
- assert.equal(cfg.injectEveryTurns, 10);
156
- assert.equal(cfg.hintScope, 'root');
157
- assert.equal(cfg.proposalMaxChars, 4000);
158
- assert.equal(cfg.sharedBoard, false);
159
- assert.equal(cfg.provider, 'deepseek-official');
160
- for (const key of ['flowSlice', 'injectEveryTurns', 'hintScope', 'proposalMaxChars', 'sharedBoard', 'provider']) {
161
- assert.ok(cfg.warnings.includes(`CONFIG_INVALID:${key}`), `missing CONFIG_INVALID:${key}`);
162
- }
163
- });
164
-
165
- test('normalizeRouterConfigFromPlugin is the same pure normalizer', () => {
166
- const cfg = normalizeRouterConfigFromPlugin({ routerEnabled: true, routerProposalEnabled: true });
167
- assert.equal(cfg.enabled, true);
168
- assert.equal(cfg.proposalEnabled, true);
169
- assert.equal(cfg.warnings.length, 0);
170
- });
@@ -1,77 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import test from 'node:test';
5
-
6
- import { createRouterService } from '../../src/router/service/service.mjs';
7
- import { MockLlm } from '../../src/router/background/mock-llm.mjs';
8
- import { apply } from '../../src/index.js';
9
- import { tempRoot } from './helpers.mjs';
10
-
11
- test('a disabled service installs no timers or listeners and writes nothing', async () => {
12
- const temp = tempRoot('longlive-router-disabled-');
13
- const vaultDir = path.join(temp, 'vault');
14
- const memoryRoot = path.join(temp, 'memory');
15
- fs.mkdirSync(memoryRoot, { recursive: true });
16
- const service = createRouterService(
17
- { logger: () => ({ warn: () => {}, info: () => {} }), on: () => () => {} },
18
- { enabled: false, provider: 'mock', model: 'mock-1', vaultDir, memoryRoot },
19
- { llm: new MockLlm(), installTimers: true, installListener: true },
20
- );
21
- try {
22
- assert.equal(service.timers.size, 0);
23
- const result = await service.start();
24
- assert.deepEqual(result, { ok: false, code: 'E_DISABLED', state: 'stopped' });
25
- assert.equal(service.timers.size, 0);
26
- assert.equal(service.tickTimer, null);
27
- assert.equal(service.heartbeatTimer, null);
28
- assert.equal(service.firstTickTimer, null);
29
- assert.equal(service.listenerDisposers.length, 0);
30
- assert.equal(fs.existsSync(vaultDir), false);
31
- const status = service.status();
32
- assert.equal(status.state, 'stopped');
33
- assert.equal(status.nextTickAt, null);
34
- assert.deepEqual(status.warnings, []);
35
- } finally {
36
- await service.dispose();
37
- fs.rmSync(temp, { recursive: true, force: true });
38
- }
39
- });
40
-
41
- test('proposal flags do not bypass the master switch: still 22 tools and no .longlive', async () => {
42
- const temp = tempRoot('longlive-router-off-proposals-');
43
- const home = path.join(temp, 'home');
44
- const memoryRoot = path.join(temp, 'memory');
45
- fs.mkdirSync(memoryRoot, { recursive: true });
46
- fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
47
- const tools = [];
48
- const effects = [];
49
- const ctx = {
50
- tools: { register: (definition) => { tools.push(definition); return () => {}; } },
51
- on: () => () => {},
52
- logger: () => ({ warn: () => {}, info: () => {} }),
53
- effect: (fn) => { const dispose = fn(); effects.push(typeof dispose === 'function' ? dispose : () => {}); return effects.at(-1); },
54
- get: () => undefined,
55
- };
56
- try {
57
- const returned = await apply(ctx, {
58
- home,
59
- memoryRoot,
60
- env: {},
61
- heartbeatMs: 0,
62
- recoverOnStart: false,
63
- defineTool: (definition) => definition,
64
- routerRuntime: 'off',
65
- routerEnabled: false,
66
- routerProposalEnabled: true,
67
- routerApplyEnabled: true,
68
- routerProposalsEnabled: true,
69
- });
70
- assert.equal(returned, 22);
71
- assert.equal(tools.some((tool) => String(tool.name).startsWith('router_')), false);
72
- assert.equal(fs.existsSync(path.join(memoryRoot, '.longlive')), false);
73
- } finally {
74
- for (const dispose of effects.reverse()) dispose();
75
- fs.rmSync(temp, { recursive: true, force: true });
76
- }
77
- });