@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,568 +0,0 @@
1
- // RC gate: every 0.9.0-rc.1 capability is opt-in, and turning one on must not
2
- // turn on another. The baseline contract is captured in
3
- // test/fixtures/baseline-0.8.0-registration.json: the 0.8.0 registration shape
4
- // plus D3's session_original (22nd tool), regenerated from the merged
5
- // 1.4.0-rc.3 tree with all flags off (see the fixture's provenance field).
6
-
7
- import assert from 'node:assert/strict';
8
- import fs from 'node:fs';
9
- import os from 'node:os';
10
- import path from 'node:path';
11
- import test from 'node:test';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- import { normalizeFeatureConfig, resolveCardPath } from '../src/features.js';
15
- import { MESSAGE_TYPES, encodeSession, foldSurface, parseSession, sha256 } from '../src/codec.js';
16
-
17
- const HERE = path.dirname(fileURLToPath(import.meta.url));
18
- const ROOT = path.resolve(HERE, '..');
19
- const BASELINE = JSON.parse(fs.readFileSync(path.join(HERE, 'fixtures', 'baseline-0.8.0-registration.json'), 'utf8'));
20
- const { apply } = await import('../src/index.js');
21
-
22
- function projectTool(tool) {
23
- return {
24
- name: tool.name,
25
- description: tool.description ?? '',
26
- parameters: tool.parameters ?? null,
27
- hasExecute: typeof tool.execute === 'function',
28
- };
29
- }
30
-
31
- function projectHooks(hooks) {
32
- return hooks.map(({ name, prepend, priority }) => ({ name, prepend, priority: priority ?? null }));
33
- }
34
-
35
- function treeOf(root) {
36
- const out = [];
37
- const walk = (dir, prefix) => {
38
- let entries;
39
- try {
40
- entries = fs.readdirSync(dir, { withFileTypes: true });
41
- } catch {
42
- return;
43
- }
44
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
45
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
46
- out.push(`${rel}${entry.isDirectory() ? '/' : ''}`);
47
- if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
48
- }
49
- };
50
- walk(root, '');
51
- return out;
52
- }
53
-
54
- function interestingFeaturePath(value) {
55
- const text = String(value).replaceAll('\\', '/');
56
- return /STATE\.md|\.longlive\/bus|\/topics\/|\/handover\/|recall|presence/i.test(text);
57
- }
58
-
59
- function runPackage(config = {}, options = {}) {
60
- const temp = options.temp ? path.resolve(options.temp) : fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-flags-'));
61
- const home = options.home ?? path.join(temp, 'home');
62
- const memoryRoot = options.memoryRoot ?? path.join(temp, 'memory');
63
- fs.mkdirSync(home, { recursive: true });
64
- fs.mkdirSync(memoryRoot, { recursive: true });
65
- if (typeof options.prepare === 'function') options.prepare({ temp, home, memoryRoot });
66
-
67
- const hooks = [];
68
- const tools = [];
69
- const warnings = [];
70
- const infos = [];
71
- const featureReads = [];
72
- const ctx = {
73
- tools: {
74
- register(definition) {
75
- tools.push(definition);
76
- return () => {};
77
- },
78
- },
79
- on(name, listener, hookOptions) {
80
- hooks.push({ name, listener, prepend: hookOptions?.prepend === true, priority: hookOptions?.priority ?? null });
81
- return () => {};
82
- },
83
- logger() {
84
- return {
85
- warn: (message) => warnings.push(String(message)),
86
- info: (message) => infos.push(String(message)),
87
- };
88
- },
89
- effect(fn) {
90
- const dispose = fn();
91
- return typeof dispose === 'function' ? dispose : () => {};
92
- },
93
- };
94
-
95
- const originals = new Map();
96
- const record = (name) => {
97
- const original = fs[name];
98
- originals.set(name, original);
99
- fs[name] = function instrumented(target, ...rest) {
100
- if (interestingFeaturePath(target)) featureReads.push(`${name}:${path.resolve(String(target))}`);
101
- return original.call(fs, target, ...rest);
102
- };
103
- };
104
- if (options.instrumentReads !== false) {
105
- for (const name of ['readFileSync', 'readdirSync', 'statSync', 'existsSync', 'openSync']) record(name);
106
- }
107
-
108
- let returned;
109
- try {
110
- returned = apply(ctx, {
111
- home,
112
- memoryRoot,
113
- env: {},
114
- heartbeatMs: 0,
115
- recoverOnStart: false,
116
- defineTool: (definition) => definition,
117
- routerRuntime: 'off',
118
- // D1/D4 metrics is an independent default-on listener; this suite is the
119
- // frozen 0.8.0 feature-flag contract, so isolate it here unless a test
120
- // explicitly turns it on.
121
- metrics: false,
122
- ...config,
123
- });
124
- } finally {
125
- for (const [name, original] of originals) fs[name] = original;
126
- }
127
-
128
- const snapshot = {
129
- returned: JSON.parse(JSON.stringify(returned ?? null)),
130
- toolCount: tools.length,
131
- tools: tools.map(projectTool),
132
- hooks: projectHooks(hooks),
133
- warnings,
134
- infos,
135
- featureReads,
136
- memoryTree: treeOf(memoryRoot),
137
- temp,
138
- home,
139
- memoryRoot,
140
- };
141
- return { ...snapshot, rawHooks: hooks, ctx };
142
- }
143
-
144
- function cleanup(result) {
145
- try {
146
- fs.rmSync(result.temp, { recursive: true, force: true });
147
- } catch {
148
- /* temp dir */
149
- }
150
- }
151
-
152
- test('defaults: all five capabilities are off and documented values are exact', () => {
153
- assert.deepEqual(normalizeFeatureConfig({}), {
154
- autoRecall: { enabled: false, topK: 3, budgetTokens: 2000, everyNTurns: 6, createUserMessage: undefined },
155
- identityCard: { enabled: false, path: '<memoryRoot>/STATE.md', maxChars: 800, createUserMessage: undefined },
156
- presenceBus: { enabled: false, pollMs: 3000, root: undefined },
157
- autoMaintenance: {
158
- enabled: false,
159
- consolidate: true,
160
- forget: true,
161
- runOnStart: true,
162
- startDelayMs: 500,
163
- },
164
- });
165
- assert.equal(resolveCardPath('<memoryRoot>/STATE.md', 'C:/mem'), path.resolve('C:/mem/STATE.md'));
166
- assert.equal(resolveCardPath('cards/STATE.md', 'C:/mem'), path.resolve('C:/mem/cards/STATE.md'));
167
- assert.equal(resolveCardPath('<memoryRoot>/STATE.md', null), null);
168
- });
169
-
170
- test('clamps: hostile values stay in range instead of changing behavior', () => {
171
- const cfg = normalizeFeatureConfig({
172
- autoRecall: { enabled: 'yes', topK: 99, budgetTokens: 1, everyNTurns: 0 },
173
- identityCard: { enabled: 1, maxChars: 1 },
174
- presenceBus: { enabled: true, pollMs: -5 },
175
- });
176
- assert.equal(cfg.autoRecall.enabled, false);
177
- assert.equal(cfg.autoRecall.topK, 10);
178
- assert.equal(cfg.autoRecall.budgetTokens, 200);
179
- assert.equal(cfg.autoRecall.everyNTurns, 1);
180
- assert.equal(cfg.identityCard.enabled, false);
181
- assert.equal(cfg.identityCard.maxChars, 120);
182
- assert.equal(cfg.presenceBus.enabled, true);
183
- assert.equal(cfg.presenceBus.pollMs, 0);
184
- });
185
-
186
- test('flags off: byte-identical tool table, hook set and startup side effects to 0.8.0', (t) => {
187
- const result = runPackage({});
188
- t.after(() => cleanup(result));
189
-
190
- assert.equal(result.toolCount, BASELINE.toolCount);
191
- assert.deepEqual(result.tools, BASELINE.tools);
192
- assert.deepEqual(result.hooks, BASELINE.hooks);
193
- assert.deepEqual(result.returned, BASELINE.returned);
194
- assert.deepEqual(result.warnings, BASELINE.warnings);
195
- assert.deepEqual(result.infos, []);
196
- assert.deepEqual(result.featureReads, []);
197
- assert.deepEqual(result.memoryTree, []);
198
- });
199
-
200
- test('identityCard on: only one prepended pre-step hook, no recall/bus hooks', (t) => {
201
- const result = runPackage({ identityCard: { enabled: true, path: '<memoryRoot>/STATE.md' } });
202
- t.after(() => cleanup(result));
203
-
204
- assert.deepEqual(result.tools, BASELINE.tools);
205
- assert.deepEqual(result.hooks, [
206
- ...BASELINE.hooks,
207
- { name: 'agent/pre-step', prepend: true, priority: null },
208
- ]);
209
- assert.deepEqual(result.featureReads, []);
210
- assert.deepEqual(result.memoryTree, []);
211
- });
212
-
213
- test('autoRecall on: only its pre-step + session/event + session/disposed hooks', (t) => {
214
- const result = runPackage({ autoRecall: { enabled: true } });
215
- t.after(() => cleanup(result));
216
-
217
- assert.deepEqual(result.tools, BASELINE.tools);
218
- assert.deepEqual(result.hooks, [
219
- ...BASELINE.hooks,
220
- { name: 'agent/pre-step', prepend: false, priority: null },
221
- { name: 'session/event', prepend: false, priority: null },
222
- { name: 'session/disposed', prepend: false, priority: null },
223
- ]);
224
- assert.deepEqual(result.featureReads, []);
225
- assert.deepEqual(result.memoryTree, []);
226
- });
227
-
228
- test('presenceBus on: only one extra session/event hook', (t) => {
229
- const result = runPackage({ presenceBus: { enabled: true } });
230
- t.after(() => cleanup(result));
231
-
232
- assert.deepEqual(result.tools, BASELINE.tools);
233
- assert.deepEqual(result.hooks, [
234
- ...BASELINE.hooks,
235
- { name: 'session/event', prepend: false, priority: null },
236
- ]);
237
- assert.deepEqual(result.featureReads, []);
238
- assert.deepEqual(result.memoryTree, []);
239
- });
240
-
241
- test('all three on: hook multiset is the sum, no cross-activation or duplicates', (t) => {
242
- const result = runPackage({
243
- identityCard: { enabled: true },
244
- autoRecall: { enabled: true },
245
- presenceBus: { enabled: true },
246
- });
247
- t.after(() => cleanup(result));
248
-
249
- assert.deepEqual(result.tools, BASELINE.tools);
250
- assert.deepEqual(result.hooks, [
251
- ...BASELINE.hooks,
252
- { name: 'agent/pre-step', prepend: true, priority: null },
253
- { name: 'agent/pre-step', prepend: false, priority: null },
254
- { name: 'session/event', prepend: false, priority: null },
255
- { name: 'session/disposed', prepend: false, priority: null },
256
- { name: 'session/event', prepend: false, priority: null },
257
- ]);
258
- assert.deepEqual(result.featureReads, []);
259
- assert.deepEqual(result.memoryTree, []);
260
- });
261
-
262
- test('metrics on: exactly one extra session/event hook, no tool or disk side effects', (t) => {
263
- const result = runPackage({ metrics: { enabled: true } });
264
- t.after(() => cleanup(result));
265
-
266
- assert.deepEqual(result.tools, BASELINE.tools);
267
- assert.deepEqual(result.hooks, [
268
- ...BASELINE.hooks,
269
- { name: 'session/event', prepend: false, priority: null },
270
- ]);
271
- assert.deepEqual(result.featureReads, []);
272
- assert.deepEqual(result.memoryTree, []);
273
- });
274
-
275
- test('identityCard with no STATE.md leaves the pre-step result untouched', (t) => {
276
- const result = runPackage({ identityCard: { enabled: true } });
277
- t.after(() => cleanup(result));
278
- const hook = result.rawHooks.find((entry) => entry.name === 'agent/pre-step');
279
- assert.ok(hook, 'identity pre-step hook registered');
280
-
281
- const payload = {
282
- step: 1,
283
- agent: { session: { id: 'session-flag-test', header: { origin: 'main' } } },
284
- messages: [{ role: 'user', content: 'hello' }],
285
- context: [{ role: 'system', content: 'ctx' }],
286
- };
287
- const decision = { kind: 'enter', messages: [...payload.messages, ...payload.context] };
288
- const returned = hook.listener(payload, async () => decision);
289
- return Promise.resolve(returned).then((value) => {
290
- assert.equal(value, decision);
291
- assert.equal(payload.messages.length, 1);
292
- });
293
- });
294
-
295
- test('presenceBus consumes a completed turn into spool/acked, ignores failed turns', async (t) => {
296
- const busRoot = path.join(os.tmpdir(), `longlive-bus-${process.pid}-${Date.now()}`);
297
- const result = runPackage({ presenceBus: { enabled: true, pollMs: 0, root: busRoot } });
298
- t.after(() => {
299
- cleanup(result);
300
- fs.rmSync(busRoot, { recursive: true, force: true });
301
- });
302
- const hook = result.rawHooks.filter((entry) => entry.name === 'session/event').at(-1);
303
- assert.ok(hook, 'bus session/event hook registered');
304
-
305
- const { createBus } = await import('../src/bus/bus.mjs');
306
- const sessionId = 'session-flag-bus';
307
- const producer = createBus({ root: busRoot, sessionId: 'session-flag-producer' });
308
- producer.send({ to: sessionId, kind: 'note', body: 'one note' });
309
-
310
- const session = { id: sessionId };
311
- hook.listener(session, { type: 'turn/end', data: { reason: { kind: 'failed' } }, turn: 1 });
312
- assert.deepEqual(fs.existsSync(path.join(busRoot, 'spool')), false, 'failed turn must not drain');
313
-
314
- hook.listener(session, { type: 'turn/end', data: { reason: { kind: 'completed' } }, turn: 2 });
315
- const spool = path.join(busRoot, 'spool', `${sessionId}.jsonl`);
316
- const lines = fs.readFileSync(spool, 'utf8').trim().split('\n');
317
- assert.equal(lines.length, 1);
318
- const record = JSON.parse(lines[0]);
319
- assert.equal(record.message.kind, 'note');
320
- assert.equal(record.message.body, 'one note');
321
- assert.equal(producer.poll({ since: 0 }).messages.length, 0, 'message was acked');
322
- assert.ok(fs.existsSync(path.join(busRoot, 'inbox', sessionId, 'acked', `${record.message.id}.json`)));
323
-
324
- hook.listener(session, { type: 'turn/end', data: { reason: { kind: 'completed' } }, turn: 3 });
325
- assert.equal(fs.readFileSync(spool, 'utf8').trim().split('\n').length, 1, 'no duplicate spool line');
326
- });
327
-
328
- test('M2 containment: identityCard.path 越界时根外内容不进上下文(canary)', async (t) => {
329
- // 红队 M2:`../`、根外绝对路径、盘符跳转必须 E_PATH_OUTSIDE_ROOT,且一个字节都不读。
330
- for (const bad of ['../secret.md', 'D:/outside/STATE.md', '\\\\server\\share\\STATE.md']) {
331
- assert.throws(
332
- () => resolveCardPath(bad, 'C:/mem'),
333
- (error) => error.code === 'E_PATH_OUTSIDE_ROOT',
334
- `expected E_PATH_OUTSIDE_ROOT for ${bad}`,
335
- );
336
- }
337
-
338
- const result = runPackage({ identityCard: { enabled: true, path: '../secret.md' } });
339
- t.after(() => cleanup(result));
340
- const secret = path.join(result.temp, 'secret.md');
341
- fs.writeFileSync(secret, 'TOP-SECRET-CANARY-9f3a');
342
-
343
- const hook = result.rawHooks.find((entry) => entry.name === 'agent/pre-step');
344
- assert.ok(hook, 'identity pre-step hook registered');
345
- const payload = {
346
- step: 1,
347
- agent: { session: { id: 'session-canary', header: { origin: 'main' } } },
348
- messages: [{ role: 'user', content: 'hello' }],
349
- };
350
- const decision = { kind: 'enter', messages: [...payload.messages] };
351
-
352
- const originalRead = fs.readFileSync;
353
- const readTargets = [];
354
- fs.readFileSync = function spy(target, ...rest) {
355
- readTargets.push(String(target));
356
- return originalRead.call(fs, target, ...rest);
357
- };
358
- let value;
359
- try {
360
- value = await hook.listener(payload, async () => decision);
361
- } finally {
362
- fs.readFileSync = originalRead;
363
- }
364
-
365
- assert.ok(!JSON.stringify(value).includes('TOP-SECRET-CANARY-9f3a'), 'canary leaked into the context');
366
- assert.ok(!readTargets.some((target) => target.includes('secret.md')), 'outside file must never be read');
367
- assert.ok(result.warnings.some((line) => /E_PATH_OUTSIDE_ROOT|越界/.test(line)), result.warnings.join('\n'));
368
- });
369
- // ---------------------------------------------------------------------------
370
- // Startup receipt fallback (rc.2): visible text, one shot, zero cost if clean
371
- // ---------------------------------------------------------------------------
372
-
373
- function buildReceiptSession(turns) {
374
- const events = [];
375
- const base = 1_760_000_000_000;
376
- const push = (event) => {
377
- event.seq = events.length;
378
- event.time = base + events.length;
379
- events.push(event);
380
- };
381
- push({
382
- type: 'system/message',
383
- surfaceOp: 'append',
384
- data: {
385
- message: {
386
- role: 'system',
387
- id: 'system-1',
388
- source: { kind: 'system-prompt' },
389
- content: [{ type: 'text', text: 'system prompt' }],
390
- },
391
- },
392
- });
393
- for (let t = 1; t <= turns; t += 1) {
394
- push({ type: 'turn/start', data: { turn: t } });
395
- push({
396
- type: 'user/message',
397
- surfaceOp: 'append',
398
- data: { turn: t, step: 1, role: 'user', id: 'user-' + t, source: { kind: 'user' }, content: [{ type: 'text', text: 'question ' + t }] },
399
- });
400
- push({
401
- type: 'assistant/message',
402
- surfaceOp: 'append',
403
- data: { turn: t, step: 1, message: { role: 'assistant', id: 'assistant-' + t, content: [{ type: 'text', text: 'answer ' + t }] } },
404
- });
405
- push({ type: 'turn/end', data: { turn: t, reason: { kind: 'completed' } } });
406
- }
407
- return encodeSession({ type: 'session', version: 4, id: 'receipt-fixture', createdAt: '2026-11-04T00:00:00.000Z', cwd: 'C:\\fixtures' }, events);
408
- }
409
-
410
- const receiptSnapshot = (buf) => {
411
- const parsed = parseSession(buf);
412
- return {
413
- bytes: buf.length,
414
- sha256: sha256(buf),
415
- events: parsed.events.length,
416
- messages: foldSurface(parsed.events).filter((node) => MESSAGE_TYPES.has(node.type)).length,
417
- };
418
- };
419
-
420
- test('receipt fallback: 有未写回执的手术 state 时,第一轮正文注入一条可见回执(只一次)', async () => {
421
- const beforeBuf = buildReceiptSession(3);
422
- const afterBuf = buildReceiptSession(1);
423
- let sessionPath = null;
424
- let backupPath = null;
425
-
426
- const result = runPackage({}, {
427
- prepare: ({ temp, memoryRoot }) => {
428
- const dir = path.join(temp, 'sessions', 'scope', 'receipt-fixture');
429
- fs.mkdirSync(dir, { recursive: true });
430
- sessionPath = path.join(dir, 'session.v4.jsonl.zstd');
431
- backupPath = sessionPath + '.pre-surgery-20261104-143000';
432
- fs.writeFileSync(sessionPath, afterBuf);
433
- fs.writeFileSync(backupPath, beforeBuf);
434
- const state = {
435
- version: 1,
436
- kind: 'longlive-surgery',
437
- surgeryId: 'surgery-receipt-fallback-1',
438
- phase: 'restart',
439
- sessionPath,
440
- sessionId: 'receipt-fixture',
441
- memoryRoot,
442
- appPath: 'C:\\app\\DSH.exe',
443
- keepTurns: 6,
444
- keepTokens: 40000,
445
- startDelaySeconds: 30,
446
- restartDelaySeconds: 0,
447
- pluginVersion: '0.9.0-rc.4',
448
- hostVersion: '0.9.0',
449
- dryRun: false,
450
- before: receiptSnapshot(beforeBuf),
451
- after: receiptSnapshot(afterBuf),
452
- backup: { path: backupPath, bytes: beforeBuf.length, sha256: sha256(beforeBuf) },
453
- cut: { after: receiptSnapshot(afterBuf), verify: { ok: true } },
454
- verify: { ok: true, method: 'independent-readback' },
455
- receipt: null,
456
- history: [],
457
- };
458
- fs.mkdirSync(path.join(memoryRoot, '.longlive'), { recursive: true });
459
- fs.writeFileSync(path.join(memoryRoot, '.longlive', 'surgery-state.json'), JSON.stringify(state, null, 2));
460
- },
461
- });
462
-
463
- try {
464
- const hooks = result.rawHooks.filter((hook) => hook.name === 'agent/pre-step');
465
- assert.equal(hooks.length, 1, 'exactly one receipt hook');
466
- assert.ok(result.returned?.enabled?.includes('surgeryReceipt') ?? true);
467
-
468
- const payload = { step: 1, agent: { session: { id: 'session-live', header: { origin: 'main' } } } };
469
- const decision = await hooks[0].listener(payload, async () => ({ kind: 'enter', messages: [] }));
470
- assert.equal(decision.messages.length, 1);
471
- const message = decision.messages[0];
472
- assert.equal(message.role, 'user');
473
- assert.equal(message.source.kind, 'user');
474
- assert.match(message.id, /^surgery-receipt-/);
475
- const text = message.content.map((part) => part.text).join('');
476
- assert.match(text, /^【手术回执】/);
477
- assert.match(text, /成功/);
478
- assert.match(text, /字节:\d[\d,]* → \d[\d,]*/);
479
- assert.match(text, /事件:\d[\d,]* → \d[\d,]*/);
480
- assert.match(text, /消息:\d[\d,]* → \d[\d,]*/);
481
- assert.match(text, /sha256:磁盘读回与记录一致/);
482
- // R2-01: names only, never absolute paths.
483
- assert.ok(!text.includes(sessionPath), text);
484
- assert.ok(!text.includes(backupPath), text);
485
- assert.ok(!/(?:[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|tmp|var|opt|root|mnt)\/)/.test(text), text);
486
- assert.ok(text.includes(path.basename(backupPath)), text);
487
- assert.match(text, /回执来源:/);
488
-
489
- const second = await hooks[0].listener(payload, async () => ({ kind: 'enter', messages: [] }));
490
- assert.equal(second.messages.length, 0, 'receipt must be injected once');
491
-
492
- // R2-08: the one-shot marker is persisted, not only held in this process.
493
- const statePath = path.join(result.memoryRoot, '.longlive', 'surgery-state.json');
494
- const stored = JSON.parse(fs.readFileSync(statePath, 'utf8'));
495
- assert.equal(stored.receipt.fallbackInjected, true);
496
- assert.notEqual(stored.receipt.written, true);
497
- } finally {
498
- cleanup(result);
499
- }
500
- });
501
-
502
- test('R2-08: a second startup sees fallbackInjected and registers no receipt hook', async () => {
503
- const beforeBuf = buildReceiptSession(3);
504
- const afterBuf = buildReceiptSession(1);
505
- let sessionPath = null;
506
- let backupPath = null;
507
-
508
- const first = runPackage({}, {
509
- prepare: ({ temp, memoryRoot }) => {
510
- const dir = path.join(temp, 'sessions', 'scope', 'receipt-fixture');
511
- fs.mkdirSync(dir, { recursive: true });
512
- sessionPath = path.join(dir, 'session.v4.jsonl.zstd');
513
- backupPath = sessionPath + '.pre-surgery-20261104-143000';
514
- fs.writeFileSync(sessionPath, afterBuf);
515
- fs.writeFileSync(backupPath, beforeBuf);
516
- const state = {
517
- version: 1,
518
- kind: 'longlive-surgery',
519
- surgeryId: 'surgery-receipt-fallback-2',
520
- phase: 'restart',
521
- sessionPath,
522
- sessionId: 'receipt-fixture',
523
- memoryRoot,
524
- appPath: 'C:\\app\\DSH.exe',
525
- keepTurns: 6,
526
- keepTokens: 40000,
527
- startDelaySeconds: 30,
528
- restartDelaySeconds: 0,
529
- pluginVersion: '0.9.0-rc.4',
530
- hostVersion: '0.9.0',
531
- dryRun: false,
532
- before: receiptSnapshot(beforeBuf),
533
- after: receiptSnapshot(afterBuf),
534
- backup: { path: backupPath, bytes: beforeBuf.length, sha256: sha256(beforeBuf) },
535
- cut: { after: receiptSnapshot(afterBuf), verify: { ok: true } },
536
- verify: { ok: true, method: 'independent-readback' },
537
- receipt: null,
538
- history: [],
539
- };
540
- fs.mkdirSync(path.join(memoryRoot, '.longlive'), { recursive: true });
541
- fs.writeFileSync(path.join(memoryRoot, '.longlive', 'surgery-state.json'), JSON.stringify(state, null, 2));
542
- },
543
- });
544
-
545
- try {
546
- const hook = first.rawHooks.find((candidate) => candidate.name === 'agent/pre-step');
547
- assert.ok(hook, 'first startup must offer the receipt');
548
- const payload = { step: 1, agent: { session: { id: 'session-live', header: { origin: 'main' } } } };
549
- const decision = await hook.listener(payload, async () => ({ kind: 'enter', messages: [] }));
550
- assert.equal(decision.messages.length, 1);
551
-
552
- const second = runPackage({}, { temp: first.temp });
553
- assert.equal(second.rawHooks.filter((candidate) => candidate.name === 'agent/pre-step').length, 0);
554
- assert.ok(!(second.returned?.enabled ?? []).includes('surgeryReceipt'));
555
- } finally {
556
- cleanup(first);
557
- }
558
- });
559
-
560
- test('receipt fallback: 没有手术 state 时零副作用(不注册钩子、不读 state)', () => {
561
- const result = runPackage({});
562
- try {
563
- assert.equal(result.rawHooks.filter((hook) => hook.name === 'agent/pre-step').length, 0);
564
- assert.ok(!result.returned?.enabled?.includes('surgeryReceipt'));
565
- } finally {
566
- cleanup(result);
567
- }
568
- });