@gobing-ai/ts-llm-jsonl-importer 0.4.48 → 0.4.50

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/README.md CHANGED
@@ -7,7 +7,7 @@ Generic JSONL import pipeline for AI-agent history-style files: discover files,
7
7
  `ts-llm-jsonl-importer` is a common JSONL importer. It is intentionally not a conversation-history domain model. Built-in source definitions cover common agent file shapes, but downstream systems consume normalized ETL rows and ledger metadata.
8
8
 
9
9
  | Export | Purpose |
10
- |--------|---------|
10
+ | -------- | --------- |
11
11
  | `runJsonlImport()` | Runs discovery, parsing, validation, redaction, dedupe, and persistence. Accepts a built-in source string **or** a custom `SourceDefinition`. |
12
12
  | `applyHistoryImportSchema()` | Installs importer-owned checkpoint, ledger, and typed contract tables |
13
13
  | `SOURCE_DEFINITIONS` | Built-in source definitions |
@@ -19,7 +19,7 @@ Generic JSONL import pipeline for AI-agent history-style files: discover files,
19
19
  | `sha256()` / `stableJson()` | Stable hash helpers used by the ledger |
20
20
  | `HISTORY_IMPORT_SCHEMA_SQL` | SQL schema string for explicit migration flows |
21
21
 
22
- Built-in source keys are `claude`, `codex`, `gemini`, `pi`, `opencode`, `antigravity`, `openclaw`, `omp`, `grok`, and `agy`.
22
+ Built-in source keys are `claude`, `codex`, `gemini`, `pi`, `opencode`, `antigravity`, `openclaw`, `omp`, `grok`, and `agy`. The `agy` source scans `~/.gemini/antigravity-cli`, covering both `history.jsonl` and `brain/**` transcripts; conversation `.db` stores under `conversations/` are not imported.
23
23
 
24
24
  ## Installation
25
25
 
@@ -52,7 +52,7 @@ result.skippedDuplicates;
52
52
  ## Import Modes
53
53
 
54
54
  | Mode | Behavior |
55
- |------|----------|
55
+ | ------ | ---------- |
56
56
  | `incremental` | Reads each file from the last imported line checkpoint |
57
57
  | `full` | Clears checkpoints for selected files, scans all lines, and still deduplicates by ledger hash |
58
58
  | `force-file` | Scans selected files without using the checkpoint, while preserving ledger-based duplicate protection |
@@ -124,7 +124,7 @@ Use `parseErrors` for invalid JSON or non-object rows. Use `validationErrors` fo
124
124
  The schema contains:
125
125
 
126
126
  | Table | Purpose |
127
- |-------|---------|
127
+ | ------- | --------- |
128
128
  | `history_import_checkpoint` | Per-source/per-file last imported line |
129
129
  | `history_import_ledger` | Stable hash ledger for dedupe and provenance |
130
130
  | `history_message` | Typed normalized messages from built-in forensic mappers |
package/dist/mappers.d.ts CHANGED
@@ -3,10 +3,11 @@ import type { JsonObject, SplitEntry, TransformContext } from './types';
3
3
  /** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
4
4
  export declare function argsDigest(args: unknown): string;
5
5
  /**
6
- * Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
7
- * otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
6
+ * Return retained raw args for a typed tool call: the shell command string for
7
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
8
+ * undefined when the event carries no invocation arguments.
8
9
  */
9
- export declare function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined;
10
+ export declare function maybeArgsRaw(_source: string, toolName: string, args: unknown): string | undefined;
10
11
  /** Map a Claude Code JSONL record into one or more ETL split entries. */
11
12
  export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
12
13
  /** Map a Pi JSONL record into one or more ETL split entries. */
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
package/dist/mappers.js CHANGED
@@ -91,37 +91,122 @@ function redactArgs(args) {
91
91
  return args;
92
92
  }
93
93
  /**
94
- * Per-source todo-writing tool names whose raw arguments are retained for phase detection.
95
- *
96
- * Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
97
- * session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
98
- * 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
99
- * opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
100
- * rows with NULL args_raw).
94
+ * Retained tool-call arguments: recognized shell tools keep the extracted plain
95
+ * command (attribution compatibility, capped); every other typed tool call keeps
96
+ * sanitized-but-valid JSON. `args_digest` is computed elsewhere from the unpruned
97
+ * args and is unaffected; secret redaction stays at the persistence `redactRecord`
98
+ * seam (task 0064 R5).
101
99
  */
102
- const TODO_TOOL_ALLOWLIST = {
103
- claude: ['TodoWrite'],
104
- opencode: ['todowrite', 'todoread'],
105
- codex: ['update_plan'],
106
- omp: ['TodoWrite', 'todo', 'todo_write'],
107
- grok: ['todo_write'],
108
- pi: ['todo', 'manage_todo_list'],
109
- agy: [],
110
- gemini: [],
111
- };
100
+ const BASH_COMMAND_MAX_CHARS = 8192;
101
+ const GENERIC_ARGS_MAX_CHARS = 8192;
102
+ /** Extract the shell command from a bash tool's raw input; undefined when absent. */
103
+ function bashCommandOf(args) {
104
+ if (args === null || typeof args !== 'object')
105
+ return undefined;
106
+ const rec = args;
107
+ const command = rec.command ?? rec.CommandLine ?? rec.cmd ?? rec.script;
108
+ if (typeof command === 'string')
109
+ return command;
110
+ if (Array.isArray(command))
111
+ return command.map(String).join(' ');
112
+ return undefined;
113
+ }
114
+ /** Bulky payload keys (normalized) pruned to an omission marker; camelCase and snake_case share one rule. */
115
+ const BULKY_PAYLOAD_KEYS = new Set([
116
+ 'content',
117
+ 'codecontent',
118
+ 'replacementcontent',
119
+ 'targetcontent',
120
+ 'filedata',
121
+ 'buffer',
122
+ 'blob',
123
+ 'image',
124
+ 'imagebase64',
125
+ ]);
126
+ /** Lowercase a key and drop separators so code_content and codeContent compare equal. */
127
+ function normalizeKey(key) {
128
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '');
129
+ }
130
+ /** Tool names whose command-shaped args retain the existing plain-string representation. */
131
+ function isShellTool(toolName) {
132
+ return /^(?:bash|shell|cmd|command|exec|execcommand|executecommand|runcommand|shellcommand|runshellcommand)$/.test(normalizeKey(toolName));
133
+ }
112
134
  /**
113
- * Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
114
- * otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
135
+ * Sanitize any JSON value recursively through objects AND arrays: bulky payload
136
+ * strings under known keys become `[omitted N chars]`, other strings longer than
137
+ * 1,000 characters keep a prefix plus an explicit original-length marker, and
138
+ * every primitive and short payload is preserved untouched (task 0064 R2).
115
139
  */
116
- export function maybeArgsRaw(source, toolName, args) {
117
- const allow = TODO_TOOL_ALLOWLIST[source];
118
- if (allow === undefined || !allow.includes(toolName))
119
- return undefined;
120
- if (typeof args === 'string')
121
- return args;
140
+ function sanitizeValue(value) {
141
+ if (typeof value === 'string') {
142
+ return value.length > 1000 ? `${value.slice(0, 1000)}[truncated ${value.length} chars]` : value;
143
+ }
144
+ if (Array.isArray(value))
145
+ return value.map(sanitizeValue);
146
+ if (value !== null && typeof value === 'object') {
147
+ const out = {};
148
+ for (const [key, entry] of Object.entries(value)) {
149
+ out[key] =
150
+ typeof entry === 'string' && entry.length > 120 && BULKY_PAYLOAD_KEYS.has(normalizeKey(key))
151
+ ? `[omitted ${entry.length} chars]`
152
+ : sanitizeValue(entry);
153
+ }
154
+ return out;
155
+ }
156
+ // JSON leaves (number/boolean/null) pass through unchanged; JSON args never
157
+ // reach here as undefined/bigint/symbol/function.
158
+ return value;
159
+ }
160
+ /**
161
+ * Serialize sanitized args within the retention ceiling without ever slicing JSON
162
+ * text: values that still serialize past the ceiling collapse into a small valid
163
+ * JSON wrapper carrying `_truncated`, the omitted character count, and a bounded
164
+ * preview reduced until the wrapper itself fits (task 0064 R3/AC4).
165
+ */
166
+ function serializeBounded(value) {
167
+ const serialized = JSON.stringify(sanitizeValue(value));
168
+ if (serialized.length <= GENERIC_ARGS_MAX_CHARS)
169
+ return serialized;
170
+ let preview = serialized;
171
+ for (;;) {
172
+ const wrapper = JSON.stringify({
173
+ _truncated: true,
174
+ omittedChars: serialized.length - preview.length,
175
+ preview,
176
+ });
177
+ if (wrapper.length <= GENERIC_ARGS_MAX_CHARS)
178
+ return wrapper;
179
+ preview = preview.slice(0, preview.length - (wrapper.length - GENERIC_ARGS_MAX_CHARS));
180
+ }
181
+ }
182
+ /**
183
+ * Return retained raw args for a typed tool call: the shell command string for
184
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
185
+ * undefined when the event carries no invocation arguments.
186
+ */
187
+ export function maybeArgsRaw(_source, toolName, args) {
122
188
  if (args === undefined || args === null)
123
189
  return undefined;
124
- return JSON.stringify(args);
190
+ let value = args;
191
+ if (typeof args === 'string') {
192
+ // Pre-stringified arguments (Codex) re-enter the structured path so the
193
+ // retained value parses and prunes like native JSON.
194
+ try {
195
+ value = JSON.parse(args);
196
+ }
197
+ catch {
198
+ return args.length > BASH_COMMAND_MAX_CHARS ? args.slice(0, BASH_COMMAND_MAX_CHARS) : args;
199
+ }
200
+ }
201
+ if (value !== null && typeof value === 'object') {
202
+ const cmd = bashCommandOf(value);
203
+ if (cmd !== undefined && isShellTool(toolName)) {
204
+ return cmd.length > BASH_COMMAND_MAX_CHARS ? cmd.slice(0, BASH_COMMAND_MAX_CHARS) : cmd;
205
+ }
206
+ return serializeBounded(value);
207
+ }
208
+ // JSON primitives (number/boolean) retain as valid JSON scalars.
209
+ return JSON.stringify(value);
125
210
  }
126
211
  // ---------------------------------------------------------------------------
127
212
  // Claude mapper
@@ -748,67 +833,83 @@ export function codexSplit(raw, context) {
748
833
  export function agySplit(raw, context) {
749
834
  const entries = [];
750
835
  const recordType = String(raw.type ?? '');
751
- const explicitSessionId = s(raw.session_id, raw.conversation_id);
836
+ const explicitSessionId = s(raw.session_id, raw.conversation_id, raw.conversationId);
752
837
  const pathSessionId = context?.sourceFile ? sessionIdFromSourcePath('agy', context.sourceFile) : undefined;
753
838
  const sessionId = explicitSessionId ?? pathSessionId ?? 'unknown';
754
- const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
839
+ const seq = typeof raw.seq === 'number'
840
+ ? raw.seq
841
+ : typeof raw.step_index === 'number'
842
+ ? raw.step_index
843
+ : (context?.sourceLine ?? 0);
755
844
  const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
756
845
  let role;
757
846
  let disposition;
758
847
  let contentText = null;
759
- // Classification from task 0463 field map (agy transcript types).
760
- switch (recordType) {
761
- case 'USER_INPUT':
762
- case 'ASK_QUESTION':
763
- role = 'user';
764
- disposition = 'keep';
765
- contentText = s(raw.content, raw.text, raw.message) ?? null;
766
- break;
767
- case 'PLANNER_RESPONSE':
768
- role = 'assistant';
769
- disposition = 'keep';
770
- contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
771
- break;
772
- case 'ERROR_MESSAGE':
773
- role = 'system';
774
- disposition = 'keep';
775
- contentText = s(raw.content, raw.text, raw.message) ?? null;
776
- break;
777
- case 'RUN_COMMAND':
778
- case 'VIEW_FILE':
779
- case 'GREP_SEARCH':
780
- case 'LIST_DIRECTORY':
781
- case 'READ_URL_CONTENT':
782
- role = 'tool';
783
- disposition = 'keep';
784
- contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
785
- break;
786
- case 'CODE_ACTION':
787
- case 'INVOKE_SUBAGENT':
788
- role = 'tool';
789
- disposition = 'keep';
790
- contentText = s(raw.content, raw.text) ?? null;
791
- break;
792
- case 'CONVERSATION_HISTORY':
793
- case 'CHECKPOINT':
794
- case 'GENERIC':
795
- role = 'meta';
796
- disposition = 'meta';
797
- contentText = s(raw.content, raw.text) ?? null;
798
- break;
799
- default:
800
- if (recordType.length === 0) {
801
- role = 'unknown';
802
- disposition = 'unknown';
803
- }
804
- else {
805
- // Known discriminator present but not in the frozen 0463 keep/meta table —
806
- // still a determined type; treat as meta bookkeeping rather than unknown.
848
+ let messageRecordType = recordType;
849
+ // history.jsonl prompt index: {display, timestamp, workspace, conversationId?, type?}.
850
+ // One branch covers absent type, 'slash_command', 'shell', and any future producer type.
851
+ // `display` never appears on legacy brain transcripts, so this cannot collide with 0463.
852
+ if (raw.display !== undefined) {
853
+ role = 'user';
854
+ disposition = 'keep';
855
+ contentText = s(raw.display) ?? null;
856
+ messageRecordType = recordType.length > 0 ? recordType : 'USER_INPUT';
857
+ }
858
+ else {
859
+ // Classification from task 0463 field map (agy transcript types).
860
+ switch (recordType) {
861
+ case 'USER_INPUT':
862
+ case 'ASK_QUESTION':
863
+ role = 'user';
864
+ disposition = 'keep';
865
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
866
+ break;
867
+ case 'PLANNER_RESPONSE':
868
+ role = 'assistant';
869
+ disposition = 'keep';
870
+ contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
871
+ break;
872
+ case 'ERROR_MESSAGE':
873
+ role = 'system';
874
+ disposition = 'keep';
875
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
876
+ break;
877
+ case 'RUN_COMMAND':
878
+ case 'VIEW_FILE':
879
+ case 'GREP_SEARCH':
880
+ case 'LIST_DIRECTORY':
881
+ case 'READ_URL_CONTENT':
882
+ role = 'tool';
883
+ disposition = 'keep';
884
+ contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
885
+ break;
886
+ case 'CODE_ACTION':
887
+ case 'INVOKE_SUBAGENT':
888
+ role = 'tool';
889
+ disposition = 'keep';
890
+ contentText = s(raw.content, raw.text) ?? null;
891
+ break;
892
+ case 'CONVERSATION_HISTORY':
893
+ case 'CHECKPOINT':
894
+ case 'GENERIC':
807
895
  role = 'meta';
808
896
  disposition = 'meta';
809
897
  contentText = s(raw.content, raw.text) ?? null;
810
- }
811
- break;
898
+ break;
899
+ default:
900
+ if (recordType.length === 0) {
901
+ role = 'unknown';
902
+ disposition = 'unknown';
903
+ }
904
+ else {
905
+ // Known discriminator present but not in the frozen 0463 keep/meta table —
906
+ // still a determined type; treat as meta bookkeeping rather than unknown.
907
+ role = 'meta';
908
+ disposition = 'meta';
909
+ contentText = s(raw.content, raw.text) ?? null;
910
+ }
911
+ break;
912
+ }
812
913
  }
813
914
  const messageSplitIndex = entries.length;
814
915
  entries.push({
@@ -817,7 +918,7 @@ export function agySplit(raw, context) {
817
918
  session_id: sessionId,
818
919
  seq,
819
920
  role,
820
- record_type: recordType,
921
+ record_type: messageRecordType,
821
922
  disposition,
822
923
  ts,
823
924
  duration_ms: undefined,
@@ -828,7 +929,7 @@ export function agySplit(raw, context) {
828
929
  cache_write_tokens: null,
829
930
  cost_usd: null,
830
931
  content_text: contentText,
831
- cwd: null,
932
+ cwd: s(raw.workspace, raw.cwd) ?? null,
832
933
  provenance: 'ambient',
833
934
  },
834
935
  });
package/dist/sources.js CHANGED
@@ -104,7 +104,7 @@ export const SOURCE_DEFINITIONS = {
104
104
  omp: customSourceDefinition('omp', 'OMP', ['.omp/agent/sessions'], ['*.jsonl'], ompSplit, OMP_FIELD_MAP, OMP_SCHEMA),
105
105
  grok: customSourceDefinition('grok', 'Grok', ['.grok/sessions'], ['*.jsonl'], grokSplit, GROK_FIELD_MAP, GROK_SCHEMA),
106
106
  agy: {
107
- ...customSourceDefinition('agy', 'Antigravity CLI', ['.gemini/antigravity-cli/brain'], ['*.jsonl'], agySplit, AGY_FIELD_MAP, AGY_SCHEMA),
107
+ ...customSourceDefinition('agy', 'Antigravity CLI', ['.gemini/antigravity-cli'], ['*.jsonl'], agySplit, AGY_FIELD_MAP, AGY_SCHEMA),
108
108
  // agy's producer interleaves non-JSON fragments and torn tail writes into its
109
109
  // brain jsonl (task 0623: 789/89,818 lines, all unrecoverable). Skipping keeps
110
110
  // the 99%+ good records importable without degrading the whole source.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.48",
3
+ "version": "0.4.50",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.48",
51
- "@gobing-ai/ts-runtime": "^0.4.48",
52
- "@gobing-ai/ts-utils": "^0.4.48",
50
+ "@gobing-ai/ts-db": "^0.4.50",
51
+ "@gobing-ai/ts-runtime": "^0.4.50",
52
+ "@gobing-ai/ts-utils": "^0.4.50",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/mappers.ts CHANGED
@@ -101,35 +101,129 @@ function redactArgs(args: unknown): RedactedValue {
101
101
  }
102
102
 
103
103
  /**
104
- * Per-source todo-writing tool names whose raw arguments are retained for phase detection.
105
- *
106
- * Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
107
- * session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
108
- * 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
109
- * opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
110
- * rows with NULL args_raw).
104
+ * Retained tool-call arguments: recognized shell tools keep the extracted plain
105
+ * command (attribution compatibility, capped); every other typed tool call keeps
106
+ * sanitized-but-valid JSON. `args_digest` is computed elsewhere from the unpruned
107
+ * args and is unaffected; secret redaction stays at the persistence `redactRecord`
108
+ * seam (task 0064 R5).
111
109
  */
112
- const TODO_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
113
- claude: ['TodoWrite'],
114
- opencode: ['todowrite', 'todoread'],
115
- codex: ['update_plan'],
116
- omp: ['TodoWrite', 'todo', 'todo_write'],
117
- grok: ['todo_write'],
118
- pi: ['todo', 'manage_todo_list'],
119
- agy: [],
120
- gemini: [],
121
- };
110
+ const BASH_COMMAND_MAX_CHARS = 8192;
111
+ const GENERIC_ARGS_MAX_CHARS = 8192;
112
+
113
+ /** Extract the shell command from a bash tool's raw input; undefined when absent. */
114
+ function bashCommandOf(args: unknown): string | undefined {
115
+ if (args === null || typeof args !== 'object') return undefined;
116
+ const rec = args as Record<string, unknown>;
117
+ const command = rec.command ?? rec.CommandLine ?? rec.cmd ?? rec.script;
118
+ if (typeof command === 'string') return command;
119
+ if (Array.isArray(command)) return command.map(String).join(' ');
120
+ return undefined;
121
+ }
122
+
123
+ /** Bulky payload keys (normalized) pruned to an omission marker; camelCase and snake_case share one rule. */
124
+ const BULKY_PAYLOAD_KEYS: ReadonlySet<string> = new Set([
125
+ 'content',
126
+ 'codecontent',
127
+ 'replacementcontent',
128
+ 'targetcontent',
129
+ 'filedata',
130
+ 'buffer',
131
+ 'blob',
132
+ 'image',
133
+ 'imagebase64',
134
+ ]);
135
+
136
+ /** Lowercase a key and drop separators so code_content and codeContent compare equal. */
137
+ function normalizeKey(key: string): string {
138
+ return key.toLowerCase().replace(/[^a-z0-9]/g, '');
139
+ }
140
+
141
+ /** Tool names whose command-shaped args retain the existing plain-string representation. */
142
+ function isShellTool(toolName: string): boolean {
143
+ return /^(?:bash|shell|cmd|command|exec|execcommand|executecommand|runcommand|shellcommand|runshellcommand)$/.test(
144
+ normalizeKey(toolName),
145
+ );
146
+ }
147
+
148
+ /** Sanitized tool args: JSON shape preserved, bulky payload strings elided. */
149
+ type SanitizedValue = string | number | boolean | null | SanitizedValue[] | { [key: string]: SanitizedValue };
122
150
 
123
151
  /**
124
- * Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
125
- * otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
152
+ * Sanitize any JSON value recursively through objects AND arrays: bulky payload
153
+ * strings under known keys become `[omitted N chars]`, other strings longer than
154
+ * 1,000 characters keep a prefix plus an explicit original-length marker, and
155
+ * every primitive and short payload is preserved untouched (task 0064 R2).
126
156
  */
127
- export function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined {
128
- const allow = TODO_TOOL_ALLOWLIST[source];
129
- if (allow === undefined || !allow.includes(toolName)) return undefined;
130
- if (typeof args === 'string') return args;
157
+ function sanitizeValue(value: unknown): SanitizedValue {
158
+ if (typeof value === 'string') {
159
+ return value.length > 1000 ? `${value.slice(0, 1000)}[truncated ${value.length} chars]` : value;
160
+ }
161
+ if (Array.isArray(value)) return value.map(sanitizeValue);
162
+ if (value !== null && typeof value === 'object') {
163
+ const out: Record<string, SanitizedValue> = {};
164
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
165
+ out[key] =
166
+ typeof entry === 'string' && entry.length > 120 && BULKY_PAYLOAD_KEYS.has(normalizeKey(key))
167
+ ? `[omitted ${entry.length} chars]`
168
+ : sanitizeValue(entry);
169
+ }
170
+ return out;
171
+ }
172
+ // JSON leaves (number/boolean/null) pass through unchanged; JSON args never
173
+ // reach here as undefined/bigint/symbol/function.
174
+ return value as SanitizedValue;
175
+ }
176
+
177
+ /**
178
+ * Serialize sanitized args within the retention ceiling without ever slicing JSON
179
+ * text: values that still serialize past the ceiling collapse into a small valid
180
+ * JSON wrapper carrying `_truncated`, the omitted character count, and a bounded
181
+ * preview reduced until the wrapper itself fits (task 0064 R3/AC4).
182
+ */
183
+ function serializeBounded(value: unknown): string {
184
+ const serialized = JSON.stringify(sanitizeValue(value));
185
+ if (serialized.length <= GENERIC_ARGS_MAX_CHARS) return serialized;
186
+ let preview = serialized;
187
+ for (;;) {
188
+ const wrapper = JSON.stringify({
189
+ _truncated: true,
190
+ omittedChars: serialized.length - preview.length,
191
+ preview,
192
+ });
193
+ if (wrapper.length <= GENERIC_ARGS_MAX_CHARS) return wrapper;
194
+ preview = preview.slice(0, preview.length - (wrapper.length - GENERIC_ARGS_MAX_CHARS));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Return retained raw args for a typed tool call: the shell command string for
200
+ * recognized bash/shell/exec/command tools, sanitized valid JSON otherwise, and
201
+ * undefined when the event carries no invocation arguments.
202
+ */
203
+ export function maybeArgsRaw(_source: string, toolName: string, args: unknown): string | undefined {
131
204
  if (args === undefined || args === null) return undefined;
132
- return JSON.stringify(args);
205
+
206
+ let value = args;
207
+ if (typeof args === 'string') {
208
+ // Pre-stringified arguments (Codex) re-enter the structured path so the
209
+ // retained value parses and prunes like native JSON.
210
+ try {
211
+ value = JSON.parse(args);
212
+ } catch {
213
+ return args.length > BASH_COMMAND_MAX_CHARS ? args.slice(0, BASH_COMMAND_MAX_CHARS) : args;
214
+ }
215
+ }
216
+
217
+ if (value !== null && typeof value === 'object') {
218
+ const cmd = bashCommandOf(value);
219
+ if (cmd !== undefined && isShellTool(toolName)) {
220
+ return cmd.length > BASH_COMMAND_MAX_CHARS ? cmd.slice(0, BASH_COMMAND_MAX_CHARS) : cmd;
221
+ }
222
+ return serializeBounded(value);
223
+ }
224
+
225
+ // JSON primitives (number/boolean) retain as valid JSON scalars.
226
+ return JSON.stringify(value);
133
227
  }
134
228
 
135
229
  // ---------------------------------------------------------------------------
@@ -825,68 +919,84 @@ export function codexSplit(raw: Record<string, unknown>, context?: TransformCont
825
919
  export function agySplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[] {
826
920
  const entries: SplitEntry[] = [];
827
921
  const recordType = String(raw.type ?? '');
828
- const explicitSessionId = s(raw.session_id, raw.conversation_id);
922
+ const explicitSessionId = s(raw.session_id, raw.conversation_id, raw.conversationId);
829
923
  const pathSessionId = context?.sourceFile ? sessionIdFromSourcePath('agy', context.sourceFile) : undefined;
830
924
  const sessionId = explicitSessionId ?? pathSessionId ?? 'unknown';
831
- const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
925
+ const seq =
926
+ typeof raw.seq === 'number'
927
+ ? raw.seq
928
+ : typeof raw.step_index === 'number'
929
+ ? raw.step_index
930
+ : (context?.sourceLine ?? 0);
832
931
  const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
833
932
 
834
933
  let role: string;
835
934
  let disposition: string;
836
935
  let contentText: string | null = null;
936
+ let messageRecordType = recordType;
837
937
 
838
- // Classification from task 0463 field map (agy transcript types).
839
- switch (recordType) {
840
- case 'USER_INPUT':
841
- case 'ASK_QUESTION':
842
- role = 'user';
843
- disposition = 'keep';
844
- contentText = s(raw.content, raw.text, raw.message) ?? null;
845
- break;
846
- case 'PLANNER_RESPONSE':
847
- role = 'assistant';
848
- disposition = 'keep';
849
- contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
850
- break;
851
- case 'ERROR_MESSAGE':
852
- role = 'system';
853
- disposition = 'keep';
854
- contentText = s(raw.content, raw.text, raw.message) ?? null;
855
- break;
856
- case 'RUN_COMMAND':
857
- case 'VIEW_FILE':
858
- case 'GREP_SEARCH':
859
- case 'LIST_DIRECTORY':
860
- case 'READ_URL_CONTENT':
861
- role = 'tool';
862
- disposition = 'keep';
863
- contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
864
- break;
865
- case 'CODE_ACTION':
866
- case 'INVOKE_SUBAGENT':
867
- role = 'tool';
868
- disposition = 'keep';
869
- contentText = s(raw.content, raw.text) ?? null;
870
- break;
871
- case 'CONVERSATION_HISTORY':
872
- case 'CHECKPOINT':
873
- case 'GENERIC':
874
- role = 'meta';
875
- disposition = 'meta';
876
- contentText = s(raw.content, raw.text) ?? null;
877
- break;
878
- default:
879
- if (recordType.length === 0) {
880
- role = 'unknown';
881
- disposition = 'unknown';
882
- } else {
883
- // Known discriminator present but not in the frozen 0463 keep/meta table —
884
- // still a determined type; treat as meta bookkeeping rather than unknown.
938
+ // history.jsonl prompt index: {display, timestamp, workspace, conversationId?, type?}.
939
+ // One branch covers absent type, 'slash_command', 'shell', and any future producer type.
940
+ // `display` never appears on legacy brain transcripts, so this cannot collide with 0463.
941
+ if (raw.display !== undefined) {
942
+ role = 'user';
943
+ disposition = 'keep';
944
+ contentText = s(raw.display) ?? null;
945
+ messageRecordType = recordType.length > 0 ? recordType : 'USER_INPUT';
946
+ } else {
947
+ // Classification from task 0463 field map (agy transcript types).
948
+ switch (recordType) {
949
+ case 'USER_INPUT':
950
+ case 'ASK_QUESTION':
951
+ role = 'user';
952
+ disposition = 'keep';
953
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
954
+ break;
955
+ case 'PLANNER_RESPONSE':
956
+ role = 'assistant';
957
+ disposition = 'keep';
958
+ contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
959
+ break;
960
+ case 'ERROR_MESSAGE':
961
+ role = 'system';
962
+ disposition = 'keep';
963
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
964
+ break;
965
+ case 'RUN_COMMAND':
966
+ case 'VIEW_FILE':
967
+ case 'GREP_SEARCH':
968
+ case 'LIST_DIRECTORY':
969
+ case 'READ_URL_CONTENT':
970
+ role = 'tool';
971
+ disposition = 'keep';
972
+ contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
973
+ break;
974
+ case 'CODE_ACTION':
975
+ case 'INVOKE_SUBAGENT':
976
+ role = 'tool';
977
+ disposition = 'keep';
978
+ contentText = s(raw.content, raw.text) ?? null;
979
+ break;
980
+ case 'CONVERSATION_HISTORY':
981
+ case 'CHECKPOINT':
982
+ case 'GENERIC':
885
983
  role = 'meta';
886
984
  disposition = 'meta';
887
985
  contentText = s(raw.content, raw.text) ?? null;
888
- }
889
- break;
986
+ break;
987
+ default:
988
+ if (recordType.length === 0) {
989
+ role = 'unknown';
990
+ disposition = 'unknown';
991
+ } else {
992
+ // Known discriminator present but not in the frozen 0463 keep/meta table —
993
+ // still a determined type; treat as meta bookkeeping rather than unknown.
994
+ role = 'meta';
995
+ disposition = 'meta';
996
+ contentText = s(raw.content, raw.text) ?? null;
997
+ }
998
+ break;
999
+ }
890
1000
  }
891
1001
 
892
1002
  const messageSplitIndex = entries.length;
@@ -897,7 +1007,7 @@ export function agySplit(raw: Record<string, unknown>, context?: TransformContex
897
1007
  session_id: sessionId,
898
1008
  seq,
899
1009
  role,
900
- record_type: recordType,
1010
+ record_type: messageRecordType,
901
1011
  disposition,
902
1012
  ts,
903
1013
  duration_ms: undefined,
@@ -908,7 +1018,7 @@ export function agySplit(raw: Record<string, unknown>, context?: TransformContex
908
1018
  cache_write_tokens: null,
909
1019
  cost_usd: null,
910
1020
  content_text: contentText,
911
- cwd: null,
1021
+ cwd: s(raw.workspace, raw.cwd) ?? null,
912
1022
  provenance: 'ambient',
913
1023
  },
914
1024
  });
package/src/sources.ts CHANGED
@@ -194,7 +194,7 @@ export const SOURCE_DEFINITIONS: Readonly<Record<LlmJsonlSource, SourceDefinitio
194
194
  ...customSourceDefinition(
195
195
  'agy',
196
196
  'Antigravity CLI',
197
- ['.gemini/antigravity-cli/brain'],
197
+ ['.gemini/antigravity-cli'],
198
198
  ['*.jsonl'],
199
199
  agySplit,
200
200
  AGY_FIELD_MAP,