@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,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,182 +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
-
15
- const HERE = path.dirname(fileURLToPath(import.meta.url));
16
- const ROOT = path.resolve(HERE, '..');
17
- const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
18
- const SKIP_DIRS = new Set(['node_modules', '.git', '.longlive', 'dist', '.cache']);
19
-
20
- /** Same minimal glob as scripts/pack.mjs: ** crosses directories, * does not. */
21
- function globToRegExp(pattern) {
22
- let source = '';
23
- for (let i = 0; i < pattern.length; i += 1) {
24
- const char = pattern[i];
25
- if (char === '*') {
26
- if (pattern[i + 1] === '*') {
27
- i += 1;
28
- if (pattern[i + 1] === '/') {
29
- i += 1;
30
- source += '(?:.*/)?';
31
- } else {
32
- source += '.*';
33
- }
34
- } else {
35
- source += '[^/]*';
36
- }
37
- } else if (char === '?') {
38
- source += '[^/]';
39
- } else {
40
- source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
41
- }
42
- }
43
- return new RegExp(`^${source}$`);
44
- }
45
-
46
- function walk(dir, out = [], base = ROOT) {
47
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
48
- if (entry.isSymbolicLink()) continue;
49
- const abs = path.join(dir, entry.name);
50
- if (entry.isDirectory()) {
51
- if (SKIP_DIRS.has(entry.name)) continue;
52
- walk(abs, out, base);
53
- } else if (entry.isFile()) {
54
- out.push(path.relative(base, abs).split(path.sep).join('/'));
55
- }
56
- }
57
- return out;
58
- }
59
-
60
- function selectPublished() {
61
- const positives = PKG.files.filter((entry) => !entry.startsWith('!')).map((entry) => globToRegExp(entry));
62
- const negatives = PKG.files.filter((entry) => entry.startsWith('!')).map((entry) => globToRegExp(entry.slice(1)));
63
- const selected = [];
64
- for (const rel of walk(ROOT)) {
65
- if (positives.some((re) => re.test(rel)) && !negatives.some((re) => re.test(rel))) selected.push(rel);
66
- }
67
- // npm always includes package.json plus root README*/LICENSE*/CHANGELOG*.
68
- selected.push('package.json');
69
- for (const rel of walk(ROOT)) {
70
- if (!rel.includes('/') && /^(README|LICENSE|CHANGELOG)/.test(rel) && !negatives.some((re) => re.test(rel))) {
71
- selected.push(rel);
72
- }
73
- }
74
- return [...new Set(selected)].sort();
75
- }
76
-
77
- function installShallow(t) {
78
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-pkg-'));
79
- t.after(() => fs.rmSync(root, { recursive: true, force: true }));
80
- for (const rel of selectPublished()) {
81
- const target = path.join(root, rel);
82
- fs.mkdirSync(path.dirname(target), { recursive: true });
83
- fs.copyFileSync(path.join(ROOT, rel), target);
84
- }
85
- return root;
86
- }
87
-
88
- test('files 白名单覆盖每一个 src 文件与四件新能力的入口', () => {
89
- const published = new Set(selectPublished());
90
- for (const rel of walk(path.join(ROOT, 'src'))) {
91
- assert.ok(published.has(rel), `src 文件不在 files 白名单内:${rel}`);
92
- }
93
- for (const rel of [
94
- 'src/identity/index.js',
95
- 'src/identity/handover.js',
96
- 'src/recall/index.js',
97
- 'src/bus/bus.mjs',
98
- 'src/router/background/index.mjs',
99
- 'src/features.js',
100
- 'test/auto-recall.test.mjs',
101
- 'test/metrics.test.mjs',
102
- 'test/bus.test.mjs',
103
- 'test/feature-flags.test.mjs',
104
- 'test/identity-card-marker.test.mjs',
105
- 'test/run-rc.mjs',
106
- 'test/rc5-regressions.test.mjs',
107
- 'test/identity-router/run.mjs',
108
- 'test/fixtures/pre-step-container.mjs',
109
- 'test/fixtures/baseline-0.8.0-registration.json',
110
- 'test/router-background/router.test.mjs',
111
- 'scripts/resident-watcher.ps1',
112
- 'scripts/install-resident-watcher.ps1',
113
- 'scripts/lib/auto-truncate-run.ps1',
114
- ]) {
115
- assert.ok(published.has(rel), `files 白名单缺 ${rel}`);
116
- }
117
- assert.ok(published.has('test/p1_probe.mjs'), 'files 白名单缺 test/p1_probe.mjs');
118
- assert.ok(published.has('package.json'), 'files 白名单缺 package.json');
119
- });
120
-
121
- test('只带白名单安装后:没有 prototype,所有 src 相对 import 都留在包内', (t) => {
122
- const root = installShallow(t);
123
- assert.equal(fs.existsSync(path.join(root, 'prototype')), false, '安装包不该依赖白名单外的 prototype/');
124
-
125
- const offenders = [];
126
- for (const rel of walk(path.join(root, 'src'), [], root)) {
127
- if (!/\.(mjs|js)$/.test(rel)) continue;
128
- const file = path.join(root, rel);
129
- const text = fs.readFileSync(file, 'utf8');
130
- for (const match of text.matchAll(/\bfrom\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
131
- const spec = match[1] ?? match[2];
132
- if (!spec.startsWith('.')) continue;
133
- const resolved = path.resolve(path.dirname(file), spec);
134
- if (resolved !== root && !resolved.startsWith(root + path.sep)) {
135
- offenders.push(`${rel} -> ${spec}`);
136
- }
137
- if (!fs.existsSync(resolved)) offenders.push(`${rel} -> ${spec}(不存在)`);
138
- }
139
- }
140
- assert.deepEqual(offenders, [], `存在跨出包目录/缺失的 import:\n${offenders.join('\n')}`);
141
- });
142
-
143
- test('R2-07 发布版本面:包版本 1.6.5,文档同步 D2/D6 能力集', () => {
144
- assert.equal(PKG.version, '1.6.5');
145
- const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
146
- const en = read('docs/README.en.md');
147
- assert.ok(en.includes('@jjh32456/dsh-longlive-panel@0.2.5'), '面板版本未统一到 0.2.5(scoped 包名)');
148
- assert.ok(en.includes('jjh32456-dsh-longlive-1.6.5.tgz'), '英文文档示例 tgz 不是 scoped 的 1.6.5');
149
- assert.ok(en.includes('@jjh32456/dsh-longlive'), '英文文档缺少 scoped 包名');
150
- assert.equal(en.includes('dsh-longlive-panel@0.1.3'), false, '英文文档仍残留 0.1.3');
151
- assert.equal(en.includes('0.9.0-rc.1'), false, '英文文档仍残留 rc.1');
152
- const features = read('docs/FEATURES.md');
153
- assert.ok(features.startsWith('# 1.6.5'), 'FEATURES.md 标题不是 1.6.5');
154
- assert.equal(features.includes('0.9.0-rc.1'), false, 'FEATURES.md 仍残留 rc.1');
155
- });
156
-
157
- test('R2-08 自动截断默认关闭(代码 enabled=false / mode=dryrun,patch 显式同值)', () => {
158
- // 1.6.0 的硬验收(滥用面评估 §4.1):代码默认与包内 patch 对齐,都是
159
- // enabled:false / mode:dryrun。1.5.0 及以前代码兜底是 enabled:true /
160
- // mode:'log',老配置漏写 autoTruncate 时会自动开始 log;这里钉住两者一致。
161
- const yml = fs.readFileSync(path.join(ROOT, 'cordis.patch.yml'), 'utf8');
162
- const block = yml.match(/autoTruncate:\s*\r?\n\s+enabled:\s*false\s*\r?\n\s+mode:\s*['"]?dryrun['"]?/);
163
- assert.ok(block, 'cordis.patch.yml 的 session-longlive 配置必须显式写 autoTruncate enabled:false / mode:dryrun');
164
- });
165
-
166
- test('只带白名单安装后每个新入口都能 import()', async (t) => {
167
- const root = installShallow(t);
168
- for (const rel of [
169
- 'src/index.js',
170
- 'src/features.js',
171
- 'src/metrics/index.mjs',
172
- 'src/identity/index.js',
173
- 'src/recall/index.js',
174
- 'src/bus/bus.mjs',
175
- 'src/router/background/index.mjs',
176
- ]) {
177
- const file = path.join(root, rel);
178
- assert.equal(fs.existsSync(file), true, `入口不在白名单复制结果里:${rel}`);
179
- const mod = await import(`${pathToFileURL(file).href}?packaging=${Date.now()}-${Math.random()}`);
180
- assert.ok(mod && typeof mod === 'object', `${rel} 没有导出对象`);
181
- }
182
- });