@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.
- package/CHANGELOG.md +69 -1539
- package/LICENSE +1 -1
- package/QUICKSTART.md +33 -12
- package/README.en.md +17 -7
- package/README.md +29 -10
- package/SPEC.md +5 -5
- package/cordis.patch.yml +17 -12
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +143 -37
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +66 -29
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +3 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +42 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +129 -18
- package/src/activity/model.js +11 -1
- package/src/activity/store.js +22 -3
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +145 -48
- package/src/deps.js +6 -4
- package/src/features.js +158 -8
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +1 -1
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/queue.mjs +1 -1
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/activity-realtime.test.mjs +0 -959
- package/test/activity-stream.test.mjs +0 -512
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -399
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -210
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -585
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/activity-append-child.mjs +0 -29
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1023
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -184
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1076
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -315
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -181
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/state-truth.test.mjs +0 -937
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
- 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
|
-
});
|