trajectories 0.0.0 → 0.1.2
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 +36 -0
- package/LICENSE +201 -0
- package/README.md +288 -2
- package/dist/cli.js +30210 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/deepagents_checkpoint.py +362 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1233 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +150 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-native.sh +35 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +21 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FSWatcher,
|
|
3
|
+
existsSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
statSync,
|
|
6
|
+
watch,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
10
|
+
import { TrajectoryDb } from "./db.js";
|
|
11
|
+
import { ingestDiscoveredSession, sessionNeedsIngest } from "./ingest.js";
|
|
12
|
+
import { scanSessions } from "./scanner.js";
|
|
13
|
+
import { cursorRoots } from "./native/cursor.js";
|
|
14
|
+
import { grokRoots } from "./native/grok.js";
|
|
15
|
+
import { kimiRoots } from "./native/kimi.js";
|
|
16
|
+
import { openCodeRoots } from "./native/opencode.js";
|
|
17
|
+
import {
|
|
18
|
+
acquireWriterLease,
|
|
19
|
+
DEFAULT_WRITER_LEASE_WAIT_MS,
|
|
20
|
+
describeLeaseHolder,
|
|
21
|
+
isMemoryStorePath,
|
|
22
|
+
WriterLeaseBusyError,
|
|
23
|
+
type WriterLease,
|
|
24
|
+
} from "./writer-lease.js";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_SOURCES = [
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"pi",
|
|
30
|
+
"omp",
|
|
31
|
+
"hermes",
|
|
32
|
+
"letta-code",
|
|
33
|
+
"openclaw",
|
|
34
|
+
"droid",
|
|
35
|
+
"deepagents",
|
|
36
|
+
"openhands",
|
|
37
|
+
"cursor",
|
|
38
|
+
"opencode",
|
|
39
|
+
"grok",
|
|
40
|
+
"kimi",
|
|
41
|
+
"gemini-cli",
|
|
42
|
+
"copilot-cli",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
interface WatchRoot { path: string; recursive: boolean }
|
|
46
|
+
interface WatchRegistration {
|
|
47
|
+
handle: FSWatcher;
|
|
48
|
+
recursive: boolean;
|
|
49
|
+
fallbackTree: boolean;
|
|
50
|
+
sources: Set<string>;
|
|
51
|
+
/** Child basenames that should wake this source on a parent-sentinel watch. */
|
|
52
|
+
expectedChildren: Map<string, Set<string>>;
|
|
53
|
+
}
|
|
54
|
+
const tree = (path: string): WatchRoot => ({ path, recursive: true });
|
|
55
|
+
const shallow = (path: string): WatchRoot => ({ path, recursive: false });
|
|
56
|
+
|
|
57
|
+
// These inputs mirror trajectory 0.3.0's non-exported OMP listing resolver.
|
|
58
|
+
function ompSessionsPath(): string {
|
|
59
|
+
const env = process.env;
|
|
60
|
+
const value = (env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE)?.trim();
|
|
61
|
+
const profile = value && value !== "default" && value !== "." && value !== ".." && !value.endsWith(".") &&
|
|
62
|
+
/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value) && !/^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i.test(value)
|
|
63
|
+
? value : undefined;
|
|
64
|
+
const config = join(homedir(), env.PI_CONFIG_DIR || ".omp", ...(profile ? ["profiles", profile] : []));
|
|
65
|
+
const override = profile ? undefined : env.PI_CODING_AGENT_DIR?.trim() || undefined;
|
|
66
|
+
if (!override && (process.platform === "linux" || process.platform === "darwin") && env.XDG_DATA_HOME?.trim()) {
|
|
67
|
+
const data = join(env.XDG_DATA_HOME.trim(), "omp", ...(profile ? ["profiles", profile] : []));
|
|
68
|
+
if (existsSync(data)) return join(data, "sessions");
|
|
69
|
+
}
|
|
70
|
+
return join(override ?? join(config, "agent"), "sessions");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function openClawStatePath(): string {
|
|
74
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim();
|
|
75
|
+
if (override) return override;
|
|
76
|
+
const current = join(homedir(), ".openclaw");
|
|
77
|
+
return existsSync(current) ? current : join(homedir(), ".clawdbot");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DEFAULT_SOURCE_ROOTS: Record<string, () => WatchRoot[]> = {
|
|
81
|
+
"claude-code": () => [tree(join(homedir(), ".claude", "projects"))],
|
|
82
|
+
codex: () => [tree(join(homedir(), ".codex", "sessions"))],
|
|
83
|
+
pi: () => [tree(join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "sessions"))],
|
|
84
|
+
omp: () => [tree(ompSessionsPath())],
|
|
85
|
+
hermes: () => [shallow(join(homedir(), ".hermes"))],
|
|
86
|
+
"letta-code": () => [tree(join(homedir(), ".letta", "transcripts"))],
|
|
87
|
+
openclaw: () => [tree(join(openClawStatePath(), "agents"))],
|
|
88
|
+
droid: () => [tree(join(homedir(), ".factory", "sessions"))],
|
|
89
|
+
deepagents: () => [shallow(join(homedir(), ".deepagents"))],
|
|
90
|
+
openhands: () => [tree(join(homedir(), ".openhands", "sessions"))],
|
|
91
|
+
// Share source-specific roots with native discovery.
|
|
92
|
+
cursor: () => cursorRoots().map(tree),
|
|
93
|
+
opencode: () => openCodeRoots().map(tree),
|
|
94
|
+
grok: () => grokRoots().map(tree),
|
|
95
|
+
kimi: () => kimiRoots().map(tree),
|
|
96
|
+
cline: () => [tree(process.env.CLINE_SESSION_DATA_DIR?.trim() || join(homedir(), ".cline", "data", "sessions"))],
|
|
97
|
+
fx: () => [tree(join(homedir(), ".fx", "sessions"))],
|
|
98
|
+
"gemini-cli": () => [tree(join(homedir(), ".gemini", "sessions"))],
|
|
99
|
+
"copilot-cli": () => [tree(join(homedir(), ".copilot", "session-state")), tree(join(homedir(), ".copilot", "history-session-state"))],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function recursiveUnsupported(error: unknown): boolean {
|
|
103
|
+
return ["ERR_FEATURE_UNAVAILABLE_ON_PLATFORM", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]
|
|
104
|
+
.includes((error as NodeJS.ErrnoException)?.code ?? "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cloneExpectedChildren(
|
|
108
|
+
existing?: Map<string, Set<string>>,
|
|
109
|
+
): Map<string, Set<string>> {
|
|
110
|
+
const cloned = new Map<string, Set<string>>();
|
|
111
|
+
if (!existing) return cloned;
|
|
112
|
+
for (const [source, names] of existing) {
|
|
113
|
+
cloned.set(source, new Set(names));
|
|
114
|
+
}
|
|
115
|
+
return cloned;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rememberExpectedChild(
|
|
119
|
+
expectedChildren: Map<string, Set<string>>,
|
|
120
|
+
source: string,
|
|
121
|
+
expectedChild?: string,
|
|
122
|
+
): void {
|
|
123
|
+
if (expectedChild === undefined) {
|
|
124
|
+
expectedChildren.delete(source);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const names = expectedChildren.get(source);
|
|
128
|
+
if (names) names.add(expectedChild);
|
|
129
|
+
else expectedChildren.set(source, new Set([expectedChild]));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sentinelAllows(
|
|
133
|
+
registration: WatchRegistration,
|
|
134
|
+
source: string,
|
|
135
|
+
filename: string | Buffer | null | undefined,
|
|
136
|
+
): boolean {
|
|
137
|
+
const expected = registration.expectedChildren.get(source);
|
|
138
|
+
if (!expected || expected.size === 0) return true;
|
|
139
|
+
if (filename == null) return true;
|
|
140
|
+
const name = basename(typeof filename === "string" ? filename : filename.toString());
|
|
141
|
+
return name.length === 0 || expected.has(name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface WatcherEvent {
|
|
145
|
+
source: string;
|
|
146
|
+
sessionId?: string;
|
|
147
|
+
kind: "imported" | "appended" | "deduped" | "skipped" | "deferred" | "tombstoned";
|
|
148
|
+
sessionsProcessed: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface WatcherBatchSummary {
|
|
152
|
+
source: string;
|
|
153
|
+
imported: number;
|
|
154
|
+
appended: number;
|
|
155
|
+
deduped: number;
|
|
156
|
+
skipped: number;
|
|
157
|
+
scanned: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface LedgerWatcherOptions {
|
|
161
|
+
dbPath: string;
|
|
162
|
+
sources?: string[];
|
|
163
|
+
roots?: Record<string, string>;
|
|
164
|
+
limit?: number;
|
|
165
|
+
onEvent?: (event: WatcherEvent) => void;
|
|
166
|
+
onBatch?: (summary: WatcherBatchSummary) => void;
|
|
167
|
+
debounceMs?: number;
|
|
168
|
+
verbose?: boolean;
|
|
169
|
+
/** When false (default), ingest is lexical-only so the watcher cannot stall on embeddings. */
|
|
170
|
+
embed?: boolean;
|
|
171
|
+
/** Periodic catch-up. `0` disables start+interval reconcile (tests). */
|
|
172
|
+
reconcileMs?: number;
|
|
173
|
+
/** Max sessions per source per tick; shared native stores also rotate prior attempts. */
|
|
174
|
+
importBudget?: number;
|
|
175
|
+
/** JSONL record or assembled native-session ceiling; defaults to 64 MiB. */
|
|
176
|
+
maxRecordBytes?: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class LedgerWatcher {
|
|
180
|
+
readonly dbPath: string;
|
|
181
|
+
readonly sources: string[];
|
|
182
|
+
readonly roots: Record<string, string>;
|
|
183
|
+
readonly limit?: number;
|
|
184
|
+
readonly debounceMs: number;
|
|
185
|
+
readonly verbose: boolean;
|
|
186
|
+
readonly embed: boolean;
|
|
187
|
+
readonly reconcileMs: number;
|
|
188
|
+
readonly importBudget: number;
|
|
189
|
+
readonly maxRecordBytes?: number;
|
|
190
|
+
|
|
191
|
+
private onEvent?: (event: WatcherEvent) => void;
|
|
192
|
+
private onBatch?: (summary: WatcherBatchSummary) => void;
|
|
193
|
+
|
|
194
|
+
private db?: TrajectoryDb;
|
|
195
|
+
/** Held only while at least one drain is writing; shared by concurrent drains. */
|
|
196
|
+
private writerLease?: WriterLease;
|
|
197
|
+
private leaseDrains = 0;
|
|
198
|
+
private watchers = new Map<string, WatchRegistration>();
|
|
199
|
+
private recursiveWatchers = new Map<string, WatchRegistration>();
|
|
200
|
+
private recursiveWatchSupported = true;
|
|
201
|
+
private debounceTimers = new Map<string, Timer>();
|
|
202
|
+
private sourceTasks = new Map<string, Promise<void>>();
|
|
203
|
+
private pendingAfterProcessing = new Set<string>();
|
|
204
|
+
private isStopped = false;
|
|
205
|
+
private batchCount = 0;
|
|
206
|
+
private reconcileTimer?: Timer;
|
|
207
|
+
private startTask?: Promise<void>;
|
|
208
|
+
private reconcileTask?: Promise<void>;
|
|
209
|
+
private stopTask?: Promise<void>;
|
|
210
|
+
|
|
211
|
+
constructor(options: LedgerWatcherOptions) {
|
|
212
|
+
this.dbPath = options.dbPath;
|
|
213
|
+
this.sources = options.sources && options.sources.length > 0
|
|
214
|
+
? options.sources
|
|
215
|
+
: [...DEFAULT_SOURCES];
|
|
216
|
+
this.roots = options.roots ?? {};
|
|
217
|
+
this.limit = options.limit;
|
|
218
|
+
this.debounceMs = options.debounceMs ?? 2000;
|
|
219
|
+
this.reconcileMs = options.reconcileMs ?? 60_000;
|
|
220
|
+
this.verbose = Boolean(options.verbose);
|
|
221
|
+
this.embed = Boolean(options.embed);
|
|
222
|
+
this.importBudget = options.importBudget ?? 25;
|
|
223
|
+
this.maxRecordBytes = options.maxRecordBytes;
|
|
224
|
+
if (this.maxRecordBytes !== undefined &&
|
|
225
|
+
(!Number.isSafeInteger(this.maxRecordBytes) || this.maxRecordBytes <= 0)) {
|
|
226
|
+
throw new Error("maxRecordBytes must be a positive safe integer");
|
|
227
|
+
}
|
|
228
|
+
this.onEvent = options.onEvent;
|
|
229
|
+
this.onBatch = options.onBatch;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
start(): Promise<void> {
|
|
233
|
+
if (this.isStopped) {
|
|
234
|
+
return Promise.reject(new Error("Cannot restart a stopped LedgerWatcher."));
|
|
235
|
+
}
|
|
236
|
+
this.startTask ??= this.startWatching();
|
|
237
|
+
return this.startTask;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async startWatching(): Promise<void> {
|
|
241
|
+
// The store is opened lazily, inside the first drain that scans: a watcher
|
|
242
|
+
// with nothing to scan must not create, migrate, or touch it, and a
|
|
243
|
+
// lease-skipped drain never reaches the store at all. A drain that does scan
|
|
244
|
+
// is not a pure read — it records that source's coverage — so it opens (and
|
|
245
|
+
// migrates) the store exactly as a foreground pass does.
|
|
246
|
+
for (const source of this.sources) {
|
|
247
|
+
this.attachSourceRoots(source);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Reconcile existing history before reporting startup complete. Filesystem
|
|
251
|
+
// events only cover changes after watchers attach; without this pass, a
|
|
252
|
+
// freshly installed daemon can run indefinitely with an empty database.
|
|
253
|
+
if (this.reconcileMs > 0) {
|
|
254
|
+
await this.runReconcileTick();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Filesystem events are best-effort on every supported platform. Keep a
|
|
258
|
+
// periodic reconciliation floor so directories created later, missed events,
|
|
259
|
+
// and history written while the service was down eventually converge.
|
|
260
|
+
if (!this.isStopped && this.reconcileMs > 0) {
|
|
261
|
+
this.reconcileTimer = setInterval(() => {
|
|
262
|
+
void this.runReconcileTick().catch(() => {
|
|
263
|
+
console.error("[ledger] reconciliation failed; retrying on the next tick.");
|
|
264
|
+
});
|
|
265
|
+
}, this.reconcileMs);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
stop(): Promise<void> {
|
|
270
|
+
if (this.stopTask) return this.stopTask;
|
|
271
|
+
this.isStopped = true;
|
|
272
|
+
|
|
273
|
+
if (this.reconcileTimer) {
|
|
274
|
+
clearInterval(this.reconcileTimer);
|
|
275
|
+
this.reconcileTimer = undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const timer of this.debounceTimers.values()) {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
}
|
|
281
|
+
this.debounceTimers.clear();
|
|
282
|
+
|
|
283
|
+
for (const watcher of this.watchers.values()) {
|
|
284
|
+
try {
|
|
285
|
+
watcher.handle.close();
|
|
286
|
+
} catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.watchers.clear();
|
|
291
|
+
this.recursiveWatchers.clear();
|
|
292
|
+
this.pendingAfterProcessing.clear();
|
|
293
|
+
|
|
294
|
+
// Stop all producers before taking the drain snapshot. Existing ingest may
|
|
295
|
+
// still write records, vectors, or cursors after its current await resumes.
|
|
296
|
+
const pending = [this.startTask, this.reconcileTask, ...this.sourceTasks.values()]
|
|
297
|
+
.filter((task): task is Promise<void> => task !== undefined);
|
|
298
|
+
this.stopTask = Promise.allSettled(pending).then(() => {
|
|
299
|
+
try {
|
|
300
|
+
this.db?.close();
|
|
301
|
+
} finally {
|
|
302
|
+
this.db = undefined;
|
|
303
|
+
// Every drain has settled, so any retained lease is leaked by
|
|
304
|
+
// definition; it must not outlive the watcher.
|
|
305
|
+
this.releaseLeaseIfIdle();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
return this.stopTask;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The watcher's store, opened on first use inside a drain that writes. */
|
|
312
|
+
private store(): TrajectoryDb {
|
|
313
|
+
return this.db ??= new TrajectoryDb(this.dbPath);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getBatchCount(): number {
|
|
317
|
+
return this.batchCount;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getWatchedPaths(): string[] {
|
|
321
|
+
return Array.from(this.watchers.keys());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private resolveSourceRoots(source: string): WatchRoot[] {
|
|
325
|
+
const customRoot = this.roots[source];
|
|
326
|
+
if (customRoot) {
|
|
327
|
+
if (source === "pi" || source === "omp") return [tree(join(customRoot, "sessions"))];
|
|
328
|
+
if (source === "openclaw") return [tree(join(customRoot, "agents"))];
|
|
329
|
+
if (source === "hermes" || source === "deepagents") return [shallow(customRoot.endsWith(".db") ? dirname(customRoot) : customRoot)];
|
|
330
|
+
if ((source === "cursor" || source === "opencode") && /\.(?:db|vscdb|sqlite|sqlite3)$/i.test(customRoot)) {
|
|
331
|
+
return [customRoot, `${customRoot}-wal`, `${customRoot}-journal`].map(shallow);
|
|
332
|
+
}
|
|
333
|
+
return [tree(customRoot)];
|
|
334
|
+
}
|
|
335
|
+
return DEFAULT_SOURCE_ROOTS[source]?.() ?? [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private attachSourceRoots(source: string): void {
|
|
339
|
+
if (this.isStopped) return;
|
|
340
|
+
for (const root of this.resolveSourceRoots(source)) {
|
|
341
|
+
const path = resolve(root.path);
|
|
342
|
+
// A shallow nearest-parent sentinel observes creation without traversing
|
|
343
|
+
// agent plugins, worktrees, caches, or configuration directories.
|
|
344
|
+
let parent = dirname(path);
|
|
345
|
+
while (!existsSync(parent) && dirname(parent) !== parent) parent = dirname(parent);
|
|
346
|
+
this.attachWatcher(parent, source, false, false, basename(path));
|
|
347
|
+
if (root.recursive) this.watchDirectoryTree(path, source);
|
|
348
|
+
else this.attachWatcher(path, source, false);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private watchDirectoryTree(targetPath: string, source: string): void {
|
|
353
|
+
if (this.isStopped || !existsSync(targetPath)) return;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const stat = statSync(targetPath);
|
|
357
|
+
if (!stat.isDirectory()) {
|
|
358
|
+
this.attachWatcher(targetPath, source, false);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (this.recursiveWatchSupported) {
|
|
362
|
+
const result = this.attachWatcher(targetPath, source, true);
|
|
363
|
+
if (result !== "unsupported") return;
|
|
364
|
+
this.recursiveWatchSupported = false;
|
|
365
|
+
}
|
|
366
|
+
if (this.attachWatcher(targetPath, source, false, true) !== "attached") return;
|
|
367
|
+
for (const entry of readdirSync(targetPath, { withFileTypes: true })) {
|
|
368
|
+
if (entry.isDirectory()) {
|
|
369
|
+
this.watchDirectoryTree(join(targetPath, entry.name), source);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
// ignore read permission errors
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private attachWatcher(
|
|
378
|
+
targetPath: string,
|
|
379
|
+
source: string,
|
|
380
|
+
recursive: boolean,
|
|
381
|
+
fallbackTree = false,
|
|
382
|
+
expectedChild?: string,
|
|
383
|
+
): "attached" | "unsupported" | "failed" {
|
|
384
|
+
if (this.isStopped || !existsSync(targetPath)) return "failed";
|
|
385
|
+
const existing = this.watchers.get(targetPath);
|
|
386
|
+
if (existing && (existing.recursive || !recursive)) {
|
|
387
|
+
existing.sources.add(source);
|
|
388
|
+
existing.fallbackTree ||= fallbackTree;
|
|
389
|
+
rememberExpectedChild(existing.expectedChildren, source, expectedChild);
|
|
390
|
+
return "attached";
|
|
391
|
+
}
|
|
392
|
+
for (const [path, registration] of this.recursiveWatchers) {
|
|
393
|
+
if (targetPath.startsWith(path + sep)) {
|
|
394
|
+
registration.sources.add(source);
|
|
395
|
+
return "attached";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const sources = new Set(existing?.sources ?? []);
|
|
400
|
+
sources.add(source);
|
|
401
|
+
const expectedChildren = cloneExpectedChildren(existing?.expectedChildren);
|
|
402
|
+
rememberExpectedChild(expectedChildren, source, expectedChild);
|
|
403
|
+
const registration: WatchRegistration = {
|
|
404
|
+
handle: watch(targetPath, { recursive }, (_eventType, filename) => {
|
|
405
|
+
if (this.isStopped) return;
|
|
406
|
+
for (const subscriber of sources) {
|
|
407
|
+
if (!recursive) {
|
|
408
|
+
if (registration.fallbackTree && filename) {
|
|
409
|
+
const fullPath = join(targetPath, filename);
|
|
410
|
+
try {
|
|
411
|
+
if (statSync(fullPath).isDirectory()) this.watchDirectoryTree(fullPath, subscriber);
|
|
412
|
+
} catch { /* deleted path or unreadable child */ }
|
|
413
|
+
} else {
|
|
414
|
+
if (!sentinelAllows(registration, subscriber, filename)) continue;
|
|
415
|
+
this.attachSourceRoots(subscriber);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.triggerSourceDebounce(subscriber);
|
|
419
|
+
}
|
|
420
|
+
}),
|
|
421
|
+
recursive, fallbackTree, sources, expectedChildren,
|
|
422
|
+
};
|
|
423
|
+
existing?.handle.close();
|
|
424
|
+
this.watchers.set(targetPath, registration);
|
|
425
|
+
this.recursiveWatchers.delete(targetPath);
|
|
426
|
+
if (recursive) {
|
|
427
|
+
this.recursiveWatchers.set(targetPath, registration);
|
|
428
|
+
for (const [path, covered] of this.watchers) {
|
|
429
|
+
if (path.startsWith(targetPath + sep)) {
|
|
430
|
+
for (const subscriber of covered.sources) sources.add(subscriber);
|
|
431
|
+
covered.handle.close();
|
|
432
|
+
this.watchers.delete(path);
|
|
433
|
+
this.recursiveWatchers.delete(path);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
registration.handle.on("error", () => {
|
|
438
|
+
registration.handle.close();
|
|
439
|
+
if (this.watchers.get(targetPath) === registration) {
|
|
440
|
+
this.watchers.delete(targetPath);
|
|
441
|
+
this.recursiveWatchers.delete(targetPath);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return "attached";
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return recursive && recursiveUnsupported(error) ? "unsupported" : "failed";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private triggerSourceDebounce(source: string): void {
|
|
451
|
+
if (this.isStopped) return;
|
|
452
|
+
|
|
453
|
+
const existingTimer = this.debounceTimers.get(source);
|
|
454
|
+
clearTimeout(existingTimer);
|
|
455
|
+
|
|
456
|
+
const timer = setTimeout(() => {
|
|
457
|
+
this.debounceTimers.delete(source);
|
|
458
|
+
this.withWriterLease(() => this.executeSourceBatch(source)).catch(() => {
|
|
459
|
+
console.error("[ledger] source batch failed:", JSON.stringify({ source }));
|
|
460
|
+
});
|
|
461
|
+
}, this.debounceMs);
|
|
462
|
+
|
|
463
|
+
this.debounceTimers.set(source, timer);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async runReconcileTick(): Promise<void> {
|
|
467
|
+
if (this.isStopped) return;
|
|
468
|
+
if (this.reconcileTask) return this.reconcileTask;
|
|
469
|
+
const task = this.withWriterLease(() => this.reconcileAllSources());
|
|
470
|
+
this.reconcileTask = task;
|
|
471
|
+
try {
|
|
472
|
+
await task;
|
|
473
|
+
} finally {
|
|
474
|
+
this.reconcileTask = undefined;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Watch holds the writer lease only while a drain writes: one reconcile tick
|
|
480
|
+
* or one debounced batch, never for the watcher lifetime. Concurrent drains
|
|
481
|
+
* share one lease (the watcher is a single writer) and it is released by
|
|
482
|
+
* whichever of them finishes last, so neither can leave the other writing
|
|
483
|
+
* unleased. A busy store means a foreground verb owns it, so the drain is
|
|
484
|
+
* skipped with a note and retried on the next tick instead of crashing the
|
|
485
|
+
* watcher or starving the verb.
|
|
486
|
+
*/
|
|
487
|
+
private async withWriterLease(drain: () => Promise<void>): Promise<void> {
|
|
488
|
+
if (isMemoryStorePath(this.dbPath)) return drain();
|
|
489
|
+
if (this.writerLease) {
|
|
490
|
+
// Joining is atomic: acquisition is synchronous, so no await intervenes.
|
|
491
|
+
this.leaseDrains++;
|
|
492
|
+
try {
|
|
493
|
+
await drain();
|
|
494
|
+
} finally {
|
|
495
|
+
this.leaseDrains--;
|
|
496
|
+
this.releaseLeaseIfIdle();
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
let lease: WriterLease;
|
|
501
|
+
try {
|
|
502
|
+
lease = acquireWriterLease(this.dbPath, { waitMs: DEFAULT_WRITER_LEASE_WAIT_MS, holder: "watch" });
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (!(error instanceof WriterLeaseBusyError)) throw error;
|
|
505
|
+
console.error(`[ledger] watch skipped: another writer holds the store (${describeLeaseHolder(error.holder)})`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.writerLease = lease;
|
|
509
|
+
this.leaseDrains = 1;
|
|
510
|
+
try {
|
|
511
|
+
this.deriveIndexIfPending();
|
|
512
|
+
await drain();
|
|
513
|
+
} finally {
|
|
514
|
+
this.leaseDrains--;
|
|
515
|
+
this.releaseLeaseIfIdle();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* A drain holds the writer lease, so a pending index re-derivation (the
|
|
521
|
+
* derive-rule counter's one-shot, C2) is the drain's work, exactly as it is a
|
|
522
|
+
* sync pass's — taken here, before the batch, so the rest of the drain reads
|
|
523
|
+
* the index it just derived. The watcher is a daemon with no summary to carry
|
|
524
|
+
* the wall time, so it reports it on stderr; a store with nothing pending
|
|
525
|
+
* pays one `schema_meta` probe.
|
|
526
|
+
*/
|
|
527
|
+
private deriveIndexIfPending(): void {
|
|
528
|
+
const store = this.store();
|
|
529
|
+
const started = performance.now();
|
|
530
|
+
if (!store.reindexFts()) return;
|
|
531
|
+
console.error(
|
|
532
|
+
`[ledger] index re-derivation: ${Math.round(performance.now() - started)}ms (records_fts and titles_fts rebuilt from this store)`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** One release path for every drain: the last one out closes the lease. */
|
|
537
|
+
private releaseLeaseIfIdle(): void {
|
|
538
|
+
if (this.leaseDrains > 0) return;
|
|
539
|
+
const lease = this.writerLease;
|
|
540
|
+
this.writerLease = undefined;
|
|
541
|
+
lease?.close();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private async reconcileAllSources(): Promise<void> {
|
|
545
|
+
if (this.isStopped) return;
|
|
546
|
+
for (const source of this.sources) {
|
|
547
|
+
if (this.isStopped) break;
|
|
548
|
+
this.attachSourceRoots(source);
|
|
549
|
+
try {
|
|
550
|
+
await this.executeSourceBatch(source, false);
|
|
551
|
+
} catch {
|
|
552
|
+
console.error("[ledger] reconciliation failed:", JSON.stringify({ source }));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private executeSourceBatch(source: string, emitSummary = true): Promise<void> {
|
|
558
|
+
if (this.isStopped) return Promise.resolve();
|
|
559
|
+
const current = this.sourceTasks.get(source);
|
|
560
|
+
if (current) {
|
|
561
|
+
this.pendingAfterProcessing.add(source);
|
|
562
|
+
return current;
|
|
563
|
+
}
|
|
564
|
+
const task = this.processSourceBatch(source, emitSummary);
|
|
565
|
+
this.sourceTasks.set(source, task);
|
|
566
|
+
const finish = () => {
|
|
567
|
+
this.sourceTasks.delete(source);
|
|
568
|
+
if (this.pendingAfterProcessing.delete(source) && !this.isStopped) {
|
|
569
|
+
this.triggerSourceDebounce(source);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
void task.then(finish, finish);
|
|
573
|
+
return task;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private async processSourceBatch(source: string, emitSummary: boolean): Promise<void> {
|
|
577
|
+
const { sessions: discovered, outcomes } = await scanSessions({
|
|
578
|
+
sources: [source],
|
|
579
|
+
roots: this.roots,
|
|
580
|
+
limit: this.limit,
|
|
581
|
+
onDiagnostic: diagnostic => {
|
|
582
|
+
console.error("[ledger] scan warning:", JSON.stringify({ source, ...diagnostic }));
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (this.isStopped) return;
|
|
587
|
+
|
|
588
|
+
// A scanning drain is not a pure read: the source's verdict is recorded
|
|
589
|
+
// under this drain's lease, before the import decision, so a source that is
|
|
590
|
+
// listable with nothing new (or missing entirely) still refreshes its row.
|
|
591
|
+
this.store().recordSourceCoverage(outcomes);
|
|
592
|
+
|
|
593
|
+
const needed = discovered.filter((item) =>
|
|
594
|
+
sessionNeedsIngest(this.store(), item, { maxRecordBytes: this.maxRecordBytes }),
|
|
595
|
+
);
|
|
596
|
+
// One WAL revision makes multiple sessions eligible. Always choosing the
|
|
597
|
+
// newest would let a busy session prevent older history from ever importing.
|
|
598
|
+
const ordered = needed.some(item => item.native)
|
|
599
|
+
? needed.map((item, index) => ({
|
|
600
|
+
item, index, attempted: this.store().getIngestCursor(item.source, item.id, item.path)?.updated_at ?? "",
|
|
601
|
+
})).sort((a, b) => a.attempted.localeCompare(b.attempted) || a.index - b.index).map(entry => entry.item)
|
|
602
|
+
: needed;
|
|
603
|
+
const toImport = ordered.slice(0, this.importBudget);
|
|
604
|
+
if (toImport.length === 0) return;
|
|
605
|
+
|
|
606
|
+
let batchImported = 0;
|
|
607
|
+
let batchAppended = 0;
|
|
608
|
+
let batchDeduped = 0;
|
|
609
|
+
let batchSkipped = 0;
|
|
610
|
+
let sessionsProcessed = 0;
|
|
611
|
+
|
|
612
|
+
for (const item of toImport) {
|
|
613
|
+
if (this.isStopped) break;
|
|
614
|
+
|
|
615
|
+
sessionsProcessed++;
|
|
616
|
+
const result = await ingestDiscoveredSession(this.store(), item, {
|
|
617
|
+
embed: this.embed,
|
|
618
|
+
maxRecordBytes: this.maxRecordBytes,
|
|
619
|
+
}).catch(() => ({
|
|
620
|
+
kind: "skipped" as const,
|
|
621
|
+
sessionId: item.id,
|
|
622
|
+
warnings: ["Ingestion failed; inspect the source and retry."],
|
|
623
|
+
}));
|
|
624
|
+
for (const warning of result.warnings ?? []) {
|
|
625
|
+
// Warnings contain controlled diagnostics, never raw exception messages
|
|
626
|
+
// or transcript content. JSON escaping keeps path metadata single-line.
|
|
627
|
+
console.error("[ledger] ingest warning:", JSON.stringify({ source, sourcePath: item.path, warning }));
|
|
628
|
+
}
|
|
629
|
+
if (this.isStopped) break;
|
|
630
|
+
if (result.kind === "unchanged") continue;
|
|
631
|
+
|
|
632
|
+
if (result.kind === "skipped" || result.kind === "deferred" || result.kind === "tombstoned") {
|
|
633
|
+
// A deletion refusal lands in the batch's skip bucket — nothing was
|
|
634
|
+
// imported, and the batch summary counts work, not reasons — while the
|
|
635
|
+
// event and the verbose line keep the reason: `sessionNeedsIngest`
|
|
636
|
+
// refuses a deleted listing before it is read, so this arrives only for
|
|
637
|
+
// the shapes that axis cannot name (a group's member file).
|
|
638
|
+
batchSkipped++;
|
|
639
|
+
} else if (result.kind === "deduped") {
|
|
640
|
+
batchDeduped++;
|
|
641
|
+
} else if (result.kind === "appended") {
|
|
642
|
+
batchAppended++;
|
|
643
|
+
} else {
|
|
644
|
+
batchImported++;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (this.verbose) {
|
|
648
|
+
console.log(` [${result.kind.toUpperCase()}] ${item.source}/${item.id}`);
|
|
649
|
+
}
|
|
650
|
+
this.onEvent?.({
|
|
651
|
+
source,
|
|
652
|
+
sessionId: result.sessionId,
|
|
653
|
+
kind: result.kind,
|
|
654
|
+
sessionsProcessed,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (emitSummary && !this.isStopped) {
|
|
659
|
+
this.batchCount++;
|
|
660
|
+
|
|
661
|
+
const summary: WatcherBatchSummary = {
|
|
662
|
+
source,
|
|
663
|
+
imported: batchImported,
|
|
664
|
+
appended: batchAppended,
|
|
665
|
+
deduped: batchDeduped,
|
|
666
|
+
skipped: batchSkipped,
|
|
667
|
+
scanned: discovered.length,
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
this.onBatch?.(summary);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|