@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,307 +0,0 @@
1
- /**
2
- * identityCard 跨进程去重(1.3.1)。
3
- *
4
- * 缺陷:opened 只记在进程内 Map,`--session-id` 续跑时新进程从空开始,
5
- * step 1 又注入一张开机卡(VM-EXTRA-3)。
6
- *
7
- * 这里用「新 ctx 实例 + 同一 memoryRoot/sessionId」模拟新进程:每个
8
- * registerFeatures() 调用都拥有全新的闭包状态,等价于一个刚启动的宿主进程;
9
- * 磁盘上的 marker 是两次「进程」之间唯一的共享状态。
10
- */
11
- import test from 'node:test';
12
- import assert from 'node:assert/strict';
13
- import fs from 'node:fs';
14
- import os from 'node:os';
15
- import path from 'node:path';
16
-
17
- import { registerFeatures } from '../src/features.js';
18
- import {
19
- MARKER_PRUNE_LIMIT,
20
- markerPathFor,
21
- pruneCardMarkers,
22
- readCardMarker,
23
- safeSessionId,
24
- } from '../src/identity/card-marker.js';
25
- import {
26
- PreStepBus,
27
- makeUserMessageFactory,
28
- textMessage,
29
- } from './fixtures/pre-step-container.mjs';
30
-
31
- const BOOT_MARK = '【开机卡】';
32
- const STATE_TEXT = [
33
- '## 我在哪',
34
- 'identityCard 跨进程修复。',
35
- '## 在等什么',
36
- '等 marker 测试全绿。',
37
- '## 最近三件',
38
- '1. 读 features.js。',
39
- '2. 写 marker 模块。',
40
- '3. 跑测试。',
41
- ].join('\n');
42
-
43
- function agentOf(sessionId, origin = 'main') {
44
- return { session: { id: sessionId, header: { origin } } };
45
- }
46
-
47
- function makeMemory(t, state = STATE_TEXT) {
48
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'identity-marker-'));
49
- t.after(() => fs.rmSync(root, { recursive: true, force: true }));
50
- fs.writeFileSync(path.join(root, 'STATE.md'), state);
51
- return root;
52
- }
53
-
54
- function makeInstance({ memoryRoot, identity = {}, warnings = [] }) {
55
- const bus = new PreStepBus();
56
- const ctx = {
57
- on: bus.on.bind(bus),
58
- logger: () => ({
59
- warn: (message) => warnings.push(String(message)),
60
- info() {},
61
- error() {},
62
- }),
63
- effect() {},
64
- tools: { register() {} },
65
- };
66
- registerFeatures(ctx, {
67
- identityCard: {
68
- enabled: true,
69
- createUserMessage: makeUserMessageFactory(),
70
- ...identity,
71
- },
72
- }, { memoryRoot });
73
- return { bus, warnings };
74
- }
75
-
76
- async function compose(instance, sessionId, { texts = ['你好'], origin = 'main', step = 1 } = {}) {
77
- const messages = texts.map((text) => textMessage(text));
78
- const composed = await instance.bus.compose({
79
- messages,
80
- agent: agentOf(sessionId, origin),
81
- step,
82
- });
83
- return composed;
84
- }
85
-
86
- function cards(decision) {
87
- return (decision?.messages ?? []).filter((message) => message?.source?.kind === 'session-reference');
88
- }
89
-
90
- function isBootCard(message) {
91
- return typeof message?.content?.[0]?.text === 'string'
92
- && message.content[0].text.startsWith(BOOT_MARK);
93
- }
94
-
95
- test('① 同一进程两次 step 1 只注入一次,marker 形状正确', async (t) => {
96
- const root = makeMemory(t);
97
- const warnings = [];
98
- const instance = makeInstance({ memoryRoot: root, warnings });
99
-
100
- const first = await compose(instance, 'sess-same', { texts: ['第一问'] });
101
- assert.equal(cards(first.decision).length, 1);
102
- assert.equal(first.decision.messages[1].source.kind, 'session-reference');
103
- assert.ok(isBootCard(first.decision.messages[1]));
104
-
105
- const second = await compose(instance, 'sess-same', { texts: ['第二问'] });
106
- assert.equal(cards(second.decision).length, 0);
107
-
108
- const file = markerPathFor(root, 'sess-same');
109
- const marker = JSON.parse(fs.readFileSync(file, 'utf8'));
110
- assert.deepEqual(Object.keys(marker).sort(), ['at', 'openedAt', 'sessionId', 'v']);
111
- assert.equal(marker.v, 1);
112
- assert.equal(marker.sessionId, 'sess-same');
113
- assert.ok(Date.parse(marker.openedAt) > 0);
114
- assert.ok(Date.parse(marker.at) > 0);
115
- assert.deepEqual(warnings, []);
116
- });
117
-
118
- test('② 新进程(新 ctx,同 memoryRoot+sessionId)不再注入', async (t) => {
119
- const root = makeMemory(t);
120
-
121
- const firstProcess = makeInstance({ memoryRoot: root });
122
- const first = await compose(firstProcess, 'sess-resume');
123
- assert.equal(cards(first.decision).length, 1);
124
-
125
- // 这一行之前,第一个「进程」已经结束;第二个实例的 Map 是空的,只有磁盘
126
- // 上的 marker 能拦住重复注入。
127
- const secondProcess = makeInstance({ memoryRoot: root });
128
- const second = await compose(secondProcess, 'sess-resume', { texts: ['续跑第一问'] });
129
- assert.equal(cards(second.decision).length, 0);
130
- assert.deepEqual(second.decision.messages.map((message) => message.content[0].text), ['续跑第一问']);
131
- });
132
-
133
- test('③ 不同 sessionId 各自注入一次', async (t) => {
134
- const root = makeMemory(t);
135
- const instance = makeInstance({ memoryRoot: root });
136
-
137
- const a = await compose(instance, 'sess-a');
138
- const b = await compose(instance, 'sess-b');
139
- const aAgain = await compose(instance, 'sess-a', { texts: ['a 再问'] });
140
-
141
- assert.equal(cards(a.decision).length, 1);
142
- assert.equal(cards(b.decision).length, 1);
143
- assert.equal(cards(aAgain.decision).length, 0);
144
- assert.ok(fs.existsSync(markerPathFor(root, 'sess-a')));
145
- assert.ok(fs.existsSync(markerPathFor(root, 'sess-b')));
146
- });
147
-
148
- test('④ 子代理不注入、也不落 marker', async (t) => {
149
- const root = makeMemory(t);
150
- const instance = makeInstance({ memoryRoot: root });
151
-
152
- const sub = await compose(instance, 'sess-sub', { origin: 'subagent' });
153
- assert.equal(cards(sub.decision).length, 0);
154
- assert.equal(fs.existsSync(path.join(root, '.longlive', 'identity-card')), false);
155
- });
156
-
157
- test('⑤ 空卡(越界路径导致生成失败)不注入、不落 marker', async (t) => {
158
- const root = makeMemory(t);
159
- const warnings = [];
160
- const instance = makeInstance({
161
- memoryRoot: root,
162
- identity: { path: '../escape/STATE.md' },
163
- warnings,
164
- });
165
-
166
- const result = await compose(instance, 'sess-empty');
167
- assert.equal(cards(result.decision).length, 0);
168
- assert.equal(fs.existsSync(path.join(root, '.longlive', 'identity-card')), false);
169
- assert.equal(warnings.length, 1);
170
- assert.match(warnings[0], /E_PATH_OUTSIDE_ROOT/);
171
- });
172
-
173
- test('⑥ marker 落盘失败:本进程只注入一次、不抛,后续进程按已注入跳过', async (t) => {
174
- const root = makeMemory(t);
175
- const warnings = [];
176
- const instance = makeInstance({ memoryRoot: root, warnings });
177
-
178
- const realRenameSync = fs.renameSync;
179
- fs.renameSync = function failingRenameSync(from, to) {
180
- if (String(to).includes(`${path.sep}identity-card${path.sep}`)) {
181
- const error = new Error('simulated read-only marker dir');
182
- error.code = 'EACCES';
183
- throw error;
184
- }
185
- return realRenameSync.call(this, from, to);
186
- };
187
- try {
188
- const first = await compose(instance, 'sess-writefail');
189
- assert.equal(cards(first.decision).length, 1);
190
- const second = await compose(instance, 'sess-writefail', { texts: ['第二次'] });
191
- assert.equal(cards(second.decision).length, 0);
192
- } finally {
193
- fs.renameSync = realRenameSync;
194
- }
195
-
196
- assert.equal(warnings.length, 1);
197
- assert.match(warnings[0], /落盘失败/);
198
-
199
- // 抢占时写的占位 marker 还在:新进程读到它就不重复注入(fail-closed)。
200
- const marker = JSON.parse(fs.readFileSync(markerPathFor(root, 'sess-writefail'), 'utf8'));
201
- assert.equal(marker.openedAt, null);
202
- const resumed = makeInstance({ memoryRoot: root });
203
- const third = await compose(resumed, 'sess-writefail', { texts: ['恢复'] });
204
- assert.equal(cards(third.decision).length, 0);
205
- });
206
-
207
- test('⑦ 并发两个实例:O_EXCL 只有一个赢家', async (t) => {
208
- const root = makeMemory(t);
209
- const one = makeInstance({ memoryRoot: root });
210
- const two = makeInstance({ memoryRoot: root });
211
-
212
- const [left, right] = await Promise.all([
213
- compose(one, 'sess-race'),
214
- compose(two, 'sess-race'),
215
- ]);
216
- const injected = [...cards(left.decision), ...cards(right.decision)];
217
- assert.equal(injected.length, 1);
218
-
219
- const marker = JSON.parse(fs.readFileSync(markerPathFor(root, 'sess-race'), 'utf8'));
220
- assert.equal(marker.v, 1);
221
- assert.ok(Date.parse(marker.openedAt) > 0);
222
- });
223
-
224
- test('⑧ marker 损坏:跳过注入并只 warn 一次', async (t) => {
225
- const root = makeMemory(t);
226
- const file = markerPathFor(root, 'sess-corrupt');
227
- fs.mkdirSync(path.dirname(file), { recursive: true });
228
- fs.writeFileSync(file, '{"v":1,"sessionId":', 'utf8');
229
-
230
- const warnings = [];
231
- const instance = makeInstance({ memoryRoot: root, warnings });
232
- const first = await compose(instance, 'sess-corrupt');
233
- const second = await compose(instance, 'sess-corrupt', { texts: ['再来'] });
234
-
235
- assert.equal(cards(first.decision).length, 0);
236
- assert.equal(cards(second.decision).length, 0);
237
- assert.equal(warnings.length, 1);
238
- assert.match(warnings[0], /E_MARKER_CORRUPT/);
239
- // 损坏的 marker 不被覆盖,仍保持坏文件(fail-closed,不猜)。
240
- assert.equal(fs.readFileSync(file, 'utf8'), '{"v":1,"sessionId":');
241
- });
242
-
243
- test('⑨ prune 只删 30 天以上的 .json,且遵守数量上限', async (t) => {
244
- const root = makeMemory(t);
245
- const dir = path.join(root, '.longlive', 'identity-card');
246
- fs.mkdirSync(dir, { recursive: true });
247
- const now = Date.now();
248
- const old = new Date(now - 31 * 24 * 60 * 60 * 1000);
249
-
250
- const make = (name, mtime) => {
251
- const file = path.join(dir, name);
252
- fs.writeFileSync(file, JSON.stringify({ v: 1, sessionId: name, openedAt: new Date().toISOString(), at: new Date().toISOString() }));
253
- fs.utimesSync(file, mtime, mtime);
254
- return file;
255
- };
256
- const staleA = make('stale-a.json', old);
257
- const staleB = make('stale-b.json', old);
258
- const fresh = make('fresh.json', new Date(now - 60 * 1000));
259
-
260
- const dry = pruneCardMarkers(root, { now, limit: 1 });
261
- assert.equal(dry.deleted, 1);
262
- assert.equal(fs.existsSync(staleA) || fs.existsSync(staleB), true);
263
- assert.ok(fs.existsSync(fresh));
264
-
265
- const rest = pruneCardMarkers(root, { now, limit: MARKER_PRUNE_LIMIT });
266
- assert.equal(rest.deleted, 1);
267
- assert.equal(fs.existsSync(staleA), false);
268
- assert.equal(fs.existsSync(staleB), false);
269
- assert.ok(fs.existsSync(fresh));
270
- });
271
-
272
- test('⑩ safeSessionId 规范化、防路径穿越、保留可读 id', () => {
273
- assert.equal(safeSessionId('session-e219dedf'), 'session-e219dedf');
274
- const traversal = safeSessionId('../evil/id');
275
- assert.equal(traversal.includes('/'), false);
276
- assert.equal(traversal.includes('\\'), false);
277
- assert.equal(safeSessionId('..').startsWith('.'), false);
278
- assert.equal(safeSessionId('..').includes('/'), false);
279
- assert.notEqual(safeSessionId('会话 一'), safeSessionId('会话 二'));
280
- // 同一个 id 总是映射到同一个文件名。
281
- assert.equal(safeSessionId('a b'), safeSessionId('a b'));
282
- });
283
-
284
- test('⑪ marker 丢失时:无法证明已开过,会再注入一次(已知边界)', async (t) => {
285
- const root = makeMemory(t);
286
- const first = makeInstance({ memoryRoot: root });
287
- const injected = await compose(first, 'sess-lost');
288
- assert.equal(cards(injected.decision).length, 1);
289
-
290
- fs.rmSync(markerPathFor(root, 'sess-lost'), { force: true });
291
- const resumed = makeInstance({ memoryRoot: root });
292
- const again = await compose(resumed, 'sess-lost', { texts: ['marker 被删后'] });
293
- assert.equal(cards(again.decision).length, 1);
294
-
295
- // readCardMarker 对缺失文件返回 exists:false,而不是错误。
296
- const read = readCardMarker({ memoryRoot: root, sessionId: 'sess-lost' });
297
- assert.equal(read.ok, true);
298
- assert.equal(read.exists, true); // 第二次注入后又写回来了
299
- });
300
-
301
- test('⑫ 非 step 1 不注入、不落 marker', async (t) => {
302
- const root = makeMemory(t);
303
- const instance = makeInstance({ memoryRoot: root });
304
- const later = await compose(instance, 'sess-step2', { step: 2 });
305
- assert.equal(cards(later.decision).length, 0);
306
- assert.equal(fs.existsSync(path.join(root, '.longlive', 'identity-card')), false);
307
- });