@jjh32456/dsh-longlive 1.6.7 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- 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 +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -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 +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- 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 +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- 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/live.js +22 -2
- 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 +105 -16
- 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/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- 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/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- 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 -204
- 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 -568
- package/test/filter-shadow.test.mjs +0 -170
- 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 -1020
- 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 -182
- 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 -1044
- 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 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- 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
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
// WP4a — queue / lease / WAL / wake / tick-lock (K16, K17, K18, K21–K25, K27).
|
|
2
|
-
//
|
|
3
|
-
// These tests drive the modules directly (no host, no timers) and use the same
|
|
4
|
-
// on-disk layout as the service so a failure here is a protocol failure.
|
|
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 { normalizeRouterConfig } from '../../src/router/service/config.mjs';
|
|
12
|
-
import { createProposalEngine } from '../../src/router/service/proposals.mjs';
|
|
13
|
-
import { createQueueManager } from '../../src/router/service/queue.mjs';
|
|
14
|
-
import { createTickLock } from '../../src/router/service/ticklock.mjs';
|
|
15
|
-
import { createWal } from '../../src/router/service/wal.mjs';
|
|
16
|
-
import { createWakeChannel, wakeIdFor } from '../../src/router/service/wake.mjs';
|
|
17
|
-
import { createWriterLease } from '../../src/router/service/lease.mjs';
|
|
18
|
-
import { scanProposalDir } from '../../src/router/service/store.mjs';
|
|
19
|
-
import { localDayKey } from '../../src/router/service/paths.mjs';
|
|
20
|
-
import { makeService, readJson, tempRoot, writeMemory } from './helpers.mjs';
|
|
21
|
-
|
|
22
|
-
function setup(prefix = 'wp4a-') {
|
|
23
|
-
const temp = tempRoot(prefix);
|
|
24
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
25
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
26
|
-
writeMemory(memoryRoot);
|
|
27
|
-
const vaultDir = path.join(temp, 'vault');
|
|
28
|
-
const config = normalizeRouterConfig({
|
|
29
|
-
proposalEnabled: true,
|
|
30
|
-
applyEnabled: true,
|
|
31
|
-
agingSeconds: 10,
|
|
32
|
-
leaseWaitTimeoutMs: 300000,
|
|
33
|
-
blockedOnUserTtlMs: 86400000,
|
|
34
|
-
// K16 issues a 10-proposal burst from one root session on purpose. The
|
|
35
|
-
// in-lock rate re-check (D-CR5) is real behaviour now, so the fixture must
|
|
36
|
-
// raise the per-session ceiling above the burst instead of relying on the
|
|
37
|
-
// old pre-lock racing window.
|
|
38
|
-
proposalsPerSessionHour: 60,
|
|
39
|
-
});
|
|
40
|
-
const audit = [];
|
|
41
|
-
const auditFn = (kind, data = {}) => audit.push({ kind, ...data });
|
|
42
|
-
const paths = { vaultDir, proposalsDir: path.join(vaultDir, 'proposals'), liveRoot: memoryRoot };
|
|
43
|
-
const wal = createWal({ file: path.join(paths.proposalsDir, 'wal.jsonl'), audit: auditFn });
|
|
44
|
-
const engine = createProposalEngine({ config, paths, memoryRoot, audit: auditFn, now: () => new Date() });
|
|
45
|
-
const wakeChannel = createWakeChannel({ vaultDir, config, audit: auditFn, now: () => new Date() });
|
|
46
|
-
engine.wake = wakeChannel;
|
|
47
|
-
const queue = createQueueManager({ paths, config, wal, audit: auditFn, now: () => new Date() });
|
|
48
|
-
return { temp, memoryRoot, vaultDir, config, paths, audit, wal, engine, queue, wakeChannel, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function propose(engine, content, extra = {}) {
|
|
52
|
-
return engine.propose({
|
|
53
|
-
op: 'journal.append',
|
|
54
|
-
target: 'journal/2026-11.md',
|
|
55
|
-
title: `t-${content}`,
|
|
56
|
-
content,
|
|
57
|
-
...extra,
|
|
58
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
test('K16: two engines sharing a vault serialize ticket allocation inside issue.lock', async () => {
|
|
62
|
-
const env = setup('wp4a-k16-');
|
|
63
|
-
try {
|
|
64
|
-
const env2 = {
|
|
65
|
-
engine: createProposalEngine({
|
|
66
|
-
config: env.config,
|
|
67
|
-
paths: env.paths,
|
|
68
|
-
memoryRoot: env.memoryRoot,
|
|
69
|
-
audit: () => {},
|
|
70
|
-
now: () => new Date(),
|
|
71
|
-
}),
|
|
72
|
-
};
|
|
73
|
-
// One target per proposal: this test isolates ticket-allocation ordering;
|
|
74
|
-
// same-target small writes would legitimately coalesce under §5.8.
|
|
75
|
-
const results = await Promise.all(Array.from({ length: 10 }, (_, index) => (
|
|
76
|
-
propose(
|
|
77
|
-
index % 2 === 0 ? env.engine : env2.engine,
|
|
78
|
-
`body-${index}`,
|
|
79
|
-
{ target: `journal/2026-${String(index + 1).padStart(2, '0')}.md` },
|
|
80
|
-
)
|
|
81
|
-
)));
|
|
82
|
-
const tickets = results.map((result) => result.ticket);
|
|
83
|
-
assert.equal(new Set(tickets).size, tickets.length, 'no ticket may be issued twice');
|
|
84
|
-
assert.deepEqual(tickets.slice().sort((a, b) => a - b), tickets, 'ticket order follows the serialized proposal order');
|
|
85
|
-
const walLines = env.wal.read().lines;
|
|
86
|
-
for (let index = 1; index < walLines.length; index += 1) {
|
|
87
|
-
assert.equal(walLines[index].seq, walLines[index - 1].seq + 1);
|
|
88
|
-
assert.equal(walLines[index].prev, walLines[index - 1].hash);
|
|
89
|
-
}
|
|
90
|
-
assert.equal(walLines.filter((line) => line.ev === 'queue.assign').length, 10);
|
|
91
|
-
} finally {
|
|
92
|
-
env.cleanup();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('K16/K17: a stale tick.lock is reclaimed through tick.reclaim.lock with evidence', () => {
|
|
97
|
-
const env = setup('wp4a-k17-');
|
|
98
|
-
try {
|
|
99
|
-
const tickFile = path.join(env.vaultDir, 'tick.lock');
|
|
100
|
-
fs.mkdirSync(path.dirname(tickFile), { recursive: true });
|
|
101
|
-
fs.writeFileSync(tickFile, JSON.stringify({ v: 1, pid: 999999, host: 'local', startedAt: new Date(Date.now() - 3600000).toISOString() }));
|
|
102
|
-
const lock = createTickLock({ file: tickFile, maxHoldMs: 1000, audit: () => {} });
|
|
103
|
-
const first = lock.tryAcquire();
|
|
104
|
-
assert.equal(first.ok, true);
|
|
105
|
-
assert.equal(first.reclaimed, true);
|
|
106
|
-
const evidence = fs.readdirSync(env.vaultDir).filter((name) => name.startsWith('tick.reclaimed-'));
|
|
107
|
-
assert.equal(evidence.length, 1, 'old lock renamed, not unlinked');
|
|
108
|
-
assert.equal(JSON.parse(fs.readFileSync(tickFile, 'utf8')).pid, process.pid);
|
|
109
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'tick.reclaim.lock')), false);
|
|
110
|
-
lock.release();
|
|
111
|
-
|
|
112
|
-
// A live holder makes the next round a quiet skip.
|
|
113
|
-
fs.writeFileSync(tickFile, JSON.stringify({ v: 1, pid: process.pid, host: 'local', startedAt: new Date().toISOString() }));
|
|
114
|
-
const busy = lock.tryAcquire();
|
|
115
|
-
assert.equal(busy.ok, false);
|
|
116
|
-
assert.equal(busy.reason, 'busy');
|
|
117
|
-
assert.equal(JSON.parse(fs.readFileSync(tickFile, 'utf8')).pid, process.pid, 'never steals a live lock');
|
|
118
|
-
lock.release();
|
|
119
|
-
} finally {
|
|
120
|
-
env.cleanup();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('K17: an unrecoverable reclaim mutex yields reclaim-busy instead of waiting', () => {
|
|
125
|
-
const env = setup('wp4a-k17b-');
|
|
126
|
-
try {
|
|
127
|
-
const tickFile = path.join(env.vaultDir, 'tick.lock');
|
|
128
|
-
fs.mkdirSync(path.dirname(tickFile), { recursive: true });
|
|
129
|
-
fs.writeFileSync(tickFile, JSON.stringify({ v: 1, pid: 999999, host: 'local', startedAt: new Date(Date.now() - 3600000).toISOString() }));
|
|
130
|
-
fs.writeFileSync(path.join(env.vaultDir, 'tick.reclaim.lock'), '');
|
|
131
|
-
const lock = createTickLock({ file: tickFile, maxHoldMs: 1000, audit: () => {} });
|
|
132
|
-
const result = lock.tryAcquire();
|
|
133
|
-
assert.equal(result.ok, false);
|
|
134
|
-
assert.equal(result.reason, 'reclaim-busy');
|
|
135
|
-
assert.equal(fs.existsSync(tickFile), true, 'old lock left untouched');
|
|
136
|
-
} finally {
|
|
137
|
-
env.cleanup();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('K21: a half-written WAL tail is dropped, audited, and the chain continues', async () => {
|
|
142
|
-
const env = setup('wp4a-k21-');
|
|
143
|
-
try {
|
|
144
|
-
await propose(env.engine, 'one');
|
|
145
|
-
await propose(env.engine, 'two');
|
|
146
|
-
const walFile = path.join(env.paths.proposalsDir, 'wal.jsonl');
|
|
147
|
-
fs.appendFileSync(walFile, '{"seq":3,"at":"2026-11-05T00:00:00.000Z","ev":"apply.be');
|
|
148
|
-
const read = env.wal.read();
|
|
149
|
-
assert.equal(read.lines.length, 2);
|
|
150
|
-
assert.equal(read.broken, false);
|
|
151
|
-
assert.ok(env.audit.some((row) => row.kind === 'wal.corrupt'), 'corruption is audited');
|
|
152
|
-
const appended = env.wal.append('queue.reconcile', { reason: 'test' });
|
|
153
|
-
assert.equal(appended.seq, 3);
|
|
154
|
-
assert.equal(env.wal.read().lines.length, 3);
|
|
155
|
-
} finally {
|
|
156
|
-
env.cleanup();
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('K22/K25: queue rebuild reissues a lost assignment and drops orphan entries', async () => {
|
|
161
|
-
const env = setup('wp4a-k22-');
|
|
162
|
-
try {
|
|
163
|
-
const created = await propose(env.engine, 'lost assign');
|
|
164
|
-
const proposalFile = path.join(env.paths.proposalsDir, `${created.id}.json`);
|
|
165
|
-
// Simulate a crash between proposal write and WAL/queue write: no assign line,
|
|
166
|
-
// no queue file. The proposal itself must survive and get a tail ticket.
|
|
167
|
-
fs.writeFileSync(path.join(env.paths.proposalsDir, 'wal.jsonl'), '');
|
|
168
|
-
fs.rmSync(path.join(env.paths.proposalsDir, 'queue.json'), { force: true });
|
|
169
|
-
const rebuilt = env.queue.rebuild();
|
|
170
|
-
assert.equal(rebuilt.recovered.length, 1);
|
|
171
|
-
const proposal = readJson(proposalFile);
|
|
172
|
-
assert.equal(proposal.status, 'pending');
|
|
173
|
-
assert.notEqual(proposal.queue.ticket, created.ticket);
|
|
174
|
-
assert.match(proposal.stateLog.at(-1).note, /queue\.assign\.recovered/);
|
|
175
|
-
const walLines = env.wal.read().lines;
|
|
176
|
-
assert.equal(walLines.at(-1).ev, 'queue.assign.recovered');
|
|
177
|
-
assert.equal(walLines.at(-1).ticket, proposal.queue.ticket);
|
|
178
|
-
|
|
179
|
-
// An orphan queue entry (proposal deleted) is reconciled away.
|
|
180
|
-
const queueFile = path.join(env.paths.proposalsDir, 'queue.json');
|
|
181
|
-
const queue = readJson(queueFile);
|
|
182
|
-
queue.entries.push({ ticket: 'q-0099', proposalId: 'p-20260101T000000-abcdef', status: 'queued' });
|
|
183
|
-
fs.writeFileSync(queueFile, JSON.stringify(queue));
|
|
184
|
-
const again = env.queue.rebuild();
|
|
185
|
-
assert.ok(again.orphans.length >= 1);
|
|
186
|
-
assert.ok(env.audit.some((row) => row.kind === 'queue.reconcile'));
|
|
187
|
-
const after = readJson(queueFile);
|
|
188
|
-
assert.equal(after.entries.some((entry) => entry.ticket === 'q-0099'), false);
|
|
189
|
-
} finally {
|
|
190
|
-
env.cleanup();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('K18/K21: lease reclaim renames evidence and clock rollback is audited', () => {
|
|
195
|
-
const env = setup('wp4a-k18-');
|
|
196
|
-
try {
|
|
197
|
-
const locksDir = path.join(env.vaultDir, 'locks');
|
|
198
|
-
const leaseFile = path.join(locksDir, 'writer.lease.json');
|
|
199
|
-
let wall = Date.now();
|
|
200
|
-
const holder = { sessionId: 's1', runId: 'run-1', pid: process.pid, host: 'local' };
|
|
201
|
-
const audit = [];
|
|
202
|
-
const lease = createWriterLease({
|
|
203
|
-
file: leaseFile,
|
|
204
|
-
lockDir: locksDir,
|
|
205
|
-
ttlMs: 120000,
|
|
206
|
-
renewalsMax: 2,
|
|
207
|
-
holder,
|
|
208
|
-
now: () => new Date(wall),
|
|
209
|
-
audit: (kind, data) => audit.push({ kind, ...data }),
|
|
210
|
-
});
|
|
211
|
-
assert.equal(lease.acquire({ ticket: 'q-0001', proposalId: 'p-20260101T000000-abcdef' }).ok, true);
|
|
212
|
-
// Force expiry and let another process reclaim it.
|
|
213
|
-
const stored = JSON.parse(fs.readFileSync(leaseFile, 'utf8'));
|
|
214
|
-
stored.expiresAt = new Date(wall - 1000).toISOString();
|
|
215
|
-
fs.writeFileSync(leaseFile, JSON.stringify(stored));
|
|
216
|
-
const lease2 = createWriterLease({
|
|
217
|
-
file: leaseFile,
|
|
218
|
-
lockDir: locksDir,
|
|
219
|
-
ttlMs: 120000,
|
|
220
|
-
renewalsMax: 2,
|
|
221
|
-
holder: { sessionId: 's2', runId: 'run-2', pid: 999999, host: 'local' },
|
|
222
|
-
now: () => new Date(wall),
|
|
223
|
-
audit: (kind, data) => audit.push({ kind, ...data }),
|
|
224
|
-
});
|
|
225
|
-
const reclaimed = lease2.acquire({ ticket: 'q-0002', proposalId: 'p-20260101T000001-abcdef' });
|
|
226
|
-
assert.equal(reclaimed.ok, true);
|
|
227
|
-
const evidence = fs.readdirSync(locksDir).filter((name) => name.startsWith('reclaimed-'));
|
|
228
|
-
assert.equal(evidence.length, 1, 'expired lease kept as evidence');
|
|
229
|
-
assert.ok(audit.some((row) => row.kind === 'lease.expired'));
|
|
230
|
-
|
|
231
|
-
// Clock going backwards is detected and audited.
|
|
232
|
-
wall -= 10 * 60 * 1000;
|
|
233
|
-
lease2.renew();
|
|
234
|
-
assert.ok(audit.some((row) => row.kind === 'lease.clock_skew'));
|
|
235
|
-
} finally {
|
|
236
|
-
env.cleanup();
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('K21: a zero-byte lease is reclaimed as evidence instead of blocking forever', () => {
|
|
241
|
-
const env = setup('wp4a-zero-lease-');
|
|
242
|
-
try {
|
|
243
|
-
const lockDir = path.join(env.vaultDir, 'locks');
|
|
244
|
-
fs.mkdirSync(lockDir, { recursive: true });
|
|
245
|
-
const leaseFile = path.join(lockDir, 'writer.lease.json');
|
|
246
|
-
fs.writeFileSync(leaseFile, '', 'utf8');
|
|
247
|
-
const lease = createWriterLease({
|
|
248
|
-
file: leaseFile,
|
|
249
|
-
lockDir,
|
|
250
|
-
ttlMs: 60000,
|
|
251
|
-
renewalsMax: 2,
|
|
252
|
-
holder: { sessionId: 'recover', runId: 'run-recover', pid: process.pid, host: 'local' },
|
|
253
|
-
audit: (kind, data) => env.audit.push({ kind, ...data }),
|
|
254
|
-
});
|
|
255
|
-
const acquired = lease.acquire({ ticket: 'q-0001', proposalId: 'p-20261105T000000-abcdef' });
|
|
256
|
-
assert.equal(acquired.ok, true, JSON.stringify(acquired));
|
|
257
|
-
assert.equal(fs.existsSync(path.join(lockDir, 'reclaim.lock')), false);
|
|
258
|
-
const evidence = fs.readdirSync(lockDir).filter((name) => name.startsWith('reclaimed-corrupt-'));
|
|
259
|
-
assert.equal(evidence.length, 1);
|
|
260
|
-
assert.ok(env.audit.some((row) => row.kind === 'lease.expired' && row.reason === 'zero-or-corrupt'));
|
|
261
|
-
} finally {
|
|
262
|
-
env.cleanup();
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test('K27: Tier A always records readiness; Tier B dedupes by wakeId and never resends after flush failure', async () => {
|
|
267
|
-
const env = setup('wp4a-k27-');
|
|
268
|
-
try {
|
|
269
|
-
const created = await propose(env.engine, 'wake me');
|
|
270
|
-
const readyFile = path.join(env.vaultDir, 'wake-ready.jsonl');
|
|
271
|
-
const lines = fs.readFileSync(readyFile, 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
272
|
-
assert.equal(lines.length, 1);
|
|
273
|
-
assert.equal(lines[0].ticket, created.ticketName);
|
|
274
|
-
assert.equal(lines[0].id, wakeIdFor('root-session', created.ticketName, localDayKey(lines[0].createdAt)));
|
|
275
|
-
assert.equal('consumedAt' in lines[0], false);
|
|
276
|
-
|
|
277
|
-
const audit = [];
|
|
278
|
-
const wake = createWakeChannel({
|
|
279
|
-
vaultDir: env.vaultDir,
|
|
280
|
-
config: { ...env.config, wakeEnabled: true, wakeCooldownMs: 0, wakeDailyQuota: 20 },
|
|
281
|
-
now: () => new Date(),
|
|
282
|
-
audit: (kind, data) => audit.push({ kind, ...data }),
|
|
283
|
-
});
|
|
284
|
-
const sessions = [{ sessionId: 'root-session', state: 'idle', isSubagent: false }];
|
|
285
|
-
const first = wake.plan({ sessions });
|
|
286
|
-
assert.equal(first.entries.length, 1);
|
|
287
|
-
wake.recordAttempt({ id: first.entries[0].id, sessionId: 'root-session', ok: false, error: 'flush', flushFailed: true });
|
|
288
|
-
assert.ok(audit.some((row) => row.kind === 'wake.flush_failed'));
|
|
289
|
-
assert.equal(wake.plan({ sessions }).entries.length, 0, 'same wakeId is never resent');
|
|
290
|
-
assert.notEqual(wakeIdFor('root-session', created.ticket, '2026-11-05', 1), first.entries[0].id);
|
|
291
|
-
} finally {
|
|
292
|
-
env.cleanup();
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('K23/K24: status projection agrees with queue.json and stays within 16 KB', async () => {
|
|
297
|
-
const temp = tempRoot('wp4a-k23-');
|
|
298
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
299
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
300
|
-
writeMemory(memoryRoot);
|
|
301
|
-
const vaultDir = path.join(temp, 'vault');
|
|
302
|
-
const env = makeService({ temp, memoryRoot, vaultDir, config: { proposalEnabled: true } });
|
|
303
|
-
try {
|
|
304
|
-
const started = await env.service.start();
|
|
305
|
-
assert.equal(started.ok, true);
|
|
306
|
-
await env.service.proposeProposal({
|
|
307
|
-
op: 'journal.append',
|
|
308
|
-
target: 'journal/2026-11.md',
|
|
309
|
-
title: 'status',
|
|
310
|
-
content: 'status body',
|
|
311
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
312
|
-
const tick = await env.service.tick({ tier: 'reflect', reason: 'test' });
|
|
313
|
-
assert.equal(tick.ok, true);
|
|
314
|
-
const status = readJson(path.join(vaultDir, 'status.json'));
|
|
315
|
-
const queue = readJson(path.join(vaultDir, 'proposals', 'queue.json'));
|
|
316
|
-
assert.equal(status.queue.pending, 1);
|
|
317
|
-
assert.equal(status.proposals.queue.depth, queue.entries.length);
|
|
318
|
-
assert.equal(status.proposals.queue.seqCounter, queue.seqCounter);
|
|
319
|
-
assert.equal(status.wal.lastSeq, queue.seqCounter - 1);
|
|
320
|
-
assert.ok(fs.statSync(path.join(vaultDir, 'status.json')).size <= 16 * 1024);
|
|
321
|
-
assert.ok(fs.statSync(path.join(vaultDir, 'proposals', 'queue.json')).size <= 64 * 1024);
|
|
322
|
-
} finally {
|
|
323
|
-
env.cleanup();
|
|
324
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test('K24: approved proposals age by one level per aging window, capped at P1', async () => {
|
|
329
|
-
const env = setup('wp4a-aging-');
|
|
330
|
-
try {
|
|
331
|
-
const created = await propose(env.engine, 'age me', { priority: 'P4' });
|
|
332
|
-
const file = path.join(env.paths.proposalsDir, `${created.id}.json`);
|
|
333
|
-
const proposal = readJson(file);
|
|
334
|
-
proposal.queue.queuedAt = new Date(Date.now() - 2 * 3600 * 1000).toISOString();
|
|
335
|
-
fs.writeFileSync(file, JSON.stringify(proposal));
|
|
336
|
-
env.queue.maintain();
|
|
337
|
-
const aged = readJson(file);
|
|
338
|
-
assert.ok(aged.queue.agingSteps >= 1);
|
|
339
|
-
assert.ok(['P1', 'P2', 'P3'].includes(aged.queue.effectivePriority));
|
|
340
|
-
assert.ok(env.audit.some((row) => row.kind === 'queue.aging'));
|
|
341
|
-
} finally {
|
|
342
|
-
env.cleanup();
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test('scanProposalDir is the single queue source (no queue-only proposal)', () => {
|
|
347
|
-
const env = setup('wp4a-scan-');
|
|
348
|
-
try {
|
|
349
|
-
const scan = scanProposalDir(env.paths.proposalsDir);
|
|
350
|
-
assert.equal(scan.proposals.length, 0);
|
|
351
|
-
assert.equal(fs.existsSync(env.paths.proposalsDir), false, 'read-only scan never creates the directory');
|
|
352
|
-
} finally {
|
|
353
|
-
env.cleanup();
|
|
354
|
-
}
|
|
355
|
-
});
|