@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -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
- });