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,570 @@
1
+ #!/usr/bin/env bun
2
+ // Read-only observer: never import Trajectories' DB class (it migrates and can load models).
3
+ import { Database } from "bun:sqlite";
4
+ import { createHash } from "node:crypto";
5
+ import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync, writeSync } from "node:fs";
6
+ import { createConnection } from "node:net";
7
+ import { arch, cpus, platform, release, totalmem } from "node:os";
8
+ import { join, resolve } from "node:path";
9
+
10
+ const HELP = `Usage: bun legacy/scripts/observe-daemon.ts --pid PID --db PATH --out NEW_DIRECTORY
11
+ --seconds 180 Observation window; never starts or stops the target
12
+ --interval-ms 1000 Process cadence (minimum 100; 500 also supported)
13
+ --db-interval-ms 5000 Read-only aggregate cadence, plus baseline/end
14
+ --timeout-ms 2000 Deadline for each ps/SQLite subprocess
15
+ --marker STRING Optional FTS5 literal-phrase counts; never saved as text
16
+ --ssh HOST:PORT Probe a loopback SSH banner; banner text is never retained
17
+ --expect-exit true Treat observed target teardown as successful completion
18
+ Output: samples.jsonl and summary.json, directory 0700/files 0600.
19
+ Exit: 0 window completed, 2 incomplete/tree exited/collection error, 130 interrupted.
20
+ RSS sums may double-count shared pages; sampled peaks/CPU deltas are lower bounds.
21
+ No raw argv, environment, transcript, title, source path, or warning log capture.
22
+ `;
23
+
24
+ export function parseOptions(args: string[]) {
25
+ const allowed = new Set(["pid", "db", "out", "seconds", "interval-ms", "db-interval-ms", "timeout-ms", "marker", "ssh", "expect-exit"]);
26
+ const flags = new Map<string, string>();
27
+ for (let i = 0; i < args.length; i += 2) {
28
+ const key = args[i]?.replace(/^--/, "");
29
+ const value = args[i + 1];
30
+ if (!args[i]?.startsWith("--") || !allowed.has(key) || flags.has(key) || !value || value.startsWith("--")) {
31
+ throw new Error("invalid_or_duplicate_option");
32
+ }
33
+ flags.set(key, value);
34
+ }
35
+ function number(key: string, fallback: number, min: number, max: number) {
36
+ const value = Number(flags.get(key) ?? fallback);
37
+ if (!Number.isFinite(value) || value < min || value > max) throw new Error(`invalid_${key}`);
38
+ return value;
39
+ }
40
+ if (!flags.has("pid") || !flags.has("db") || !flags.has("out")) throw new Error("pid_db_out_required");
41
+ const pid = number("pid", 0, 2, 2147483647);
42
+ if (!Number.isInteger(pid) || pid === process.pid) throw new Error("invalid_pid");
43
+ const intervalMs = number("interval-ms", 1000, 100, 60000);
44
+ const marker = flags.get("marker");
45
+ if (marker !== undefined && (!marker.trim() || marker.length > 1024)) throw new Error("invalid_marker");
46
+ const ssh = flags.get("ssh");
47
+ let sshTarget: { host: string; port: number } | undefined;
48
+ if (ssh !== undefined) {
49
+ const match = /^(127\.0\.0\.1|localhost|\[::1\]):(\d{1,5})$/.exec(ssh);
50
+ const port = Number(match?.[2]);
51
+ if (!match || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error("invalid_ssh");
52
+ sshTarget = { host: match[1] === "[::1]" ? "::1" : match[1]!, port };
53
+ }
54
+ const expectExit = flags.get("expect-exit") ?? "false";
55
+ if (!["true", "false"].includes(expectExit)) throw new Error("invalid_expect_exit");
56
+ return {
57
+ pid, db: resolve(flags.get("db")!), out: resolve(flags.get("out")!), marker,
58
+ ssh: sshTarget, expectExit: expectExit === "true",
59
+ seconds: number("seconds", 180, 0.1, 86400), intervalMs,
60
+ dbIntervalMs: number("db-interval-ms", Math.max(5000, intervalMs), intervalMs, 86400000),
61
+ timeoutMs: number("timeout-ms", 2000, 1, 60000),
62
+ };
63
+ }
64
+ type Options = ReturnType<typeof parseOptions>;
65
+ const unavailable = (reason: string) => ({ status: "unavailable" as const, reason });
66
+ const sha256 = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
67
+
68
+ export async function runBounded(command: string[], timeoutMs: number, input?: string) {
69
+ const start = performance.now();
70
+ try {
71
+ const child = Bun.spawn(command, {
72
+ stdin: input === undefined ? "ignore" : Buffer.from(input), stdout: "pipe", stderr: "ignore",
73
+ env: { PATH: "/usr/bin:/bin", LC_ALL: "C", TZ: "UTC" },
74
+ });
75
+ let timedOut = false;
76
+ const timer = setTimeout(() => {
77
+ timedOut = true;
78
+ // Only the observer's own ps/SQLite subprocess, never the target.
79
+ try { child.kill("SIGKILL"); } catch { /* May already have exited. */ }
80
+ }, timeoutMs);
81
+ try {
82
+ const stdout = await new Response(child.stdout).text();
83
+ const exitCode = await child.exited;
84
+ return {
85
+ stdout,
86
+ report: {
87
+ status: timedOut ? "timed_out" : exitCode === 0 ? "ok" : "failed",
88
+ exit_code: exitCode, signal: child.signalCode ?? null, elapsed_ms: performance.now() - start,
89
+ },
90
+ };
91
+ } finally {
92
+ clearTimeout(timer);
93
+ if (child.exitCode === null) {
94
+ child.kill("SIGKILL");
95
+ await child.exited;
96
+ }
97
+ }
98
+ } catch {
99
+ return { stdout: "", report: { status: "spawn_failed", exit_code: null, signal: null, elapsed_ms: performance.now() - start } };
100
+ }
101
+ }
102
+
103
+ type ProcIo = { rchar: number; wchar: number; read_bytes: number; write_bytes: number };
104
+ type ProcessRow = { pid: number; ppid: number; rss_bytes: number; cpu_seconds: number; ps_cpu_percent: number; state: string; start_time: string; proc_io?: ProcIo };
105
+
106
+ function getconf(name: string): number | null {
107
+ if (platform() !== "linux") return null;
108
+ try {
109
+ const result = Bun.spawnSync(["/usr/bin/getconf", name], { stdout: "pipe", stderr: "ignore" });
110
+ const value = Number(result.stdout.toString().trim());
111
+ return result.exitCode === 0 && Number.isFinite(value) && value > 0 ? value : null;
112
+ } catch { return null; }
113
+ }
114
+
115
+ const CLOCK_TICKS = getconf("CLK_TCK");
116
+ const PAGE_SIZE = getconf("PAGESIZE");
117
+
118
+ function linuxProcessMetrics(row: ProcessRow): ProcessRow {
119
+ if (!CLOCK_TICKS || !PAGE_SIZE) return row;
120
+ try {
121
+ const stat = readFileSync(`/proc/${row.pid}/stat`, "utf8");
122
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
123
+ if (fields.length < 22) return row;
124
+ const cpu = (Number(fields[11]) + Number(fields[12])) / CLOCK_TICKS;
125
+ const rss = Number(fields[21]) * PAGE_SIZE;
126
+ const io = Object.fromEntries(readFileSync(`/proc/${row.pid}/io`, "utf8").trim().split("\n")
127
+ .map((line) => line.split(/:\s+/, 2))) as Record<string, string>;
128
+ const procIo: ProcIo = {
129
+ rchar: Number(io.rchar), wchar: Number(io.wchar),
130
+ read_bytes: Number(io.read_bytes), write_bytes: Number(io.write_bytes),
131
+ };
132
+ if (![cpu, rss, ...Object.values(procIo)].every(Number.isFinite)) return row;
133
+ return { ...row, cpu_seconds: cpu, rss_bytes: rss, proc_io: procIo };
134
+ } catch { return row; }
135
+ }
136
+ export function parsePs(text: string): ProcessRow[] {
137
+ return text.trim().split("\n").filter(Boolean).map((line) => {
138
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+([\d:.\-]+)\s+([\d.]+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
139
+ if (!match) throw new Error("invalid_ps_row");
140
+ const [, pid, ppid, rss, time, cpu, state, start] = match;
141
+ const [days, clock] = time.includes("-") ? time.split("-") : ["0", time];
142
+ const parts = clock.split(":").map(Number);
143
+ if (parts.length < 2 || parts.length > 3 || parts.some((value) => !Number.isFinite(value))) throw new Error("invalid_ps_time");
144
+ const seconds = Number(days) * 86400 + parts.reduce((total, value) => total * 60 + value, 0);
145
+ return { pid: Number(pid), ppid: Number(ppid), rss_bytes: Number(rss) * 1024,
146
+ cpu_seconds: seconds, ps_cpu_percent: Number(cpu), state, start_time: start.replace(/\s+/g, " ") };
147
+ });
148
+ }
149
+ const identity = (row: ProcessRow) => `${row.pid}:${row.start_time}`;
150
+
151
+ export class ProcessTree {
152
+ private rootIdentity?: string;
153
+ private seen = new Set<string>();
154
+ private previous = new Map<string, number>();
155
+ private previousAt?: number;
156
+ private previousIo = new Map<string, ProcIo>();
157
+ observedCpuSeconds = 0;
158
+ observedIo: ProcIo = { rchar: 0, wchar: 0, read_bytes: 0, write_bytes: 0 };
159
+
160
+ constructor(private pid: number, private observerPid = process.pid) {}
161
+
162
+ sample(rows: ProcessRow[], atMs: number) {
163
+ const live = rows.filter((row) => !row.state.startsWith("Z"));
164
+ const root = live.find((row) => row.pid === this.pid);
165
+ if (!this.rootIdentity && root) this.rootIdentity = identity(root);
166
+ const rootStatus = root
167
+ ? identity(root) === this.rootIdentity ? "alive" : "pid_reused"
168
+ : this.rootIdentity ? "exited" : "not_found";
169
+ const excluded = new Set([this.observerPid]);
170
+ const selected = new Map<number, ProcessRow>();
171
+ // Retain already observed descendants after reparenting; do not attach a reused root PID.
172
+ for (let changed = true; changed;) {
173
+ changed = false;
174
+ for (const row of live) {
175
+ if (excluded.has(row.ppid) && !excluded.has(row.pid)) { excluded.add(row.pid); changed = true; }
176
+ }
177
+ }
178
+ for (const row of live) {
179
+ if (!excluded.has(row.pid) && (identity(row) === this.rootIdentity || this.seen.has(identity(row)))) selected.set(row.pid, row);
180
+ }
181
+ for (let changed = true; changed;) {
182
+ changed = false;
183
+ for (const row of live) {
184
+ if (!excluded.has(row.pid) && selected.has(row.ppid) && !selected.has(row.pid)) {
185
+ selected.set(row.pid, row); changed = true;
186
+ }
187
+ }
188
+ }
189
+ const members = [...selected.values()].sort((a, b) => a.pid - b.pid)
190
+ .map((row) => platform() === "linux" ? linuxProcessMetrics(row) : row);
191
+ let delta = 0;
192
+ const next = new Map<string, number>();
193
+ const nextIo = new Map<string, ProcIo>();
194
+ for (const row of members) {
195
+ const key = identity(row);
196
+ const previous = this.previous.get(key);
197
+ if (previous !== undefined) delta += Math.max(0, row.cpu_seconds - previous);
198
+ next.set(key, row.cpu_seconds);
199
+ if (row.proc_io) {
200
+ const previousIo = this.previousIo.get(key);
201
+ if (previousIo) {
202
+ for (const field of ["rchar", "wchar", "read_bytes", "write_bytes"] as const) {
203
+ this.observedIo[field] += Math.max(0, row.proc_io[field] - previousIo[field]);
204
+ }
205
+ }
206
+ nextIo.set(key, row.proc_io);
207
+ }
208
+ this.seen.add(key);
209
+ }
210
+ const sampledCores = this.previousAt === undefined || atMs <= this.previousAt ? null : delta / ((atMs - this.previousAt) / 1000);
211
+ this.previous = next;
212
+ this.previousIo = nextIo;
213
+ this.previousAt = atMs;
214
+ this.observedCpuSeconds += delta;
215
+ return { status: "ok" as const, root_status: rootStatus, members,
216
+ observed_identities: this.seen.size, rss_bytes: members.reduce((sum, row) => sum + row.rss_bytes, 0),
217
+ cpu_lifetime_seconds_sum: members.reduce((sum, row) => sum + row.cpu_seconds, 0),
218
+ ps_cpu_percent_sum: members.reduce((sum, row) => sum + row.ps_cpu_percent, 0),
219
+ observed_cpu_seconds_lower_bound: this.observedCpuSeconds, sampled_cpu_cores: sampledCores,
220
+ proc_io_members: nextIo.size,
221
+ proc_io_lifetime_sum: members.reduce((total, row) => {
222
+ if (!row.proc_io) return total;
223
+ for (const field of ["rchar", "wchar", "read_bytes", "write_bytes"] as const) total[field] += row.proc_io[field];
224
+ return total;
225
+ }, { rchar: 0, wchar: 0, read_bytes: 0, write_bytes: 0 } as ProcIo),
226
+ proc_io_observed_delta_lower_bound: { ...this.observedIo } };
227
+ }
228
+ }
229
+
230
+ export function readCoverage(path: string, marker?: string) {
231
+ const db = new Database(path, { readonly: true, strict: true });
232
+ try {
233
+ db.exec("PRAGMA query_only = ON; PRAGMA trusted_schema = OFF; PRAGMA busy_timeout = 100; PRAGMA cache_size = -2048; BEGIN");
234
+ const tables = new Set((db.query(`SELECT name FROM sqlite_master WHERE type = 'table'
235
+ AND name IN ('sessions', 'records', 'records_embeddings', 'records_fts', 'ingest_cursors', 'ingest_queue')`)
236
+ .all() as { name: string }[]).map((row) => row.name));
237
+ if (!tables.has("sessions") || !tables.has("records")) throw new Error("ledger_schema_absent");
238
+ const sources = "'codex','claude-code','pi','omp','hermes','letta-code','openclaw','droid','deepagents','openhands','cursor','opencode','gemini-cli','copilot-cli','grok','kimi','cline','fx','managed','atif'";
239
+ function count(table: string, column?: string) {
240
+ if (!tables.has(table)) return unavailable("table_absent");
241
+ const by_source = column ? db.query(`SELECT CASE WHEN ${column} IN (${sources}) THEN ${column} ELSE 'other' END AS source,
242
+ count(*) AS count FROM ${table} GROUP BY 1 ORDER BY 1`).all() as { source: string; count: number }[] : undefined;
243
+ const total = by_source ? by_source.reduce((sum, row) => sum + row.count, 0)
244
+ : (db.query(`SELECT count(*) AS total FROM ${table}`).get() as { total: number }).total;
245
+ return { status: "ok", total, ...(by_source ? { by_source } : {}) };
246
+ }
247
+ const cursors = tables.has("ingest_cursors") ? {
248
+ status: "ok", ...db.query(`SELECT count(*) AS total, coalesce(sum(size), 0) AS source_bytes,
249
+ coalesce(sum(bytes_ingested), 0) AS bytes_ingested,
250
+ coalesce(sum(max(size - bytes_ingested, 0)), 0) AS bytes_pending,
251
+ coalesce(sum(record_count), 0) AS record_count,
252
+ coalesce(sum(status = 'failed'), 0) AS failed,
253
+ coalesce(sum(status = 'pending'), 0) AS pending,
254
+ coalesce(sum(status = 'ok'), 0) AS ok,
255
+ coalesce(sum(status NOT IN ('ok', 'failed', 'pending')), 0) AS other_status,
256
+ max(unixepoch(updated_at)) AS latest_update_epoch_seconds FROM ingest_cursors`).get() as Record<string, number | null>,
257
+ } : unavailable("ingest_cursors_table_absent");
258
+ const queue = tables.has("ingest_queue") ? {
259
+ status: "ok", ...db.query(`SELECT count(*) AS total,
260
+ coalesce(sum(next_attempt_at IS NULL OR next_attempt_at <= datetime('now')), 0) AS eligible,
261
+ coalesce(sum(next_attempt_at > datetime('now')), 0) AS retrying FROM ingest_queue`).get() as Record<string, number>,
262
+ } : unavailable("ingest_queue_table_absent");
263
+ const markerCounts = marker === undefined ? { status: "not_requested" }
264
+ : !tables.has("records_fts") ? unavailable("records_fts_table_absent")
265
+ : { status: "ok", method: "fts5_literal_phrase", ...db.query(`SELECT count(*) AS records,
266
+ count(DISTINCT session_id) AS sessions FROM records_fts WHERE records_fts MATCH ?`)
267
+ .get(`"${marker.replaceAll('"', '""')}"`) as Record<string, number> };
268
+ return {
269
+ status: "ok", schema_version: (db.query("PRAGMA user_version").get() as { user_version: number }).user_version,
270
+ coverage: { sessions: count("sessions", "source"), records: count("records", "source_type"),
271
+ embeddings: count("records_embeddings"), fts: count("records_fts") },
272
+ cursors, queue, marker: markerCounts,
273
+ warnings: unavailable("Warnings are stderr events, not durable SQLite counters; logs are deliberately not read. Failed cursors and retry queue state are reported separately."),
274
+ };
275
+ } finally {
276
+ db.close();
277
+ }
278
+ }
279
+
280
+ type NumericMap = Record<string, number>;
281
+
282
+ function keyValues(path: string): NumericMap {
283
+ return Object.fromEntries(readFileSync(path, "utf8").trim().split("\n").filter(Boolean).map((line) => {
284
+ const [key, value] = line.trim().split(/\s+/, 2);
285
+ return [key!, Number(value)];
286
+ }).filter((entry) => Number.isFinite(entry[1]))) as NumericMap;
287
+ }
288
+
289
+ function pressure(path: string): Record<string, NumericMap> {
290
+ return Object.fromEntries(readFileSync(path, "utf8").trim().split("\n").filter(Boolean).map((line) => {
291
+ const [kind, ...fields] = line.trim().split(/\s+/);
292
+ return [kind!, Object.fromEntries(fields.map((field) => {
293
+ const [key, value] = field.split("=", 2);
294
+ return [key!, Number(value)];
295
+ }).filter((entry) => Number.isFinite(entry[1])))];
296
+ }));
297
+ }
298
+
299
+ function ioStat(path: string): NumericMap {
300
+ const total: NumericMap = {};
301
+ for (const line of readFileSync(path, "utf8").trim().split("\n").filter(Boolean)) {
302
+ for (const field of line.trim().split(/\s+/).slice(1)) {
303
+ const [key, raw] = field.split("=", 2);
304
+ const value = Number(raw);
305
+ if (key && Number.isFinite(value)) total[key] = (total[key] ?? 0) + value;
306
+ }
307
+ }
308
+ return total;
309
+ }
310
+
311
+ function cgroupDirectory(pid: number): string | null {
312
+ if (platform() !== "linux") return null;
313
+ try {
314
+ const line = readFileSync(`/proc/${pid}/cgroup`, "utf8").split("\n").find((entry) => entry.startsWith("0::"));
315
+ if (!line) return null;
316
+ const root = "/sys/fs/cgroup";
317
+ const path = resolve(root, `.${line.slice(3)}`);
318
+ return path === root || path.startsWith(root + "/") ? path : null;
319
+ } catch { return null; }
320
+ }
321
+
322
+ function cgroupSample(directory: string | null) {
323
+ if (!directory) return unavailable("unified_target_cgroup_unavailable");
324
+ try {
325
+ const numberFile = (name: string) => Number(readFileSync(join(directory, name), "utf8").trim());
326
+ return {
327
+ status: "ok" as const,
328
+ memory_current_bytes: numberFile("memory.current"),
329
+ memory_peak_bytes: numberFile("memory.peak"),
330
+ pids_current: numberFile("pids.current"),
331
+ cpu: keyValues(join(directory, "cpu.stat")),
332
+ io: (() => {
333
+ try { return { status: "ok" as const, ...ioStat(join(directory, "io.stat")) }; }
334
+ catch { return unavailable("I/O controller is not delegated to the target cgroup."); }
335
+ })(),
336
+ memory_events: keyValues(join(directory, "memory.events")),
337
+ pressure: {
338
+ cpu: pressure(join(directory, "cpu.pressure")),
339
+ memory: pressure(join(directory, "memory.pressure")),
340
+ io: pressure(join(directory, "io.pressure")),
341
+ },
342
+ };
343
+ } catch { return unavailable("target_cgroup_metrics_unreadable"); }
344
+ }
345
+
346
+ function cgroupLimits(directory: string | null) {
347
+ if (!directory) return unavailable("unified_target_cgroup_unavailable");
348
+ try {
349
+ const value = (name: string) => readFileSync(join(directory, name), "utf8").trim();
350
+ return {
351
+ status: "ok" as const,
352
+ cpu_max: value("cpu.max"), cpu_weight: value("cpu.weight"),
353
+ memory_high: value("memory.high"), memory_max: value("memory.max"),
354
+ pids_max: value("pids.max"), io_weight: (() => { try { return value("io.weight"); } catch { return null; } })(),
355
+ };
356
+ } catch { return unavailable("target_cgroup_limits_unreadable"); }
357
+ }
358
+
359
+ function numericDelta(end: unknown, start: unknown): unknown {
360
+ if (!end || !start || typeof end !== "object" || typeof start !== "object") {
361
+ return unavailable("cgroup_boundary_sample_unavailable");
362
+ }
363
+ const result: Record<string, unknown> = {};
364
+ for (const [key, value] of Object.entries(end as Record<string, unknown>)) {
365
+ const previous = (start as Record<string, unknown>)[key];
366
+ if (typeof value === "number" && typeof previous === "number") result[key] = Math.max(0, value - previous);
367
+ else if (value && previous && typeof value === "object" && typeof previous === "object") result[key] = numericDelta(value, previous);
368
+ }
369
+ return result;
370
+ }
371
+
372
+ export async function probeSshBanner(
373
+ target: { host: string; port: number },
374
+ timeoutMs: number,
375
+ ): Promise<{ status: "ok" | "failed"; latency_ms: number; reason?: string }> {
376
+ const start = performance.now();
377
+ return new Promise((resolveProbe) => {
378
+ const socket = createConnection(target);
379
+ let settled = false;
380
+ let received = "";
381
+ const settle = (status: "ok" | "failed", reason?: string) => {
382
+ if (settled) return;
383
+ settled = true;
384
+ clearTimeout(timer);
385
+ socket.destroy();
386
+ resolveProbe({ status, latency_ms: performance.now() - start, ...(reason ? { reason } : {}) });
387
+ };
388
+ const timer = setTimeout(() => settle("failed", "timeout"), timeoutMs);
389
+ socket.on("data", (chunk) => {
390
+ received += chunk.toString("utf8");
391
+ if (received.length > 255) return settle("failed", "invalid_banner");
392
+ const newline = received.indexOf("\n");
393
+ if (newline >= 0) {
394
+ const valid = received.slice(0, newline).replace(/\r$/, "").startsWith("SSH-");
395
+ settle(valid ? "ok" : "failed", valid ? undefined : "invalid_banner");
396
+ }
397
+ });
398
+ socket.on("error", () => settle("failed", "connection_error"));
399
+ socket.on("end", () => settle("failed", "connection_closed"));
400
+ });
401
+ }
402
+
403
+ export async function observe(options: Options) {
404
+ const settings = { pid: options.pid, database_path: options.db, seconds: options.seconds, interval_ms: options.intervalMs,
405
+ db_interval_ms: options.dbIntervalMs, command_timeout_ms: options.timeoutMs,
406
+ marker_sha256: options.marker === undefined ? null : sha256(options.marker),
407
+ ssh_probe: options.ssh ? { host: options.ssh.host, port: options.ssh.port } : { status: "not_requested" },
408
+ expect_exit: options.expectExit };
409
+ const cgroup = cgroupDirectory(options.pid);
410
+ const provenance = { helper_sha256: sha256(readFileSync(import.meta.path)), runtime: `Bun ${Bun.version}`,
411
+ os: platform(), os_release: release(), arch: arch(), logical_cpus: cpus().length, host_memory_bytes: totalmem(),
412
+ target_revision: unavailable("An explicit PID does not establish the target build revision."),
413
+ target_limits: cgroupLimits(cgroup), cache_state: "uncontrolled observation" };
414
+ // A new directory and exclusive files avoid overwriting or following existing evidence symlinks.
415
+ mkdirSync(options.out, { mode: 0o700 });
416
+ const samplePath = join(options.out, "samples.jsonl");
417
+ const fd = openSync(samplePath, "wx", 0o600);
418
+ const digest = createHash("sha256");
419
+ const tree = new ProcessTree(options.pid);
420
+ const start = performance.now();
421
+ const startedAt = new Date().toISOString();
422
+ const startRecord = JSON.stringify({ type: "start", schema_version: 2, started_at: startedAt, settings, provenance }) + "\n";
423
+ writeSync(fd, startRecord);
424
+ digest.update(startRecord);
425
+ const deadline = start + options.seconds * 1000;
426
+ let interrupted = false;
427
+ const onSignal = () => { interrupted = true; };
428
+ process.on("SIGINT", onSignal);
429
+ process.on("SIGTERM", onSignal);
430
+ let count = 0, errors = 0, timeouts = 0, missedIntervals = 0, nextDbAt = start;
431
+ let stopReason = "duration_elapsed";
432
+ let baseline: ReturnType<ProcessTree["sample"]> | null = null;
433
+ let last: ReturnType<ProcessTree["sample"]> | null = null;
434
+ let peakRss: number | null = null, peakCores: number | null = null;
435
+ let dbBaseline: unknown = null, dbEnd: unknown = null;
436
+ let cgroupBaseline: unknown = null, cgroupEnd: unknown = null;
437
+ let procIoSeen = false;
438
+ let sshProbes = 0, sshFailures = 0, sshMaxLatency: number | null = null;
439
+ try {
440
+ while (true) {
441
+ const ps = await runBounded(["/bin/ps", "-A", "-o", "pid=", "-o", "ppid=", "-o", "rss=",
442
+ "-o", "time=", "-o", "pcpu=", "-o", "stat=", "-o", "lstart="], options.timeoutMs);
443
+ const at = performance.now();
444
+ last = null;
445
+ let processError = "ps_command_failed";
446
+ if (ps.report.status === "ok") {
447
+ try { last = tree.sample(parsePs(ps.stdout), at); } catch { processError = "ps_parse_failed"; }
448
+ }
449
+ if (last && last.root_status !== "not_found") {
450
+ baseline ??= last;
451
+ peakRss = Math.max(peakRss ?? 0, last.rss_bytes);
452
+ if (last.sampled_cpu_cores !== null) peakCores = Math.max(peakCores ?? 0, last.sampled_cpu_cores);
453
+ procIoSeen ||= last.proc_io_members > 0;
454
+ } else if (!last) errors++;
455
+ if (ps.report.status === "timed_out") timeouts++;
456
+ const treeExited = last !== null && last.members.length === 0;
457
+ const finish = interrupted || at >= deadline || treeExited || (!last && count === 0);
458
+ let database: unknown = { status: "not_sampled" };
459
+ let databaseCommand: unknown = null;
460
+ if (at >= nextDbAt || finish) {
461
+ const result = await runBounded([process.execPath, import.meta.path, "--sample-db"], options.timeoutMs,
462
+ JSON.stringify({ path: options.db, marker: options.marker }));
463
+ databaseCommand = result.report;
464
+ database = unavailable(result.report.status === "timed_out" ? "database_timeout" : "database_query_failed");
465
+ if (result.report.status === "ok") {
466
+ try { database = JSON.parse(result.stdout); } catch { errors++; }
467
+ } else errors++;
468
+ if (result.report.status === "timed_out") timeouts++;
469
+ dbBaseline ??= database;
470
+ dbEnd = database;
471
+ nextDbAt = performance.now() + options.dbIntervalMs;
472
+ }
473
+ const cgroupMetrics = cgroupSample(cgroup);
474
+ if (cgroupMetrics.status === "ok") {
475
+ cgroupBaseline ??= cgroupMetrics;
476
+ cgroupEnd = cgroupMetrics;
477
+ }
478
+ const ssh = options.ssh
479
+ ? await probeSshBanner(options.ssh, Math.min(options.timeoutMs, Math.max(100, options.intervalMs)))
480
+ : { status: "not_requested" as const };
481
+ if (ssh.status !== "not_requested") {
482
+ sshProbes++;
483
+ if (ssh.status === "failed") sshFailures++;
484
+ sshMaxLatency = Math.max(sshMaxLatency ?? 0, ssh.latency_ms);
485
+ }
486
+ const sample = JSON.stringify({ type: "sample", index: count++, at: new Date(Date.parse(startedAt) + at - start).toISOString(),
487
+ elapsed_ms: at - start, sample_finished_elapsed_ms: performance.now() - start,
488
+ ps_command: ps.report, processes: last ?? unavailable(processError),
489
+ database_command: databaseCommand, database, cgroup: cgroupMetrics, ssh });
490
+ writeSync(fd, sample + "\n");
491
+ digest.update(sample + "\n");
492
+ if (finish) {
493
+ stopReason = interrupted ? "interrupted" : treeExited ? (last?.root_status === "not_found" ? "pid_not_found" : "tree_exited")
494
+ : !last ? "process_snapshot_failed" : "duration_elapsed";
495
+ break;
496
+ }
497
+ const now = performance.now();
498
+ const nominalNext = at + options.intervalMs;
499
+ if (now > nominalNext) missedIntervals += Math.floor((now - at) / options.intervalMs);
500
+ await Bun.sleep(Math.max(0, Math.min(nominalNext, deadline) - now));
501
+ }
502
+ } finally {
503
+ closeSync(fd);
504
+ process.off("SIGINT", onSignal);
505
+ process.off("SIGTERM", onSignal);
506
+ }
507
+ const normalCompletion = stopReason === "duration_elapsed" || (options.expectExit && stopReason === "tree_exited");
508
+ const summary = {
509
+ schema_version: 2, kind: "process_tree_observation", stop_reason: stopReason,
510
+ status: normalCompletion && errors === 0 ? "completed" : "incomplete",
511
+ started_at: startedAt, ended_at: new Date().toISOString(), elapsed_seconds: (performance.now() - start) / 1000,
512
+ sample_count: count, collection_errors: errors, command_timeouts: timeouts, missed_intervals: missedIntervals,
513
+ settings, provenance,
514
+ tree: { root_pid: options.pid, root_status_at_end: last?.root_status ?? "unavailable",
515
+ alive_at_end: baseline && last ? last.members.length > 0 : null, observed_identities: last?.observed_identities ?? null },
516
+ memory: { method: "sum of ps RSS * 1024 for sampled tree members", baseline_rss_bytes: baseline?.rss_bytes ?? null,
517
+ peak_rss_bytes: peakRss, end_rss_bytes: baseline && last ? last.rss_bytes : null,
518
+ after_teardown: baseline && last?.members.length === 0 ? { status: "measured", bytes: 0 } : unavailable("Tree teardown not observed; target is never stopped by this helper.") },
519
+ cpu: { method: "positive ps user+system time deltas for same PID+start-time identities, divided by actual sample intervals",
520
+ observed_seconds_lower_bound: baseline ? tree.observedCpuSeconds : null, peak_sampled_cores: peakCores,
521
+ baseline_lifetime_seconds_sum: baseline?.cpu_lifetime_seconds_sum ?? null, end_lifetime_seconds_sum: baseline && last ? last.cpu_lifetime_seconds_sum : null },
522
+ io: procIoSeen ? { status: "measured", method: "positive /proc/<pid>/io deltas for sampled PID+start-time identities; lower bounds",
523
+ logical_read_bytes: tree.observedIo.rchar, logical_write_bytes: tree.observedIo.wchar,
524
+ storage_read_bytes: tree.observedIo.read_bytes, storage_write_bytes: tree.observedIo.write_bytes }
525
+ : unavailable(platform() === "linux" ? "No readable target /proc I/O sample was captured." : "Per-process byte counters are unavailable on this platform."),
526
+ cgroup: { baseline: cgroupBaseline ?? unavailable("No readable target cgroup sample was captured."),
527
+ end: cgroupEnd ?? unavailable("No readable target cgroup sample was captured."),
528
+ delta: numericDelta(cgroupEnd, cgroupBaseline) },
529
+ ssh: options.ssh ? { status: "measured", method: "loopback TCP connect through complete SSH identification line",
530
+ probes: sshProbes, failures: sshFailures, max_banner_latency_ms: sshMaxLatency }
531
+ : { status: "not_requested" },
532
+ usage: { observer_models: { status: "not_applicable", reason: "Collector imports only builtins and makes no model/network calls." },
533
+ target_tokens: unavailable("Target token/cache accounting is not exposed by ps or these SQLite aggregates.") },
534
+ database: { baseline: dbBaseline, end: dbEnd },
535
+ artifacts: [{ path: "samples.jsonl", sha256: digest.digest("hex") }],
536
+ limitations: [
537
+ "RSS includes shared pages; sums can double-count. Baseline is attach time, not pre-start memory.",
538
+ "Sampled RSS peaks and CPU deltas are lower bounds. Short-lived/unseen children and CPU before first sighting or after last sighting are missed.",
539
+ "Linux /proc I/O deltas have the same sampling boundary; sync's application work counters are the source-read authority for short-lived workers.",
540
+ "PID identity uses ps lstart with second precision; same-second PID reuse cannot be distinguished. Only observed descendants survive reparenting in attribution.",
541
+ "ps %CPU is a platform-reported average (decaying on macOS), not interval CPU. Interval cores are computed separately from counters.",
542
+ "Each SQLite subprocess opens read-only with query_only and a short read transaction. Aggregates add observer load; actual timings/timeouts are recorded.",
543
+ "Cursor byte totals sum logical cursors; they are neither unique source-file bytes nor I/O counters.",
544
+ "Duration bounds observation, not an external kill of the target; in-flight/final samples may add command-timeout overhead. Actual elapsed time is reported.",
545
+ "A completed collection is not evidence that ingestion is correct or complete. Missing tables/counters are unavailable, not zero.",
546
+ ],
547
+ };
548
+ writeFileSync(join(options.out, "summary.json"), JSON.stringify(summary, null, 2) + "\n", { flag: "wx", mode: 0o600 });
549
+ return summary;
550
+ }
551
+
552
+ if (import.meta.main) {
553
+ if (process.argv[2] === "--sample-db") {
554
+ try {
555
+ const { path, marker } = JSON.parse(await Bun.stdin.text());
556
+ console.log(JSON.stringify(readCoverage(path, marker)));
557
+ } catch { process.exitCode = 2; }
558
+ } else if (process.argv.slice(2).includes("--help")) {
559
+ console.log(HELP);
560
+ } else {
561
+ try {
562
+ const summary = await observe(parseOptions(process.argv.slice(2)));
563
+ console.log(JSON.stringify({ status: summary.status, stop_reason: summary.stop_reason, sample_count: summary.sample_count }));
564
+ process.exitCode = summary.stop_reason === "interrupted" ? 130 : summary.status === "completed" ? 0 : 2;
565
+ } catch {
566
+ console.error("Observation failed: check required options, a new output directory, and readable input paths. Use --help.");
567
+ process.exitCode = 2;
568
+ }
569
+ }
570
+ }
@@ -0,0 +1,19 @@
1
+ # One-shot Trajectories session retitle (historical: 2026-09-06)
2
+
3
+ Sessions whose title derived from harness boilerplate (`<recommended_plugins>`
4
+ etc.) were retitled in-place via SQL mirroring the normalizer's fixed logic
5
+ (first non-boilerplate user record). Run only if an old DB predates the fix:
6
+
7
+ ```sql
8
+ UPDATE sessions SET title = (
9
+ SELECT substr(r.content, 1, 120) FROM records r
10
+ WHERE r.session_id = sessions.id AND r.record_type = 'user'
11
+ AND r.content IS NOT NULL AND trim(r.content) != ''
12
+ AND trim(r.content) NOT LIKE '<recommended_plugins>%'
13
+ AND trim(r.content) NOT LIKE '<environment_context>%'
14
+ AND trim(r.content) NOT LIKE '<user_info>%'
15
+ AND trim(r.content) NOT LIKE '<system_warning>%'
16
+ ORDER BY r.source_order_id LIMIT 1
17
+ )
18
+ WHERE title LIKE '<recommended_plugins>%';
19
+ ```