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.
Files changed (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,310 @@
1
+ import { existsSync, renameSync, statSync } from "node:fs";
2
+ import { Database } from "bun:sqlite";
3
+ import { ensurePrivateDatabase } from "./private-storage.js";
4
+
5
+ /**
6
+ * Cross-process writer arbitration for one store.
7
+ *
8
+ * The lease is a sibling SQLite database (`<dbPath>-write.lock`) whose write
9
+ * lock *is* the lease: a holder commits its label, then keeps an uncommitted
10
+ * `BEGIN IMMEDIATE` transaction open for the lease lifetime. Process death
11
+ * releases it by descriptor teardown, so there is no TTL, no lease row
12
+ * expiry, and no reaper. The lock file is never unlinked — unlink plus
13
+ * recreate splits the inode and hands two writers the same "exclusive" lock.
14
+ * It holds no user data: deleting it while no writer runs is a safe repair,
15
+ * and an unreadable one is renamed aside automatically.
16
+ */
17
+
18
+ /** Bounded wait before a contended writer refuses. */
19
+ export const DEFAULT_WRITER_LEASE_WAIT_MS = 250;
20
+ /** Busy retry spacing; the attempt budget is derived from it. */
21
+ export const WRITER_LEASE_RETRY_MS = 25;
22
+
23
+ const LEASE_TABLE = "lease_state";
24
+ const LEASE_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${LEASE_TABLE} (
25
+ id INTEGER PRIMARY KEY CHECK (id = 1),
26
+ holder TEXT NOT NULL,
27
+ pid INTEGER NOT NULL,
28
+ started_at TEXT NOT NULL
29
+ )`;
30
+
31
+ /** Label committed by the process that currently holds the lease. */
32
+ export interface WriterLeaseMetadata {
33
+ holder: string;
34
+ pid: number;
35
+ started_at: string;
36
+ }
37
+
38
+ export interface WriterLease {
39
+ /** Sibling lock database holding the lease. */
40
+ readonly lockPath: string;
41
+ /** Label this holder committed when it took the lease. */
42
+ readonly metadata: WriterLeaseMetadata;
43
+ /** Idempotent release: rollback the held transaction, then close. */
44
+ close(): void;
45
+ }
46
+
47
+ export interface WriterLeaseOptions {
48
+ /** Bounded wait for a contended lease; `0` tries once (default 250 ms). */
49
+ waitMs?: number;
50
+ /** Verb or service class recorded in the lease label. */
51
+ holder: string;
52
+ }
53
+
54
+ /** Lease path derived from the store path; a sibling, never the store itself. */
55
+ export function writerLockPath(dbPath: string): string {
56
+ return `${dbPath}-write.lock`;
57
+ }
58
+
59
+ /**
60
+ * In-memory stores are process-local: there is no file to derive a lock from and
61
+ * no other process to arbitrate with, so writers skip the lease. Only SQLite's
62
+ * URI forms count — `:memory:`, `file::memory:…`, and a `file:` URI whose query
63
+ * asks for `mode=memory` — never a filename that merely contains those words.
64
+ */
65
+ export function isMemoryStorePath(dbPath: string): boolean {
66
+ if (dbPath === ":memory:" || dbPath.startsWith("file::memory:")) return true;
67
+ if (!dbPath.startsWith("file:") || !dbPath.includes("?")) return false;
68
+ return new URLSearchParams(dbPath.slice(dbPath.indexOf("?"))).get("mode") === "memory";
69
+ }
70
+
71
+ /** Attempts for a wait budget: one immediate try plus one per retry slot. */
72
+ export function writerLeaseAttempts(waitMs: number, retryMs = WRITER_LEASE_RETRY_MS): number {
73
+ return waitMs > 0 ? Math.ceil(waitMs / Math.max(1, retryMs)) + 1 : 1;
74
+ }
75
+
76
+ /**
77
+ * Human label for a peer's lease, used by every refusal and skip message. A null
78
+ * label means the peek could not read one (no holder ever committed one, or the
79
+ * lock database was unreadable under contention) — say exactly that instead of a
80
+ * vague "unknown".
81
+ */
82
+ export function describeLeaseHolder(holder: WriterLeaseMetadata | null): string {
83
+ return holder ? `pid ${holder.pid}, holder ${holder.holder}` : "holder unknown (label unavailable)";
84
+ }
85
+
86
+ /** Typed refusal: another process holds the store and this writer may not take it. */
87
+ export class WriterLeaseBusyError extends Error {
88
+ readonly code = "WRITER_BUSY";
89
+ readonly lockPath: string;
90
+ /** Current label from the lock database, when it was readable. */
91
+ readonly holder: WriterLeaseMetadata | null;
92
+
93
+ constructor(lockPath: string, holder: WriterLeaseMetadata | null, message?: string) {
94
+ super(
95
+ message ??
96
+ `store is held by another writer (${describeLeaseHolder(holder)}); ` +
97
+ "retry shortly or stop the scheduled ledger service",
98
+ );
99
+ this.name = "WriterLeaseBusyError";
100
+ this.lockPath = lockPath;
101
+ this.holder = holder;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Take the writer lease for `dbPath`, waiting up to `waitMs` for a busy peer.
107
+ * Busy failures retry inside that budget; an unreadable lock database is
108
+ * quarantined once and retried; every other failure propagates as-is.
109
+ */
110
+ export function acquireWriterLease(dbPath: string, options: WriterLeaseOptions): WriterLease {
111
+ const lockPath = writerLockPath(dbPath);
112
+ const waitMs = Math.max(0, options.waitMs ?? DEFAULT_WRITER_LEASE_WAIT_MS);
113
+ const attempts = writerLeaseAttempts(waitMs);
114
+ const startedAt = Date.now();
115
+ let repaired = false;
116
+
117
+ for (let attempt = 1; ; attempt++) {
118
+ let db: Database | null = null;
119
+ let metadata: WriterLeaseMetadata | null = null;
120
+ let failure: unknown = null;
121
+ try {
122
+ db = openLockDatabase(lockPath);
123
+ if (beginImmediate(db)) metadata = holdLease(db, options.holder);
124
+ } catch (error) {
125
+ failure = error;
126
+ }
127
+ if (metadata && db) return heldWriterLease(db, lockPath, metadata);
128
+ db?.close();
129
+ if (failure && !isBusyError(failure)) {
130
+ // Only a lock database that is not a SQLite database at all is safe to
131
+ // rename aside; every other failure class fails closed (see below).
132
+ if (isNotADatabaseError(failure) && !repaired && quarantineLockDatabase(lockPath)) {
133
+ repaired = true;
134
+ continue;
135
+ }
136
+ throw failure;
137
+ }
138
+ const remaining = waitMs - (Date.now() - startedAt);
139
+ if (attempt >= attempts || remaining <= 0) break;
140
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(WRITER_LEASE_RETRY_MS, remaining));
141
+ }
142
+ throw new WriterLeaseBusyError(lockPath, readWriterLease(dbPath));
143
+ }
144
+
145
+ /**
146
+ * Best-effort peek at the committed lease label. It answers while another
147
+ * process holds the lease (readers do not conflict with the held RESERVED
148
+ * lock) and returns null when no holder committed a readable label.
149
+ */
150
+ export function readWriterLease(dbPath: string): WriterLeaseMetadata | null {
151
+ const lockPath = writerLockPath(dbPath);
152
+ if (!existsSync(lockPath)) return null;
153
+ let db: Database;
154
+ try {
155
+ db = new Database(lockPath, { readonly: true });
156
+ // The label must be readable while another process holds the lease: without
157
+ // this, a reader that meets a writer's commit fails and reports "holder
158
+ // unknown" at exactly the moment the label matters.
159
+ db.exec("PRAGMA busy_timeout = 250");
160
+ } catch {
161
+ return null;
162
+ }
163
+ try {
164
+ const row = db.query(`SELECT holder, pid, started_at FROM ${LEASE_TABLE} WHERE id = 1`).get() as
165
+ | { holder: unknown; pid: unknown; started_at: unknown }
166
+ | null;
167
+ if (!row || typeof row.holder !== "string" || !Number.isSafeInteger(row.pid) ||
168
+ typeof row.started_at !== "string") {
169
+ return null;
170
+ }
171
+ return { holder: row.holder, pid: row.pid as number, started_at: row.started_at };
172
+ } catch {
173
+ // A fresh, empty, or conflicting lock database has no readable label yet.
174
+ return null;
175
+ } finally {
176
+ db.close();
177
+ }
178
+ }
179
+
180
+ function openLockDatabase(lockPath: string): Database {
181
+ ensurePrivateDatabase(lockPath);
182
+ const db = new Database(lockPath);
183
+ // Disable SQLite's own wait loop: the caller's clock and attempt budget are
184
+ // the only timeouts in play.
185
+ db.exec("PRAGMA busy_timeout = 0");
186
+ return db;
187
+ }
188
+
189
+ /**
190
+ * Called while holding the write lock: commit the label, then take the
191
+ * uncommitted transaction that the lease actually is. Returns null when a
192
+ * contender won the commit-to-hold gap, which is a busy attempt, not a lease.
193
+ */
194
+ function holdLease(db: Database, holder: string): WriterLeaseMetadata | null {
195
+ const metadata: WriterLeaseMetadata = { holder, pid: process.pid, started_at: new Date().toISOString() };
196
+ try {
197
+ db.exec(LEASE_TABLE_SQL);
198
+ // Row id=1 is the single label slot: a stale label (a killed holder, a
199
+ // skipped clear) must be replaced, never collide with this holder.
200
+ db.query(`INSERT OR REPLACE INTO ${LEASE_TABLE} (id, holder, pid, started_at) VALUES (1, ?, ?, ?)`)
201
+ .run(metadata.holder, metadata.pid, metadata.started_at);
202
+ db.exec("COMMIT");
203
+ } catch (error) {
204
+ // The label is bookkeeping, the lock is the lease: keep holding either way.
205
+ try {
206
+ db.exec("ROLLBACK");
207
+ } catch {
208
+ // Closing the connection releases any remaining SQLite lock.
209
+ }
210
+ if (isBusyError(error)) throw error;
211
+ console.error(
212
+ "[ledger] writer lease label unavailable " +
213
+ `(${error instanceof Error ? error.message : String(error)}); holding the store unlabelled`,
214
+ );
215
+ }
216
+ if (beginImmediate(db)) return metadata;
217
+ // A contender slipped into the commit-to-hold gap: drop our own label and
218
+ // report a busy attempt instead of a half-held lease.
219
+ clearLeaseLabel(db, metadata.pid);
220
+ return null;
221
+ }
222
+
223
+ function heldWriterLease(db: Database, lockPath: string, metadata: WriterLeaseMetadata): WriterLease {
224
+ let released = false;
225
+ return {
226
+ lockPath,
227
+ metadata,
228
+ close(): void {
229
+ if (released) return;
230
+ released = true;
231
+ try {
232
+ db.exec("ROLLBACK");
233
+ } catch {
234
+ // Closing the connection releases any remaining SQLite lock.
235
+ }
236
+ // Best-effort: drop our own label so a later peek does not name a
237
+ // released holder. A successor's label is never touched.
238
+ clearLeaseLabel(db, metadata.pid);
239
+ db.close();
240
+ },
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Drop the label only when it is still this process's own: an unconditional
246
+ * delete would erase the label a successor just committed, leaving contended
247
+ * refusals unable to name a holder.
248
+ */
249
+ function clearLeaseLabel(db: Database, pid: number): void {
250
+ try {
251
+ if (!beginImmediate(db)) return;
252
+ db.query(`DELETE FROM ${LEASE_TABLE} WHERE id = 1 AND pid = ?`).run(pid);
253
+ db.exec("COMMIT");
254
+ } catch {
255
+ // Bookkeeping only: the lock, not the label, is the lease.
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Rename an unreadable lock database aside so it cannot wedge every writer
261
+ * forever. The bytes are kept for inspection, the path is never unlinked, and
262
+ * only a regular file is touched.
263
+ *
264
+ * Renaming is safe *because* of how narrow the caller keeps the condition: only
265
+ * SQLITE_NOTADB (the path is not a SQLite database at all) reaches here. A peer
266
+ * that genuinely holds the lease necessarily holds a valid SQLite write lock on
267
+ * a valid database file, so a NOTADB path cannot be a live hold and this rename
268
+ * cannot split one live inode between two holders. Every ambiguous failure —
269
+ * permissions, I/O errors, an unexpected path type, any other open failure —
270
+ * fails closed instead: the caller rethrows it untouched and never renames.
271
+ */
272
+ function quarantineLockDatabase(lockPath: string): boolean {
273
+ const target = `${lockPath}.corrupt-${Date.now()}`;
274
+ try {
275
+ if (!statSync(lockPath).isFile()) return false;
276
+ renameSync(lockPath, target);
277
+ } catch {
278
+ return false;
279
+ }
280
+ console.error(
281
+ `[ledger] writer lease lock database is not a SQLite database; ` +
282
+ `quarantined as ${target} and retried`,
283
+ );
284
+ return true;
285
+ }
286
+
287
+ function beginImmediate(db: Database): boolean {
288
+ try {
289
+ db.exec("BEGIN IMMEDIATE");
290
+ return true;
291
+ } catch (error) {
292
+ if (isBusyError(error)) return false;
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ function isBusyError(error: unknown): boolean {
298
+ const code = (error as { code?: unknown } | null | undefined)?.code;
299
+ if (typeof code === "string" && code.startsWith("SQLITE_BUSY")) return true;
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ return /SQLITE_BUSY|database is locked|database is busy/i.test(message);
302
+ }
303
+
304
+ /** The one failure class that proves no valid holder can be using this file. */
305
+ function isNotADatabaseError(error: unknown): boolean {
306
+ const code = (error as { code?: unknown } | null | undefined)?.code;
307
+ if (code === "SQLITE_NOTADB") return true;
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ return /file is not a database|file is encrypted or is not a database/i.test(message);
310
+ }
package/ops/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Trajectories installation and scheduled indexing
2
+
3
+ These operations port Ledger's CLI and OS-supervised bounded lexical sync.
4
+ CLI and release installation leave the existing database and native histories
5
+ unchanged. Scheduled native sync remains separate from indexing a managed
6
+ session/log store, which requires an explicit `index --store` command.
7
+
8
+ ## Install the CLI
9
+
10
+ ### Native CLI
11
+
12
+ Native releases require no Bun, Node or npm. The repository is currently private,
13
+ so GitHub downloads require an authenticated `gh` client with repository access.
14
+ On an Apple Silicon Mac:
15
+
16
+ ```sh
17
+ gh release download 'trajectories@0.1.2' --repo archeism/oh-my-ai \
18
+ --pattern 'trajectories-0.1.2-darwin-arm64.tar.gz*'
19
+ shasum -a 256 --check trajectories-0.1.2-darwin-arm64.tar.gz.sha256
20
+ mkdir -p "$HOME/.local/share/trajectories/native"
21
+ tar -xzf trajectories-0.1.2-darwin-arm64.tar.gz -C "$HOME/.local/share/trajectories/native"
22
+ bash "$HOME/.local/share/trajectories/native/trajectories-0.1.2-darwin-arm64/install.sh"
23
+ trajectories --version
24
+ ```
25
+
26
+ On Linux, choose `linux-x64` or `linux-arm64`, and use `sha256sum --check`.
27
+ The installer refuses existing commands unless `--force` explicitly authorizes
28
+ replacement. It creates a symlink to the retained release directory; do not
29
+ delete that directory or separate the executable from `lib/`. It does not change
30
+ the search database or enable an indexing service. Ensure `~/.local/bin` is on PATH.
31
+ Local ONNX inference requires glibc 2.28+ on Linux or macOS 14+ on Apple Silicon. Deep Agents SQLite conversion
32
+ still uses the harness's own Python/LangGraph environment.
33
+
34
+ The service installers below are for source/npm installations and invoke Bun.
35
+ For a native service, its command is the installed `trajectories sync` executable
36
+ directly; the CLI's bounded sync and scheduler protocol are unchanged.
37
+
38
+ ### npm or source CLI
39
+
40
+ For the npm distribution, use Bun 1.4.2 or newer:
41
+
42
+ ```sh
43
+ bun add --global trajectories@0.1.2
44
+ trajectories --version
45
+ trajectories status --robot
46
+ ```
47
+
48
+ Put Bun's global bin directory on PATH (`bun pm bin -g`). Packaged `ops/` and
49
+ `skills/` live under `~/.bun/install/global/node_modules/trajectories`
50
+ with the default Bun layout; resolve the installed package location before
51
+ using its OS service installers. The Git-only `install-release.sh` below is
52
+ for source checkouts, not npm installations.
53
+
54
+ Use Bun 1.4.2 or newer. From a monorepo checkout with dependencies installed and the SDK built:
55
+
56
+ ```sh
57
+ bun install --frozen-lockfile
58
+ bun run --cwd packages/trajectories build
59
+ bash packages/trajectories/ops/install-cli.sh --bin-dir "$HOME/.local/bin"
60
+ trajectories status --json
61
+ ```
62
+
63
+ This creates a worktree-following `trajectories` wrapper. Existing unrelated
64
+ executables are refused unless `--force` is supplied. `TRAJECTORIES_BUN` and
65
+ `TRAJECTORIES_BIN_DIR` override Bun and the target directory; their `LEDGER_*`
66
+ equivalents remain fallback inputs. Installation leaves the `ledger` executable
67
+ and any running Ledger service untouched.
68
+ The wrapper prefers the checkout's `src/cli.ts`; a packed package uses
69
+ `dist/cli.js`. The service installers use the same entrypoint selection.
70
+ Both require `dist/index.js`: the source search/managed-session commands load
71
+ the built SDK. An unbuilt checkout fails before writing a wrapper or service
72
+ definition, with an explicit build command. Installers do not build silently.
73
+ Rebuild after changing SDK source.
74
+
75
+ For reviewed committed bytes, rather than the live worktree:
76
+
77
+ ```sh
78
+ bash packages/trajectories/ops/install-release.sh REVIEWED_COMMIT --bin-dir "$HOME/.local/bin"
79
+ ```
80
+
81
+ The release gate uses Bun 1.4.2 and stores a full reviewed monorepo archive under
82
+ `${TRAJECTORIES_RELEASES_DIR:-${XDG_DATA_HOME:-~/.local/share}/trajectories/releases}/COMMIT`.
83
+ `LEDGER_RELEASES_DIR` remains a fallback. At the audited `7eecb3b` checkpoint the
84
+ tracked archive was approximately 126 MB; dependencies are additional. The root
85
+ frozen lockfile refers to other workspace manifests and the patched Letta
86
+ dependency, so the reviewed archive retains them. The install filters to the
87
+ Trajectories dependency graph with lifecycle scripts disabled, then builds only
88
+ Trajectories. Unrelated root postinstall builds do not run. This is a source
89
+ release path; npm installation uses the separately packed package.
90
+
91
+ Release publication is atomic after dependency installation and build. An
92
+ incomplete, foreign, or symlink release directory is refused. The commit marker
93
+ must match the directory's resolved commit. Reinstallation reuses a completed
94
+ release; do not modify its files. The installer never enables a service.
95
+
96
+ ## Scheduled local indexing
97
+
98
+ Keep the original database path during cutover:
99
+
100
+ ```sh
101
+ export TRAJECTORIES_DB="$HOME/.local/share/ledger/ledger.sqlite"
102
+ trajectories status --json
103
+ trajectories sync --robot
104
+ ```
105
+
106
+ The database is `TRAJECTORIES_DB`, then `LEDGER_DB`, then
107
+ `${XDG_DATA_HOME:-~/.local/share}/ledger/ledger.sqlite`. `TRAJECTORIES_DATA_DIR`
108
+ (`LEDGER_DATA_DIR` fallback) changes the fallback directory. Managed sessions
109
+ under `TRAJECTORIES_HOME` are a separate store.
110
+
111
+ Stop the previous Ledger writer before enabling a replacement against that
112
+ database. The installers operate only on the new Trajectories unit/label and
113
+ do not stop the old one for you.
114
+
115
+ The first writable `sync` can upgrade a compatible older search database to
116
+ schema 16. Preserve one consistent SQLite backup before that cutover: read-only
117
+ commands serve schema 15 without upgrading it, but the old Ledger writer cannot
118
+ write the upgraded database. The [Mac cutover receipt](../evidence/mac-cutover-2026-10-07/README.md)
119
+ records a restored schema-15 rollback and the observed writer migration.
120
+
121
+ ```sh
122
+ # Linux: writes private units and reloads systemd; default does not enable them.
123
+ bash packages/trajectories/ops/linux/install-systemd-user.sh --write-only
124
+ systemctl --user cat archeism-trajectories.service archeism-trajectories.timer
125
+ # After inspecting the definition and one bounded manual pass:
126
+ bash packages/trajectories/ops/linux/install-systemd-user.sh --enable-now
127
+
128
+ # macOS: writes private logs/plist and enables the 15-minute RunAtLoad job.
129
+ bash packages/trajectories/ops/macos/install-launchd.sh
130
+ ```
131
+
132
+ The Linux installer reads `${TRAJECTORIES_ENV_FILE:-${LEDGER_ENV_FILE:-${XDG_CONFIG_HOME:-~/.config}/ledger/daemon.env}}`
133
+ when present. On both systems, every `TRAJECTORIES_*` sync setting takes
134
+ precedence over the equivalent `LEDGER_*` setting. Defaults retain Ledger's
135
+ availability bounds: 25 sessions, 64 MiB source work, 120-second pass, three
136
+ retries, eight attempts/session, 8 MiB/s aggregate reads, 30-second worker
137
+ deadline, two Linux workers (one macOS worker), five reserved fresh sessions,
138
+ one-hour freshness window, 1 MiB chunks, automatic pressure response,
139
+ five-second maintenance and 64 FTS merge pages. Defaults cover all 18 current
140
+ history readers. `ops/sync-settings.sh` lists the exact setting names.
141
+
142
+ The supervised command is lexical `sync` every 15 minutes, with no restart
143
+ loop. Linux also sets 1 GiB memory, 50% CPU and 16-task limits. The retained
144
+ engine intentionally receives `LEDGER_SCHEDULER=1` so scheduled passes yield
145
+ to another writer. Foreground `watch` remains available; it is not the service
146
+ command. Pending work is retained for later passes, rather than treated as a
147
+ complete archive.
148
+
149
+ Fixtures exercise temporary homes, fake OS supervisors and synthetic databases;
150
+ tests never install real units or modify the global CLI.
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # Install a worktree-following trajectories wrapper. Does not install dependencies
3
+ # or enable services. Usage: ops/install-cli.sh [--bin-dir DIR] [--force]
4
+ set -euo pipefail
5
+
6
+ SOURCE="${BASH_SOURCE[0]}"
7
+ while [[ -h "$SOURCE" ]]; do
8
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
9
+ SOURCE="$(readlink "$SOURCE")"
10
+ [[ "$SOURCE" == /* ]] || SOURCE="$DIR/$SOURCE"
11
+ done
12
+ PACKAGE_DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
13
+ CLI_PATH="$PACKAGE_DIR/src/cli.ts"
14
+ [[ -f "$CLI_PATH" ]] || CLI_PATH="$PACKAGE_DIR/dist/cli.js"
15
+ BIN_DIR="${TRAJECTORIES_BIN_DIR:-${LEDGER_BIN_DIR:-$HOME/.local/bin}}"
16
+ FORCE=0
17
+ usage() { echo 'Usage: install-cli.sh [--bin-dir DIR] [--force]'; }
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --bin-dir)
21
+ [[ $# -ge 2 && -n "$2" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }
22
+ BIN_DIR="$2"; shift 2 ;;
23
+ --bin-dir=*)
24
+ BIN_DIR="${1#--bin-dir=}"
25
+ [[ -n "$BIN_DIR" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }
26
+ shift ;;
27
+ --force) FORCE=1; shift ;;
28
+ --help|-h) usage; exit 0 ;;
29
+ *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
30
+ esac
31
+ done
32
+ [[ -f "$CLI_PATH" ]] || { echo "Trajectories CLI not found: $CLI_PATH" >&2; exit 1; }
33
+ [[ -f "$PACKAGE_DIR/dist/index.js" ]] || {
34
+ printf 'Build Trajectories before installation: bun run --cwd %q build\n' "$PACKAGE_DIR" >&2
35
+ exit 1
36
+ }
37
+ BUN_BIN="${TRAJECTORIES_BUN:-${LEDGER_BUN:-$(command -v bun || true)}}"
38
+ [[ -n "$BUN_BIN" && -x "$BUN_BIN" ]] || { echo 'Bun executable not found; set TRAJECTORIES_BUN.' >&2; exit 1; }
39
+ mkdir -p "$BIN_DIR"
40
+ TARGET="$BIN_DIR/trajectories"
41
+ if [[ -e "$TARGET" || -L "$TARGET" ]]; then
42
+ if [[ -f "$TARGET" ]] && grep -q 'TRAJECTORIES_CLI_WRAPPER' "$TARGET" && grep -qxF "# package: $PACKAGE_DIR" "$TARGET"; then
43
+ :
44
+ elif [[ "$FORCE" == 1 && ! -d "$TARGET" ]]; then
45
+ echo "Replacing existing $TARGET (--force)." >&2
46
+ else
47
+ echo "Refusing to overwrite $TARGET; use --force or another --bin-dir." >&2
48
+ exit 1
49
+ fi
50
+ fi
51
+ TEMPORARY="$(mktemp "$BIN_DIR/.trajectories.XXXXXX")"
52
+ trap 'rm -f "$TEMPORARY"' EXIT
53
+ {
54
+ echo '#!/usr/bin/env bash'
55
+ echo '# TRAJECTORIES_CLI_WRAPPER v1; delete to uninstall.'
56
+ printf '# package: %s\n' "$PACKAGE_DIR"
57
+ printf 'exec %q run %q "$@"\n' "$BUN_BIN" "$CLI_PATH"
58
+ } > "$TEMPORARY"
59
+ chmod 755 "$TEMPORARY"
60
+ mv -f "$TEMPORARY" "$TARGET"
61
+ trap - EXIT
62
+ echo "Installed trajectories CLI wrapper: $TARGET"
63
+ case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "Add $BIN_DIR to PATH to use trajectories." >&2 ;; esac
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Run from an extracted native release kept in a permanent directory.
3
+ set -euo pipefail
4
+ RELEASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ BIN_DIR="${TRAJECTORIES_BIN_DIR:-$HOME/.local/bin}"
6
+ FORCE=0
7
+ while [[ $# -gt 0 ]]; do
8
+ case "$1" in
9
+ --bin-dir) [[ $# -ge 2 && -n "$2" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }; BIN_DIR="$2"; shift 2 ;;
10
+ --force) FORCE=1; shift ;;
11
+ --help|-h) echo 'Usage: install.sh [--bin-dir DIR] [--force]'; exit 0 ;;
12
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
13
+ esac
14
+ done
15
+ [[ -x "$RELEASE_DIR/trajectories" && -d "$RELEASE_DIR/lib" ]] || {
16
+ echo 'Keep install.sh, the trajectories executable, and lib/ together in the extracted release.' >&2; exit 1;
17
+ }
18
+ "$RELEASE_DIR/trajectories" --version
19
+ mkdir -p "$BIN_DIR"
20
+ TARGET="$BIN_DIR/trajectories"
21
+ if [[ -e "$TARGET" || -L "$TARGET" ]]; then
22
+ if [[ -L "$TARGET" && "$(readlink "$TARGET")" == "$RELEASE_DIR/trajectories" ]]; then
23
+ echo "Already installed: $TARGET"; exit 0
24
+ fi
25
+ [[ "$FORCE" == 1 && ! -d "$TARGET" ]] || {
26
+ echo "Refusing to overwrite $TARGET; use --force for an explicit cutover." >&2; exit 1;
27
+ }
28
+ fi
29
+ STAGE="$(mktemp -d "$BIN_DIR/.trajectories-install.XXXXXX")"
30
+ trap 'rm -rf "$STAGE"' EXIT
31
+ ln -s "$RELEASE_DIR/trajectories" "$STAGE/trajectories"
32
+ mv -f "$STAGE/trajectories" "$TARGET"
33
+ echo "Installed: $TARGET"
34
+ echo "Keep the release directory: $RELEASE_DIR"
35
+ case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "Add $BIN_DIR to PATH." ;; esac
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Install a reviewed monorepo revision into a SHA-pinned directory, never the
3
+ # live worktree. Usage: ops/install-release.sh REF [--bin-dir DIR] [--force]
4
+ set -euo pipefail
5
+ umask 077
6
+ if [[ $# == 0 || "$1" == --help || "$1" == -h ]]; then
7
+ echo 'Usage: install-release.sh REF [--bin-dir DIR] [--force]'
8
+ echo 'Requires git, tar and the verified Bun 1.4.2 runtime; does not enable services.'
9
+ exit 0
10
+ fi
11
+ REF="$1"; shift
12
+ PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
13
+ REPO="$(git -C "$PACKAGE_DIR" rev-parse --show-toplevel)"
14
+ BUN_BIN="${TRAJECTORIES_BUN:-${LEDGER_BUN:-$(command -v bun || true)}}"
15
+ [[ -n "$BUN_BIN" && -x "$BUN_BIN" ]] || { echo 'Bun is required; set TRAJECTORIES_BUN.' >&2; exit 1; }
16
+ [[ "$("$BUN_BIN" --version)" == 1.4.2 ]] || { echo 'This release gate is verified with Bun 1.4.2; use that runtime.' >&2; exit 1; }
17
+ SHA="$(git -C "$REPO" rev-parse --verify --end-of-options "${REF}^{commit}")"
18
+ RELEASES="${TRAJECTORIES_RELEASES_DIR:-${LEDGER_RELEASES_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/trajectories/releases}}"
19
+ mkdir -p "$RELEASES"
20
+ [[ ! -L "$RELEASES" ]] || { echo 'Refusing a symlink release directory.' >&2; exit 1; }
21
+ chmod 700 "$RELEASES"
22
+ TARGET="$RELEASES/$SHA"
23
+ STAGE=''
24
+ trap '[[ -z "$STAGE" ]] || rm -rf "$STAGE"' EXIT
25
+ if [[ -e "$TARGET" || -L "$TARGET" ]]; then
26
+ [[ ! -L "$TARGET" && -f "$TARGET/.trajectories-release" && "$(cat "$TARGET/.trajectories-release")" == "$SHA" && -f "$TARGET/packages/trajectories/src/cli.ts" && -f "$TARGET/packages/trajectories/ops/install-cli.sh" ]] || {
27
+ echo "Refusing an incomplete or foreign release directory: $TARGET" >&2; exit 1;
28
+ }
29
+ else
30
+ STAGE="$(mktemp -d "$RELEASES/.install.XXXXXX")"
31
+ # The root lockfile references workspace manifests and dependency patches.
32
+ # Preserve the reviewed monorepo, then install only Trajectories' graph.
33
+ git -C "$REPO" archive --format=tar "$SHA" | tar -xf - -C "$STAGE"
34
+ [[ -f "$STAGE/bun.lock" && -f "$STAGE/package.json" && -f "$STAGE/packages/trajectories/package.json" && -f "$STAGE/packages/trajectories/ops/install-cli.sh" ]] || {
35
+ echo 'Revision lacks the supported Trajectories installer and frozen monorepo lockfile.' >&2; exit 1;
36
+ }
37
+ (
38
+ cd "$STAGE"
39
+ "$BUN_BIN" install --filter trajectories --frozen-lockfile --ignore-scripts
40
+ cd packages/trajectories
41
+ "$BUN_BIN" run build
42
+ )
43
+ printf '%s\n' "$SHA" > "$STAGE/.trajectories-release"
44
+ [[ ! -e "$TARGET" && ! -L "$TARGET" ]] || { echo 'Another installer completed this revision; retry.' >&2; exit 1; }
45
+ mv "$STAGE" "$TARGET"
46
+ STAGE=''
47
+ fi
48
+ TRAJECTORIES_BUN="$BUN_BIN" bash "$TARGET/packages/trajectories/ops/install-cli.sh" "$@"
49
+ printf 'Pinned revision: %s\nRelease directory: %s\n' "$SHA" "$TARGET"