@gobing-ai/ts-llm-jsonl-importer 0.4.47 → 0.4.49

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,8 +3,10 @@ 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 tools whose arguments carry forensic value, otherwise
7
+ * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
8
+ * retain just the command string (capped). Codex `arguments` arrives as a JSON string
9
+ * already — store as-is for todo tools.
8
10
  */
9
11
  export declare function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined;
10
12
  /** Map a Claude Code 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
@@ -110,18 +110,60 @@ const TODO_TOOL_ALLOWLIST = {
110
110
  gemini: [],
111
111
  };
112
112
  /**
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.
113
+ * Per-source shell/exec tool names whose command string is retained in args_raw.
114
+ *
115
+ * WHY: bash commands are the primary forensic signal for session-task attribution and ops
116
+ * history (spur's task classifier regexes `spur task <verb> <wbs>` out of args_raw). Only
117
+ * evidenced sources are listed; unverified shapes stay empty the same way the todo allowlist
118
+ * treats agy/gemini. Commands pass through the importer's redaction layer like any other
119
+ * retained field (importer.ts redacts every split record before insert).
115
120
  */
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;
122
- if (args === undefined || args === null)
121
+ const BASH_TOOL_ALLOWLIST = {
122
+ claude: ['Bash'], // input: { command: string } (mappers.test.ts fixture)
123
+ opencode: ['bash'], // input: { command: string } (opencode-importer.test.ts)
124
+ codex: [], // arguments arrive as a pre-stringified JSON string; command shape unverified
125
+ omp: [],
126
+ grok: [],
127
+ pi: ['bash'], // arguments: { command: string } (live pi session transcripts)
128
+ agy: [],
129
+ gemini: [],
130
+ };
131
+ /** Retained bash commands are capped; attribution only needs the invocation line. */
132
+ const BASH_COMMAND_MAX_CHARS = 8192;
133
+ /** Extract the shell command from a bash tool's raw input; undefined when absent. */
134
+ function bashCommandOf(args) {
135
+ if (args === null || typeof args !== 'object')
123
136
  return undefined;
124
- return JSON.stringify(args);
137
+ const command = args.command;
138
+ if (typeof command === 'string')
139
+ return command;
140
+ if (Array.isArray(command))
141
+ return command.map(String).join(' ');
142
+ return undefined;
143
+ }
144
+ /**
145
+ * Return retained raw args for tools whose arguments carry forensic value, otherwise
146
+ * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
147
+ * retain just the command string (capped). Codex `arguments` arrives as a JSON string
148
+ * already — store as-is for todo tools.
149
+ */
150
+ export function maybeArgsRaw(source, toolName, args) {
151
+ const todo = TODO_TOOL_ALLOWLIST[source];
152
+ if (todo?.includes(toolName)) {
153
+ if (typeof args === 'string')
154
+ return args;
155
+ if (args === undefined || args === null)
156
+ return undefined;
157
+ return JSON.stringify(args);
158
+ }
159
+ const bash = BASH_TOOL_ALLOWLIST[source];
160
+ if (bash?.includes(toolName)) {
161
+ const command = bashCommandOf(args);
162
+ if (command === undefined)
163
+ return undefined;
164
+ return command.length > BASH_COMMAND_MAX_CHARS ? command.slice(0, BASH_COMMAND_MAX_CHARS) : command;
165
+ }
166
+ return undefined;
125
167
  }
126
168
  // ---------------------------------------------------------------------------
127
169
  // Claude mapper
@@ -748,67 +790,83 @@ export function codexSplit(raw, context) {
748
790
  export function agySplit(raw, context) {
749
791
  const entries = [];
750
792
  const recordType = String(raw.type ?? '');
751
- const explicitSessionId = s(raw.session_id, raw.conversation_id);
793
+ const explicitSessionId = s(raw.session_id, raw.conversation_id, raw.conversationId);
752
794
  const pathSessionId = context?.sourceFile ? sessionIdFromSourcePath('agy', context.sourceFile) : undefined;
753
795
  const sessionId = explicitSessionId ?? pathSessionId ?? 'unknown';
754
- const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
796
+ const seq = typeof raw.seq === 'number'
797
+ ? raw.seq
798
+ : typeof raw.step_index === 'number'
799
+ ? raw.step_index
800
+ : (context?.sourceLine ?? 0);
755
801
  const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
756
802
  let role;
757
803
  let disposition;
758
804
  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.
805
+ let messageRecordType = recordType;
806
+ // history.jsonl prompt index: {display, timestamp, workspace, conversationId?, type?}.
807
+ // One branch covers absent type, 'slash_command', 'shell', and any future producer type.
808
+ // `display` never appears on legacy brain transcripts, so this cannot collide with 0463.
809
+ if (raw.display !== undefined) {
810
+ role = 'user';
811
+ disposition = 'keep';
812
+ contentText = s(raw.display) ?? null;
813
+ messageRecordType = recordType.length > 0 ? recordType : 'USER_INPUT';
814
+ }
815
+ else {
816
+ // Classification from task 0463 field map (agy transcript types).
817
+ switch (recordType) {
818
+ case 'USER_INPUT':
819
+ case 'ASK_QUESTION':
820
+ role = 'user';
821
+ disposition = 'keep';
822
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
823
+ break;
824
+ case 'PLANNER_RESPONSE':
825
+ role = 'assistant';
826
+ disposition = 'keep';
827
+ contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
828
+ break;
829
+ case 'ERROR_MESSAGE':
830
+ role = 'system';
831
+ disposition = 'keep';
832
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
833
+ break;
834
+ case 'RUN_COMMAND':
835
+ case 'VIEW_FILE':
836
+ case 'GREP_SEARCH':
837
+ case 'LIST_DIRECTORY':
838
+ case 'READ_URL_CONTENT':
839
+ role = 'tool';
840
+ disposition = 'keep';
841
+ contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
842
+ break;
843
+ case 'CODE_ACTION':
844
+ case 'INVOKE_SUBAGENT':
845
+ role = 'tool';
846
+ disposition = 'keep';
847
+ contentText = s(raw.content, raw.text) ?? null;
848
+ break;
849
+ case 'CONVERSATION_HISTORY':
850
+ case 'CHECKPOINT':
851
+ case 'GENERIC':
807
852
  role = 'meta';
808
853
  disposition = 'meta';
809
854
  contentText = s(raw.content, raw.text) ?? null;
810
- }
811
- break;
855
+ break;
856
+ default:
857
+ if (recordType.length === 0) {
858
+ role = 'unknown';
859
+ disposition = 'unknown';
860
+ }
861
+ else {
862
+ // Known discriminator present but not in the frozen 0463 keep/meta table —
863
+ // still a determined type; treat as meta bookkeeping rather than unknown.
864
+ role = 'meta';
865
+ disposition = 'meta';
866
+ contentText = s(raw.content, raw.text) ?? null;
867
+ }
868
+ break;
869
+ }
812
870
  }
813
871
  const messageSplitIndex = entries.length;
814
872
  entries.push({
@@ -817,7 +875,7 @@ export function agySplit(raw, context) {
817
875
  session_id: sessionId,
818
876
  seq,
819
877
  role,
820
- record_type: recordType,
878
+ record_type: messageRecordType,
821
879
  disposition,
822
880
  ts,
823
881
  duration_ms: undefined,
@@ -828,7 +886,7 @@ export function agySplit(raw, context) {
828
886
  cache_write_tokens: null,
829
887
  cost_usd: null,
830
888
  content_text: contentText,
831
- cwd: null,
889
+ cwd: s(raw.workspace, raw.cwd) ?? null,
832
890
  provenance: 'ambient',
833
891
  },
834
892
  });
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.47",
3
+ "version": "0.4.49",
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.47",
51
- "@gobing-ai/ts-runtime": "^0.4.47",
52
- "@gobing-ai/ts-utils": "^0.4.47",
50
+ "@gobing-ai/ts-db": "^0.4.49",
51
+ "@gobing-ai/ts-runtime": "^0.4.49",
52
+ "@gobing-ai/ts-utils": "^0.4.49",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/mappers.ts CHANGED
@@ -121,15 +121,57 @@ const TODO_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
121
121
  };
122
122
 
123
123
  /**
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.
124
+ * Per-source shell/exec tool names whose command string is retained in args_raw.
125
+ *
126
+ * WHY: bash commands are the primary forensic signal for session-task attribution and ops
127
+ * history (spur's task classifier regexes `spur task <verb> <wbs>` out of args_raw). Only
128
+ * evidenced sources are listed; unverified shapes stay empty the same way the todo allowlist
129
+ * treats agy/gemini. Commands pass through the importer's redaction layer like any other
130
+ * retained field (importer.ts redacts every split record before insert).
131
+ */
132
+ const BASH_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
133
+ claude: ['Bash'], // input: { command: string } (mappers.test.ts fixture)
134
+ opencode: ['bash'], // input: { command: string } (opencode-importer.test.ts)
135
+ codex: [], // arguments arrive as a pre-stringified JSON string; command shape unverified
136
+ omp: [],
137
+ grok: [],
138
+ pi: ['bash'], // arguments: { command: string } (live pi session transcripts)
139
+ agy: [],
140
+ gemini: [],
141
+ };
142
+
143
+ /** Retained bash commands are capped; attribution only needs the invocation line. */
144
+ const BASH_COMMAND_MAX_CHARS = 8192;
145
+
146
+ /** Extract the shell command from a bash tool's raw input; undefined when absent. */
147
+ function bashCommandOf(args: unknown): string | undefined {
148
+ if (args === null || typeof args !== 'object') return undefined;
149
+ const command = (args as Record<string, unknown>).command;
150
+ if (typeof command === 'string') return command;
151
+ if (Array.isArray(command)) return command.map(String).join(' ');
152
+ return undefined;
153
+ }
154
+
155
+ /**
156
+ * Return retained raw args for tools whose arguments carry forensic value, otherwise
157
+ * undefined. Todo-writing tools retain the full JSON args (phase detection); bash tools
158
+ * retain just the command string (capped). Codex `arguments` arrives as a JSON string
159
+ * already — store as-is for todo tools.
126
160
  */
127
161
  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;
131
- if (args === undefined || args === null) return undefined;
132
- return JSON.stringify(args);
162
+ const todo = TODO_TOOL_ALLOWLIST[source];
163
+ if (todo?.includes(toolName)) {
164
+ if (typeof args === 'string') return args;
165
+ if (args === undefined || args === null) return undefined;
166
+ return JSON.stringify(args);
167
+ }
168
+ const bash = BASH_TOOL_ALLOWLIST[source];
169
+ if (bash?.includes(toolName)) {
170
+ const command = bashCommandOf(args);
171
+ if (command === undefined) return undefined;
172
+ return command.length > BASH_COMMAND_MAX_CHARS ? command.slice(0, BASH_COMMAND_MAX_CHARS) : command;
173
+ }
174
+ return undefined;
133
175
  }
134
176
 
135
177
  // ---------------------------------------------------------------------------
@@ -825,68 +867,84 @@ export function codexSplit(raw: Record<string, unknown>, context?: TransformCont
825
867
  export function agySplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[] {
826
868
  const entries: SplitEntry[] = [];
827
869
  const recordType = String(raw.type ?? '');
828
- const explicitSessionId = s(raw.session_id, raw.conversation_id);
870
+ const explicitSessionId = s(raw.session_id, raw.conversation_id, raw.conversationId);
829
871
  const pathSessionId = context?.sourceFile ? sessionIdFromSourcePath('agy', context.sourceFile) : undefined;
830
872
  const sessionId = explicitSessionId ?? pathSessionId ?? 'unknown';
831
- const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
873
+ const seq =
874
+ typeof raw.seq === 'number'
875
+ ? raw.seq
876
+ : typeof raw.step_index === 'number'
877
+ ? raw.step_index
878
+ : (context?.sourceLine ?? 0);
832
879
  const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
833
880
 
834
881
  let role: string;
835
882
  let disposition: string;
836
883
  let contentText: string | null = null;
884
+ let messageRecordType = recordType;
837
885
 
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.
886
+ // history.jsonl prompt index: {display, timestamp, workspace, conversationId?, type?}.
887
+ // One branch covers absent type, 'slash_command', 'shell', and any future producer type.
888
+ // `display` never appears on legacy brain transcripts, so this cannot collide with 0463.
889
+ if (raw.display !== undefined) {
890
+ role = 'user';
891
+ disposition = 'keep';
892
+ contentText = s(raw.display) ?? null;
893
+ messageRecordType = recordType.length > 0 ? recordType : 'USER_INPUT';
894
+ } else {
895
+ // Classification from task 0463 field map (agy transcript types).
896
+ switch (recordType) {
897
+ case 'USER_INPUT':
898
+ case 'ASK_QUESTION':
899
+ role = 'user';
900
+ disposition = 'keep';
901
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
902
+ break;
903
+ case 'PLANNER_RESPONSE':
904
+ role = 'assistant';
905
+ disposition = 'keep';
906
+ contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
907
+ break;
908
+ case 'ERROR_MESSAGE':
909
+ role = 'system';
910
+ disposition = 'keep';
911
+ contentText = s(raw.content, raw.text, raw.message) ?? null;
912
+ break;
913
+ case 'RUN_COMMAND':
914
+ case 'VIEW_FILE':
915
+ case 'GREP_SEARCH':
916
+ case 'LIST_DIRECTORY':
917
+ case 'READ_URL_CONTENT':
918
+ role = 'tool';
919
+ disposition = 'keep';
920
+ contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
921
+ break;
922
+ case 'CODE_ACTION':
923
+ case 'INVOKE_SUBAGENT':
924
+ role = 'tool';
925
+ disposition = 'keep';
926
+ contentText = s(raw.content, raw.text) ?? null;
927
+ break;
928
+ case 'CONVERSATION_HISTORY':
929
+ case 'CHECKPOINT':
930
+ case 'GENERIC':
885
931
  role = 'meta';
886
932
  disposition = 'meta';
887
933
  contentText = s(raw.content, raw.text) ?? null;
888
- }
889
- break;
934
+ break;
935
+ default:
936
+ if (recordType.length === 0) {
937
+ role = 'unknown';
938
+ disposition = 'unknown';
939
+ } else {
940
+ // Known discriminator present but not in the frozen 0463 keep/meta table —
941
+ // still a determined type; treat as meta bookkeeping rather than unknown.
942
+ role = 'meta';
943
+ disposition = 'meta';
944
+ contentText = s(raw.content, raw.text) ?? null;
945
+ }
946
+ break;
947
+ }
890
948
  }
891
949
 
892
950
  const messageSplitIndex = entries.length;
@@ -897,7 +955,7 @@ export function agySplit(raw: Record<string, unknown>, context?: TransformContex
897
955
  session_id: sessionId,
898
956
  seq,
899
957
  role,
900
- record_type: recordType,
958
+ record_type: messageRecordType,
901
959
  disposition,
902
960
  ts,
903
961
  duration_ms: undefined,
@@ -908,7 +966,7 @@ export function agySplit(raw: Record<string, unknown>, context?: TransformContex
908
966
  cache_write_tokens: null,
909
967
  cost_usd: null,
910
968
  content_text: contentText,
911
- cwd: null,
969
+ cwd: s(raw.workspace, raw.cwd) ?? null,
912
970
  provenance: 'ambient',
913
971
  },
914
972
  });
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,