@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,266 +0,0 @@
1
- // @local/dsh-longlive — orchestrator (scripts/auto-truncate-run.ps1) contract
2
- //
3
- // The orchestrator closes the app, cuts the log through truncate-cli.mjs,
4
- // verifies the rewrite and restarts the app. These cases pin the parts that
5
- // used to be unguarded:
6
- // * the request's sessionPath must resolve to a file under the sessions root
7
- // (case-insensitive prefix compare after Resolve-Path normalisation);
8
- // * a refusal writes structured JSON with SESSION_PATH_OUTSIDE_ROOT and
9
- // exits 2 without touching the file;
10
- // * a close failure writes structured JSON with CLOSE_FAILED and exits 3
11
- // (exercised with -SimulateCloseFailure, which never looks at a process);
12
- // * a valid in-root request still passes -DryRun with exit 0.
13
- //
14
- // Nothing here closes an app or reads ~/.dsh/sessions: the sessions root, the
15
- // memory root and the "app" are all temp fixtures.
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import crypto from 'node:crypto';
20
- import { spawnSync } from 'node:child_process';
21
- import { fileURLToPath } from 'node:url';
22
-
23
- const HERE = path.dirname(fileURLToPath(import.meta.url));
24
- const ROOT = path.resolve(HERE, '..');
25
- const PS1 = path.join(ROOT, 'scripts', 'auto-truncate-run.ps1');
26
-
27
- function sha256(file) {
28
- return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
29
- }
30
-
31
- function writeRequest(file, values) {
32
- fs.mkdirSync(path.dirname(file), { recursive: true });
33
- fs.writeFileSync(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
34
- }
35
-
36
- function baseRequest({ sessionPath, appExe, keepTurns = 5 }) {
37
- return {
38
- version: 1,
39
- requestId: 'req-orchestrator-test',
40
- at: new Date().toISOString(),
41
- sessionPath,
42
- sessionId: 'session-orchestrator-test',
43
- reason: 'unit test',
44
- estimate: { events: 10, bytes: 100 },
45
- plan: { keepTurns, keepTokens: 1000 },
46
- app: { exe: appExe },
47
- pluginDir: ROOT,
48
- };
49
- }
50
-
51
- function runOrchestrator({ memoryRoot, requestFile, sessionsRoot, extra = [] }) {
52
- const args = [
53
- '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', PS1,
54
- '-MemoryRoot', memoryRoot,
55
- '-RequestFile', requestFile,
56
- '-SessionsRoot', sessionsRoot,
57
- ...extra,
58
- ];
59
- const run = spawnSync('powershell.exe', args, {
60
- cwd: ROOT,
61
- encoding: 'utf8',
62
- timeout: 120_000,
63
- });
64
- return {
65
- status: run.status,
66
- stdout: String(run.stdout ?? ''),
67
- stderr: String(run.stderr ?? ''),
68
- };
69
- }
70
-
71
- function readResultLog(memoryRoot) {
72
- const file = path.join(memoryRoot, '.longlive', 'logs', 'auto-truncate.log');
73
- if (!fs.existsSync(file)) return [];
74
- const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
75
- const out = [];
76
- for (const line of text.split(/\r?\n/)) {
77
- const trimmed = line.replace(/^\uFEFF/, '').trim();
78
- if (!trimmed.startsWith('{')) continue;
79
- try { out.push(JSON.parse(trimmed)); } catch { /* keep scanning */ }
80
- }
81
- return out;
82
- }
83
-
84
- function readRunLog(memoryRoot) {
85
- const file = path.join(memoryRoot, '.longlive', 'logs', 'auto-truncate-run.log');
86
- if (!fs.existsSync(file)) return '';
87
- return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
88
- }
89
-
90
- // The dry-run plan is a pretty-printed ConvertTo-Json object on stdout, mixed
91
- // with the "[timestamp] ..." run lines. Pull the first balanced object out.
92
- function parsePlanBlock(stdout) {
93
- const text = String(stdout);
94
- const start = text.indexOf('{');
95
- if (start < 0) return null;
96
- let depth = 0;
97
- let inString = false;
98
- let escaped = false;
99
- for (let i = start; i < text.length; i += 1) {
100
- const ch = text[i];
101
- if (inString) {
102
- if (escaped) escaped = false;
103
- else if (ch === '\\') escaped = true;
104
- else if (ch === '"') inString = false;
105
- continue;
106
- }
107
- if (ch === '"') { inString = true; continue; }
108
- if (ch === '{') depth += 1;
109
- else if (ch === '}') {
110
- depth -= 1;
111
- if (depth === 0) {
112
- try { return JSON.parse(text.slice(start, i + 1)); } catch { return null; }
113
- }
114
- }
115
- }
116
- return null;
117
- }
118
-
119
- export function runOrchestratorSuite({ runCase, assert, TMP }) {
120
- const base = path.join(TMP, 'orchestrator');
121
- const sessionsRoot = path.join(base, 'sessions');
122
- fs.mkdirSync(sessionsRoot, { recursive: true });
123
-
124
- runCase('O1', 'auto-truncate-run.ps1:ASCII 主体 + 硬杀关闭策略 + sessionPath 约束 + 结构化结果', () => {
125
- const bytes = fs.readFileSync(PS1);
126
- for (let i = 0; i < bytes.length; i += 1) {
127
- assert(bytes[i] < 0x80, `script byte ${i} is non-ASCII (0x${bytes[i].toString(16)})`);
128
- }
129
- const text = bytes.toString('ascii');
130
- for (const needle of [
131
- 'SessionsRoot', 'Resolve-Path', 'OrdinalIgnoreCase', 'StartsWith',
132
- 'SESSION_PATH_OUTSIDE_ROOT', 'CLOSE_FAILED', 'close-failed', 'SimulateCloseFailure',
133
- 'exit 3', '--memory-root',
134
- // hard-kill close, not a one-minute standoff
135
- 'CloseMainWindow', 'Stop-Process -Id', 'AddSeconds(3)', 'settleMs', 'gracefulMs',
136
- // the watchdog reports how far the cut drifted from the request plan
137
- 'snapshotEvents', 'eventsDelta', 'drifted', 'drift = $drift',
138
- ]) {
139
- assert(text.includes(needle), `orchestrator script is missing ${needle}`);
140
- }
141
- // The old flow waited 60 seconds for taskkill without /F, then swept by image
142
- // name. Neither may come back: the close has to be a one-shot polite signal
143
- // plus a PID-scoped force kill.
144
- assert(!/taskkill\s+\/[IMF]/i.test(text), 'the script still invokes taskkill by image name');
145
- assert(!/AddSeconds\((?!3\))/.test(text), 'the script still has a non-3-second grace window');
146
- assert(!text.includes('AddSeconds(60)') && !text.includes('AddSeconds(30)'),
147
- 'the script still waits 30/60 seconds for the app to close');
148
- assert(text.includes('Stop-Process -Id $proc.Id -Force'),
149
- 'the force kill is not scoped to a PID');
150
- // The drift numbers must ride on the success result, not only in the run log.
151
- const doneResult = text.slice(text.indexOf("status = 'done'"), text.indexOf('Remove-Item -LiteralPath $RequestFile'));
152
- assert(doneResult.includes('drift = $drift'), 'the done result does not carry the drift report');
153
- assert(!text.includes('RESULT close-failed') || text.includes('Write-Result'),
154
- 'close-failed path has no structured result');
155
- return 'script contract holds';
156
- });
157
-
158
- runCase('O2', 'sessionPath 在 sessions 根外:exit 2 + SESSION_PATH_OUTSIDE_ROOT,字节不变', () => {
159
- const caseDir = path.join(base, 'outside');
160
- const memoryRoot = path.join(caseDir, 'memory');
161
- const outsideDir = path.join(caseDir, 'elsewhere');
162
- fs.mkdirSync(memoryRoot, { recursive: true });
163
- fs.mkdirSync(outsideDir, { recursive: true });
164
- const fakeApp = path.join(caseDir, 'fake.exe');
165
- fs.writeFileSync(fakeApp, Buffer.from('not a real executable'));
166
- const sessionPath = path.join(outsideDir, 'session.v4.jsonl.zstd');
167
- fs.writeFileSync(sessionPath, Buffer.from('synthetic-log-bytes'));
168
- const requestFile = path.join(caseDir, 'request.json');
169
- writeRequest(requestFile, baseRequest({ sessionPath, appExe: fakeApp }));
170
-
171
- const before = sha256(sessionPath);
172
- const result = runOrchestrator({ memoryRoot, requestFile, sessionsRoot });
173
- assert(result.status === 2, `exit ${result.status} (stderr ${result.stderr.slice(0, 300)})`);
174
- const results = readResultLog(memoryRoot);
175
- assert(results.length === 1, `expected one structured result, got ${results.length}`);
176
- const entry = results[0];
177
- assert(entry.ok === false && entry.status === 'bad-request',
178
- `structured result ${entry.ok}/${entry.status}`);
179
- assert(entry.code === 'SESSION_PATH_OUTSIDE_ROOT', `structured code ${entry.code}`);
180
- assert(sha256(sessionPath) === before, 'refused run changed the session bytes');
181
- assert(readRunLog(memoryRoot).includes('SESSION_PATH_OUTSIDE_ROOT'),
182
- 'run log does not name the refusal');
183
- assert(fs.existsSync(requestFile), 'refused run removed the request');
184
- return 'outside-root path refused with structured JSON';
185
- });
186
-
187
- runCase('O3', 'sessionPath 在 sessions 根内 + -DryRun:exit 0,干跑声明硬杀策略,不关应用不写结果', () => {
188
- const caseDir = path.join(base, 'inside');
189
- const memoryRoot = path.join(caseDir, 'memory');
190
- fs.mkdirSync(memoryRoot, { recursive: true });
191
- const fakeApp = path.join(caseDir, 'fake.exe');
192
- fs.writeFileSync(fakeApp, Buffer.from('not a real executable'));
193
- const sessionDir = path.join(sessionsRoot, 'scope-fixtures', 'inside-session');
194
- fs.mkdirSync(sessionDir, { recursive: true });
195
- const sessionPath = path.join(sessionDir, 'session.v4.jsonl.zstd');
196
- fs.writeFileSync(sessionPath, Buffer.from('synthetic-log-bytes'));
197
- const requestFile = path.join(caseDir, 'request.json');
198
- writeRequest(requestFile, baseRequest({ sessionPath, appExe: fakeApp }));
199
-
200
- const before = sha256(sessionPath);
201
- const result = runOrchestrator({
202
- memoryRoot, requestFile, sessionsRoot, extra: ['-DryRun'],
203
- });
204
- assert(result.status === 0, `exit ${result.status} (stdout ${result.stdout.slice(0, 400)})`);
205
- assert(result.stdout.includes('"dryRun"'), 'dry-run printed no plan JSON');
206
- const plan = parsePlanBlock(result.stdout);
207
- assert(plan && plan.ok === true && plan.dryRun === true, 'dry-run JSON is not parseable');
208
- // A dry run has to say out loud which close policy the real run would use,
209
- // so an operator can see the hard kill before it happens.
210
- assert(plan.close && plan.close.policy === 'hard-kill',
211
- `dry-run close policy ${JSON.stringify(plan.close)}`);
212
- assert(plan.close.gracefulMs === 3000, `gracefulMs ${plan.close.gracefulMs}`);
213
- assert(plan.close.settleMs === 2000, `settleMs ${plan.close.settleMs}`);
214
- assert(String(plan.close.kill).includes('Stop-Process'),
215
- `close.kill ${plan.close.kill}`);
216
- assert(readResultLog(memoryRoot).length === 0, 'dry-run wrote a result line');
217
- assert(sha256(sessionPath) === before, 'dry-run changed the session bytes');
218
- assert(fs.existsSync(requestFile), 'dry-run removed the request');
219
- return 'in-root dry-run passes';
220
- });
221
-
222
- runCase('O4', '关闭失败:exit 3 + CLOSE_FAILED 结构化结果,会话与请求都保留', () => {
223
- const caseDir = path.join(base, 'close-failed');
224
- const memoryRoot = path.join(caseDir, 'memory');
225
- fs.mkdirSync(memoryRoot, { recursive: true });
226
- const fakeApp = path.join(caseDir, 'fake.exe');
227
- fs.writeFileSync(fakeApp, Buffer.from('not a real executable'));
228
- const sessionDir = path.join(sessionsRoot, 'scope-fixtures', 'close-session');
229
- fs.mkdirSync(sessionDir, { recursive: true });
230
- const sessionPath = path.join(sessionDir, 'session.v4.jsonl.zstd');
231
- fs.writeFileSync(sessionPath, Buffer.from('synthetic-log-bytes'));
232
- const requestFile = path.join(caseDir, 'request.json');
233
- writeRequest(requestFile, baseRequest({ sessionPath, appExe: fakeApp }));
234
-
235
- const beforeSha = sha256(sessionPath);
236
- const beforeFiles = fs.readdirSync(sessionDir).sort();
237
- const result = runOrchestrator({
238
- memoryRoot, requestFile, sessionsRoot, extra: ['-SimulateCloseFailure'],
239
- });
240
- assert(result.status === 3, `exit ${result.status} (stderr ${result.stderr.slice(0, 300)})`);
241
- const results = readResultLog(memoryRoot);
242
- assert(results.length === 1, `expected one structured result, got ${results.length}`);
243
- const entry = results[0];
244
- assert(entry.ok === false && entry.status === 'close-failed',
245
- `structured result ${entry.ok}/${entry.status}`);
246
- assert(entry.code === 'CLOSE_FAILED', `structured code ${entry.code}`);
247
- assert(entry.restored === false && entry.restart === false,
248
- 'close-failed result claims a restore or restart');
249
- assert(sha256(sessionPath) === beforeSha, 'close-failed run changed the session bytes');
250
- assert(fs.readdirSync(sessionDir).sort().join('|') === beforeFiles.join('|'),
251
- 'close-failed run created files beside the session');
252
- assert(fs.existsSync(requestFile), 'close-failed run removed the request');
253
- assert(readRunLog(memoryRoot).includes('simulated failure'),
254
- 'run log does not record the simulated close');
255
- // A failed close must never reach the force-kill or the cut: the session is
256
- // still open, so the only correct outcome is "nothing happened".
257
- const runLog = readRunLog(memoryRoot);
258
- assert(!runLog.includes('force-killing'), 'close-failed run still tried to force-kill');
259
- assert(!runLog.includes('Stop-Process'), 'close-failed run reached Stop-Process');
260
- assert(!runLog.includes('cut: cli exit='), 'close-failed run ran truncate-cli');
261
- assert(entry.drift === undefined, 'close-failed result carries a drift report');
262
- return 'close failure reported structurally without touching the app';
263
- });
264
- }
265
-
266
- export const ORCHESTRATOR_CASES = ['O1', 'O2', 'O3', 'O4'];
package/test/p1_probe.mjs DELETED
@@ -1,97 +0,0 @@
1
- // P1 runtime witness. Runs the plugin's own tools inside a throwaway home and
2
- // records EVERY file the fs layer is asked to open, so "this code never reads
3
- // the credential file" is observed rather than asserted from source text.
4
- //
5
- // The parent (run.mjs) plants a decoy credential file in the fake home, sets
6
- // HOME/USERPROFILE/DSH_HOME at it, then requires: (a) the canary read below is
7
- // in the witness list — proof the witness is live, not silently unpatched —
8
- // and (b) nothing the tools touched is the credential file or the real home.
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import { fileURLToPath } from 'node:url';
12
-
13
- const reads = [];
14
- const calls = [];
15
-
16
- // The fs layer accepts a path string, a Buffer, a file-descriptor number, or a
17
- // WHATWG URL — and Node's own ESM loader passes a URL when it reads a module
18
- // through our patched readFileSync. `path.resolve(String(url))` turns
19
- // `file:///C:/x/y.js` into `<cwd>\file:\C:\x\y.js`: a path that does not
20
- // exist, and that shares the real home's prefix whenever the package is
21
- // installed under ~/.dsh. Normalise URLs to real file paths first, or the
22
- // witness reports a phantom "the tools touched the real ~/.dsh".
23
- const toPath = (file) => {
24
- if (typeof file === 'number') return `<fd ${file}>`;
25
- if (file instanceof URL) {
26
- try { return fileURLToPath(file); } catch { return String(file); }
27
- }
28
- if (Buffer.isBuffer(file)) return file.toString();
29
- const text = String(file);
30
- if (text.startsWith('file:')) {
31
- try { return fileURLToPath(new URL(text)); } catch { return text; }
32
- }
33
- try { return path.resolve(text); } catch { return text; }
34
- };
35
-
36
- const original = {
37
- readFileSync: fs.readFileSync,
38
- openSync: fs.openSync,
39
- readFile: fs.readFile,
40
- createReadStream: fs.createReadStream,
41
- };
42
-
43
- // Keep every wrapper bound to the ORIGINAL implementation: no wrapper may call
44
- // another wrapper, or the same read would be recorded twice.
45
- fs.readFileSync = function witnessReadFileSync(file, ...rest) {
46
- reads.push(toPath(file));
47
- return original.readFileSync.call(fs, file, ...rest);
48
- };
49
- fs.openSync = function witnessOpenSync(file, ...rest) {
50
- reads.push(toPath(file));
51
- return original.openSync.call(fs, file, ...rest);
52
- };
53
- fs.readFile = function witnessReadFile(file, ...rest) {
54
- reads.push(toPath(file));
55
- return original.readFile.call(fs, file, ...rest);
56
- };
57
- fs.createReadStream = function witnessCreateReadStream(file, ...rest) {
58
- reads.push(toPath(file));
59
- return original.createReadStream.call(fs, file, ...rest);
60
- };
61
-
62
- const home = process.env.P1_PROBE_HOME;
63
- if (!home) {
64
- console.error('P1_PROBE_HOME is required');
65
- process.exit(2);
66
- }
67
- const memRoot = path.join(home, 'memory');
68
- const sessionFile = path.join(home, 'sessions', '--p1--', 'session-p1', 'session.v4.jsonl.zstd');
69
-
70
- // Canary: a read the witness MUST see. If this path is missing from `reads`,
71
- // the instrumentation failed and the credential check below would be vacuous.
72
- fs.readFileSync(path.join(home, 'canary.txt'), 'utf8');
73
-
74
- const tools = await import('../src/tools.js');
75
- const plugin = await import('../src/index.js');
76
-
77
- const run = (name, fn) => {
78
- try {
79
- const value = fn();
80
- calls.push({ name, ok: true, status: value?.status ?? null, code: value?.code ?? null });
81
- return value;
82
- } catch (error) {
83
- calls.push({ name, ok: false, thrown: String(error?.message ?? error) });
84
- return null;
85
- }
86
- };
87
-
88
- run('sessionHealth(list)', () => tools.sessionHealth({}, { home }));
89
- run('sessionHealth(latest)', () => tools.sessionHealth({ session: 'latest' }, { home }));
90
- run('sessionHealth(sessionPath)', () => tools.sessionHealth({ sessionPath: sessionFile }, { home }));
91
- run('sessionBackupPlan(dry)', () => tools.sessionBackupPlan({ sessionPath: sessionFile, dryRun: true }, { home }));
92
- run('sessionWake', () => tools.sessionWake({}, { memRoot }));
93
- run('leaseAcquire', () => tools.leaseAcquire({ holder: 'p1-probe', ttlSec: 30 }, { memRoot }));
94
-
95
- const payload = { reads, calls, inject: plugin.inject };
96
- // One JSON line on the last line of stdout: the parent parses this, nothing else.
97
- console.log('P1_WITNESS ' + JSON.stringify(payload));
@@ -1,184 +0,0 @@
1
- /**
2
- * 打包红线(0.9.0-rc.1 合并包版):安装时只带走 package.json 的 files 白名单。
3
- * 这里按 pack.mjs 的同一套 glob 语义把白名单复制到临时目录,再按安装后的真实
4
- * 路径逐个 import。跨目录 re-export、漏掉新模块的 files 条目都会在这里变成
5
- * ERR_MODULE_NOT_FOUND 或白名单覆盖失败。
6
- */
7
-
8
- import test from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import fs from 'node:fs';
11
- import os from 'node:os';
12
- import path from 'node:path';
13
- import { fileURLToPath, pathToFileURL } from 'node:url';
14
- import { VERSION } from '../src/version.js';
15
-
16
- const HERE = path.dirname(fileURLToPath(import.meta.url));
17
- const ROOT = path.resolve(HERE, '..');
18
- const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
19
- const SKIP_DIRS = new Set(['node_modules', '.git', '.longlive', 'dist', '.cache']);
20
-
21
- /** Same minimal glob as scripts/pack.mjs: ** crosses directories, * does not. */
22
- function globToRegExp(pattern) {
23
- let source = '';
24
- for (let i = 0; i < pattern.length; i += 1) {
25
- const char = pattern[i];
26
- if (char === '*') {
27
- if (pattern[i + 1] === '*') {
28
- i += 1;
29
- if (pattern[i + 1] === '/') {
30
- i += 1;
31
- source += '(?:.*/)?';
32
- } else {
33
- source += '.*';
34
- }
35
- } else {
36
- source += '[^/]*';
37
- }
38
- } else if (char === '?') {
39
- source += '[^/]';
40
- } else {
41
- source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
42
- }
43
- }
44
- return new RegExp(`^${source}$`);
45
- }
46
-
47
- function walk(dir, out = [], base = ROOT) {
48
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
49
- if (entry.isSymbolicLink()) continue;
50
- const abs = path.join(dir, entry.name);
51
- if (entry.isDirectory()) {
52
- if (SKIP_DIRS.has(entry.name)) continue;
53
- walk(abs, out, base);
54
- } else if (entry.isFile()) {
55
- out.push(path.relative(base, abs).split(path.sep).join('/'));
56
- }
57
- }
58
- return out;
59
- }
60
-
61
- function selectPublished() {
62
- const positives = PKG.files.filter((entry) => !entry.startsWith('!')).map((entry) => globToRegExp(entry));
63
- const negatives = PKG.files.filter((entry) => entry.startsWith('!')).map((entry) => globToRegExp(entry.slice(1)));
64
- const selected = [];
65
- for (const rel of walk(ROOT)) {
66
- if (positives.some((re) => re.test(rel)) && !negatives.some((re) => re.test(rel))) selected.push(rel);
67
- }
68
- // npm always includes package.json plus root README*/LICENSE*/CHANGELOG*.
69
- selected.push('package.json');
70
- for (const rel of walk(ROOT)) {
71
- if (!rel.includes('/') && /^(README|LICENSE|CHANGELOG)/.test(rel) && !negatives.some((re) => re.test(rel))) {
72
- selected.push(rel);
73
- }
74
- }
75
- return [...new Set(selected)].sort();
76
- }
77
-
78
- function installShallow(t) {
79
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-pkg-'));
80
- t.after(() => fs.rmSync(root, { recursive: true, force: true }));
81
- for (const rel of selectPublished()) {
82
- const target = path.join(root, rel);
83
- fs.mkdirSync(path.dirname(target), { recursive: true });
84
- fs.copyFileSync(path.join(ROOT, rel), target);
85
- }
86
- return root;
87
- }
88
-
89
- test('files 白名单覆盖每一个 src 文件与四件新能力的入口', () => {
90
- const published = new Set(selectPublished());
91
- for (const rel of walk(path.join(ROOT, 'src'))) {
92
- assert.ok(published.has(rel), `src 文件不在 files 白名单内:${rel}`);
93
- }
94
- for (const rel of [
95
- 'src/identity/index.js',
96
- 'src/identity/handover.js',
97
- 'src/recall/index.js',
98
- 'src/bus/bus.mjs',
99
- 'src/router/background/index.mjs',
100
- 'src/features.js',
101
- 'test/auto-recall.test.mjs',
102
- 'test/metrics.test.mjs',
103
- 'test/bus.test.mjs',
104
- 'test/feature-flags.test.mjs',
105
- 'test/identity-card-marker.test.mjs',
106
- 'test/run-rc.mjs',
107
- 'test/rc5-regressions.test.mjs',
108
- 'test/identity-router/run.mjs',
109
- 'test/fixtures/pre-step-container.mjs',
110
- 'test/fixtures/baseline-0.8.0-registration.json',
111
- 'test/router-background/router.test.mjs',
112
- 'scripts/resident-watcher.ps1',
113
- 'scripts/install-resident-watcher.ps1',
114
- 'scripts/lib/auto-truncate-run.ps1',
115
- ]) {
116
- assert.ok(published.has(rel), `files 白名单缺 ${rel}`);
117
- }
118
- assert.ok(published.has('test/p1_probe.mjs'), 'files 白名单缺 test/p1_probe.mjs');
119
- assert.ok(published.has('package.json'), 'files 白名单缺 package.json');
120
- });
121
-
122
- test('只带白名单安装后:没有 prototype,所有 src 相对 import 都留在包内', (t) => {
123
- const root = installShallow(t);
124
- assert.equal(fs.existsSync(path.join(root, 'prototype')), false, '安装包不该依赖白名单外的 prototype/');
125
-
126
- const offenders = [];
127
- for (const rel of walk(path.join(root, 'src'), [], root)) {
128
- if (!/\.(mjs|js)$/.test(rel)) continue;
129
- const file = path.join(root, rel);
130
- const text = fs.readFileSync(file, 'utf8');
131
- for (const match of text.matchAll(/\bfrom\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
132
- const spec = match[1] ?? match[2];
133
- if (!spec.startsWith('.')) continue;
134
- const resolved = path.resolve(path.dirname(file), spec);
135
- if (resolved !== root && !resolved.startsWith(root + path.sep)) {
136
- offenders.push(`${rel} -> ${spec}`);
137
- }
138
- if (!fs.existsSync(resolved)) offenders.push(`${rel} -> ${spec}(不存在)`);
139
- }
140
- }
141
- assert.deepEqual(offenders, [], `存在跨出包目录/缺失的 import:\n${offenders.join('\n')}`);
142
- });
143
-
144
- test(`R2-07 发布版本面:包版本 ${VERSION} + 活动流,文档同步 D2/D6 能力集`, () => {
145
- assert.equal(PKG.version, VERSION);
146
- const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
147
- const en = read('docs/README.en.md');
148
- assert.ok(en.includes('@jjh32456/dsh-longlive-panel@0.2.6'), '面板版本未统一到 0.2.6(scoped 包名)');
149
- assert.ok(en.includes(`jjh32456-dsh-longlive-${VERSION}.tgz`), `英文文档示例 tgz 不是 scoped 的 ${VERSION}`);
150
- assert.ok(en.includes('@jjh32456/dsh-longlive'), '英文文档缺少 scoped 包名');
151
- assert.equal(en.includes('dsh-longlive-panel@0.1.3'), false, '英文文档仍残留 0.1.3');
152
- assert.equal(en.includes('0.9.0-rc.1'), false, '英文文档仍残留 rc.1');
153
- const features = read('docs/FEATURES.md');
154
- assert.ok(features.startsWith(`# ${VERSION}`), `FEATURES.md 标题不是 # ${VERSION}`);
155
- assert.ok(features.includes('sharedBrain.activity'), 'FEATURES.md 未记录活动流开关');
156
- assert.equal(features.includes('0.9.0-rc.1'), false, 'FEATURES.md 仍残留 rc.1');
157
- });
158
-
159
- test('R2-08 自动截断默认关闭(代码 enabled=false / mode=dryrun,patch 显式同值)', () => {
160
- // 1.6.0 的硬验收(滥用面评估 §4.1):代码默认与包内 patch 对齐,都是
161
- // enabled:false / mode:dryrun。1.5.0 及以前代码兜底是 enabled:true /
162
- // mode:'log',老配置漏写 autoTruncate 时会自动开始 log;这里钉住两者一致。
163
- const yml = fs.readFileSync(path.join(ROOT, 'cordis.patch.yml'), 'utf8');
164
- const block = yml.match(/autoTruncate:\s*\r?\n\s+enabled:\s*false\s*\r?\n\s+mode:\s*['"]?dryrun['"]?/);
165
- assert.ok(block, 'cordis.patch.yml 的 session-longlive 配置必须显式写 autoTruncate enabled:false / mode:dryrun');
166
- });
167
-
168
- test('只带白名单安装后每个新入口都能 import()', async (t) => {
169
- const root = installShallow(t);
170
- for (const rel of [
171
- 'src/index.js',
172
- 'src/features.js',
173
- 'src/metrics/index.mjs',
174
- 'src/identity/index.js',
175
- 'src/recall/index.js',
176
- 'src/bus/bus.mjs',
177
- 'src/router/background/index.mjs',
178
- ]) {
179
- const file = path.join(root, rel);
180
- assert.equal(fs.existsSync(file), true, `入口不在白名单复制结果里:${rel}`);
181
- const mod = await import(`${pathToFileURL(file).href}?packaging=${Date.now()}-${Math.random()}`);
182
- assert.ok(mod && typeof mod === 'object', `${rel} 没有导出对象`);
183
- }
184
- });