@aiwg/cli 2026.7.21 → 2026.7.24

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.
Files changed (32) hide show
  1. package/README.md +14 -3
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +2 -0
  5. package/dist/src/artifacts/index-builder.js +44 -8
  6. package/dist/src/artifacts/query-engine.js +1 -1
  7. package/dist/src/artifacts/types.js +1 -0
  8. package/dist/src/cli/handlers/index.js +5 -1
  9. package/dist/src/cli/handlers/sessions.js +339 -40
  10. package/dist/src/cli/handlers/setup-manifest.js +800 -0
  11. package/dist/src/cli/handlers/use.js +127 -17
  12. package/dist/src/config/aiwg-config.js +18 -2
  13. package/dist/src/config/cli.js +16 -3
  14. package/dist/src/extensions/commands/definitions.js +99 -0
  15. package/dist/src/security/threat-assessment-config.js +296 -0
  16. package/dist/src/serve/sandbox-registry.js +34 -0
  17. package/dist/src/sessions/adapters/claude.js +37 -9
  18. package/dist/src/sessions/adapters/codex.js +38 -11
  19. package/dist/src/sessions/adapters/cursor.js +166 -10
  20. package/dist/src/sessions/adapters/factory.js +50 -9
  21. package/dist/src/sessions/batch-contracts.js +121 -0
  22. package/dist/src/sessions/batch-import.js +265 -0
  23. package/dist/src/sessions/contracts.js +32 -5
  24. package/dist/src/sessions/import-lease.js +152 -0
  25. package/dist/src/sessions/importer.js +163 -14
  26. package/dist/src/sessions/index.js +6 -0
  27. package/dist/src/sessions/origin.js +117 -0
  28. package/dist/src/sessions/readers.js +1 -1
  29. package/dist/src/sessions/repository.js +354 -13
  30. package/dist/src/sessions/timeline.js +148 -0
  31. package/dist/src/sessions/workspace-discovery.js +319 -0
  32. package/package.json +1 -1
@@ -0,0 +1,296 @@
1
+ export const THREAT_ASSESSMENT_MODES = ['off', 'audit', 'enforce'];
2
+ export const THREAT_ASSESSMENT_SEVERITIES = [
3
+ 'informational',
4
+ 'low',
5
+ 'moderate',
6
+ 'high',
7
+ 'critical',
8
+ ];
9
+ export const THREAT_ASSESSMENT_SURFACES = [
10
+ 'issue-title',
11
+ 'issue-body',
12
+ 'issue-comment',
13
+ 'pull-request-title',
14
+ 'pull-request-body',
15
+ 'pull-request-diff-summary',
16
+ 'review-comment',
17
+ 'release-note',
18
+ 'handoff',
19
+ 'outbound-maintainer-comment',
20
+ ];
21
+ const BUILTIN_PROFILES = new Set(['trusted', 'audit', 'balanced', 'strict', 'high-assurance']);
22
+ const BUILTIN_RULE_PACKS = new Set([
23
+ 'aiwg:all',
24
+ 'aiwg:prompt-injection',
25
+ 'aiwg:supply-chain',
26
+ 'aiwg:credential-protection',
27
+ ]);
28
+ const BUILTIN_RULE_IDS = new Set([
29
+ 'instruction-override',
30
+ 'sensitive-file-target',
31
+ 'third-party-execution',
32
+ 'floating-version',
33
+ 'credential-or-env-probing',
34
+ 'pressure-without-evidence',
35
+ 'unverifiable-authority-claim',
36
+ 'security-framing-conflict',
37
+ ]);
38
+ function objectValue(value) {
39
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
40
+ }
41
+ function stringArray(value) {
42
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
43
+ }
44
+ function validateStatement(statement, where, errors) {
45
+ if (!objectValue(statement)) {
46
+ errors.push(`${where}: must be an object`);
47
+ return;
48
+ }
49
+ if (typeof statement.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(statement.id)) {
50
+ errors.push(`${where}.id: must be a kebab-case identifier`);
51
+ }
52
+ if (!['suppress', 'set-severity'].includes(String(statement.effect))) {
53
+ errors.push(`${where}.effect: must be suppress or set-severity`);
54
+ }
55
+ if (typeof statement.reason !== 'string' || !statement.reason.trim()) {
56
+ errors.push(`${where}.reason: must be a non-empty string`);
57
+ }
58
+ if (statement.signals !== undefined && !stringArray(statement.signals)) {
59
+ errors.push(`${where}.signals: must be an array of strings`);
60
+ }
61
+ if (statement.severity !== undefined
62
+ && !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
63
+ errors.push(`${where}.severity: unknown severity`);
64
+ }
65
+ if (statement.when !== undefined && !objectValue(statement.when)) {
66
+ errors.push(`${where}.when: must be an object`);
67
+ }
68
+ if (statement.riskAcceptance !== undefined) {
69
+ if (!objectValue(statement.riskAcceptance)
70
+ || typeof statement.riskAcceptance.acceptedBy !== 'string'
71
+ || typeof statement.riskAcceptance.rationale !== 'string') {
72
+ errors.push(`${where}.riskAcceptance: requires acceptedBy and rationale`);
73
+ }
74
+ }
75
+ if (statement.effect === 'suppress') {
76
+ if (!stringArray(statement.signals) || statement.signals.length === 0) {
77
+ errors.push(`${where}.signals: suppress statements must name at least one signal`);
78
+ }
79
+ const when = objectValue(statement.when) ? statement.when : undefined;
80
+ if (!when || !Object.values(when).some(value => Array.isArray(value) && value.length > 0)) {
81
+ errors.push(`${where}.when: suppress statements require at least one narrow condition`);
82
+ }
83
+ if (!objectValue(statement.riskAcceptance)
84
+ || typeof statement.riskAcceptance.acceptedBy !== 'string'
85
+ || typeof statement.riskAcceptance.rationale !== 'string') {
86
+ errors.push(`${where}.riskAcceptance: suppress statements require acceptedBy and rationale`);
87
+ }
88
+ }
89
+ if (statement.effect === 'set-severity'
90
+ && !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
91
+ errors.push(`${where}.severity: set-severity requires a known severity`);
92
+ }
93
+ }
94
+ /**
95
+ * Fail-closed validation for project threat-assessment policy. The evaluator
96
+ * performs the same checks at runtime; config loading validates first so a
97
+ * malformed policy never degrades into the default or off mode.
98
+ */
99
+ export function validateThreatAssessmentConfig(value) {
100
+ const errors = [];
101
+ if (value === undefined || value === null)
102
+ return errors;
103
+ if (!objectValue(value))
104
+ return ['security.threatAssessment: must be an object'];
105
+ if (value.schemaVersion !== undefined && value.schemaVersion !== '1') {
106
+ errors.push("security.threatAssessment.schemaVersion: must be '1'");
107
+ }
108
+ if (value.mode !== undefined
109
+ && !THREAT_ASSESSMENT_MODES.includes(value.mode)) {
110
+ errors.push('security.threatAssessment.mode: must be off, audit, or enforce');
111
+ }
112
+ if (value.defaultProfile !== undefined && typeof value.defaultProfile !== 'string') {
113
+ errors.push('security.threatAssessment.defaultProfile: must be a string');
114
+ }
115
+ if (value.surfaces !== undefined) {
116
+ if (!objectValue(value.surfaces))
117
+ errors.push('security.threatAssessment.surfaces: must be an object');
118
+ else {
119
+ for (const [surface, entry] of Object.entries(value.surfaces)) {
120
+ const where = `security.threatAssessment.surfaces.${surface}`;
121
+ if (!THREAT_ASSESSMENT_SURFACES.includes(surface)) {
122
+ errors.push(`${where}: unknown surface`);
123
+ }
124
+ if (!objectValue(entry)) {
125
+ errors.push(`${where}: must be an object`);
126
+ continue;
127
+ }
128
+ if (entry.mode !== undefined
129
+ && !THREAT_ASSESSMENT_MODES.includes(entry.mode)) {
130
+ errors.push(`${where}.mode: must be off, audit, or enforce`);
131
+ }
132
+ if (entry.profile !== undefined && typeof entry.profile !== 'string') {
133
+ errors.push(`${where}.profile: must be a string`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ const rulePacks = objectValue(value.rulePacks) ? value.rulePacks : {};
139
+ if (value.rulePacks !== undefined && !objectValue(value.rulePacks)) {
140
+ errors.push('security.threatAssessment.rulePacks: must be an object');
141
+ }
142
+ for (const [name, pack] of Object.entries(rulePacks)) {
143
+ const where = `security.threatAssessment.rulePacks.${name}`;
144
+ if (name.startsWith('aiwg:'))
145
+ errors.push(`${where}: cannot shadow a built-in rule pack`);
146
+ if (!objectValue(pack) || !Array.isArray(pack.rules)) {
147
+ errors.push(`${where}.rules: must be an array`);
148
+ continue;
149
+ }
150
+ if (typeof pack.version !== 'string' || !pack.version.trim()) {
151
+ errors.push(`${where}.version: must be a non-empty string`);
152
+ }
153
+ const ids = new Set();
154
+ pack.rules.forEach((rule, index) => {
155
+ const ruleWhere = `${where}.rules[${index}]`;
156
+ if (!objectValue(rule)) {
157
+ errors.push(`${ruleWhere}: must be an object`);
158
+ return;
159
+ }
160
+ if (typeof rule.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(rule.id)) {
161
+ errors.push(`${ruleWhere}.id: must be a kebab-case identifier`);
162
+ }
163
+ else if (BUILTIN_RULE_IDS.has(rule.id)) {
164
+ errors.push(`${ruleWhere}.id: cannot shadow built-in rule '${rule.id}'`);
165
+ }
166
+ else if (ids.has(rule.id)) {
167
+ errors.push(`${ruleWhere}.id: duplicate '${rule.id}'`);
168
+ }
169
+ else
170
+ ids.add(rule.id);
171
+ if (!THREAT_ASSESSMENT_SEVERITIES.includes(rule.severity)) {
172
+ errors.push(`${ruleWhere}.severity: unknown severity`);
173
+ }
174
+ for (const dimension of ['likelihood', 'impact']) {
175
+ const dimensionValue = rule[dimension];
176
+ if (dimensionValue !== undefined
177
+ && (!Number.isInteger(dimensionValue) || Number(dimensionValue) < 1 || Number(dimensionValue) > 5)) {
178
+ errors.push(`${ruleWhere}.${dimension}: must be an integer from 1 to 5`);
179
+ }
180
+ }
181
+ if (!stringArray(rule.patterns) || rule.patterns.length === 0) {
182
+ errors.push(`${ruleWhere}.patterns: must be a non-empty array of strings`);
183
+ }
184
+ else {
185
+ rule.patterns.forEach((pattern, patternIndex) => {
186
+ const patternWhere = `${ruleWhere}.patterns[${patternIndex}]`;
187
+ if (!pattern || pattern.length > 500) {
188
+ errors.push(`${patternWhere}: must be 1-500 characters`);
189
+ return;
190
+ }
191
+ try {
192
+ new RegExp(pattern, 'imu');
193
+ }
194
+ catch (error) {
195
+ errors.push(`${patternWhere}: invalid regular expression (${error.message})`);
196
+ }
197
+ if (/\\[1-9]/.test(pattern) || /\(\?<([=!])/.test(pattern)
198
+ || /\([^)]*[+*][^)]*\)[+*{]/.test(pattern) || /(?:\.\*){2,}/.test(pattern)) {
199
+ errors.push(`${patternWhere}: unsafe regex construct (backreference, lookbehind, or nested unbounded quantifier)`);
200
+ }
201
+ });
202
+ }
203
+ });
204
+ }
205
+ const profiles = objectValue(value.profiles) ? value.profiles : {};
206
+ if (value.profiles !== undefined && !objectValue(value.profiles)) {
207
+ errors.push('security.threatAssessment.profiles: must be an object');
208
+ }
209
+ const visit = (name, stack) => {
210
+ if (stack.includes(name)) {
211
+ errors.push(`security.threatAssessment.profiles.${name}.extends: cyclic inheritance (${[...stack, name].join(' -> ')})`);
212
+ return;
213
+ }
214
+ const profile = profiles[name];
215
+ if (!objectValue(profile))
216
+ return;
217
+ const parents = profile.extends ?? [];
218
+ if (!stringArray(parents)) {
219
+ errors.push(`security.threatAssessment.profiles.${name}.extends: must be an array of strings`);
220
+ return;
221
+ }
222
+ for (const rawParent of parents) {
223
+ const parent = rawParent.replace(/^aiwg:/, '');
224
+ if (!BUILTIN_PROFILES.has(parent) && !(parent in profiles)) {
225
+ errors.push(`security.threatAssessment.profiles.${name}.extends: unknown profile '${rawParent}'`);
226
+ }
227
+ else if (parent in profiles)
228
+ visit(parent, [...stack, name]);
229
+ }
230
+ };
231
+ for (const [name, profile] of Object.entries(profiles)) {
232
+ const where = `security.threatAssessment.profiles.${name}`;
233
+ if (BUILTIN_PROFILES.has(name) || name.startsWith('aiwg:')) {
234
+ errors.push(`${where}: cannot shadow a built-in profile`);
235
+ }
236
+ if (!objectValue(profile)) {
237
+ errors.push(`${where}: must be an object`);
238
+ continue;
239
+ }
240
+ if (profile.mode !== undefined
241
+ && !THREAT_ASSESSMENT_MODES.includes(profile.mode)) {
242
+ errors.push(`${where}.mode: must be off, audit, or enforce`);
243
+ }
244
+ if (profile.version !== undefined && (typeof profile.version !== 'string' || !profile.version.trim())) {
245
+ errors.push(`${where}.version: must be a non-empty string`);
246
+ }
247
+ if (profile.ruleSets !== undefined && !stringArray(profile.ruleSets)) {
248
+ errors.push(`${where}.ruleSets: must be an array of strings`);
249
+ }
250
+ else {
251
+ for (const pack of (profile.ruleSets ?? [])) {
252
+ if (!BUILTIN_RULE_PACKS.has(pack) && !(pack in rulePacks)) {
253
+ errors.push(`${where}.ruleSets: unavailable rule pack '${pack}'`);
254
+ }
255
+ }
256
+ }
257
+ if (profile.thresholds !== undefined) {
258
+ if (!objectValue(profile.thresholds))
259
+ errors.push(`${where}.thresholds: must be an object`);
260
+ else {
261
+ for (const [action, severity] of Object.entries(profile.thresholds)) {
262
+ if (!['flag', 'requireAuthorization', 'reject'].includes(action)
263
+ || !THREAT_ASSESSMENT_SEVERITIES.includes(severity)) {
264
+ errors.push(`${where}.thresholds.${action}: unknown threshold or severity`);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ if (profile.statements !== undefined) {
270
+ if (!Array.isArray(profile.statements))
271
+ errors.push(`${where}.statements: must be an array`);
272
+ else
273
+ profile.statements.forEach((statement, index) => validateStatement(statement, `${where}.statements[${index}]`, errors));
274
+ }
275
+ visit(name, []);
276
+ }
277
+ if (value.statements !== undefined) {
278
+ if (!Array.isArray(value.statements))
279
+ errors.push('security.threatAssessment.statements: must be an array');
280
+ else
281
+ value.statements.forEach((statement, index) => validateStatement(statement, `security.threatAssessment.statements[${index}]`, errors));
282
+ }
283
+ const defaultProfile = typeof value.defaultProfile === 'string' ? value.defaultProfile : 'balanced';
284
+ if (!BUILTIN_PROFILES.has(defaultProfile) && !(defaultProfile in profiles)) {
285
+ errors.push(`security.threatAssessment.defaultProfile: unknown profile '${defaultProfile}'`);
286
+ }
287
+ return Array.from(new Set(errors));
288
+ }
289
+ export function defaultThreatAssessmentConfig() {
290
+ return {
291
+ schemaVersion: '1',
292
+ mode: 'enforce',
293
+ defaultProfile: 'balanced',
294
+ };
295
+ }
296
+ //# sourceMappingURL=threat-assessment-config.js.map
@@ -241,6 +241,35 @@ export function normalizeSandboxEvent(raw) {
241
241
  * Matches the sandbox's 5 s retry interval — suppressess flicker on rapid restarts.
242
242
  */
243
243
  const DEBOUNCE_MS = 5_000;
244
+ function safeTrustRef(value) {
245
+ const ref = typeof value === 'string' ? value.trim() : '';
246
+ if (!ref)
247
+ return undefined;
248
+ if (/-----BEGIN|PRIVATE KEY|TOKEN|SECRET|PASSWORD|[\r\n]/i.test(ref))
249
+ return '[redacted]';
250
+ return ref.slice(0, 160);
251
+ }
252
+ function sanitizeTrustPosture(posture) {
253
+ if (!posture || typeof posture !== 'object')
254
+ return undefined;
255
+ const status = ['secure', 'degraded', 'disabled', 'unknown'].includes(String(posture.status))
256
+ ? posture.status
257
+ : 'unknown';
258
+ return {
259
+ status,
260
+ mode: safeTrustRef(posture.mode),
261
+ ca_provider_ref: safeTrustRef(posture.ca_provider_ref),
262
+ trust_bundle_ref: safeTrustRef(posture.trust_bundle_ref),
263
+ client_identity_ref: safeTrustRef(posture.client_identity_ref),
264
+ rotation_state: safeTrustRef(posture.rotation_state),
265
+ expires_at: safeTrustRef(posture.expires_at),
266
+ trust_bundle_fresh: posture.trust_bundle_fresh,
267
+ missing_required_material: Array.isArray(posture.missing_required_material)
268
+ ? posture.missing_required_material.map((item) => safeTrustRef(item)).filter((item) => Boolean(item))
269
+ : undefined,
270
+ recovery: safeTrustRef(posture.recovery),
271
+ };
272
+ }
244
273
  export class SandboxRegistry {
245
274
  sandboxes = new Map();
246
275
  hitlRequests = new Map();
@@ -336,6 +365,9 @@ export class SandboxRegistry {
336
365
  if (req.ws_capabilities) {
337
366
  existing.wsCapabilities = req.ws_capabilities;
338
367
  }
368
+ if (req.trust_posture) {
369
+ existing.trustPosture = sanitizeTrustPosture(req.trust_posture);
370
+ }
339
371
  this.lastRegistrationTime.set(instanceId, now);
340
372
  return { sandbox_id: existingId, token: existing.token };
341
373
  }
@@ -370,6 +402,7 @@ export class SandboxRegistry {
370
402
  agents: new Map(),
371
403
  sandboxInventory,
372
404
  wsCapabilities: req.ws_capabilities,
405
+ trustPosture: sanitizeTrustPosture(req.trust_posture),
373
406
  };
374
407
  this.sandboxes.set(id, registration);
375
408
  if (instanceId) {
@@ -825,6 +858,7 @@ function toSummary(s) {
825
858
  agents: [...s.agents.values()],
826
859
  sandboxInventory: s.sandboxInventory,
827
860
  wsCapabilities: s.wsCapabilities,
861
+ trustPosture: s.trustPosture,
828
862
  };
829
863
  }
830
864
  // Singleton instance
@@ -16,6 +16,10 @@ const ClaudeRecordSchema = z.object({
16
16
  timestamp: z.string().datetime({ offset: true }).optional(),
17
17
  cwd: z.string().optional(),
18
18
  gitBranch: z.string().optional(),
19
+ parentSessionId: z.string().min(1).optional(),
20
+ isSidechain: z.boolean().optional(),
21
+ agentId: z.string().min(1).optional(),
22
+ slug: z.string().min(1).optional(),
19
23
  version: z.string().optional(),
20
24
  schemaVersion: z.string().optional(),
21
25
  message: z.object({
@@ -138,13 +142,11 @@ function normalizeTranscript(records, locator) {
138
142
  throw new SessionContractError('MALFORMED_SOURCE', 'Claude transcript record is malformed');
139
143
  }
140
144
  const value = parsed.data;
141
- const nativeSessionId = value.sessionId ?? value.session_id ?? filenameSessionId;
142
- if ((value.sessionId || value.session_id) && nativeSessionId !== filenameSessionId) {
143
- throw new SessionContractError('SCHEMA_DRIFT', 'Claude transcript session identity differs from its documented filename identity');
144
- }
145
+ const embeddedSessionId = value.sessionId ?? value.session_id;
146
+ const nativeSessionId = embeddedSessionId ?? filenameSessionId;
145
147
  const blocks = messageBlocks(value);
146
148
  if (blocks.length === 0) {
147
- output.push(providerRecord(value, record, nativeSessionId, {
149
+ output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
148
150
  kind: `claude.${value.type}`,
149
151
  text: '',
150
152
  opaque: true,
@@ -153,7 +155,7 @@ function normalizeTranscript(records, locator) {
153
155
  continue;
154
156
  }
155
157
  for (const [blockIndex, block] of blocks.entries()) {
156
- output.push(providerRecord(value, record, nativeSessionId, {
158
+ output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
157
159
  kind: block.kind,
158
160
  text: block.text,
159
161
  blockIndex,
@@ -184,11 +186,24 @@ function normalizeHooks(records) {
184
186
  participant: 'claude',
185
187
  model: hook.model,
186
188
  occurredAt: hook.timestamp,
189
+ activityBoundary: hook.hook_event_name === 'SessionEnd'
190
+ ? 'end'
191
+ : hook.source === 'resume'
192
+ ? 'resume'
193
+ : hook.source === 'compact'
194
+ ? 'continuation'
195
+ : undefined,
196
+ activityBoundaryBasis: hook.hook_event_name === 'SessionEnd' || hook.source
197
+ ? `claude-hook:${hook.hook_event_name}:${hook.source ?? 'none'}`
198
+ : undefined,
199
+ activityBoundaryConfidence: hook.hook_event_name === 'SessionEnd' || hook.source
200
+ ? 'high'
201
+ : undefined,
187
202
  text: '',
188
203
  rawReference: { locatorClass: 'claude-hook-jsonl', offset: record.byteOffset },
189
204
  extensions: {
190
205
  hookEventName: hook.hook_event_name,
191
- lifecycle: hook.hook_event_name === 'SessionEnd' ? 'complete' : 'active',
206
+ ...(hook.hook_event_name === 'SessionEnd' ? { lifecycle: 'complete' } : {}),
192
207
  startSource: hook.source,
193
208
  model: hook.model,
194
209
  reason: hook.reason,
@@ -201,7 +216,7 @@ function normalizeHooks(records) {
201
216
  }
202
217
  return { records: output, complete };
203
218
  }
204
- function providerRecord(value, record, nativeSessionId, block) {
219
+ function providerRecord(value, record, nativeSessionId, sourceArtifactSessionId, block) {
205
220
  const nativeBase = value.uuid ?? value.message?.id;
206
221
  const nativeEventId = block.blockNativeId
207
222
  ?? (nativeBase ? `${nativeBase}:${block.blockIndex ?? 0}` : undefined);
@@ -221,6 +236,18 @@ function providerRecord(value, record, nativeSessionId, block) {
221
236
  productVersion: value.version,
222
237
  workspace: { cwdClass: value.cwd ? '<workspace>' : undefined, gitBranch: value.gitBranch },
223
238
  provenance: { acquisition: 'claude-transcript', schema: CLAUDE_TRANSCRIPT_SCHEMA_VERSION },
239
+ transcriptFamily: {
240
+ sourceArtifactSessionId,
241
+ nativeSessionId,
242
+ identityRelation: nativeSessionId === sourceArtifactSessionId ? 'self' : 'related',
243
+ parentUuid: value.parentUuid,
244
+ parentSessionId: value.parentSessionId,
245
+ gitBranch: value.gitBranch,
246
+ continuation: record.sequence > 0 && value.parentUuid !== null,
247
+ subagent: value.isSidechain === true || Boolean(value.agentId),
248
+ agentId: value.agentId,
249
+ agentSlug: value.slug,
250
+ },
224
251
  opaque: block.opaque,
225
252
  unknownFields: {
226
253
  ...unknownFields(value, TRANSCRIPT_KEYS),
@@ -348,7 +375,8 @@ function unknownFields(value, known) {
348
375
  }
349
376
  const TRANSCRIPT_KEYS = new Set([
350
377
  'type', 'subtype', 'uuid', 'parentUuid', 'sessionId', 'session_id',
351
- 'timestamp', 'cwd', 'gitBranch', 'version', 'schemaVersion', 'message',
378
+ 'timestamp', 'cwd', 'gitBranch', 'parentSessionId', 'isSidechain',
379
+ 'agentId', 'slug', 'version', 'schemaVersion', 'message',
352
380
  ]);
353
381
  const HOOK_KEYS = new Set([
354
382
  'session_id', 'transcript_path', 'cwd', 'hook_event_name', 'permission_mode',
@@ -83,7 +83,8 @@ export class CodexSessionAdapter {
83
83
  let mode = isAppServerLocator(source.locator)
84
84
  ? 'app-server' : null;
85
85
  let schemaVersion = null;
86
- let nativeSessionId = rolloutIdFromFilename(source.locator);
86
+ const sourceArtifactSessionId = rolloutIdFromFilename(source.locator);
87
+ let nativeSessionId = sourceArtifactSessionId;
87
88
  const identities = new Map();
88
89
  let outputIndex = 0;
89
90
  let sawRecord = false;
@@ -118,15 +119,12 @@ export class CodexSessionAdapter {
118
119
  if (!declaredId) {
119
120
  throw new SessionContractError('MALFORMED_SOURCE', 'Codex session metadata is missing its id');
120
121
  }
121
- if (nativeSessionId && nativeSessionId !== declaredId) {
122
- throw new SessionContractError('SCHEMA_DRIFT', 'Codex rollout session identity differs from its filename identity');
123
- }
124
122
  nativeSessionId = declaredId;
125
123
  }
126
124
  if (!nativeSessionId) {
127
125
  throw new SessionContractError('MALFORMED_SOURCE', 'Codex rollout has no session identity before content records');
128
126
  }
129
- normalized = [rolloutRecord(nativeSessionId, parsed.data, payload, line)];
127
+ normalized = [rolloutRecord(nativeSessionId, parsed.data, payload, line, sourceArtifactSessionId)];
130
128
  }
131
129
  for (const record of normalized) {
132
130
  if (outputIndex++ >= start)
@@ -188,6 +186,15 @@ function normalizeAppServer(input) {
188
186
  sequence: line.sequence * 1_000,
189
187
  kind: event === 'compacted' ? 'summary' : 'codex.lifecycle',
190
188
  role: 'system',
189
+ activityBoundary: event === 'unarchived'
190
+ ? 'resume'
191
+ : event === 'compacted'
192
+ ? 'continuation'
193
+ : event === 'archived' || event === 'deleted'
194
+ ? 'end'
195
+ : undefined,
196
+ activityBoundaryBasis: `codex-app-server:${event}`,
197
+ activityBoundaryConfidence: event === 'status-changed' ? 'medium' : 'high',
191
198
  text: '',
192
199
  rawReference: { locatorClass: 'codex-app-server-jsonl', offset: line.byteOffset },
193
200
  extensions: {
@@ -201,7 +208,8 @@ function normalizeAppServer(input) {
201
208
  return { records, complete: complete && !sawActive };
202
209
  }
203
210
  function normalizeRollout(input, locator) {
204
- let nativeSessionId = rolloutIdFromFilename(locator);
211
+ const sourceArtifactSessionId = rolloutIdFromFilename(locator);
212
+ let nativeSessionId = sourceArtifactSessionId;
205
213
  const records = [];
206
214
  for (const line of input) {
207
215
  const parsed = RolloutEnvelopeSchema.safeParse(line.value);
@@ -215,15 +223,12 @@ function normalizeRollout(input, locator) {
215
223
  if (!declaredId) {
216
224
  throw new SessionContractError('MALFORMED_SOURCE', 'Codex session metadata is missing its id');
217
225
  }
218
- if (nativeSessionId && nativeSessionId !== declaredId) {
219
- throw new SessionContractError('SCHEMA_DRIFT', 'Codex rollout session identity differs from its filename identity');
220
- }
221
226
  nativeSessionId = declaredId;
222
227
  }
223
228
  if (!nativeSessionId) {
224
229
  throw new SessionContractError('MALFORMED_SOURCE', 'Codex rollout has no session identity before content records');
225
230
  }
226
- records.push(rolloutRecord(nativeSessionId, envelope, payload, line));
231
+ records.push(rolloutRecord(nativeSessionId, envelope, payload, line, sourceArtifactSessionId));
227
232
  }
228
233
  return { records, complete: false };
229
234
  }
@@ -281,7 +286,7 @@ function threadItem(thread, turn, item, line, turnIndex, itemIndex) {
281
286
  },
282
287
  };
283
288
  }
284
- function rolloutRecord(nativeSessionId, envelope, payload, line) {
289
+ function rolloutRecord(nativeSessionId, envelope, payload, line, sourceArtifactSessionId) {
285
290
  const nativeId = stringValue(payload.id)
286
291
  ?? stringValue(payload.call_id)
287
292
  ?? `${envelope.type}:${line.sequence}`;
@@ -298,6 +303,21 @@ function rolloutRecord(nativeSessionId, envelope, payload, line) {
298
303
  toolCallId: stringValue(payload.call_id),
299
304
  model: stringValue(payload.model),
300
305
  occurredAt: envelope.timestamp,
306
+ activityBoundary: envelope.type === 'compacted'
307
+ ? 'continuation'
308
+ : envelope.type === 'event_msg'
309
+ && stringValue(payload.type) === 'task_complete'
310
+ ? 'end'
311
+ : undefined,
312
+ activityBoundaryBasis: envelope.type === 'compacted'
313
+ ? 'codex-rollout:compacted'
314
+ : envelope.type === 'event_msg' && stringValue(payload.type) === 'task_complete'
315
+ ? 'codex-rollout:task-complete'
316
+ : undefined,
317
+ activityBoundaryConfidence: envelope.type === 'compacted'
318
+ || (envelope.type === 'event_msg' && stringValue(payload.type) === 'task_complete')
319
+ ? 'high'
320
+ : undefined,
301
321
  text: rolloutText(payload),
302
322
  rawReference: { locatorClass: 'codex-rollout-jsonl', offset: line.byteOffset },
303
323
  extensions: {
@@ -310,6 +330,13 @@ function rolloutRecord(nativeSessionId, envelope, payload, line) {
310
330
  : undefined,
311
331
  productVersion: stringValue(payload.cli_version),
312
332
  provenance: { acquisition: 'codex-rollout', durableReplay: true },
333
+ transcriptFamily: {
334
+ sourceArtifactSessionId,
335
+ nativeSessionId,
336
+ identityRelation: !sourceArtifactSessionId || sourceArtifactSessionId === nativeSessionId
337
+ ? 'self'
338
+ : 'related',
339
+ },
313
340
  opaque: !KNOWN_ROLLOUT_TYPES.has(envelope.type),
314
341
  unknownFields: unknownFields(payload, ROLLOUT_KEYS),
315
342
  },