@inixiative/archive 0.12.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/archive",
3
- "version": "0.12.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.66",
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/client.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  destinationToken,
7
7
  destinationUrl,
8
8
  } from './config';
9
+ import type { RoutingPreview, SyncResult } from './remote';
9
10
  import type { ArchiveStore } from './store';
10
11
 
11
12
  export { type ArchiveDestination, archiveDestinationSchema } from './config';
@@ -106,7 +107,10 @@ export async function publishArchive(
106
107
  throw new Error('Archive changed repeatedly during publication; retry sync');
107
108
  }
108
109
 
109
- export async function routingPreview(store: ArchiveStore, destinations: ArchiveDestination[]) {
110
+ export async function routingPreview(
111
+ store: ArchiveStore,
112
+ destinations: ArchiveDestination[],
113
+ ): Promise<RoutingPreview> {
110
114
  return (await store.list()).map((archive) => ({
111
115
  id: archive.id,
112
116
  projectId: archive.projectId,
@@ -122,8 +126,11 @@ export async function routingPreview(store: ArchiveStore, destinations: ArchiveD
122
126
  }));
123
127
  }
124
128
 
125
- export async function syncArchives(store: ArchiveStore, destinations: ArchiveDestination[]) {
126
- const results: { id: string; destination: string; status: string }[] = [];
129
+ export async function syncArchives(
130
+ store: ArchiveStore,
131
+ destinations: ArchiveDestination[],
132
+ ): Promise<SyncResult[]> {
133
+ const results: SyncResult[] = [];
127
134
  for (const archive of await store.list())
128
135
  for (const destination of destinations.filter((d) => d.projectId === archive.projectId)) {
129
136
  try {
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 (!prompt || injectedOpening.test(prompt) || /^\[Request interrupted by user/.test(prompt))
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
- // Claude Code marks expanded commands and caveats as meta: a title only when nothing was typed.
122
- if (kind === 'user' && !prompt && !row.isCompactSummary) {
123
- if (!row.isMeta) prompt = typedPrompt(content);
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
- if (row.type === 'response_item') {
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',
package/src/remote.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { defaultHome } from './agents';
4
- import type { routingPreview, syncArchives } from './client';
5
- import type { DescribedDestination } from './config';
4
+ import type { ArchiveDestination, DescribedDestination } from './config';
6
5
  import type { ArchiveChunk, ArchiveSnapshot } from './index';
7
6
  import { archiveKey, archiveSnapshotSchema } from './index';
8
7
  import type { ArchiveFilter, ArchiveSettings, TagDefinition } from './schemas';
@@ -23,8 +22,16 @@ export type DestinationStatus = DescribedDestination & { delivered: number; pend
23
22
  /** A hosted Archive library a held Signet may write to. */
24
23
  export type ArchiveLibrary = { integrationId: string; resourceId: string; name: string };
25
24
  export type ArchiveLibraries = { paired: boolean; libraries: ArchiveLibrary[] };
26
- export type RoutingPreview = Awaited<ReturnType<typeof routingPreview>>;
27
- export type SyncResult = Awaited<ReturnType<typeof syncArchives>>[number];
25
+ export type RoutedDestination = Pick<ArchiveDestination, 'kind' | 'url'> & {
26
+ integrationId?: string;
27
+ };
28
+ export type RoutingPreview = (Pick<
29
+ ArchiveListing,
30
+ 'id' | 'projectId' | 'tags' | 'suggestedTags'
31
+ > & {
32
+ destinations: RoutedDestination[];
33
+ })[];
34
+ export type SyncResult = { id: string; destination: string; status: string };
28
35
  /** A project routed to a hosted Archive library through Kingdom. */
29
36
  export type KingdomRoute = { projectId: string; integrationId: string; resourceId: string };
30
37