@inixiative/archive 0.12.1 → 0.13.0
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/package.json +2 -2
- package/src/import.ts +76 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/archive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@biomejs/biome": "2.5.2",
|
|
46
|
-
"@inixiative/config": "^0.5.
|
|
46
|
+
"@inixiative/config": "^0.5.70",
|
|
47
47
|
"@types/bun": "1.4.2",
|
|
48
48
|
"lefthook": "2.1.9",
|
|
49
49
|
"typescript": "6.0.3"
|
package/src/import.ts
CHANGED
|
@@ -27,7 +27,35 @@ const injectedTags = [
|
|
|
27
27
|
'user_instructions',
|
|
28
28
|
'permissions',
|
|
29
29
|
'INSTRUCTIONS',
|
|
30
|
+
'codex_internal_context',
|
|
31
|
+
'heartbeat',
|
|
32
|
+
'in-app-browser-context',
|
|
33
|
+
'external_codex_apps_open_page',
|
|
34
|
+
'send_user_message_question_reply',
|
|
35
|
+
'turn_aborted',
|
|
30
36
|
].join('|');
|
|
37
|
+
/** Prompts Codex writes in the user's role. */
|
|
38
|
+
const injectedPreamble = /^(?:# In app browser:|The following is the Codex agent history)/;
|
|
39
|
+
/**
|
|
40
|
+
* Nothing but blocks of harness-style tags such as `<codex_internal_context>`: `snake_case` or
|
|
41
|
+
* `kebab-case` names, unlike markup a person pastes. A linear scan, since a backtracking pattern is
|
|
42
|
+
* exponential on many sibling blocks.
|
|
43
|
+
*/
|
|
44
|
+
function onlyTagBlocks(text: string) {
|
|
45
|
+
const open = /\s*<([a-z][a-z0-9]*(?:[_-][a-z0-9]+)+)(?:\s[^>]*)?>/y;
|
|
46
|
+
let at = 0;
|
|
47
|
+
let blocks = 0;
|
|
48
|
+
while (at < text.length) {
|
|
49
|
+
open.lastIndex = at;
|
|
50
|
+
const match = open.exec(text);
|
|
51
|
+
if (!match) return blocks > 0 && !text.slice(at).trim();
|
|
52
|
+
const close = text.indexOf(`</${match[1]}>`, open.lastIndex);
|
|
53
|
+
if (close < 0) return false;
|
|
54
|
+
at = close + match[1]!.length + 3;
|
|
55
|
+
blocks++;
|
|
56
|
+
}
|
|
57
|
+
return blocks > 0;
|
|
58
|
+
}
|
|
31
59
|
const injectedBlock = new RegExp(`<(${injectedTags})(?:\\s[^>]*)?>[\\s\\S]*?</\\1>`, 'g');
|
|
32
60
|
const injectedOpening = new RegExp(`^<(?:${injectedTags})(?:>|\\s)`);
|
|
33
61
|
/** Foundry's native-session envelope; the person's text is the first User Message section. */
|
|
@@ -35,7 +63,7 @@ const foundryUserMessage =
|
|
|
35
63
|
/^# User Message[ \t]*\n([\s\S]*?)(?=\n# (?:User Message|Assistant Message|Foundry Injection)[ \t]*\n|(?![\s\S]))/m;
|
|
36
64
|
|
|
37
65
|
/** What the person typed in a user message, without injected content; undefined when nothing is left. */
|
|
38
|
-
function typedPrompt(text: string) {
|
|
66
|
+
export function typedPrompt(text: string) {
|
|
39
67
|
let prompt = text
|
|
40
68
|
.replace(injectedBlock, '')
|
|
41
69
|
.replace(/<\/?pasted_content(?:\s[^>]*)?>/g, '')
|
|
@@ -43,7 +71,13 @@ function typedPrompt(text: string) {
|
|
|
43
71
|
.trim();
|
|
44
72
|
if (/^# (?:System Context|User Message)[ \t]*\n/.test(prompt))
|
|
45
73
|
prompt = prompt.match(foundryUserMessage)?.[1].trim() ?? '';
|
|
46
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
!prompt ||
|
|
76
|
+
injectedOpening.test(prompt) ||
|
|
77
|
+
injectedPreamble.test(prompt) ||
|
|
78
|
+
onlyTagBlocks(prompt) ||
|
|
79
|
+
/^\[Request interrupted by user/.test(prompt)
|
|
80
|
+
)
|
|
47
81
|
return undefined;
|
|
48
82
|
return prompt;
|
|
49
83
|
}
|
|
@@ -66,6 +100,8 @@ export function importTranscriptLines(
|
|
|
66
100
|
let contentBytes = 0;
|
|
67
101
|
// Codex states the model per turn; Claude Code per assistant record.
|
|
68
102
|
let turn: { model?: string; effort?: string } = {};
|
|
103
|
+
// A Codex session started by a script (codex exec, a custom originator) has no typed prompts.
|
|
104
|
+
let scripted = false;
|
|
69
105
|
for (const rawLine of lines) {
|
|
70
106
|
index++;
|
|
71
107
|
const line = rawLine.trim();
|
|
@@ -83,6 +119,12 @@ export function importTranscriptLines(
|
|
|
83
119
|
if (sessionId && id && id !== sessionId)
|
|
84
120
|
throw new Error('Transcript session identity mismatch');
|
|
85
121
|
sessionId ??= id;
|
|
122
|
+
// Subagent and guardian threads, and `codex exec`, are driven by an agent or a script.
|
|
123
|
+
const { originator, source } = row.payload ?? {};
|
|
124
|
+
if (typeof originator === 'string')
|
|
125
|
+
scripted = !/^codex/i.test(originator) || /^codex_(?:exec|sdk|mcp)/.test(originator);
|
|
126
|
+
if (source === 'exec' || (source && typeof source === 'object' && 'subagent' in source))
|
|
127
|
+
scripted = true;
|
|
86
128
|
}
|
|
87
129
|
if (options.source === 'claude-code' && row.sessionId) {
|
|
88
130
|
// Subagent transcripts reuse their parent's sessionId; each is its own archive.
|
|
@@ -102,6 +144,26 @@ export function importTranscriptLines(
|
|
|
102
144
|
: row.type === 'assistant'
|
|
103
145
|
? { model: text(row.message?.model), effort: text(row.effort) }
|
|
104
146
|
: {};
|
|
147
|
+
// A user record the person did not type: an expansion, a compaction summary, a subagent's prompt,
|
|
148
|
+
// a message from the harness, another agent or an SDK caller, or a scripted Codex session's prompt.
|
|
149
|
+
// It is kept as an event, not a user turn.
|
|
150
|
+
const written =
|
|
151
|
+
options.source === 'claude-code'
|
|
152
|
+
? row.type === 'user' &&
|
|
153
|
+
Boolean(
|
|
154
|
+
row.isMeta ||
|
|
155
|
+
row.isCompactSummary ||
|
|
156
|
+
row.isSidechain ||
|
|
157
|
+
(row.origin && typeof row.origin === 'object'
|
|
158
|
+
? row.origin.kind !== 'human'
|
|
159
|
+
: typeof row.entrypoint === 'string' && row.entrypoint.startsWith('sdk')),
|
|
160
|
+
)
|
|
161
|
+
: scripted && row.type === 'response_item' && row.payload?.role === 'user';
|
|
162
|
+
// User text is a user entry only when the person typed it; markers and injected blocks are events.
|
|
163
|
+
const textKind = (role: 'user' | 'assistant', content: unknown) =>
|
|
164
|
+
role === 'user' && (written || typeof content !== 'string' || !typedPrompt(content))
|
|
165
|
+
? 'event'
|
|
166
|
+
: role;
|
|
105
167
|
const timestamp = typeof row.timestamp === 'number' ? row.timestamp : Date.parse(row.timestamp);
|
|
106
168
|
const add = (kind: ArchiveEntry['kind'], content: unknown, suffix: string, callId?: string) => {
|
|
107
169
|
if (typeof content !== 'string' || !content.length) return;
|
|
@@ -118,10 +180,10 @@ export function importTranscriptLines(
|
|
|
118
180
|
'Supported transcript content exceeds the archive size limit; no archive imported',
|
|
119
181
|
);
|
|
120
182
|
seen.set(id, identity);
|
|
121
|
-
//
|
|
122
|
-
if (
|
|
123
|
-
if (
|
|
124
|
-
else metaPrompt ??= typedPrompt(content);
|
|
183
|
+
// A prompt someone else wrote (an expansion, a subagent's task) titles the session only when nothing was typed.
|
|
184
|
+
if (!prompt && !row.isCompactSummary) {
|
|
185
|
+
if (kind === 'user') prompt = typedPrompt(content);
|
|
186
|
+
else if (kind === 'event' && written) metaPrompt ??= typedPrompt(content);
|
|
125
187
|
}
|
|
126
188
|
entries.push({
|
|
127
189
|
id,
|
|
@@ -139,12 +201,16 @@ export function importTranscriptLines(
|
|
|
139
201
|
});
|
|
140
202
|
};
|
|
141
203
|
if (options.source === 'codex') {
|
|
142
|
-
|
|
204
|
+
// Codex records the person stopping a turn as its own event; Claude Code writes this marker.
|
|
205
|
+
if (row.type === 'event_msg' && row.payload?.type === 'turn_aborted') {
|
|
206
|
+
if (row.payload.reason === 'interrupted')
|
|
207
|
+
add('event', '[Request interrupted by user]', 'aborted');
|
|
208
|
+
} else if (row.type === 'response_item') {
|
|
143
209
|
const p = row.payload ?? {};
|
|
144
210
|
if (p.type === 'message' && ['user', 'assistant'].includes(p.role)) {
|
|
145
211
|
for (const [i, block] of (p.content ?? []).entries()) {
|
|
146
212
|
if (['input_text', 'output_text', 'text'].includes(block.type))
|
|
147
|
-
add(p.role, block.text, `text-${i}`);
|
|
213
|
+
add(textKind(p.role, block.text), block.text, `text-${i}`);
|
|
148
214
|
else omissions.add('Non-text message content is not included');
|
|
149
215
|
}
|
|
150
216
|
} else if (['function_call', 'custom_tool_call'].includes(p.type)) {
|
|
@@ -181,10 +247,10 @@ export function importTranscriptLines(
|
|
|
181
247
|
}
|
|
182
248
|
} else if (['user', 'assistant'].includes(row.type) && row.message) {
|
|
183
249
|
const blocks = row.message.content;
|
|
184
|
-
if (typeof blocks === 'string') add(row.type, blocks, 'text');
|
|
250
|
+
if (typeof blocks === 'string') add(textKind(row.type, blocks), blocks, 'text');
|
|
185
251
|
else if (Array.isArray(blocks))
|
|
186
252
|
for (const [i, block] of blocks.entries()) {
|
|
187
|
-
if (block.type === 'text') add(row.type, block.text, `text-${i}`);
|
|
253
|
+
if (block.type === 'text') add(textKind(row.type, block.text), block.text, `text-${i}`);
|
|
188
254
|
else if (block.type === 'tool_use')
|
|
189
255
|
add(
|
|
190
256
|
'tool-call',
|