trajectories 0.0.0 → 0.1.1
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 +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -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 +1219 -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 +120 -0
- package/ops/install-cli.sh +63 -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 +19 -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,540 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import type { Diagnostic } from "@letta-ai/trajectory";
|
|
5
|
+
import type { DiscoveredSession } from "../types.js";
|
|
6
|
+
import {
|
|
7
|
+
MAX_NATIVE_RECORDS, NativeInputError, nativeByteLimit, object, readBoundedText,
|
|
8
|
+
reportNativeError, safeNativeId, type NativeDiscoveryOptions, type NativeReadOptions, type NativeTranscript,
|
|
9
|
+
} from "./common.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Grok Build keeps one directory per session under
|
|
13
|
+
* `~/.grok/sessions/<url-encoded-cwd>/<session-id>/`. `updates.jsonl` is the
|
|
14
|
+
* transcript source of truth: an append-only ACP notification stream
|
|
15
|
+
* (`{timestamp, method, params:{sessionId, update}}`) whose prose arrives as
|
|
16
|
+
* role-tagged chunks and whose tool results arrive as `tool_call_update` rows
|
|
17
|
+
* keyed by `toolCallId`. `summary.json` is a metadata sidecar (cwd, title,
|
|
18
|
+
* timestamps, model, branch); `chat_history.jsonl` is a compacted snapshot and
|
|
19
|
+
* `session_search.sqlite` is Grok's own index — neither is a session source.
|
|
20
|
+
* Format facts: docs/research/2026-09-16-c9-grok.md.
|
|
21
|
+
*/
|
|
22
|
+
const GROK_SESSION_FILE = "updates.jsonl";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The pinned library has no Grok adapter, so this reader projects each replayed
|
|
26
|
+
* update into the Copilot CLI event contract — the closest documented
|
|
27
|
+
* event-per-line contract to an ACP update stream, and one that keeps what a
|
|
28
|
+
* canonical record needs: a native per-update id, a per-event timestamp, tool
|
|
29
|
+
* calls linked to their results by call id, and a terminal outcome flag.
|
|
30
|
+
* `source_type` is restored to this source in the normalizer, so the projection
|
|
31
|
+
* stays a detail of a source the library cannot read.
|
|
32
|
+
*/
|
|
33
|
+
export const GROK_PROJECTION_SOURCE = "copilot-cli";
|
|
34
|
+
|
|
35
|
+
const SUMMARY_FILE = "summary.json";
|
|
36
|
+
const REGISTRY_DIRECTORY = "subagents";
|
|
37
|
+
const REGISTRY_FILE = "meta.json";
|
|
38
|
+
// <root>/<cwd>/<session>/updates.jsonl is the documented layout; the spare
|
|
39
|
+
// levels hold a listing rooted deeper, without turning discovery into an
|
|
40
|
+
// unbounded walk. A directory that holds a session is not descended into: its
|
|
41
|
+
// other subdirectories hold a session's own logs, not sessions.
|
|
42
|
+
const MAX_DEPTH = 4;
|
|
43
|
+
const MAX_SIDECAR_BYTES = 4 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
/** Lifecycle updates Grok writes beside the transcript: state, not conversation. */
|
|
46
|
+
const LIFECYCLE_KINDS = new Set([
|
|
47
|
+
"hook_execution",
|
|
48
|
+
"turn_completed",
|
|
49
|
+
"session_recap",
|
|
50
|
+
"retry_state",
|
|
51
|
+
"task_backgrounded",
|
|
52
|
+
"task_completed",
|
|
53
|
+
"auto_compact_started",
|
|
54
|
+
"auto_compact_completed",
|
|
55
|
+
"compaction_checkpoint",
|
|
56
|
+
"plan",
|
|
57
|
+
"current_mode_update",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export function grokRoots(): string[] {
|
|
61
|
+
return [join(homedir(), ".grok", "sessions")];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Summary {
|
|
65
|
+
cwd?: string;
|
|
66
|
+
branch?: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
title?: string;
|
|
69
|
+
startedAt?: string;
|
|
70
|
+
updatedAt?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isoTime(value: unknown): string | undefined {
|
|
74
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
75
|
+
const time = new Date(value);
|
|
76
|
+
return Number.isFinite(time.getTime()) ? time.toISOString() : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The sidecar is metadata, not transcript: a missing, oversized, or malformed
|
|
81
|
+
* summary costs title, cwd and model, and never the session, so every failure
|
|
82
|
+
* here degrades instead of throwing.
|
|
83
|
+
*/
|
|
84
|
+
function readSummary(path: string): { summary: Summary; unreadable: boolean } {
|
|
85
|
+
if (!existsSync(path)) return { summary: {}, unreadable: false };
|
|
86
|
+
try {
|
|
87
|
+
const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
|
|
88
|
+
if (!object(value)) return { summary: {}, unreadable: true };
|
|
89
|
+
const info = object(value.info) ? value.info : {};
|
|
90
|
+
const title = [value.generated_title, value.session_summary]
|
|
91
|
+
.find((candidate): candidate is string => typeof candidate === "string" && candidate.trim().length > 0);
|
|
92
|
+
return {
|
|
93
|
+
summary: {
|
|
94
|
+
cwd: typeof info.cwd === "string" && info.cwd.trim() ? info.cwd : undefined,
|
|
95
|
+
branch: typeof value.head_branch === "string" && value.head_branch.trim() ? value.head_branch : undefined,
|
|
96
|
+
model: typeof value.current_model_id === "string" && value.current_model_id.trim() ? value.current_model_id : undefined,
|
|
97
|
+
title,
|
|
98
|
+
startedAt: isoTime(value.created_at),
|
|
99
|
+
updatedAt: isoTime(value.updated_at) ?? isoTime(value.last_active_at),
|
|
100
|
+
},
|
|
101
|
+
unreadable: false,
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return { summary: {}, unreadable: true };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function discoverGrokSessions(roots: string[], options: NativeDiscoveryOptions): DiscoveredSession[] {
|
|
109
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1) throw new RangeError("Grok discovery limit must be positive.");
|
|
110
|
+
const items: DiscoveredSession[] = [];
|
|
111
|
+
const edges = new Map<string, string>();
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
|
|
114
|
+
function walk(path: string, depth: number): void {
|
|
115
|
+
if (seen.has(path)) return;
|
|
116
|
+
seen.add(path);
|
|
117
|
+
try {
|
|
118
|
+
const stat = lstatSync(path);
|
|
119
|
+
if (stat.isSymbolicLink() || stat.isFile()) return;
|
|
120
|
+
if (depth > MAX_DEPTH) {
|
|
121
|
+
options.onDiagnostic?.({ code: "incomplete_evidence", sourcePath: path, message: "Grok discovery reached its directory depth limit." });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const entries = readdirSync(path, { withFileTypes: true });
|
|
125
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
126
|
+
if (names.has(GROK_SESSION_FILE)) {
|
|
127
|
+
const id = basename(path);
|
|
128
|
+
if (!safeNativeId(id)) {
|
|
129
|
+
options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: path, message: "Grok session has an invalid native session ID." });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const updates = statSync(join(path, GROK_SESSION_FILE));
|
|
133
|
+
const { summary } = readSummary(join(path, SUMMARY_FILE));
|
|
134
|
+
items.push({
|
|
135
|
+
id,
|
|
136
|
+
source: "grok",
|
|
137
|
+
path: join(path, GROK_SESSION_FILE),
|
|
138
|
+
updatedAt: summary.updatedAt ?? updates.mtime.toISOString(),
|
|
139
|
+
sizeBytes: updates.size + (names.has(SUMMARY_FILE) ? statSync(join(path, SUMMARY_FILE)).size : 0),
|
|
140
|
+
...(summary.title ? { title: summary.title } : {}),
|
|
141
|
+
});
|
|
142
|
+
if (names.has(REGISTRY_DIRECTORY)) collectSubagentEdges(path, id, options, edges);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry.isDirectory()) walk(join(path, entry.name), depth + 1);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (object(error) && error.code === "ENOENT" && depth === 0) return;
|
|
150
|
+
reportNativeError(options, path);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const root of roots) walk(resolve(root), 0);
|
|
155
|
+
|
|
156
|
+
const ordered = items
|
|
157
|
+
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "") || a.path.localeCompare(b.path));
|
|
158
|
+
const nativeIds = new Set<string>();
|
|
159
|
+
const deduplicated = ordered.filter((item) => {
|
|
160
|
+
if (nativeIds.has(item.id)) return false;
|
|
161
|
+
nativeIds.add(item.id);
|
|
162
|
+
return true;
|
|
163
|
+
}).map((item) => {
|
|
164
|
+
const parent = edges.get(item.id);
|
|
165
|
+
return { ...item, native: { kind: "grok-updates" as const, ...(parent ? { parentSessionId: parent } : {}) } };
|
|
166
|
+
});
|
|
167
|
+
return options.select ? options.select(deduplicated) : deduplicated.slice(0, options.limit);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parentage evidence read from the parent's side: every subagent Grok spawns is
|
|
172
|
+
* registered at `<parent>/subagents/<child>/meta.json`. The registration names
|
|
173
|
+
* the child by either of two fields and the parent by `parent_session_id`,
|
|
174
|
+
* falling back to the enclosing session directory; a session is never its own
|
|
175
|
+
* parent, and the first registration wins — the rules Wake's
|
|
176
|
+
* `load_subagent_parents` applies (`crates/wake-core/src/adapters/grok_group.rs`).
|
|
177
|
+
*
|
|
178
|
+
* The edge travels with the child's listing, so ingest never re-walks the store
|
|
179
|
+
* and a registry added between passes is visible to the next scan.
|
|
180
|
+
*/
|
|
181
|
+
function collectSubagentEdges(
|
|
182
|
+
directory: string,
|
|
183
|
+
parent: string,
|
|
184
|
+
options: NativeDiscoveryOptions,
|
|
185
|
+
edges: Map<string, string>,
|
|
186
|
+
): void {
|
|
187
|
+
const registry = join(directory, REGISTRY_DIRECTORY);
|
|
188
|
+
let entries: string[];
|
|
189
|
+
try {
|
|
190
|
+
entries = readdirSync(registry);
|
|
191
|
+
} catch {
|
|
192
|
+
reportNativeError(options, registry);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const metaPath = join(registry, entry, REGISTRY_FILE);
|
|
197
|
+
let meta: Record<string, unknown>;
|
|
198
|
+
try {
|
|
199
|
+
const parsed: unknown = JSON.parse(readBoundedText(metaPath, MAX_SIDECAR_BYTES));
|
|
200
|
+
if (!object(parsed)) throw new NativeInputError("Grok subagent registration must be a JSON object.");
|
|
201
|
+
meta = parsed;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (object(error) && error.code === "ENOENT") continue;
|
|
204
|
+
options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: metaPath, message: "Grok subagent registration could not be read." });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const parentId = typeof meta.parent_session_id === "string" && meta.parent_session_id.trim() ? meta.parent_session_id : parent;
|
|
208
|
+
for (const field of ["child_session_id", "subagent_id"]) {
|
|
209
|
+
const child = meta[field];
|
|
210
|
+
if (typeof child !== "string" || !safeNativeId(child) || child === parentId) continue;
|
|
211
|
+
if (!edges.has(child)) edges.set(child, parentId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type RunRole = "user" | "thought" | "message";
|
|
217
|
+
type Row = Record<string, unknown>;
|
|
218
|
+
|
|
219
|
+
interface Run {
|
|
220
|
+
role: RunRole;
|
|
221
|
+
text: string;
|
|
222
|
+
/** The run's identity: the id of its first fragment. */
|
|
223
|
+
id: string;
|
|
224
|
+
timestamp: string | null;
|
|
225
|
+
model: string | undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface Skips {
|
|
229
|
+
lifecycle: Map<string, number>;
|
|
230
|
+
progress: number;
|
|
231
|
+
unknown: Map<string, number>;
|
|
232
|
+
content: number;
|
|
233
|
+
invalidLines: number;
|
|
234
|
+
malformedLines: number;
|
|
235
|
+
/** Terminal rows for a call whose result was already projected. */
|
|
236
|
+
repeatedTerminals: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function tally(values: Map<string, number>): number {
|
|
240
|
+
let total = 0;
|
|
241
|
+
for (const count of values.values()) total += count;
|
|
242
|
+
return total;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function named(values: Map<string, number>, limit = 4): string {
|
|
246
|
+
return [...values.keys()].sort().slice(0, limit).join(", ");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Replay the ACP stream.
|
|
251
|
+
*
|
|
252
|
+
* A run of same-role chunks becomes one record: chunk text is concatenated
|
|
253
|
+
* without a separator, because Grok splits tokens (including the spaces between
|
|
254
|
+
* them) across rows. A run flushes on a role change or when tool activity
|
|
255
|
+
* interrupts it, so one record's text never spans two speakers or straddles a
|
|
256
|
+
* tool call. A `tool_call_update` is a result only in its terminal state:
|
|
257
|
+
* in_progress and status-less progress rows are superseded by the terminal row
|
|
258
|
+
* that follows, and are counted rather than projected.
|
|
259
|
+
*
|
|
260
|
+
* Grok appends while it runs, so a torn last line or a line from a newer
|
|
261
|
+
* version is counted, never fatal: the transcript is what the prefix holds.
|
|
262
|
+
*/
|
|
263
|
+
function replayUpdates(text: string, sessionId: string, summary: Summary, limit: number): {
|
|
264
|
+
rows: Row[]; skips: Skips; model: string | undefined;
|
|
265
|
+
} {
|
|
266
|
+
const rows: Row[] = [];
|
|
267
|
+
const skips: Skips = {
|
|
268
|
+
lifecycle: new Map(), progress: 0, unknown: new Map(), content: 0, invalidLines: 0, malformedLines: 0,
|
|
269
|
+
repeatedTerminals: 0,
|
|
270
|
+
};
|
|
271
|
+
// One result per call id: a repeated terminal row (a retry after a failure, a
|
|
272
|
+
// re-emitted update) must not project a second result for the same call.
|
|
273
|
+
const terminals = new Set<string>();
|
|
274
|
+
let run: Run | null = null;
|
|
275
|
+
let model = summary.model;
|
|
276
|
+
let written = 0;
|
|
277
|
+
|
|
278
|
+
function emit(row: Row): void {
|
|
279
|
+
written += Buffer.byteLength(JSON.stringify(row)) + 1;
|
|
280
|
+
if (written > limit) throw new NativeInputError("Grok transcript exceeds the native byte limit.");
|
|
281
|
+
rows.push(row);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function flush(): void {
|
|
285
|
+
const pending = run;
|
|
286
|
+
run = null;
|
|
287
|
+
if (!pending || !pending.text.length) return;
|
|
288
|
+
if (pending.role === "user") {
|
|
289
|
+
emit({
|
|
290
|
+
type: "hook.start",
|
|
291
|
+
id: pending.id,
|
|
292
|
+
...(pending.timestamp ? { timestamp: pending.timestamp } : {}),
|
|
293
|
+
data: {
|
|
294
|
+
hookType: "userPromptSubmitted",
|
|
295
|
+
input: { prompt: pending.text, sessionId, ...(summary.cwd ? { cwd: summary.cwd } : {}) },
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const eventModel = pending.model ?? model;
|
|
301
|
+
emit({
|
|
302
|
+
type: "assistant.message",
|
|
303
|
+
id: pending.id,
|
|
304
|
+
...(pending.timestamp ? { timestamp: pending.timestamp } : {}),
|
|
305
|
+
data: {
|
|
306
|
+
...(pending.role === "thought" ? { reasoningText: pending.text } : { content: pending.text }),
|
|
307
|
+
...(eventModel ? { model: eventModel } : {}),
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const lines = text.split("\n");
|
|
313
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
314
|
+
const line = lines[index];
|
|
315
|
+
if (!line || !line.trim()) continue;
|
|
316
|
+
let parsed: unknown;
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(line);
|
|
319
|
+
} catch {
|
|
320
|
+
skips.invalidLines += 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const row = object(parsed) ? parsed : null;
|
|
324
|
+
const params = row && object(row.params) ? row.params : null;
|
|
325
|
+
const update = params && object(params.update) ? params.update : null;
|
|
326
|
+
if (!row || !params || !update) {
|
|
327
|
+
skips.malformedLines += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const meta = object(params._meta) ? params._meta : {};
|
|
331
|
+
const agentTimestampMs = typeof meta.agentTimestampMs === "number" && Number.isFinite(meta.agentTimestampMs)
|
|
332
|
+
? meta.agentTimestampMs
|
|
333
|
+
: typeof update._meta === "object" && object(update._meta) && typeof update._meta.agentTimestampMs === "number"
|
|
334
|
+
&& Number.isFinite(update._meta.agentTimestampMs)
|
|
335
|
+
? update._meta.agentTimestampMs
|
|
336
|
+
: null;
|
|
337
|
+
const timestamp = agentTimestampMs !== null
|
|
338
|
+
? new Date(agentTimestampMs).toISOString()
|
|
339
|
+
: typeof row.timestamp === "number" && Number.isFinite(row.timestamp) && row.timestamp > 0
|
|
340
|
+
? new Date(row.timestamp * 1000).toISOString()
|
|
341
|
+
: null;
|
|
342
|
+
const chunkModel = object(update._meta) && typeof update._meta.modelId === "string" ? update._meta.modelId : undefined;
|
|
343
|
+
if (chunkModel) model = chunkModel;
|
|
344
|
+
// A row without a usable update id keeps a stable identity from its own
|
|
345
|
+
// line: the file is append-only, so a line number never moves.
|
|
346
|
+
const id = typeof meta.eventId === "string" && safeNativeId(meta.eventId) ? meta.eventId : `grok-line-${index + 1}`;
|
|
347
|
+
const kind = update.sessionUpdate;
|
|
348
|
+
if (kind === "user_message_chunk" || kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
|
|
349
|
+
const content = update.content;
|
|
350
|
+
const chunk = object(content) && content.type === "text" && typeof content.text === "string" ? content.text : null;
|
|
351
|
+
if (chunk === null) {
|
|
352
|
+
skips.content += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const role: RunRole = kind === "user_message_chunk" ? "user" : kind === "agent_thought_chunk" ? "thought" : "message";
|
|
356
|
+
if (run && run.role !== role) flush();
|
|
357
|
+
run ??= { role, text: "", id, timestamp, model: chunkModel };
|
|
358
|
+
run.text += chunk;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (kind === "tool_call") {
|
|
362
|
+
const callId = update.toolCallId;
|
|
363
|
+
if (!safeNativeId(callId)) {
|
|
364
|
+
skips.unknown.set("tool_call_without_id", (skips.unknown.get("tool_call_without_id") ?? 0) + 1);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
flush();
|
|
368
|
+
const tool = object(update._meta) && object(update._meta["x.ai/tool"]) ? update._meta["x.ai/tool"] : null;
|
|
369
|
+
const name = typeof update.title === "string" && update.title.trim()
|
|
370
|
+
? update.title
|
|
371
|
+
: tool && typeof tool.name === "string" && tool.name.trim() ? tool.name : undefined;
|
|
372
|
+
const eventModel = chunkModel ?? model;
|
|
373
|
+
emit({
|
|
374
|
+
type: "assistant.message",
|
|
375
|
+
id,
|
|
376
|
+
...(timestamp ? { timestamp } : {}),
|
|
377
|
+
data: {
|
|
378
|
+
toolRequests: [{ toolCallId: callId, ...(name ? { name } : {}), arguments: update.rawInput ?? {} }],
|
|
379
|
+
...(eventModel ? { model: eventModel } : {}),
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (kind === "tool_call_update") {
|
|
385
|
+
const callId = update.toolCallId;
|
|
386
|
+
if (!safeNativeId(callId)) {
|
|
387
|
+
skips.unknown.set("tool_call_update_without_id", (skips.unknown.get("tool_call_update_without_id") ?? 0) + 1);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const status = update.status;
|
|
391
|
+
if (status !== "completed" && status !== "failed") {
|
|
392
|
+
skips.progress += 1;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (terminals.has(callId)) {
|
|
396
|
+
skips.repeatedTerminals += 1;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
terminals.add(callId);
|
|
400
|
+
flush();
|
|
401
|
+
// ACP tool output is content blocks; a structured rawOutput is the only
|
|
402
|
+
// text a result can carry when no content block was written.
|
|
403
|
+
const blocks = update.content;
|
|
404
|
+
const parts = Array.isArray(blocks)
|
|
405
|
+
? blocks
|
|
406
|
+
.map((block) => (object(block) && object(block.content) && block.content.type === "text" && typeof block.content.text === "string"
|
|
407
|
+
? block.content.text
|
|
408
|
+
: null))
|
|
409
|
+
.filter((part): part is string => part !== null && part.length > 0)
|
|
410
|
+
: [];
|
|
411
|
+
const raw = update.rawOutput;
|
|
412
|
+
const output = parts.length
|
|
413
|
+
? parts.join("\n")
|
|
414
|
+
: raw === undefined || raw === null ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
415
|
+
emit({
|
|
416
|
+
type: "tool.execution_complete",
|
|
417
|
+
id,
|
|
418
|
+
...(timestamp ? { timestamp } : {}),
|
|
419
|
+
data: {
|
|
420
|
+
toolCallId: callId,
|
|
421
|
+
success: status !== "failed",
|
|
422
|
+
...(status === "failed" ? { error: { message: output } } : { result: { content: output } }),
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (typeof kind === "string" && LIFECYCLE_KINDS.has(kind)) {
|
|
428
|
+
skips.lifecycle.set(kind, (skips.lifecycle.get(kind) ?? 0) + 1);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const label = typeof kind === "string" && kind ? kind : "missing_kind";
|
|
432
|
+
skips.unknown.set(label, (skips.unknown.get(label) ?? 0) + 1);
|
|
433
|
+
}
|
|
434
|
+
flush();
|
|
435
|
+
return { rows, skips, model };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Every skip is typed and counted, so a dropped update never vanishes silently. */
|
|
439
|
+
function skipDiagnostics(skips: Skips): Diagnostic[] {
|
|
440
|
+
const diagnostics: Diagnostic[] = [];
|
|
441
|
+
if (skips.lifecycle.size) {
|
|
442
|
+
diagnostics.push({
|
|
443
|
+
code: "noise_record_dropped",
|
|
444
|
+
message: `Skipped ${tally(skips.lifecycle)} Grok lifecycle update(s) (${named(skips.lifecycle)}).`,
|
|
445
|
+
count: tally(skips.lifecycle),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
if (skips.progress) {
|
|
449
|
+
diagnostics.push({
|
|
450
|
+
code: "noise_record_dropped",
|
|
451
|
+
message: `Skipped ${skips.progress} non-terminal Grok tool progress update(s); the terminal update carries the result.`,
|
|
452
|
+
count: skips.progress,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (skips.repeatedTerminals) {
|
|
456
|
+
diagnostics.push({
|
|
457
|
+
code: "noise_record_dropped",
|
|
458
|
+
message: `Skipped ${skips.repeatedTerminals} repeated Grok tool terminal update(s); the call's first terminal update is its result.`,
|
|
459
|
+
count: skips.repeatedTerminals,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (skips.unknown.size) {
|
|
463
|
+
diagnostics.push({
|
|
464
|
+
code: "noise_record_dropped",
|
|
465
|
+
message: `Skipped ${tally(skips.unknown)} Grok update(s) with no supported transcript shape (${named(skips.unknown)}).`,
|
|
466
|
+
count: tally(skips.unknown),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (skips.content) {
|
|
470
|
+
diagnostics.push({
|
|
471
|
+
code: "noise_record_dropped",
|
|
472
|
+
message: `Skipped ${skips.content} Grok message update(s) whose content is not a text block.`,
|
|
473
|
+
count: skips.content,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (skips.invalidLines) {
|
|
477
|
+
diagnostics.push({
|
|
478
|
+
code: "invalid_json_line",
|
|
479
|
+
message: `Skipped ${skips.invalidLines} invalid JSON line(s) in the Grok update stream.`,
|
|
480
|
+
count: skips.invalidLines,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (skips.malformedLines) {
|
|
484
|
+
diagnostics.push({
|
|
485
|
+
code: "invalid_json_line",
|
|
486
|
+
message: `Skipped ${skips.malformedLines} Grok line(s) with no session update object.`,
|
|
487
|
+
count: skips.malformedLines,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return diagnostics;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function readGrokSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
|
|
494
|
+
if (!safeNativeId(item.id)) throw new NativeInputError("Unsupported Grok native session ID.");
|
|
495
|
+
if (item.native && item.native.kind !== "grok-updates") throw new NativeInputError("Unsupported Grok native session.");
|
|
496
|
+
const limit = nativeByteLimit(options);
|
|
497
|
+
const text = readBoundedText(item.path, limit);
|
|
498
|
+
const { summary, unreadable } = readSummary(join(dirname(item.path), SUMMARY_FILE));
|
|
499
|
+
const replay = replayUpdates(text, item.id, summary, limit);
|
|
500
|
+
if (replay.rows.length > MAX_NATIVE_RECORDS) throw new NativeInputError("Grok session exceeds the native record limit.");
|
|
501
|
+
|
|
502
|
+
const rows: Row[] = summary.startedAt
|
|
503
|
+
? [{
|
|
504
|
+
type: "session.start",
|
|
505
|
+
timestamp: summary.startedAt,
|
|
506
|
+
data: {
|
|
507
|
+
sessionId: item.id,
|
|
508
|
+
startTime: summary.startedAt,
|
|
509
|
+
context: { ...(summary.cwd ? { cwd: summary.cwd } : {}), ...(summary.branch ? { branch: summary.branch } : {}) },
|
|
510
|
+
},
|
|
511
|
+
}, ...replay.rows]
|
|
512
|
+
: replay.rows;
|
|
513
|
+
|
|
514
|
+
const diagnostics = skipDiagnostics(replay.skips);
|
|
515
|
+
if (unreadable) {
|
|
516
|
+
diagnostics.push({
|
|
517
|
+
code: "invalid_json_line",
|
|
518
|
+
message: "Ignored the Grok session summary sidecar: it is not a readable JSON object, so title, cwd and model stay unset.",
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const model = replay.model ?? summary.model;
|
|
523
|
+
return {
|
|
524
|
+
transcript: rows.length ? `${rows.map((row) => JSON.stringify(row)).join("\n")}\n` : "",
|
|
525
|
+
...(summary.title ? { title: summary.title } : {}),
|
|
526
|
+
...(summary.cwd ? { cwd: summary.cwd } : {}),
|
|
527
|
+
...(model ? { model } : {}),
|
|
528
|
+
// The sidecar's own envelope bounds the session: it starts before the first
|
|
529
|
+
// update and ends at Grok's last recorded activity, which a hook-only
|
|
530
|
+
// session (no records at all) would otherwise have no span for.
|
|
531
|
+
...(summary.startedAt ? { startedAt: summary.startedAt } : {}),
|
|
532
|
+
...(summary.updatedAt ? { endedAt: summary.updatedAt } : {}),
|
|
533
|
+
...(diagnostics.length ? { diagnostics } : {}),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** The files whose change invalidates a Grok session's snapshot. */
|
|
538
|
+
export function grokSessionFiles(item: DiscoveredSession): string[] {
|
|
539
|
+
return [item.path, join(dirname(item.path), SUMMARY_FILE)];
|
|
540
|
+
}
|