@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,164 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import { createRouterTools } from '../../src/router/service/tools.mjs';
5
-
6
- function fakeService(overrides = {}) {
7
- const calls = { tick: [], ask: [], report: [] };
8
- const service = {
9
- config: { enabled: true },
10
- runId: 'run-test',
11
- panelState: 'running',
12
- status: () => ({
13
- v: 1,
14
- state: 'running',
15
- runId: 'run-test',
16
- today: { day: '2026-11-04', usedTokens: 10, reservedTokens: 0, capTokens: 1000 },
17
- run: { usedTokens: 10, reservedTokens: 0, capTokens: 5000 },
18
- queue: { pending: 0, invalid: 0, oldestAgeMs: 0 },
19
- files: { vault: 'v' },
20
- }),
21
- requestTick: (input) => {
22
- calls.tick.push(input);
23
- return overrides.tick ?? { ok: true, accepted: true };
24
- },
25
- ask: async (input) => {
26
- calls.ask.push(input);
27
- return overrides.ask ?? { ok: true, accepted: true, taskId: 'task-1' };
28
- },
29
- reportHistory: (limit) => {
30
- calls.report.push(limit);
31
- return {
32
- lastReport: { at: '2026-11-04T00:00:00.000Z', summary: 'ok' },
33
- reports: Array.from({ length: Math.min(limit, 3) }, (_, index) => ({ summary: `r${index}` })),
34
- };
35
- },
36
- };
37
- return { service, calls };
38
- }
39
-
40
- const HOST_PARAM_KEYS = new Set([
41
- 'description', 'title', 'default', 'examples', 'required',
42
- 'type', 'enum', 'const', 'items', 'properties', 'additionalProperties', 'oneOf',
43
- ]);
44
- const HOST_VALUE_TYPES = new Set(['json', 'object', 'array', 'string', 'number', 'integer', 'boolean', 'null']);
45
- const FORBIDDEN_TOP_LEVEL = ['type', 'properties', 'required', 'additionalProperties', 'items', 'oneOf', 'enum', 'minLength', 'maxLength', 'minimum', 'maximum', 'minItems', 'maxItems'];
46
-
47
- function assertValueSchema(schema, path) {
48
- assert.ok(typeof schema === 'object' && schema !== null && !Array.isArray(schema), `${path} must be a schema object`);
49
- for (const key of Object.keys(schema)) {
50
- assert.equal(HOST_PARAM_KEYS.has(key), true, `${path}.${key} is not in the host value-schema DSL`);
51
- }
52
- if (Object.hasOwn(schema, 'required')) assert.equal(schema.required, true, `${path}.required must be true`);
53
- if (Object.hasOwn(schema, 'oneOf')) {
54
- assert.equal(Object.hasOwn(schema, 'type'), false, `${path} cannot have both type and oneOf`);
55
- assert.ok(Array.isArray(schema.oneOf) && schema.oneOf.length >= 2, `${path}.oneOf needs >= 2 branches`);
56
- schema.oneOf.forEach((branch, index) => assertValueSchema(branch, `${path}.oneOf[${index}]`));
57
- return;
58
- }
59
- assert.equal(HOST_VALUE_TYPES.has(schema.type), true, `${path}.type must be a host value type`);
60
- if (Object.hasOwn(schema, 'items')) assertValueSchema(schema.items, `${path}.items`);
61
- if (Object.hasOwn(schema, 'properties')) {
62
- for (const [key, value] of Object.entries(schema.properties)) assertValueSchema(value, `${path}.properties.${key}`);
63
- }
64
- }
65
-
66
- /** Host contract: `parameters` is a map of argument name -> value schema, never a JSON Schema object. */
67
- function assertHostParameters(parameters, name) {
68
- assert.ok(typeof parameters === 'object' && parameters !== null && !Array.isArray(parameters), `${name}.parameters must be a map`);
69
- for (const forbidden of FORBIDDEN_TOP_LEVEL) {
70
- assert.equal(Object.hasOwn(parameters, forbidden), false, `${name}.parameters.${forbidden} must not appear at the top level`);
71
- }
72
- for (const [key, schema] of Object.entries(parameters)) assertValueSchema(schema, `${name}.parameters.${key}`);
73
- }
74
-
75
- test('the four router tools have the DESIGN names, host-shaped parameter maps and a text renderer', () => {
76
- const { service } = fakeService();
77
- const tools = createRouterTools(service);
78
- assert.deepEqual(tools.map((tool) => tool.name), [
79
- 'router_status',
80
- 'router_tick',
81
- 'router_ask',
82
- 'router_report',
83
- ]);
84
- for (const tool of tools) {
85
- assertHostParameters(tool.parameters, tool.name);
86
- assert.equal(tool.output.schema.type, 'object');
87
- const rendered = tool.output.render({}, { ok: true, value: 1 });
88
- assert.equal(Array.isArray(rendered), true);
89
- assert.equal(rendered[0].type, 'text');
90
- }
91
- });
92
-
93
- test('router_status returns a read-only snapshot without waking the router', async () => {
94
- const { service, calls } = fakeService();
95
- const status = createRouterTools(service).find((tool) => tool.name === 'router_status');
96
- const result = await status.invoke({}, {});
97
- assert.equal(result.ok, true);
98
- assert.equal(result.state, 'running');
99
- assert.equal(result.run.capTokens, 5000);
100
- assert.equal(calls.tick.length, 0);
101
- assert.equal(calls.ask.length, 0);
102
- });
103
-
104
- test('router_tick maps the tier, forwards caller, and preserves E_BUSY', async () => {
105
- const { service, calls } = fakeService({ tick: { ok: false, code: 'E_BUSY', state: 'running' } });
106
- const tick = createRouterTools(service).find((tool) => tool.name === 'router_tick');
107
- const busy = await tick.invoke({ tier: 'reflect' }, { exec: { caller: 'session-1' } });
108
- assert.deepEqual(busy, { ok: false, code: 'E_BUSY', state: 'running' });
109
- assert.equal(calls.tick[0].tier, 'reflect');
110
- assert.equal(calls.tick[0].manual, true);
111
- assert.equal(calls.tick[0].reason, 'tool');
112
- assert.equal(calls.tick[0].caller, 'session-1');
113
- });
114
-
115
- test('router_ask rejects a blank question and forwards a valid one with the default tier', async () => {
116
- const { service, calls } = fakeService();
117
- const ask = createRouterTools(service).find((tool) => tool.name === 'router_ask');
118
- const blank = await ask.invoke({ question: ' ' }, {});
119
- assert.equal(blank.ok, false);
120
- assert.equal(blank.code, 'E_INVALID_QUESTION');
121
- assert.equal(calls.ask.length, 0);
122
-
123
- const ok = await ask.invoke({ question: '现在该做什么?' }, {});
124
- assert.equal(ok.ok, true);
125
- assert.equal(ok.taskId, 'task-1');
126
- assert.equal(calls.ask[0].question, '现在该做什么?');
127
- assert.equal(calls.ask[0].tier, 'full');
128
- });
129
-
130
- test('router_report defaults to 5 and passes an explicit limit through', async () => {
131
- const { service, calls } = fakeService();
132
- const report = createRouterTools(service).find((tool) => tool.name === 'router_report');
133
- const first = await report.invoke({}, {});
134
- assert.equal(first.ok, true);
135
- assert.equal(first.state, 'running');
136
- assert.equal(calls.report[0], 5);
137
- await report.invoke({ limit: 2 }, {});
138
- assert.equal(calls.report[1], 2);
139
- });
140
-
141
- test('all router tools (incl. proposal layer) expose host-honest value schemas', () => {
142
- const { service } = fakeService();
143
- service.config = { enabled: true, proposalEnabled: true };
144
- const tools = createRouterTools(service);
145
- assert.deepEqual(tools.map((tool) => tool.name), [
146
- 'router_status', 'router_tick', 'router_ask', 'router_report',
147
- 'router_propose', 'router_proposals', 'router_apply',
148
- ]);
149
- for (const tool of tools) assertHostParameters(tool.parameters, tool.name);
150
-
151
- const byName = new Map(tools.map((tool) => [tool.name, tool]));
152
- const propose = byName.get('router_propose').parameters;
153
- for (const required of ['op', 'target', 'title', 'content']) {
154
- assert.equal(propose[required].required, true, `router_propose.${required} must be required`);
155
- }
156
- assert.deepEqual(propose.priority.enum, ['P0', 'P1', 'P2', 'P3', 'P4']);
157
- assert.equal(byName.get('router_apply').parameters.mode.required, true);
158
- assert.equal(byName.get('router_ask').parameters.question.required, true);
159
- // Length/count limits are enforced in code; the host DSL has no such keywords.
160
- const text = JSON.stringify(tools.map((tool) => tool.parameters));
161
- for (const keyword of ['minLength', 'maxLength', 'minimum', 'maximum', 'maxItems', 'minItems']) {
162
- assert.equal(text.includes(`"${keyword}"`), false, `${keyword} must not leak into tool schemas`);
163
- }
164
- });
@@ -1,89 +0,0 @@
1
- // C-02 / C-06: turn/end reflect 轻量整理必须真的跑一次模型;running 时
2
- // status.nextTickAt 必须保持为下一次定时 tick(不能因为空队列被置 null)。
3
- import assert from 'node:assert/strict';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import test from 'node:test';
7
-
8
- import { makeService, startedService } from './helpers.mjs';
9
-
10
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
-
12
- function auditRecords(vaultDir) {
13
- const file = path.join(vaultDir, 'audit.jsonl');
14
- if (!fs.existsSync(file)) return [];
15
- return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
16
- }
17
-
18
- test('C-02: turn/end completed enqueues a synthetic reflect pass with exactly one model call', async () => {
19
- const env = await startedService({ deps: { turnDebounceMs: 5 } });
20
- try {
21
- assert.equal(env.result.state, 'running');
22
- const delivered = env.service.handleSessionEvent({ type: 'turn/end', data: { reason: { kind: 'completed' }, turn: 1 } });
23
- assert.equal(delivered.scheduled, true);
24
-
25
- for (let i = 0; i < 100 && env.llm.calls === 0; i += 1) await sleep(5);
26
- assert.equal(env.llm.calls, 1, 'turn/end reflect must call the model once');
27
-
28
- const records = auditRecords(env.vaultDir);
29
- const starts = records.filter((record) => record.kind === 'task.start');
30
- assert.equal(starts.length, 1);
31
- assert.equal(starts[0].type, 'reflect');
32
- assert.equal(starts[0].tier, 'reflect');
33
-
34
- // error / aborted turns must not trigger a model call.
35
- env.service.handleSessionEvent({ type: 'turn/end', data: { reason: { kind: 'error' }, turn: 2 } });
36
- env.service.handleSessionEvent({ type: 'turn/end', data: { reason: { kind: 'aborted' }, turn: 3 } });
37
- await sleep(40);
38
- assert.equal(env.llm.calls, 1, 'error/aborted turns must stay quiet');
39
- } finally {
40
- await env.service.dispose();
41
- env.cleanup();
42
- }
43
- });
44
-
45
- test('C-06: running service keeps nextTickAt as the next scheduled tick after an idle tick', async () => {
46
- const env = await startedService({
47
- config: { tickMs: 60 },
48
- deps: { firstTickMs: 5 },
49
- });
50
- try {
51
- assert.equal(env.result.state, 'running');
52
- await sleep(20);
53
- env.service.nextTickAt = new Date(Date.now() + 30000).toISOString();
54
- const before = env.service.status().nextTickAt;
55
-
56
- // Force one idle tick (no queued tasks): the old bug nulled nextTickAt here.
57
- const idle = await env.service.tick({ tier: 'reflect', reason: 'test-idle' });
58
- assert.equal(idle.ok, true);
59
- assert.equal(idle.idle, true);
60
- assert.equal(env.service.status().nextTickAt, before, 'idle tick must not null nextTickAt');
61
- assert.equal(env.service.status().nextTickAt, before);
62
- } finally {
63
- await env.service.dispose();
64
- env.cleanup();
65
- }
66
- });
67
-
68
- test('C-06: a successful tick also keeps nextTickAt (no null on completion)', async () => {
69
- const env = await startedService();
70
- try {
71
- assert.equal(env.result.state, 'running');
72
- const scheduled = new Date(Date.now() + 30000).toISOString();
73
- env.service.nextTickAt = scheduled;
74
- env.service.queue.writer.append({
75
- id: 'c06-task-1',
76
- type: 'question',
77
- payload: { question: 'nextTickAt 保持' },
78
- origin: 'test',
79
- depth: 0,
80
- });
81
- const done = await env.service.tick({ tier: 'reflect', reason: 'test-task' });
82
- assert.equal(done.ok, true);
83
- assert.equal(done.processed, 1);
84
- assert.equal(env.service.status().nextTickAt, scheduled, 'successful tick must not null nextTickAt');
85
- } finally {
86
- await env.service.dispose();
87
- env.cleanup();
88
- }
89
- });
@@ -1,158 +0,0 @@
1
- // 1.1.1 contract checks for the VM-v5 regressions that are cheap to pin
2
- // locally: ledger `kind` mirror + overshoot accounting (E-01/E-05), the
3
- // pending/numeric-ticket proposal contract (K-04/N-01), and the bounded
4
- // E_WAIT_TIMEOUT lease window (L-04).
5
-
6
- import assert from 'node:assert/strict';
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import test from 'node:test';
10
-
11
- import { BudgetLedger } from '../../src/router/background/budget-ledger.mjs';
12
- import { makeService, readJson, tempRoot, writeMemory } from './helpers.mjs';
13
-
14
- test('v6 E-01: every ledger row carries the VM-facing kind mirror', () => {
15
- const temp = tempRoot('v6-ledger-kind-');
16
- try {
17
- const ledger = new BudgetLedger({ file: path.join(temp, 'budget.ledger.jsonl'), maxTokens: 50000 });
18
- const reservation = ledger.reserve({ taskId: 't-kind', tokens: 1000, owner: 'run-v6' });
19
- assert.equal(reservation.ok, true);
20
- const settled = ledger.settle({ id: reservation.id, tokens: 900 });
21
- assert.equal(settled.ok, true);
22
- const rows = fs.readFileSync(path.join(temp, 'budget.ledger.jsonl'), 'utf8')
23
- .trim().split('\n').map((line) => JSON.parse(line));
24
- assert.deepEqual(rows.map((row) => row.kind), ['reserve', 'settle']);
25
- assert.deepEqual(rows.map((row) => row.op), ['reserve', 'settle']);
26
- } finally {
27
- fs.rmSync(temp, { recursive: true, force: true });
28
- }
29
- });
30
-
31
- test('v6 E-05: allowOvershoot records the real usage instead of truncating it', () => {
32
- const temp = tempRoot('v6-ledger-overshoot-');
33
- try {
34
- const ledger = new BudgetLedger({ file: path.join(temp, 'budget.ledger.jsonl'), maxTokens: 50000 });
35
- const reservation = ledger.reserve({ taskId: 't-over', tokens: 1000, owner: 'run-v6' });
36
- assert.equal(reservation.ok, true);
37
- const settled = ledger.settle({ id: reservation.id, tokens: 40000, allowOvershoot: true });
38
- assert.equal(settled.ok, true);
39
- assert.equal(settled.applied, 40000);
40
- const rows = fs.readFileSync(path.join(temp, 'budget.ledger.jsonl'), 'utf8')
41
- .trim().split('\n').map((line) => JSON.parse(line));
42
- assert.equal(rows.at(-1).kind, 'settle');
43
- assert.equal(rows.at(-1).tokens, 40000);
44
- assert.equal(ledger.snapshot().used, 40000);
45
- } finally {
46
- fs.rmSync(temp, { recursive: true, force: true });
47
- }
48
- });
49
-
50
- test('v6 K-04/N-01: a new proposal is pending and returns a numeric ticket', async () => {
51
- const temp = tempRoot('v6-proposal-pending-');
52
- const memoryRoot = path.join(temp, 'memory');
53
- fs.mkdirSync(memoryRoot, { recursive: true });
54
- writeMemory(memoryRoot);
55
- const env = makeService({
56
- temp,
57
- memoryRoot,
58
- vaultDir: path.join(temp, 'vault'),
59
- config: { proposalEnabled: true, applyEnabled: true },
60
- });
61
- try {
62
- const started = await env.service.start();
63
- assert.equal(started.ok, true, JSON.stringify(started));
64
- const proposed = await env.service.proposeProposal({
65
- op: 'journal.append',
66
- target: 'journal/2026-11.md',
67
- title: 'v6 pending',
68
- content: 'pending body',
69
- }, { caller: { kind: 'root', sessionId: 'root-session' } });
70
- assert.equal(proposed.ok, true, JSON.stringify(proposed));
71
- assert.equal(proposed.ticket, 1);
72
- assert.equal(proposed.ticketName, 'q-0001');
73
- const stored = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
74
- assert.equal(stored.status, 'pending');
75
- assert.equal(stored.queue.ticket, 'q-0001');
76
- } finally {
77
- await env.service.dispose();
78
- fs.rmSync(temp, { recursive: true, force: true });
79
- }
80
- });
81
-
82
- test('v6 F-05: allowMissingAnchor is visible as a warning and audit row', async () => {
83
- const temp = tempRoot('v6-anchor-allowed-');
84
- const memoryRoot = path.join(temp, 'memory');
85
- fs.mkdirSync(memoryRoot, { recursive: true });
86
- writeMemory(memoryRoot);
87
- const vaultDir = path.join(temp, 'vault');
88
- const seedDir = path.join(vaultDir, 'runs', 'seed-no-anchor');
89
- fs.mkdirSync(seedDir, { recursive: true });
90
- const ledger = new BudgetLedger({ file: path.join(seedDir, 'budget.ledger.jsonl'), maxTokens: 50000 });
91
- const reservation = ledger.reserve({ taskId: 'seed', tokens: 1000, owner: 'seed-run' });
92
- ledger.settle({ id: reservation.id, tokens: 900 });
93
- const env = makeService({
94
- temp,
95
- memoryRoot,
96
- vaultDir,
97
- config: { proposalEnabled: true, allowMissingAnchor: true },
98
- });
99
- try {
100
- const started = await env.service.start();
101
- assert.equal(started.ok, true, JSON.stringify(started));
102
- assert.equal(env.service.status().state, 'running');
103
- assert.ok(env.service.status().warnings.includes('ANCHOR_MISSING_ALLOWED'));
104
- const rows = fs.readFileSync(path.join(vaultDir, 'audit.jsonl'), 'utf8')
105
- .trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
106
- assert.ok(rows.some((row) => row.kind === 'anchor.missing.allowed'));
107
- } finally {
108
- await env.service.dispose();
109
- fs.rmSync(temp, { recursive: true, force: true });
110
- }
111
- });
112
-
113
- test('v6 L-04: a short lease window yields E_WAIT_TIMEOUT and blocked-on-user', async () => {
114
- const temp = tempRoot('v6-lease-timeout-');
115
- const memoryRoot = path.join(temp, 'memory');
116
- fs.mkdirSync(memoryRoot, { recursive: true });
117
- writeMemory(memoryRoot);
118
- const vaultDir = path.join(temp, 'vault');
119
- const env = makeService({
120
- temp,
121
- memoryRoot,
122
- vaultDir,
123
- config: { proposalEnabled: true, applyEnabled: true, leaseWaitTimeoutMs: 1000 },
124
- });
125
- try {
126
- const started = await env.service.start();
127
- assert.equal(started.ok, true, JSON.stringify(started));
128
- const proposed = await env.service.proposeProposal({
129
- op: 'journal.append',
130
- target: 'journal/2026-11.md',
131
- title: 'v6 lease',
132
- content: 'lease body',
133
- }, { caller: { kind: 'root', sessionId: 'root-session' } });
134
- assert.equal(proposed.ok, true, JSON.stringify(proposed));
135
- await env.service.requestApply({ mode: 'approve', id: proposed.id, confirm: proposed.id.slice(-6) },
136
- { caller: { kind: 'root', sessionId: 'root-session' } });
137
-
138
- const held = env.service.lease.acquire({ ticket: 'q-0001', proposalId: proposed.id });
139
- assert.equal(held.ok, true, JSON.stringify(held));
140
-
141
- const result = await env.service.requestApply({
142
- mode: 'apply',
143
- id: proposed.id,
144
- confirm: proposed.id.slice(-6),
145
- }, { caller: { kind: 'root', sessionId: 'root-session' } });
146
- assert.equal(result.ok, false);
147
- assert.equal(result.code, 'E_WAIT_TIMEOUT');
148
- assert.equal(result.state, 'blocked-on-user');
149
- assert.equal(result.reason, 'LEASE_TIMEOUT');
150
- const stored = readJson(path.join(vaultDir, 'proposals', `${proposed.id}.json`));
151
- assert.equal(stored.status, 'blocked-on-user');
152
- assert.equal(stored.reason, 'LEASE_TIMEOUT');
153
- assert.equal(fs.existsSync(path.join(memoryRoot, 'journal', '2026-11.md')), false);
154
- } finally {
155
- await env.service.dispose();
156
- fs.rmSync(temp, { recursive: true, force: true });
157
- }
158
- });