@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,175 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import test from 'node:test';
|
|
5
|
-
|
|
6
|
-
import { apply } from '../../src/index.js';
|
|
7
|
-
import { MockLlm } from '../../src/router/background/mock-llm.mjs';
|
|
8
|
-
import { tempRoot } from './helpers.mjs';
|
|
9
|
-
|
|
10
|
-
function fakeContext({ llm = null } = {}) {
|
|
11
|
-
const tools = [];
|
|
12
|
-
const effects = [];
|
|
13
|
-
const hooks = [];
|
|
14
|
-
const warnings = [];
|
|
15
|
-
const ctx = {
|
|
16
|
-
tools: {
|
|
17
|
-
register(definition) {
|
|
18
|
-
tools.push(definition);
|
|
19
|
-
return () => {};
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
on(name, listener) {
|
|
23
|
-
hooks.push({ name, listener });
|
|
24
|
-
return () => {};
|
|
25
|
-
},
|
|
26
|
-
logger() {
|
|
27
|
-
return { warn: (message) => warnings.push(String(message)), info: () => {} };
|
|
28
|
-
},
|
|
29
|
-
effect(fn) {
|
|
30
|
-
const dispose = fn();
|
|
31
|
-
effects.push(typeof dispose === 'function' ? dispose : () => {});
|
|
32
|
-
return effects[effects.length - 1];
|
|
33
|
-
},
|
|
34
|
-
get(name) {
|
|
35
|
-
return name === 'llm' ? llm : undefined;
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
return { ctx, tools, effects, hooks, warnings };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function memoryTree(root) {
|
|
42
|
-
const out = [];
|
|
43
|
-
const walk = (dir, rel = '') => {
|
|
44
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
45
|
-
const child = path.join(dir, entry.name);
|
|
46
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
47
|
-
out.push(entry.isDirectory() ? `${childRel}/` : childRel);
|
|
48
|
-
if (entry.isDirectory()) walk(child, childRel);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
if (fs.existsSync(root)) walk(root);
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
test('routerEnabled=false is the 0.9.0 surface: 22 tools, no router_* hooks, no .longlive', async () => {
|
|
56
|
-
const temp = tempRoot('longlive-router-off-');
|
|
57
|
-
const home = path.join(temp, 'home');
|
|
58
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
59
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
60
|
-
fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
|
|
61
|
-
const before = memoryTree(memoryRoot);
|
|
62
|
-
const harness = fakeContext();
|
|
63
|
-
try {
|
|
64
|
-
const returned = await apply(harness.ctx, {
|
|
65
|
-
home,
|
|
66
|
-
memoryRoot,
|
|
67
|
-
env: {},
|
|
68
|
-
heartbeatMs: 0,
|
|
69
|
-
recoverOnStart: false,
|
|
70
|
-
defineTool: (definition) => definition,
|
|
71
|
-
routerRuntime: 'off',
|
|
72
|
-
});
|
|
73
|
-
assert.equal(typeof returned, 'number');
|
|
74
|
-
assert.equal(harness.tools.length, 22);
|
|
75
|
-
assert.equal(harness.tools.some((tool) => String(tool.name).startsWith('router_')), false);
|
|
76
|
-
assert.deepEqual(memoryTree(memoryRoot), before);
|
|
77
|
-
assert.equal(fs.existsSync(path.join(memoryRoot, '.longlive')), false);
|
|
78
|
-
} finally {
|
|
79
|
-
for (const dispose of harness.effects.reverse()) dispose();
|
|
80
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('routerEnabled=true wires the service and registers exactly the four router tools', async () => {
|
|
85
|
-
const temp = tempRoot('longlive-router-on-');
|
|
86
|
-
const home = path.join(temp, 'home');
|
|
87
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
88
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
89
|
-
fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
|
|
90
|
-
const llm = new MockLlm({ usage: { inputTokens: 10, outputTokens: 5 } });
|
|
91
|
-
const harness = fakeContext({ llm });
|
|
92
|
-
try {
|
|
93
|
-
const returned = await apply(harness.ctx, {
|
|
94
|
-
home,
|
|
95
|
-
memoryRoot,
|
|
96
|
-
env: {},
|
|
97
|
-
heartbeatMs: 0,
|
|
98
|
-
recoverOnStart: false,
|
|
99
|
-
defineTool: (definition) => definition,
|
|
100
|
-
routerRuntime: 'off',
|
|
101
|
-
routerEnabled: true,
|
|
102
|
-
routerProvider: 'mock',
|
|
103
|
-
routerModel: 'mock-1',
|
|
104
|
-
routerTickMs: 60000,
|
|
105
|
-
});
|
|
106
|
-
assert.equal(returned, 26);
|
|
107
|
-
const names = harness.tools.map((tool) => tool.name);
|
|
108
|
-
for (const name of ['router_status', 'router_tick', 'router_ask', 'router_report']) {
|
|
109
|
-
assert.ok(names.includes(name), `${name} must be registered`);
|
|
110
|
-
}
|
|
111
|
-
assert.equal(harness.tools.length, 26);
|
|
112
|
-
|
|
113
|
-
const statusTool = harness.tools.find((tool) => tool.name === 'router_status');
|
|
114
|
-
let status = null;
|
|
115
|
-
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
116
|
-
status = await statusTool.execute({}, {});
|
|
117
|
-
if (status?.state === 'running' || status?.state === 'blocked') break;
|
|
118
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
119
|
-
}
|
|
120
|
-
assert.equal(status.ok, true);
|
|
121
|
-
assert.equal(status.state, 'running');
|
|
122
|
-
assert.ok(fs.existsSync(path.join(memoryRoot, '.longlive', 'router', 'status.json')));
|
|
123
|
-
} finally {
|
|
124
|
-
for (const dispose of harness.effects.reverse()) {
|
|
125
|
-
try { await dispose(); } catch { /* best effort */ }
|
|
126
|
-
}
|
|
127
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('routerProposalEnabled=true adds the three proposal tools: 29', async () => {
|
|
132
|
-
const temp = tempRoot('longlive-router-proposals-');
|
|
133
|
-
const home = path.join(temp, 'home');
|
|
134
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
135
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
136
|
-
fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
|
|
137
|
-
const llm = new MockLlm({ usage: { inputTokens: 10, outputTokens: 5 } });
|
|
138
|
-
const harness = fakeContext({ llm });
|
|
139
|
-
try {
|
|
140
|
-
const returned = await apply(harness.ctx, {
|
|
141
|
-
home,
|
|
142
|
-
memoryRoot,
|
|
143
|
-
env: {},
|
|
144
|
-
heartbeatMs: 0,
|
|
145
|
-
recoverOnStart: false,
|
|
146
|
-
defineTool: (definition) => definition,
|
|
147
|
-
routerRuntime: 'off',
|
|
148
|
-
routerEnabled: true,
|
|
149
|
-
routerProvider: 'mock',
|
|
150
|
-
routerModel: 'mock-1',
|
|
151
|
-
routerTickMs: 60000,
|
|
152
|
-
routerProposalEnabled: true,
|
|
153
|
-
});
|
|
154
|
-
assert.equal(returned, 29);
|
|
155
|
-
const names = harness.tools.map((tool) => tool.name);
|
|
156
|
-
assert.deepEqual(
|
|
157
|
-
names.filter((name) => name.startsWith('router_')).sort(),
|
|
158
|
-
[
|
|
159
|
-
'router_apply',
|
|
160
|
-
'router_ask',
|
|
161
|
-
'router_proposals',
|
|
162
|
-
'router_propose',
|
|
163
|
-
'router_report',
|
|
164
|
-
'router_status',
|
|
165
|
-
'router_tick',
|
|
166
|
-
],
|
|
167
|
-
);
|
|
168
|
-
} finally {
|
|
169
|
-
for (const dispose of harness.effects.reverse()) {
|
|
170
|
-
try { await dispose(); } catch { /* best effort */ }
|
|
171
|
-
}
|
|
172
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import test from 'node:test';
|
|
5
|
-
|
|
6
|
-
import { AuditLog } from '../../src/router/background/audit.mjs';
|
|
7
|
-
import { BudgetLedger } from '../../src/router/background/budget-ledger.mjs';
|
|
8
|
-
import { makeService, startedService, taskInput } from './helpers.mjs';
|
|
9
|
-
|
|
10
|
-
function auditRecords(vaultDir) {
|
|
11
|
-
return fs.readFileSync(path.join(vaultDir, 'audit.jsonl'), 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function seedOldRun(vaultDir, { used = 0, runId = 'old-run', auditSettle = null } = {}) {
|
|
15
|
-
const runDir = path.join(vaultDir, 'runs', runId);
|
|
16
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
17
|
-
const ledgerFile = path.join(runDir, 'budget.ledger.jsonl');
|
|
18
|
-
if (used > 0) {
|
|
19
|
-
const ledger = new BudgetLedger({ file: ledgerFile, runId, maxTokens: 50000 });
|
|
20
|
-
const reserved = ledger.reserve({ taskId: `${runId}-task`, tokens: used, owner: runId, tick: 1 });
|
|
21
|
-
assert.equal(reserved.ok, true);
|
|
22
|
-
const settled = ledger.settle({ id: reserved.id, tokens: used });
|
|
23
|
-
assert.equal(settled.ok, true);
|
|
24
|
-
} else {
|
|
25
|
-
fs.writeFileSync(ledgerFile, '');
|
|
26
|
-
}
|
|
27
|
-
if (auditSettle !== null) {
|
|
28
|
-
const audit = new AuditLog({ file: path.join(vaultDir, 'audit.jsonl'), runId });
|
|
29
|
-
audit.open();
|
|
30
|
-
audit.append('router.start', { run_id: runId });
|
|
31
|
-
audit.append('budget.settle', { run_id: runId, task_id: `${runId}-task`, tokens: auditSettle });
|
|
32
|
-
audit.append('router.stop', { run_id: runId });
|
|
33
|
-
audit.close();
|
|
34
|
-
}
|
|
35
|
-
return runDir;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
test('a normal tick reserves and settles the actual usage, never the reservation', async () => {
|
|
39
|
-
const env = await startedService();
|
|
40
|
-
try {
|
|
41
|
-
env.service.queue.writer.append(taskInput('settle-1'));
|
|
42
|
-
const result = await env.service.tick({ tier: 'reflect', reason: 'test' });
|
|
43
|
-
assert.equal(result.ok, true);
|
|
44
|
-
assert.equal(result.processed, 1);
|
|
45
|
-
const records = auditRecords(env.vaultDir);
|
|
46
|
-
const reserve = records.find((record) => record.kind === 'budget.reserve' && record.task_id === 'settle-1');
|
|
47
|
-
const settle = records.find((record) => record.kind === 'budget.settle' && record.task_id === 'settle-1');
|
|
48
|
-
assert.ok(reserve);
|
|
49
|
-
assert.ok(settle);
|
|
50
|
-
assert.ok(reserve.tokens > 150, `reservation ${reserve.tokens} should exceed actual usage`);
|
|
51
|
-
assert.equal(settle.tokens, 150);
|
|
52
|
-
assert.equal(settle.usage_estimated, false);
|
|
53
|
-
assert.equal(env.service.status().run.usedTokens, 150);
|
|
54
|
-
const ledgerLines = fs.readFileSync(path.join(env.vaultDir, 'runs', env.service.runId, 'budget.ledger.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
|
55
|
-
assert.deepEqual(ledgerLines.map((line) => line.op), ['reserve', 'settle']);
|
|
56
|
-
assert.equal(ledgerLines[1].tokens, 150);
|
|
57
|
-
} finally {
|
|
58
|
-
await env.service.dispose();
|
|
59
|
-
env.cleanup();
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('budget exhaustion blocks, leaves the task queued, and never calls the model', async () => {
|
|
64
|
-
const env = await startedService({ config: { maxTokensPerRun: 5000 } });
|
|
65
|
-
try {
|
|
66
|
-
// 4900 leaves 100: even the reflect reservation (>= ~200) cannot fit, so no degrade.
|
|
67
|
-
const adopted = env.service.ledger.adopt({ used: 4900, reason: 'test preuse' });
|
|
68
|
-
assert.equal(adopted.ok, true);
|
|
69
|
-
env.service.queue.writer.append(taskInput('no-budget'));
|
|
70
|
-
const result = await env.service.tick({ tier: 'full', reason: 'test' });
|
|
71
|
-
assert.equal(result.ok, true);
|
|
72
|
-
assert.equal(env.service.status().state, 'blocked');
|
|
73
|
-
assert.equal(env.service.status().lastError.code, 'E_BUDGET_EXHAUSTED');
|
|
74
|
-
assert.equal(env.llm.calls, 0);
|
|
75
|
-
const pending = env.service.queue.pending(env.service.seen);
|
|
76
|
-
assert.equal(pending.tasks.length, 1);
|
|
77
|
-
assert.equal(pending.tasks[0].id, 'no-budget');
|
|
78
|
-
const kinds = auditRecords(env.vaultDir).map((record) => record.kind);
|
|
79
|
-
assert.ok(kinds.includes('budget.exceeded'));
|
|
80
|
-
} finally {
|
|
81
|
-
await env.service.dispose();
|
|
82
|
-
env.cleanup();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('full does not fit but reflect does: degrade, audit it, and make one reflect call', async () => {
|
|
87
|
-
const env = await startedService({ config: { maxTokensPerRun: 5000 } });
|
|
88
|
-
try {
|
|
89
|
-
const adopted = env.service.ledger.adopt({ used: 3300, reason: 'test preuse' });
|
|
90
|
-
assert.equal(adopted.ok, true);
|
|
91
|
-
env.service.queue.writer.append(taskInput('degrade-1', { payload: { question: '问'.repeat(2000) } }));
|
|
92
|
-
const result = await env.service.tick({ tier: 'full', reason: 'test' });
|
|
93
|
-
assert.equal(result.ok, true);
|
|
94
|
-
assert.equal(env.llm.calls, 1);
|
|
95
|
-
const records = auditRecords(env.vaultDir);
|
|
96
|
-
const degrade = records.find((record) => record.kind === 'budget.degrade');
|
|
97
|
-
assert.ok(degrade, 'degrade must be audited');
|
|
98
|
-
assert.equal(degrade.from, 'full');
|
|
99
|
-
assert.equal(degrade.to, 'reflect');
|
|
100
|
-
assert.equal(degrade.reason, 'reserve_not_fit');
|
|
101
|
-
assert.equal(env.service.status().state, 'running');
|
|
102
|
-
} finally {
|
|
103
|
-
await env.service.dispose();
|
|
104
|
-
env.cleanup();
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('daily cap across an older run blocks a new tick without a second call', async () => {
|
|
109
|
-
const temp = fs.mkdtempSync(path.join(requireTmp(), 'longlive-router-day-'));
|
|
110
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
111
|
-
const vaultDir = path.join(temp, 'vault');
|
|
112
|
-
fs.mkdirSync(vaultDir, { recursive: true });
|
|
113
|
-
const env = makeService({ temp, memoryRoot, vaultDir, memory: false, config: { allowMissingAnchor: true, maxTokensPerDay: 5000 } });
|
|
114
|
-
try {
|
|
115
|
-
seedOldRun(vaultDir, { used: 4900, runId: 'yesterday-run' });
|
|
116
|
-
const started = await env.service.start();
|
|
117
|
-
assert.equal(started.state, 'running');
|
|
118
|
-
env.service.queue.writer.append(taskInput('day-cap'));
|
|
119
|
-
const result = await env.service.tick({ tier: 'full', reason: 'test' });
|
|
120
|
-
assert.equal(result.ok, true);
|
|
121
|
-
assert.equal(env.service.status().state, 'blocked');
|
|
122
|
-
assert.equal(env.service.status().lastError.code, 'E_BUDGET_EXHAUSTED');
|
|
123
|
-
assert.equal(env.llm.calls, 0);
|
|
124
|
-
assert.equal(env.service.status().tickQueue.pending, 1);
|
|
125
|
-
} finally {
|
|
126
|
-
await env.service.dispose();
|
|
127
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('usage above the reservation is an overshoot: record it and stop', async () => {
|
|
132
|
-
const env = await startedService({
|
|
133
|
-
llmOptions: { usage: { inputTokens: 5000, outputTokens: 100 } },
|
|
134
|
-
config: { maxTokensPerRun: 50000 },
|
|
135
|
-
});
|
|
136
|
-
try {
|
|
137
|
-
env.service.queue.writer.append(taskInput('overshoot-1'));
|
|
138
|
-
const result = await env.service.tick({ tier: 'reflect', reason: 'test' });
|
|
139
|
-
assert.equal(result.ok, true);
|
|
140
|
-
assert.equal(env.service.status().state, 'stopped');
|
|
141
|
-
assert.equal(env.service.status().lastError.code, 'E_BUDGET_OVERSHOOT');
|
|
142
|
-
const kinds = auditRecords(env.vaultDir).map((record) => record.kind);
|
|
143
|
-
assert.ok(kinds.includes('budget.overshoot'));
|
|
144
|
-
} finally {
|
|
145
|
-
await env.service.dispose();
|
|
146
|
-
env.cleanup();
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('startup reconcile repairs a crash window from the ledger side and audits it', async () => {
|
|
151
|
-
const temp = fs.mkdtempSync(path.join(requireTmp(), 'longlive-router-reconcile-'));
|
|
152
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
153
|
-
const vaultDir = path.join(temp, 'vault');
|
|
154
|
-
fs.mkdirSync(vaultDir, { recursive: true });
|
|
155
|
-
const env = makeService({
|
|
156
|
-
temp,
|
|
157
|
-
memoryRoot,
|
|
158
|
-
vaultDir,
|
|
159
|
-
memory: false,
|
|
160
|
-
config: { allowMissingAnchor: true },
|
|
161
|
-
});
|
|
162
|
-
try {
|
|
163
|
-
seedOldRun(vaultDir, { used: 120, runId: 'crashed-run' });
|
|
164
|
-
const started = await env.service.start();
|
|
165
|
-
assert.equal(started.state, 'running');
|
|
166
|
-
const reconcile = auditRecords(vaultDir).find((record) => record.kind === 'audit.reconcile');
|
|
167
|
-
assert.ok(reconcile, 'audit.reconcile must be written');
|
|
168
|
-
assert.equal(reconcile.run_id, 'crashed-run');
|
|
169
|
-
assert.equal(reconcile.delta, 120);
|
|
170
|
-
} finally {
|
|
171
|
-
await env.service.dispose();
|
|
172
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('audit settles more than the ledger: fail closed as E_LEDGER_TAMPERED', async () => {
|
|
177
|
-
const temp = fs.mkdtempSync(path.join(requireTmp(), 'longlive-router-tamper-'));
|
|
178
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
179
|
-
const vaultDir = path.join(temp, 'vault');
|
|
180
|
-
fs.mkdirSync(vaultDir, { recursive: true });
|
|
181
|
-
const env = makeService({ temp, memoryRoot, vaultDir, memory: false });
|
|
182
|
-
try {
|
|
183
|
-
seedOldRun(vaultDir, { used: 0, runId: 'ghost-run', auditSettle: 100 });
|
|
184
|
-
const started = await env.service.start();
|
|
185
|
-
assert.equal(started.state, 'blocked');
|
|
186
|
-
assert.equal(started.lastError.code, 'E_LEDGER_TAMPERED');
|
|
187
|
-
const panel = env.service.status();
|
|
188
|
-
assert.ok(panel.warnings.includes('E_LEDGER_TAMPERED'));
|
|
189
|
-
assert.ok(panel.alerts.some((alert) => alert.code === 'E_LEDGER_TAMPERED'));
|
|
190
|
-
} finally {
|
|
191
|
-
await env.service.dispose();
|
|
192
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('missing historical anchor blocks unless routerAllowMissingAnchor is true', async () => {
|
|
197
|
-
const temp = fs.mkdtempSync(path.join(requireTmp(), 'longlive-router-anchor-'));
|
|
198
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
199
|
-
const vaultDir = path.join(temp, 'vault');
|
|
200
|
-
fs.mkdirSync(vaultDir, { recursive: true });
|
|
201
|
-
const env = makeService({ temp, memoryRoot, vaultDir, memory: false });
|
|
202
|
-
try {
|
|
203
|
-
seedOldRun(vaultDir, { used: 100, runId: 'anchored-run' });
|
|
204
|
-
const started = await env.service.start();
|
|
205
|
-
assert.equal(started.state, 'blocked');
|
|
206
|
-
assert.equal(started.lastError.code, 'E_ANCHOR_MISSING');
|
|
207
|
-
} finally {
|
|
208
|
-
await env.service.dispose();
|
|
209
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
function requireTmp() {
|
|
214
|
-
return process.env.TEMP || process.env.TMP || '.';
|
|
215
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import test from 'node:test';
|
|
5
|
-
|
|
6
|
-
import { normalizeRouterConfig, ROUTER_DEFAULTS } from '../../src/router/service/config.mjs';
|
|
7
|
-
import { assembleContext, CONTEXT_TIERS } from '../../src/router/service/context.mjs';
|
|
8
|
-
import { LIMITS, loadContextSources, parseIndexRows, readCappedFile } from '../../src/router/service/sources.mjs';
|
|
9
|
-
import { estimateTokens, headTailTruncate, truncateToTokens } from '../../src/router/service/tokens.mjs';
|
|
10
|
-
import { tempRoot, writeMemory } from './helpers.mjs';
|
|
11
|
-
|
|
12
|
-
function minimalSources(overrides = {}) {
|
|
13
|
-
return {
|
|
14
|
-
stateText: '## 我在哪\n在测试。\n\n## 在等什么\n等 tick。\n\n## 最近三件\n- 一件\n',
|
|
15
|
-
handover: { name: '2026-11-04-router.md', text: '# 交接卡\n\n测试。\n' },
|
|
16
|
-
index: { rows: [{ text: '测试 | topics/test.md' }] },
|
|
17
|
-
cards: [{ name: 'test.md', text: '# 测试卡\n\n内容。' }],
|
|
18
|
-
queue: { rows: [{ id: 't1', type: 'question', depth: 0, ageMs: 1000 }] },
|
|
19
|
-
ledger: { runUsed: 10, runCap: 50000, dayUsed: 20, dayCap: 150000, reserved: 0 },
|
|
20
|
-
warnings: [],
|
|
21
|
-
now: new Date('2026-11-04T12:00:00.000Z'),
|
|
22
|
-
...overrides,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test('config defaults are off and match DESIGN §8.1', () => {
|
|
27
|
-
const cfg = normalizeRouterConfig({});
|
|
28
|
-
assert.equal(cfg.enabled, false);
|
|
29
|
-
assert.equal(cfg.provider, 'deepseek-official');
|
|
30
|
-
assert.equal(cfg.model, 'auto');
|
|
31
|
-
assert.equal(cfg.tickMs, 60000);
|
|
32
|
-
assert.equal(cfg.reflectOnTurn, true);
|
|
33
|
-
assert.equal(cfg.reflectCoalesceMs, 300000);
|
|
34
|
-
assert.equal(cfg.autonomy, 'report');
|
|
35
|
-
assert.equal(cfg.allowAgent, false);
|
|
36
|
-
assert.equal(cfg.maxTokensPerRun, 80000);
|
|
37
|
-
assert.equal(cfg.maxTokensPerDay, 1000000);
|
|
38
|
-
assert.equal(cfg.callMaxTokens, 2048);
|
|
39
|
-
assert.equal(cfg.maxAttempts, 2);
|
|
40
|
-
assert.equal(cfg.maxConsecutiveErrors, 3);
|
|
41
|
-
assert.equal(cfg.injectMaxPerSessionPerDay, 24);
|
|
42
|
-
assert.equal(cfg.injectMaxTotalTokens, 12000);
|
|
43
|
-
assert.equal(cfg.l4MergeEnabled, false);
|
|
44
|
-
assert.equal(cfg.writerLeaseMs, 120000);
|
|
45
|
-
assert.equal(cfg.queueMaxBytes, 65536);
|
|
46
|
-
assert.equal(cfg.proposalEnabled, false);
|
|
47
|
-
assert.equal(cfg.wakeEnabled, false);
|
|
48
|
-
assert.equal(cfg.disposeTimeoutMs, 2000);
|
|
49
|
-
assert.equal(Object.keys(ROUTER_DEFAULTS).length, 50);
|
|
50
|
-
assert.deepEqual(cfg.warnings, []);
|
|
51
|
-
assert.equal(ROUTER_DEFAULTS.enabled, false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('config only enables on literal true, and invalid values fall back with warnings', () => {
|
|
55
|
-
assert.equal(normalizeRouterConfig({ routerEnabled: 'true' }).enabled, false);
|
|
56
|
-
assert.equal(normalizeRouterConfig({ routerEnabled: 1 }).enabled, false);
|
|
57
|
-
const cfg = normalizeRouterConfig({
|
|
58
|
-
routerEnabled: true,
|
|
59
|
-
routerTickMs: 1,
|
|
60
|
-
routerMaxAttempts: 9,
|
|
61
|
-
routerAutonomy: 'everything',
|
|
62
|
-
routerMaxQueue: 'lots',
|
|
63
|
-
routerProvider: ' mock ',
|
|
64
|
-
unknownKey: 'ignored',
|
|
65
|
-
});
|
|
66
|
-
assert.equal(cfg.enabled, true);
|
|
67
|
-
assert.equal(cfg.tickMs, 60000);
|
|
68
|
-
assert.equal(cfg.maxAttempts, 2);
|
|
69
|
-
assert.equal(cfg.autonomy, 'report');
|
|
70
|
-
assert.equal(cfg.maxQueue, undefined);
|
|
71
|
-
assert.equal(cfg.provider, 'mock');
|
|
72
|
-
assert.ok(cfg.warnings.includes('CONFIG_INVALID:tickMs'));
|
|
73
|
-
assert.ok(cfg.warnings.includes('CONFIG_INVALID:maxAttempts'));
|
|
74
|
-
assert.ok(cfg.warnings.includes('CONFIG_INVALID:autonomy'));
|
|
75
|
-
assert.ok(cfg.warnings.includes('CONFIG_DEPRECATED:routerMaxQueue'));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('token estimate uses §3.6 CJK/4-chars ×1.05 and truncation respects the cap', () => {
|
|
79
|
-
assert.equal(estimateTokens(''), 0);
|
|
80
|
-
assert.equal(estimateTokens('中文'), 3);
|
|
81
|
-
assert.equal(estimateTokens('a'.repeat(4)), 2);
|
|
82
|
-
const cut = truncateToTokens('中'.repeat(100), 10);
|
|
83
|
-
assert.equal(cut.truncated, true);
|
|
84
|
-
assert.ok(estimateTokens(cut.text) <= 10, `got ${estimateTokens(cut.text)}`);
|
|
85
|
-
assert.ok(cut.text.endsWith('…'));
|
|
86
|
-
const headTail = headTailTruncate('a'.repeat(1000), 100);
|
|
87
|
-
assert.equal(headTail.truncated, true);
|
|
88
|
-
assert.match(headTail.text, /…\[truncated \d+ chars\]…/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('full assembly stays inside 11,000 including reserves and keeps all parts', () => {
|
|
92
|
-
const result = assembleContext({ tier: 'full', task: { id: 't1', type: 'question', payload: { question: 'hi' } }, sources: minimalSources() });
|
|
93
|
-
assert.equal(result.ok, true);
|
|
94
|
-
assert.equal(result.tier, 'full');
|
|
95
|
-
assert.ok(result.totalTokens <= CONTEXT_TIERS.full.budget);
|
|
96
|
-
assert.equal(result.outputReserve, 1000);
|
|
97
|
-
assert.equal(result.safety, 1000);
|
|
98
|
-
assert.ok(result.parts.system.includes('后台路由器'));
|
|
99
|
-
assert.ok(result.parts.openCard.includes('▍我在哪'));
|
|
100
|
-
assert.match(result.messages.prompt, /【队列】/);
|
|
101
|
-
assert.match(result.messages.prompt, /【预算】/);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('reflect assembly excludes INDEX and topic cards and stays inside 2,000', () => {
|
|
105
|
-
const result = assembleContext({ tier: 'reflect', task: { id: 't1', type: 'question', payload: { question: 'hi' } }, sources: minimalSources() });
|
|
106
|
-
assert.equal(result.ok, true);
|
|
107
|
-
assert.equal(result.tier, 'reflect');
|
|
108
|
-
assert.ok(result.totalTokens <= CONTEXT_TIERS.reflect.budget);
|
|
109
|
-
assert.equal(result.parts.index, undefined);
|
|
110
|
-
assert.equal(Object.keys(result.parts).some((key) => key.startsWith('card:')), false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('trim order starts with least-relevant topic card, then queue, then task', () => {
|
|
114
|
-
const longCjk = '测'.repeat(20000);
|
|
115
|
-
const sources = minimalSources({
|
|
116
|
-
stateText: `## 我在哪\n${longCjk}\n\n## 在等什么\n等。\n\n## 最近三件\n- 一\n`,
|
|
117
|
-
cards: [
|
|
118
|
-
{ name: 'a.md', text: `# A\n${'甲'.repeat(4000)}` },
|
|
119
|
-
{ name: 'b.md', text: `# B\n${'乙'.repeat(4000)}` },
|
|
120
|
-
],
|
|
121
|
-
queue: {
|
|
122
|
-
rows: Array.from({ length: 8 }, (_, i) => ({ id: `q${i}`, type: 'question', depth: 0, ageMs: 1000 * i })),
|
|
123
|
-
},
|
|
124
|
-
index: { rows: Array.from({ length: 12 }, (_, i) => ({ text: `行${i} | topics/t${i}.md` })) },
|
|
125
|
-
});
|
|
126
|
-
const result = assembleContext({
|
|
127
|
-
tier: 'full',
|
|
128
|
-
task: { id: 'big', type: 'question', payload: { question: '问'.repeat(5000) } },
|
|
129
|
-
sources,
|
|
130
|
-
});
|
|
131
|
-
assert.equal(result.ok, true);
|
|
132
|
-
assert.ok(result.dropped.length > 0);
|
|
133
|
-
assert.equal(result.dropped[0], 'card:b.md');
|
|
134
|
-
assert.ok(result.totalTokens <= 11000);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('over-budget input reports E_CONTEXT_OVER_BUDGET and can degrade from full only', () => {
|
|
138
|
-
const huge = minimalSources({
|
|
139
|
-
stateText: `## 我在哪\n${'测'.repeat(50000)}\n\n## 在等什么\n等。\n\n## 最近三件\n- 一\n`,
|
|
140
|
-
cards: [
|
|
141
|
-
{ name: 'a.md', text: '甲'.repeat(1200) },
|
|
142
|
-
{ name: 'b.md', text: '乙'.repeat(1200) },
|
|
143
|
-
],
|
|
144
|
-
});
|
|
145
|
-
const full = assembleContext({ tier: 'full', task: { id: 't', payload: { question: '问'.repeat(100000) } }, sources: huge });
|
|
146
|
-
// Huge cards/queue/index are trimmed; if it still does not fit the contract is
|
|
147
|
-
// a fail-closed E_CONTEXT_OVER_BUDGET with canDegrade=true, never a forced fit.
|
|
148
|
-
if (full.ok) {
|
|
149
|
-
assert.ok(full.totalTokens <= 11000);
|
|
150
|
-
} else {
|
|
151
|
-
assert.equal(full.code, 'E_CONTEXT_OVER_BUDGET');
|
|
152
|
-
assert.equal(full.canDegrade, true);
|
|
153
|
-
assert.equal(full.degradeTo, 'reflect');
|
|
154
|
-
assert.ok(full.dropped.length > 0);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('parseIndexRows reads the first markdown table and trims cells to 80 chars', () => {
|
|
159
|
-
const rows = parseIndexRows([
|
|
160
|
-
'# INDEX',
|
|
161
|
-
'| 主题 | 文件 |',
|
|
162
|
-
'|---|---|',
|
|
163
|
-
`| ${'长'.repeat(120)} | topics/a.md |`,
|
|
164
|
-
'| 第二 | topics/b.md |',
|
|
165
|
-
].join('\n'));
|
|
166
|
-
assert.equal(rows.length, 2);
|
|
167
|
-
assert.equal(Array.from(rows[0].text).length, 80);
|
|
168
|
-
assert.equal(rows[1].cells[1], 'topics/b.md');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('readCappedFile never reads more than maxBytes and reports truncation', async () => {
|
|
172
|
-
const temp = tempRoot();
|
|
173
|
-
try {
|
|
174
|
-
const file = path.join(temp, 'big.txt');
|
|
175
|
-
fs.writeFileSync(file, 'a'.repeat(100000));
|
|
176
|
-
const read = await readCappedFile(file, { maxBytes: 1024, stability: false });
|
|
177
|
-
assert.equal(read.ok, true);
|
|
178
|
-
assert.equal(read.truncated, true);
|
|
179
|
-
assert.equal(Buffer.byteLength(read.text, 'utf8'), 1024);
|
|
180
|
-
assert.equal(read.bytes, 100000);
|
|
181
|
-
} finally {
|
|
182
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('loadContextSources reads only STATE/handover/INDEX/topics and never journal or flow', async () => {
|
|
187
|
-
const temp = tempRoot();
|
|
188
|
-
try {
|
|
189
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
190
|
-
writeMemory(memoryRoot);
|
|
191
|
-
fs.mkdirSync(path.join(memoryRoot, 'journal'), { recursive: true });
|
|
192
|
-
fs.writeFileSync(path.join(memoryRoot, 'journal', '2026-11.md'), '# journal\nSHOULD NOT BE READ\n');
|
|
193
|
-
fs.mkdirSync(path.join(memoryRoot, 'flow'), { recursive: true });
|
|
194
|
-
fs.writeFileSync(path.join(memoryRoot, 'flow', 'current.md'), '# flow\nSHOULD NOT BE READ\n');
|
|
195
|
-
const sources = await loadContextSources({
|
|
196
|
-
memoryRoot,
|
|
197
|
-
task: { type: 'question', payload: { question: '测试' } },
|
|
198
|
-
now: new Date('2026-11-04T12:00:00.000Z'),
|
|
199
|
-
stability: { enabled: false },
|
|
200
|
-
});
|
|
201
|
-
assert.equal(sources.stateFound, true);
|
|
202
|
-
assert.ok(Array.isArray(sources.cards));
|
|
203
|
-
assert.equal(sources.cards.length <= LIMITS.cardsKeep, true);
|
|
204
|
-
const serialized = JSON.stringify(sources);
|
|
205
|
-
assert.equal(serialized.includes('SHOULD NOT BE READ'), false);
|
|
206
|
-
assert.equal(serialized.includes('journal'), false);
|
|
207
|
-
assert.equal(serialized.includes('flow/current'), false);
|
|
208
|
-
} finally {
|
|
209
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
});
|