@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
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// anything and never touches Task Scheduler.
|
|
21
21
|
//
|
|
22
22
|
// node scripts/session-watch-drill.mjs
|
|
23
|
-
// node scripts/session-watch-drill.mjs --workdir
|
|
23
|
+
// node scripts/session-watch-drill.mjs --workdir <drill-dir> --clean
|
|
24
24
|
//
|
|
25
25
|
// stdout is one JSON document; the same document is written to
|
|
26
26
|
// <workdir>/drill-result.json. Exit 0 = every check passed, 1 = a check failed,
|
package/scripts/surgery-run.cmd
CHANGED
|
@@ -16,7 +16,7 @@ exit /b 0
|
|
|
16
16
|
|
|
17
17
|
:usage
|
|
18
18
|
echo Usage:
|
|
19
|
-
echo surgery-run.cmd -SessionPath "
|
|
19
|
+
echo surgery-run.cmd -SessionPath "<session-file>" -AppPath "<app folder>\DeepSeek Harness.exe" -Confirm TRUNCATE-VISIBLE [options]
|
|
20
20
|
echo.
|
|
21
21
|
echo Options:
|
|
22
22
|
echo -KeepTurns N default 12
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass `
|
|
76
76
|
-File scripts\unattended-surgery.ps1 `
|
|
77
77
|
-SessionPath "%USERPROFILE%\.dsh\sessions\scope\abc\session.v4.jsonl.zstd" `
|
|
78
|
-
-AppPath "
|
|
78
|
+
-AppPath "<path-to-DSH-host.exe>" -Confirm TRUNCATE-VISIBLE
|
|
79
79
|
#>
|
|
80
80
|
[CmdletBinding()]
|
|
81
81
|
param(
|
|
@@ -576,8 +576,11 @@ if ($RestartDelaySeconds -lt 0 -or $RestartDelaySeconds -gt 86400) {
|
|
|
576
576
|
if (-not $DryRun -and $Confirm -ne 'TRUNCATE-VISIBLE') {
|
|
577
577
|
Write-StderrAndExit "Refusing to cut: pass -Confirm TRUNCATE-VISIBLE (or use -DryRun)." 2
|
|
578
578
|
}
|
|
579
|
+
if (-not $DryRun -and -not $AppPath -and $env:DSH_APP_PATH) {
|
|
580
|
+
$AppPath = $env:DSH_APP_PATH
|
|
581
|
+
}
|
|
579
582
|
if (-not $DryRun -and -not $AppPath) {
|
|
580
|
-
Write-StderrAndExit "Refusing to cut: pass -AppPath <
|
|
583
|
+
Write-StderrAndExit "Refusing to cut: pass -AppPath <dsh host exe> or set DSH_APP_PATH so the host can be restarted." 2
|
|
581
584
|
}
|
|
582
585
|
if ($AppPath) {
|
|
583
586
|
if (-not (Test-Path -LiteralPath $AppPath -PathType Leaf)) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/verify-card-anchors.mjs — 1.8.0 AutoCard anchor liveness check.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/verify-card-anchors.mjs [--memory-root <dir>] [--sessions-root <dir>]
|
|
5
|
+
// [--all | --sample N] [--json] [--out <file>]
|
|
6
|
+
//
|
|
7
|
+
// Reads `.longlive/index/cards.jsonl`, loads the original L0 session for every
|
|
8
|
+
// card anchor, recomputes turnDigest, and labels each card live / stale / missing.
|
|
9
|
+
// Exit 1 when any card is not live (unless --allow-stale for inspection runs).
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { parseSession } from '../src/codec.js';
|
|
16
|
+
import { discoverSessionLogs, extractTurns } from '../src/memory/event-index.js';
|
|
17
|
+
import { readAutoCards, cardsPathOf } from '../src/memory/auto-cards.js';
|
|
18
|
+
|
|
19
|
+
function dshHome() {
|
|
20
|
+
const raw = process.env.DSH_HOME;
|
|
21
|
+
return typeof raw === 'string' && raw.trim() !== '' ? path.resolve(raw.trim()) : path.join(os.homedir(), '.dsh');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultMemoryRoot() {
|
|
25
|
+
for (const envName of ['DSH_LONGLIVE_MEMORY_ROOT', 'DSH_MEM']) {
|
|
26
|
+
const value = process.env[envName];
|
|
27
|
+
if (typeof value === 'string' && value.trim() !== '') return path.resolve(value.trim());
|
|
28
|
+
}
|
|
29
|
+
return path.join(dshHome(), 'longlive-memory');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const out = { memoryRoot: '', sessionsRoot: '', sample: 0, json: false, out: '', allowStale: false };
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const arg = argv[i];
|
|
36
|
+
const match = /^--([a-z-]+)(?:=(.*))?$/.exec(arg);
|
|
37
|
+
if (!match) continue;
|
|
38
|
+
const [, key, inline] = match;
|
|
39
|
+
const value = inline !== undefined ? inline : (argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : '');
|
|
40
|
+
if (key === 'memory-root') out.memoryRoot = value;
|
|
41
|
+
else if (key === 'sessions-root') out.sessionsRoot = value;
|
|
42
|
+
else if (key === 'sample') out.sample = Number.parseInt(value, 10) || 0;
|
|
43
|
+
else if (key === 'json') out.json = true;
|
|
44
|
+
else if (key === 'out') out.out = value;
|
|
45
|
+
else if (key === 'allow-stale') out.allowStale = true;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Discover session files without parsing them (avoid loading 900MB at once). */
|
|
51
|
+
function sessionFiles(sessionsRoot) {
|
|
52
|
+
const files = new Map();
|
|
53
|
+
for (const item of discoverSessionLogs([sessionsRoot])) files.set(item.sessionId, item.file);
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function verifyCards({ memoryRoot, sessionsRoot } = {}) {
|
|
58
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
59
|
+
const cards = readAutoCards(root);
|
|
60
|
+
const files = sessionFiles(path.resolve(String(sessionsRoot ?? '')));
|
|
61
|
+
const items = [];
|
|
62
|
+
const bySession = new Map();
|
|
63
|
+
for (const card of cards) {
|
|
64
|
+
const ids = [card.sessionId, card.anchor?.sessionId].filter(Boolean);
|
|
65
|
+
const sessionId = ids[0];
|
|
66
|
+
if (!bySession.has(sessionId)) bySession.set(sessionId, []);
|
|
67
|
+
bySession.get(sessionId).push(card);
|
|
68
|
+
}
|
|
69
|
+
for (const [sessionId, group] of bySession) {
|
|
70
|
+
let parsed = null;
|
|
71
|
+
const file = files.get(sessionId);
|
|
72
|
+
if (file) {
|
|
73
|
+
try { parsed = parseSession(fs.readFileSync(file)); } catch { parsed = null; }
|
|
74
|
+
}
|
|
75
|
+
// Use the same per-turn extraction as the index builder; turnWindowOf would
|
|
76
|
+
// include preamble events differently and produce false stale digests.
|
|
77
|
+
const records = new Map();
|
|
78
|
+
if (parsed) {
|
|
79
|
+
try {
|
|
80
|
+
for (const record of extractTurns(parsed.header, parsed.events, parsed.frames, parsed.texts)) {
|
|
81
|
+
records.set(record.turn, record);
|
|
82
|
+
}
|
|
83
|
+
} catch { /* unreadable session => missing below */ }
|
|
84
|
+
}
|
|
85
|
+
for (const card of group) {
|
|
86
|
+
const record = records.get(card.turn);
|
|
87
|
+
if (!record) {
|
|
88
|
+
items.push({
|
|
89
|
+
cardId: card.cardId, eventId: card.eventId, sessionId: card.sessionId, turn: card.turn,
|
|
90
|
+
status: 'missing', code: parsed ? 'E_TURN_MISSING' : 'E_SESSION_MISSING',
|
|
91
|
+
expectedDigest: card.anchor.digest, actualDigest: null,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const anchor = card.anchor;
|
|
96
|
+
const sameWindow = anchor.seqStart === record.seqStart && anchor.seqEnd === record.seqEnd;
|
|
97
|
+
const live = sameWindow && anchor.digest === record.digest;
|
|
98
|
+
items.push({
|
|
99
|
+
cardId: card.cardId, eventId: card.eventId, sessionId: card.sessionId, turn: card.turn,
|
|
100
|
+
status: live ? 'live' : 'stale',
|
|
101
|
+
code: live ? 'OK' : (sameWindow ? 'E_TURN_DIGEST_MISMATCH' : 'E_TURN_WINDOW_MISMATCH'),
|
|
102
|
+
expectedDigest: anchor.digest,
|
|
103
|
+
actualDigest: record.digest,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const counts = { total: items.length, live: 0, stale: 0, missing: 0 };
|
|
108
|
+
for (const item of items) counts[item.status] = (counts[item.status] ?? 0) + 1;
|
|
109
|
+
return { schema: 'card-anchors/v1', memoryRoot: root, cardsFile: cardsPathOf(root), sessions: files.size, counts, items };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'));
|
|
113
|
+
if (isMain) {
|
|
114
|
+
const args = parseArgs(process.argv.slice(2));
|
|
115
|
+
const memoryRoot = args.memoryRoot !== '' ? args.memoryRoot : defaultMemoryRoot();
|
|
116
|
+
const sessionsRoot = args.sessionsRoot !== '' ? args.sessionsRoot : path.join(dshHome(), 'sessions');
|
|
117
|
+
const report = verifyCards({ memoryRoot, sessionsRoot });
|
|
118
|
+
let items = report.items;
|
|
119
|
+
if (args.sample > 0 && items.length > args.sample) {
|
|
120
|
+
items = [...items].sort((a, b) => a.cardId.localeCompare(b.cardId)).slice(0, args.sample);
|
|
121
|
+
}
|
|
122
|
+
const output = { ...report, items, sampled: args.sample > 0 && args.sample < report.items.length };
|
|
123
|
+
const body = args.json || args.out !== '';
|
|
124
|
+
if (body) {
|
|
125
|
+
const text = JSON.stringify(output, null, 2);
|
|
126
|
+
if (args.json) console.log(text);
|
|
127
|
+
if (args.out !== '') {
|
|
128
|
+
fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
|
|
129
|
+
fs.writeFileSync(path.resolve(args.out), `${text}\n`, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.log(`[verify-card-anchors] total=${output.counts.total} live=${output.counts.live} stale=${output.counts.stale} missing=${output.counts.missing}`);
|
|
133
|
+
for (const item of items.filter((x) => x.status !== 'live').slice(0, 20)) {
|
|
134
|
+
console.log(` ${item.status} ${item.cardId} ${item.sessionId}#${item.turn} ${item.code}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if ((output.counts.stale > 0 || output.counts.missing > 0) && !args.allowStale) process.exitCode = 1;
|
|
138
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/actions/audit.js — HMAC 链式审计(M2-G / §12.4 / §12.7)
|
|
2
|
+
//
|
|
3
|
+
// 一行一个 JSON:
|
|
4
|
+
// {seq,ts,actorId,action,target,beforeHash,afterHash,prevHash,hmac}
|
|
5
|
+
// - seq 严格 +1(从 1 起),prevHash 链接上一行 hmac(首行 GENESIS);
|
|
6
|
+
// - hmac = HMAC-SHA256(auditKey, 规范化前 8 字段),auditKey **仅宿主内存**;
|
|
7
|
+
// - 逆序/断链/篡改 → E_AUDIT_BROKEN(校验失败,绝不静默);
|
|
8
|
+
// - actorId 只记哈希前缀,正文不记(隐私)。
|
|
9
|
+
// 审计 key 不落盘:跨重启延续审计链需要独立服务账号 + OS 凭据库(§13 第二层,1.6.9 不做)。
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { GENESIS_HASH, actionsDir, appendJsonl, hmacHexFor, readJsonl, sha256Hex, stableStringify } from './fs.js';
|
|
14
|
+
|
|
15
|
+
export const AUDIT_SCHEMA = 'audit/v1';
|
|
16
|
+
export const AUDIT_FILE = 'actions.log';
|
|
17
|
+
|
|
18
|
+
export class AuditError extends Error {
|
|
19
|
+
constructor(code, message, details = undefined) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'AuditError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
if (details !== undefined) this.details = details;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const AUDIT_FILE_NAME = AUDIT_FILE;
|
|
28
|
+
|
|
29
|
+
export function auditFilePath(memoryRoot) {
|
|
30
|
+
return path.join(actionsDir(memoryRoot), AUDIT_FILE_NAME);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Canonical string of the 8 signed fields (order fixed; target stringified stably). */
|
|
34
|
+
export function canonicalAuditBody(entry) {
|
|
35
|
+
return [
|
|
36
|
+
String(entry.seq),
|
|
37
|
+
String(entry.ts),
|
|
38
|
+
String(entry.actorId),
|
|
39
|
+
String(entry.action),
|
|
40
|
+
stableStringify(entry.target ?? null),
|
|
41
|
+
String(entry.beforeHash ?? ''),
|
|
42
|
+
String(entry.afterHash ?? ''),
|
|
43
|
+
String(entry.prevHash ?? ''),
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Host-only actor identity: hashed prefix only, never the raw id. */
|
|
48
|
+
export function hashActorId(raw) {
|
|
49
|
+
return `host:${sha256Hex(String(raw ?? 'host')).slice(0, 8)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hmacFor = hmacHexFor;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create the audit chain writer/verifier.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} options
|
|
58
|
+
* @param {string} options.memoryRoot
|
|
59
|
+
* @param {Buffer|string} options.auditKey host-memory key (32B Buffer preferred)
|
|
60
|
+
* @param {() => Date} [options.now]
|
|
61
|
+
* @returns {{append:Function, readAll:Function, verify:Function, latest:Function, file:string}}
|
|
62
|
+
*/
|
|
63
|
+
export function createAuditLog(options = {}) {
|
|
64
|
+
const memoryRoot = String(options.memoryRoot ?? '');
|
|
65
|
+
const auditKey = options.auditKey;
|
|
66
|
+
const now = typeof options.now === 'function' ? options.now : () => new Date();
|
|
67
|
+
const file = auditFilePath(memoryRoot);
|
|
68
|
+
|
|
69
|
+
function readAll() {
|
|
70
|
+
const read = readJsonl(file);
|
|
71
|
+
return { rows: Array.isArray(read.rows) ? read.rows : [], bad: read.bad ?? 0, exists: read.exists === true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function verify(rows = readAll().rows) {
|
|
75
|
+
let prevHash = GENESIS_HASH;
|
|
76
|
+
let prevSeq = 0;
|
|
77
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
78
|
+
const entry = rows[index];
|
|
79
|
+
if (entry === null || typeof entry !== 'object') {
|
|
80
|
+
return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'not-an-object' };
|
|
81
|
+
}
|
|
82
|
+
if (entry.seq !== prevSeq + 1) {
|
|
83
|
+
return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: `seq ${entry.seq} != ${prevSeq + 1}` };
|
|
84
|
+
}
|
|
85
|
+
if (entry.prevHash !== undefined && entry.prevHash !== prevHash) {
|
|
86
|
+
return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'prevHash mismatch' };
|
|
87
|
+
}
|
|
88
|
+
const expected = hmacFor(auditKey, canonicalAuditBody(entry));
|
|
89
|
+
if (entry.hmac !== expected) {
|
|
90
|
+
return { ok: false, code: 'E_AUDIT_BROKEN', at: index, reason: 'hmac mismatch' };
|
|
91
|
+
}
|
|
92
|
+
prevHash = entry.hmac;
|
|
93
|
+
prevSeq = entry.seq;
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, rows: rows.length, lastSeq: prevSeq, lastHash: prevHash };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function append({ actorId = 'host', action, target = null, beforeHash = '', afterHash = '' } = {}) {
|
|
99
|
+
const current = readAll();
|
|
100
|
+
const checked = verify(current.rows);
|
|
101
|
+
if (!checked.ok) throw new AuditError('E_AUDIT_BROKEN', '审计链已损坏:拒绝继续追加。', checked);
|
|
102
|
+
const seq = (current.rows.length > 0 ? current.rows[current.rows.length - 1].seq : 0) + 1;
|
|
103
|
+
const prevHash = current.rows.length > 0 ? current.rows[current.rows.length - 1].hmac : GENESIS_HASH;
|
|
104
|
+
const entry = {
|
|
105
|
+
seq,
|
|
106
|
+
ts: now().toISOString(),
|
|
107
|
+
actorId: String(actorId),
|
|
108
|
+
action: String(action),
|
|
109
|
+
target,
|
|
110
|
+
beforeHash: String(beforeHash ?? ''),
|
|
111
|
+
afterHash: String(afterHash ?? ''),
|
|
112
|
+
prevHash,
|
|
113
|
+
};
|
|
114
|
+
entry.hmac = hmacFor(auditKey, canonicalAuditBody(entry));
|
|
115
|
+
const written = appendJsonl(file, entry);
|
|
116
|
+
return { ok: true, seq, entry, file, offset: written.offset };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function latest() {
|
|
120
|
+
const { rows } = readAll();
|
|
121
|
+
return rows.length > 0 ? rows[rows.length - 1] : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { file, readAll, verify, append, latest };
|
|
125
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/actions/evidence.js — evidence 私有写路径 + HMAC 验签(N38 三写 probe 的靶子)
|
|
2
|
+
//
|
|
3
|
+
// evidence 包只由宿主写:写盘前用**仅宿主内存**的 auditKey 签名,原子写
|
|
4
|
+
// `<memoryRoot>/.longlive/evidence/latest.json`,并把 `packageSha256` 一起落盘供复核。
|
|
5
|
+
// 模型/子脑没有 token、没有 auditKey:
|
|
6
|
+
// - 走动作门 → 401 `E_NO_CAP`(见 panel-auth.js);
|
|
7
|
+
// - 伪造证据(自己拼 JSON + 假签名)→ 验签失败 `E_HMAC_INVALID`,不落盘、不覆盖真证据。
|
|
8
|
+
// 本模块不读取任何凭据文件,也不把 key 写进返回值。
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { hmacHexFor, readJsonFile, sha256Hex, stableStringify, writeJsonAtomic } from './fs.js';
|
|
13
|
+
|
|
14
|
+
export const EVIDENCE_SCHEMA = 'evidence-package/v1';
|
|
15
|
+
export const EVIDENCE_DIR = 'evidence';
|
|
16
|
+
export const EVIDENCE_FILE = 'latest.json';
|
|
17
|
+
|
|
18
|
+
export class EvidenceError extends Error {
|
|
19
|
+
constructor(code, message, status = 403, details = undefined) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'EvidenceError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.status = status;
|
|
24
|
+
if (details !== undefined) this.details = details;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function evidenceFilePath(memoryRoot) {
|
|
29
|
+
return path.join(String(memoryRoot ?? ''), '.longlive', EVIDENCE_DIR, EVIDENCE_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalEvidencePayload(pkg) {
|
|
33
|
+
return stableStringify(pkg ?? null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create the host-side evidence writer.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} options
|
|
40
|
+
* @param {string} options.memoryRoot
|
|
41
|
+
* @param {Buffer|string} options.auditKey host-memory signing key (never serialized)
|
|
42
|
+
* @param {() => Date} [options.now]
|
|
43
|
+
*/
|
|
44
|
+
export function createEvidenceWriter(options = {}) {
|
|
45
|
+
const memoryRoot = String(options.memoryRoot ?? '');
|
|
46
|
+
const auditKey = options.auditKey;
|
|
47
|
+
const now = typeof options.now === 'function' ? options.now : () => new Date();
|
|
48
|
+
const file = evidenceFilePath(memoryRoot);
|
|
49
|
+
|
|
50
|
+
if (!auditKey) throw new EvidenceError('E_NO_CAP', '缺少宿主签名 key:evidence 只允许宿主进程写。', 401);
|
|
51
|
+
|
|
52
|
+
function sign(pkg) {
|
|
53
|
+
return hmacHexFor(auditKey, canonicalEvidencePayload(pkg));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function persist(pkg, signature, source) {
|
|
57
|
+
const packageSha256 = sha256Hex(canonicalEvidencePayload(pkg));
|
|
58
|
+
const record = {
|
|
59
|
+
schemaVersion: EVIDENCE_SCHEMA,
|
|
60
|
+
package: pkg,
|
|
61
|
+
packageSha256,
|
|
62
|
+
signature,
|
|
63
|
+
signatureSource: source,
|
|
64
|
+
writtenAt: now().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
writeJsonAtomic(file, record);
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
path: file,
|
|
70
|
+
packageSha256,
|
|
71
|
+
fileSha256: sha256Hex(JSON.stringify(record)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Host path: sign with the in-memory key, then write (N38 host probe). */
|
|
76
|
+
function writeHost(pkg) {
|
|
77
|
+
if (pkg === null || typeof pkg !== 'object') {
|
|
78
|
+
throw new EvidenceError('E_EVIDENCE_INVALID', 'evidence 包必须是对象。', 422);
|
|
79
|
+
}
|
|
80
|
+
return persist(pkg, sign(pkg), 'host');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Any caller may submit a package + signature, but only a host-signed one
|
|
85
|
+
* verifies. A forged signature fails closed with `E_HMAC_INVALID`.
|
|
86
|
+
*/
|
|
87
|
+
function submitSigned({ pkg, signature } = {}) {
|
|
88
|
+
if (pkg === null || typeof pkg !== 'object') {
|
|
89
|
+
throw new EvidenceError('E_EVIDENCE_INVALID', 'evidence 包必须是对象。', 422);
|
|
90
|
+
}
|
|
91
|
+
if (typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) {
|
|
92
|
+
throw new EvidenceError('E_HMAC_INVALID', 'evidence 签名缺失或格式非法:拒绝写入。', 403);
|
|
93
|
+
}
|
|
94
|
+
const expected = sign(pkg);
|
|
95
|
+
if (signature.toLowerCase() !== expected) {
|
|
96
|
+
throw new EvidenceError('E_HMAC_INVALID', 'evidence 验签失败:拒绝写入(真证据未被覆盖)。', 403);
|
|
97
|
+
}
|
|
98
|
+
return persist(pkg, expected, 'host');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Re-read + re-verify the on-disk evidence (used by probe/action returns). */
|
|
102
|
+
function readLatest() {
|
|
103
|
+
const read = readJsonFile(file);
|
|
104
|
+
if (!read.ok) return { ok: false, present: false, code: read.code };
|
|
105
|
+
const record = read.value;
|
|
106
|
+
const packageSha256 = record?.package === undefined ? null : sha256Hex(canonicalEvidencePayload(record.package));
|
|
107
|
+
const signatureValid = typeof record?.signature === 'string'
|
|
108
|
+
&& record.signature === sign(record.package);
|
|
109
|
+
return {
|
|
110
|
+
ok: signatureValid && packageSha256 === record.packageSha256,
|
|
111
|
+
present: true,
|
|
112
|
+
schemaVersion: record?.schemaVersion ?? null,
|
|
113
|
+
package: record?.package ?? null,
|
|
114
|
+
packageSha256,
|
|
115
|
+
recordedSha256: record?.packageSha256 ?? null,
|
|
116
|
+
signatureValid,
|
|
117
|
+
writtenAt: record?.writtenAt ?? null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { file, writeHost, submitSigned, readLatest, signPackage: sign };
|
|
122
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/actions/fs.js — 动作层私有文件原语(1.6.9 Gate-1)
|
|
2
|
+
//
|
|
3
|
+
// 动作/审计/证据只写宿主私有目录:`<memoryRoot>/.longlive/{actions,evidence,distill,protected.json,...}`。
|
|
4
|
+
// 全部写入走 tmp + fsync + 读回校验 + rename;读取对坏行只计数不抛错。
|
|
5
|
+
// 不依赖任何第三方包,不联网。
|
|
6
|
+
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const ACTIONS_DIR = 'actions';
|
|
12
|
+
export const GENESIS_HASH = '0'.repeat(64);
|
|
13
|
+
|
|
14
|
+
export function sha256Hex(value) {
|
|
15
|
+
return crypto.createHash('sha256').update(typeof value === 'string' ? value : Buffer.from(value)).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** HMAC-SHA256 hex. Used by the audit chain and the evidence signature. */
|
|
19
|
+
export function hmacHexFor(key, text) {
|
|
20
|
+
return crypto.createHmac('sha256', key).update(String(text), 'utf8').digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stableStringify(value) {
|
|
24
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
25
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
26
|
+
const keys = Object.keys(value).sort();
|
|
27
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ensureDir(dir) {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Atomic JSON write (tmp + fsync + read-back + rename). */
|
|
35
|
+
export function writeJsonAtomic(file, value) {
|
|
36
|
+
const dir = path.dirname(file);
|
|
37
|
+
ensureDir(dir);
|
|
38
|
+
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
39
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
40
|
+
const fd = fs.openSync(tmp, 'w');
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
43
|
+
try {
|
|
44
|
+
fs.fsyncSync(fd);
|
|
45
|
+
} catch {
|
|
46
|
+
/* best effort on some filesystems */
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
try {
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
} catch {
|
|
52
|
+
/* ignore */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const readBack = fs.readFileSync(tmp, 'utf8');
|
|
56
|
+
if (readBack !== text) {
|
|
57
|
+
try {
|
|
58
|
+
fs.rmSync(tmp, { force: true });
|
|
59
|
+
} catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
throw new Error('E_WRITE_READBACK');
|
|
63
|
+
}
|
|
64
|
+
fs.renameSync(tmp, file);
|
|
65
|
+
return { file, bytes: Buffer.byteLength(text, 'utf8') };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readJsonFile(file) {
|
|
69
|
+
try {
|
|
70
|
+
return { ok: true, value: JSON.parse(fs.readFileSync(file, 'utf8')) };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return { ok: false, code: error?.code === 'ENOENT' ? 'ENOENT' : (error?.code ?? 'E_READ') };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Append one JSON line, fsynced. Returns the byte offset before the append. */
|
|
77
|
+
export function appendJsonl(file, value) {
|
|
78
|
+
ensureDir(path.dirname(file));
|
|
79
|
+
const text = `${JSON.stringify(value)}\n`;
|
|
80
|
+
const before = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
81
|
+
const fd = fs.openSync(file, 'a');
|
|
82
|
+
try {
|
|
83
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
84
|
+
try {
|
|
85
|
+
fs.fsyncSync(fd);
|
|
86
|
+
} catch {
|
|
87
|
+
/* best effort */
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
try {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
} catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { file, bytes: Buffer.byteLength(text, 'utf8'), offset: before };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read JSONL defensively: `{rows, bad, exists}`; never throws on bad lines. */
|
|
100
|
+
export function readJsonl(file) {
|
|
101
|
+
let text = '';
|
|
102
|
+
try {
|
|
103
|
+
text = fs.readFileSync(file, 'utf8');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { rows: [], bad: 0, exists: false, code: error?.code === 'ENOENT' ? 'ENOENT' : (error?.code ?? 'E_READ') };
|
|
106
|
+
}
|
|
107
|
+
const rows = [];
|
|
108
|
+
let bad = 0;
|
|
109
|
+
for (const line of text.replace(/^\uFEFF/, '').split(/\r?\n/)) {
|
|
110
|
+
if (line.trim() === '') continue;
|
|
111
|
+
try {
|
|
112
|
+
rows.push(JSON.parse(line));
|
|
113
|
+
} catch {
|
|
114
|
+
bad += 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { rows, bad, exists: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Atomic JSONL rewrite (tmp + read-back + rename). */
|
|
121
|
+
export function writeJsonlAtomic(file, rows) {
|
|
122
|
+
const dir = path.dirname(file);
|
|
123
|
+
ensureDir(dir);
|
|
124
|
+
const text = Array.isArray(rows) && rows.length > 0 ? `${rows.map((row) => JSON.stringify(row)).join('\n')}\n` : '';
|
|
125
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
126
|
+
const fd = fs.openSync(tmp, 'w');
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(fd, text, 'utf8');
|
|
129
|
+
try {
|
|
130
|
+
fs.fsyncSync(fd);
|
|
131
|
+
} catch {
|
|
132
|
+
/* best effort */
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
try {
|
|
136
|
+
fs.closeSync(fd);
|
|
137
|
+
} catch {
|
|
138
|
+
/* ignore */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const readBack = fs.readFileSync(tmp, 'utf8');
|
|
142
|
+
if (readBack !== text) {
|
|
143
|
+
try {
|
|
144
|
+
fs.rmSync(tmp, { force: true });
|
|
145
|
+
} catch {
|
|
146
|
+
/* ignore */
|
|
147
|
+
}
|
|
148
|
+
throw new Error('E_WRITE_READBACK');
|
|
149
|
+
}
|
|
150
|
+
fs.renameSync(tmp, file);
|
|
151
|
+
return { file, bytes: Buffer.byteLength(text, 'utf8') };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function newId(prefix) {
|
|
155
|
+
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
156
|
+
return `${prefix}_${stamp}_${crypto.randomBytes(3).toString('hex')}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Directory of this memory root's private action state. */
|
|
160
|
+
export function actionsDir(memoryRoot) {
|
|
161
|
+
return path.join(String(memoryRoot ?? ''), '.longlive', ACTIONS_DIR);
|
|
162
|
+
}
|