shennian 0.3.85 → 0.3.86

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 (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +28 -28
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
@@ -1,3 +1,130 @@
1
- import d from"node:fs";import _ from"node:os";import u from"node:path";import{resolveBuiltinCommand as S,spawnResolvedCommandSync as v}from"../agents/command-spec.js";import{normalizeNativeTimestamp as f}from"./session-header.js";function l(t){try{const e=JSON.parse(t);return typeof e=="object"&&e!==null?e:null}catch{return null}}function O(t){return t.filter(e=>e.type==="text"||e.type==="reasoning").map(e=>typeof e.text=="string"?e.text:"").filter(Boolean).join(`
2
-
3
- `).trim()}function h(t,e,s,r,c,n,i,o){if(!n)return;const a=f(r.time_created),p=f(s.time_created);!a||!p||t.push({agentType:"opencode",sourceMode:"opencode_session_import",sourceSessionKey:s.id,sourceEventKey:`opencode:${s.id}:${r.id}:${c}`,cursor:`${e}:${r.time_created}:${r.id}`,role:c,ts:a,payload:n,title:s.title,titleSource:"agent_native",modelId:i,workDir:s.directory,terminal:o,sourceSessionCreatedAtMs:p})}function x(t,e){const s=[],r=new Map(e.sessions.map(n=>[n.id,n])),c=new Map;for(const n of e.parts){const i=l(n.data);if(!i)continue;const o=c.get(n.message_id)??[];o.push(i),c.set(n.message_id,o)}for(const n of[...e.messages].sort((i,o)=>i.time_created-o.time_created)){const i=r.get(n.session_id);if(!i)continue;const o=l(n.data);if(!o)continue;const a=o.role==="user"?"user":o.role==="assistant"?"agent":null;if(!a)continue;const p=c.get(n.id)??[],y=O(p),g=typeof o.modelID=="string"?`${typeof o.providerID=="string"?`${o.providerID}/`:""}${o.modelID}`:null;h(s,t,i,n,a,y,g,a==="agent")}return s}function I(){const t=u.join(_.homedir(),".local","share","opencode"),e=u.join(t,"shennian-opencode-session-snapshot.opencode-session.json");return C(e),d.existsSync(e)?[e]:[]}function m(t){const e=S("opencode");if(!e)return[];const s=v(e,["db",t,"--format","json"],{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e4});if(s.status!==0)return[];try{const r=JSON.parse(String(s.stdout??""));return Array.isArray(r)?r:[]}catch{return[]}}function C(t){try{const e=m("select id, title, directory, time_created, time_updated from session where time_archived is null order by time_updated asc");if(e.length===0)return;const s=m("select id, session_id, time_created, data from message order by time_created asc, id asc"),r=m("select id, message_id, session_id, time_created, data from part order by message_id asc, id asc");d.mkdirSync(u.dirname(t),{recursive:!0}),d.writeFileSync(t,JSON.stringify({sessions:e,messages:s,parts:r}))}catch{}}function z(t,e){const s=d.statSync(t);if(e>=s.size)return{nextOffset:s.size,events:[]};if(e>0)return{nextOffset:s.size,events:[]};const r=JSON.parse(d.readFileSync(t,"utf8"));return{nextOffset:s.size,events:x(t,r)}}export{I as listOpenCodeSessionFiles,z as parseOpenCodeSessionFile,x as parseOpenCodeSessionSnapshot};
1
+ // @arch docs/architecture/cli/native-session-fusion.md
2
+ // @test src/__tests__/native-fusion-parsers.test.ts
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { resolveBuiltinCommand, spawnResolvedCommandSync } from '../agents/command-spec.js';
7
+ import { normalizeNativeTimestamp } from './session-header.js';
8
+ function parseJsonObject(value) {
9
+ try {
10
+ const parsed = JSON.parse(value);
11
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function textFromOpenCodeParts(parts) {
18
+ return parts
19
+ .filter((part) => part.type === 'text' || part.type === 'reasoning')
20
+ .map((part) => typeof part.text === 'string' ? part.text : '')
21
+ .filter(Boolean)
22
+ .join('\n\n')
23
+ .trim();
24
+ }
25
+ function pushOpenCodeEvent(events, snapshotPath, session, message, role, payload, modelId, terminal) {
26
+ if (!payload)
27
+ return;
28
+ const eventTimestamp = normalizeNativeTimestamp(message.time_created);
29
+ const sourceSessionCreatedAtMs = normalizeNativeTimestamp(session.time_created);
30
+ if (!eventTimestamp || !sourceSessionCreatedAtMs)
31
+ return;
32
+ events.push({
33
+ agentType: 'opencode',
34
+ sourceMode: 'opencode_session_import',
35
+ sourceSessionKey: session.id,
36
+ sourceEventKey: `opencode:${session.id}:${message.id}:${role}`,
37
+ cursor: `${snapshotPath}:${message.time_created}:${message.id}`,
38
+ role,
39
+ ts: eventTimestamp,
40
+ payload,
41
+ title: session.title,
42
+ titleSource: 'agent_native',
43
+ modelId,
44
+ workDir: session.directory,
45
+ terminal,
46
+ sourceSessionCreatedAtMs,
47
+ });
48
+ }
49
+ export function parseOpenCodeSessionSnapshot(snapshotPath, snapshot) {
50
+ const events = [];
51
+ const sessionsById = new Map(snapshot.sessions.map((session) => [session.id, session]));
52
+ const partsByMessage = new Map();
53
+ for (const partRow of snapshot.parts) {
54
+ const data = parseJsonObject(partRow.data);
55
+ if (!data)
56
+ continue;
57
+ const list = partsByMessage.get(partRow.message_id) ?? [];
58
+ list.push(data);
59
+ partsByMessage.set(partRow.message_id, list);
60
+ }
61
+ for (const message of [...snapshot.messages].sort((left, right) => left.time_created - right.time_created)) {
62
+ const session = sessionsById.get(message.session_id);
63
+ if (!session)
64
+ continue;
65
+ const data = parseJsonObject(message.data);
66
+ if (!data)
67
+ continue;
68
+ const role = data.role === 'user' ? 'user' : data.role === 'assistant' ? 'agent' : null;
69
+ if (!role)
70
+ continue;
71
+ const parts = partsByMessage.get(message.id) ?? [];
72
+ const payload = textFromOpenCodeParts(parts);
73
+ const modelId = typeof data.modelID === 'string'
74
+ ? `${typeof data.providerID === 'string' ? `${data.providerID}/` : ''}${data.modelID}`
75
+ : null;
76
+ pushOpenCodeEvent(events, snapshotPath, session, message, role, payload, modelId, role === 'agent');
77
+ }
78
+ return events;
79
+ }
80
+ export function listOpenCodeSessionFiles() {
81
+ const dataDir = path.join(os.homedir(), '.local', 'share', 'opencode');
82
+ const snapshot = path.join(dataDir, 'shennian-opencode-session-snapshot.opencode-session.json');
83
+ refreshOpenCodeSessionSnapshot(snapshot);
84
+ return fs.existsSync(snapshot) ? [snapshot] : [];
85
+ }
86
+ function queryOpenCodeDb(query) {
87
+ const spec = resolveBuiltinCommand('opencode');
88
+ if (!spec)
89
+ return [];
90
+ const result = spawnResolvedCommandSync(spec, ['db', query, '--format', 'json'], {
91
+ encoding: 'utf-8',
92
+ stdio: ['ignore', 'pipe', 'ignore'],
93
+ timeout: 10_000,
94
+ });
95
+ if (result.status !== 0)
96
+ return [];
97
+ try {
98
+ const parsed = JSON.parse(String(result.stdout ?? ''));
99
+ return Array.isArray(parsed) ? parsed : [];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ function refreshOpenCodeSessionSnapshot(snapshotPath) {
106
+ try {
107
+ const sessions = queryOpenCodeDb('select id, title, directory, time_created, time_updated from session where time_archived is null order by time_updated asc');
108
+ if (sessions.length === 0)
109
+ return;
110
+ const messages = queryOpenCodeDb('select id, session_id, time_created, data from message order by time_created asc, id asc');
111
+ const parts = queryOpenCodeDb('select id, message_id, session_id, time_created, data from part order by message_id asc, id asc');
112
+ fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
113
+ fs.writeFileSync(snapshotPath, JSON.stringify({ sessions, messages, parts }));
114
+ }
115
+ catch {
116
+ /* best-effort native import */
117
+ }
118
+ }
119
+ export function parseOpenCodeSessionFile(filePath, startOffset) {
120
+ const stat = fs.statSync(filePath);
121
+ if (startOffset >= stat.size)
122
+ return { nextOffset: stat.size, events: [] };
123
+ if (startOffset > 0)
124
+ return { nextOffset: stat.size, events: [] };
125
+ const snapshot = JSON.parse(fs.readFileSync(filePath, 'utf8'));
126
+ return {
127
+ nextOffset: stat.size,
128
+ events: parseOpenCodeSessionSnapshot(filePath, snapshot),
129
+ };
130
+ }
@@ -1,3 +1,140 @@
1
- import g from"node:fs";import l from"node:os";import r from"node:path";import{listCodexRolloutFiles as G,lookupCodexThreadName as H,parseCodexRolloutChunk as Q}from"./codex-parser.js";import{inspectNativeSessionFile as V,normalizeNativeTimestamp as X}from"./session-header.js";import{listOpenCodeSessionFiles as Z,parseOpenCodeSessionFile as $,parseOpenCodeSessionSnapshot as P}from"./opencode-parser.js";import{makeCursor as j,makeEventKey as C,normalizeText as v,readClaudeEventCwd as k,readJsonlLines as S,readTimestamp as w,safeParse as T,isClaudeSubagentTranscript as D,shouldIgnoreClaudeProjectDir as N,shouldIgnoreClaudeTranscriptPath as z,shouldIgnoreNativeScanPath as b}from"./parser-common.js";function M(){return[r.join(l.homedir(),".codex","sessions"),r.join(l.homedir(),".codex","archived_sessions"),r.join(l.homedir(),".claude","projects"),r.join(l.homedir(),".local","share","opencode")]}function E(e){const n=l.homedir();return e.startsWith(n)?e.slice(n.length).replace(/^\//,"")||"~":e.replace(/\//g,"-")}function I(){const e=r.join(l.homedir(),".claude","projects");if(!g.existsSync(e))return[];const n=[],i=t=>{for(const o of g.readdirSync(t,{withFileTypes:!0})){const a=r.join(t,o.name);if(o.isDirectory()){if(t===e&&N(o.name))continue;i(a)}else o.isFile()&&o.name.endsWith(".jsonl")&&n.push(a)}};return i(e),n.sort()}function B(e){if(!e)return null;const n=I().find(t=>r.basename(t,".jsonl")===e);if(!n)return null;let i=null;return S(n,0,t=>{if(i||!t.trim())return;const o=T(t);if(!o)return;const a=k(o);a&&(i=a)}),i}function J(e,n){const i=g.statSync(e).size;if(D(e)||z(e))return{nextOffset:i,events:[]};const t=[],o=r.basename(e,".jsonl"),a=E(r.dirname(e).replace(r.join(l.homedir(),".claude","projects")+r.sep,""));if(b(a))return{nextOffset:i,events:[]};let p="";return{nextOffset:S(e,n,(_,d)=>{const s=T(_);if(!s)return;const y=w(s.timestamp),x=typeof s.type=="string"?s.type:"";if(!y||!x)return;const m=k(s);if(!(m&&b(m))){if(x==="user"){const c=typeof s.message=="object"&&s.message!==null?s.message:null,u=typeof c?.content=="string"?v(c.content):"";if(!u)return;p||(p=u.slice(0,80)),t.push({agentType:"claude",sourceMode:"claude_transcript_import",sourceSessionKey:o,sourceEventKey:C(e,d,"user"),cursor:j(e,d),role:"user",ts:y,payload:u,title:p,titleSource:"first_user_fallback",workDir:m??a})}else if(x==="assistant"){const c=typeof s.message=="object"&&s.message!==null?s.message:null,u=Array.isArray(c?.content)?c.content:[],h=v(u.map(f=>typeof f=="object"&&f!==null&&typeof f.text=="string"?f.text:"").filter(Boolean).join(`
2
-
3
- `));if(!h)return;const F=typeof c?.model=="string"?c.model:null;t.push({agentType:"claude",sourceMode:"claude_transcript_import",sourceSessionKey:o,sourceEventKey:C(e,d,"assistant"),cursor:j(e,d),role:"agent",ts:y,payload:h,title:p,titleSource:"first_user_fallback",modelId:F,workDir:m??a})}}}),events:t}}export{V as inspectNativeSessionFile,I as listClaudeTranscriptFiles,G as listCodexRolloutFiles,M as listNativeTranscriptRoots,Z as listOpenCodeSessionFiles,B as lookupClaudeTranscriptCwd,H as lookupCodexThreadName,X as normalizeNativeTimestamp,J as parseClaudeTranscriptChunk,Q as parseCodexRolloutChunk,$ as parseOpenCodeSessionFile,P as parseOpenCodeSessionSnapshot};
1
+ // @arch docs/architecture/cli/native-session-fusion.md
2
+ // @test src/__tests__/native-fusion-parsers.test.ts
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ export { listCodexRolloutFiles, lookupCodexThreadName, parseCodexRolloutChunk, } from './codex-parser.js';
7
+ export { inspectNativeSessionFile, normalizeNativeTimestamp } from './session-header.js';
8
+ export { listOpenCodeSessionFiles, parseOpenCodeSessionFile, parseOpenCodeSessionSnapshot, } from './opencode-parser.js';
9
+ import { makeCursor, makeEventKey, normalizeText, readClaudeEventCwd, readJsonlLines, readTimestamp, safeParse, isClaudeSubagentTranscript, shouldIgnoreClaudeProjectDir, shouldIgnoreClaudeTranscriptPath, shouldIgnoreNativeScanPath, } from './parser-common.js';
10
+ export function listNativeTranscriptRoots() {
11
+ return [
12
+ path.join(os.homedir(), '.codex', 'sessions'),
13
+ path.join(os.homedir(), '.codex', 'archived_sessions'),
14
+ path.join(os.homedir(), '.claude', 'projects'),
15
+ path.join(os.homedir(), '.local', 'share', 'opencode'),
16
+ ];
17
+ }
18
+ function relativeProjectDir(cwd) {
19
+ const home = os.homedir();
20
+ if (cwd.startsWith(home))
21
+ return cwd.slice(home.length).replace(/^\//, '') || '~';
22
+ return cwd.replace(/\//g, '-');
23
+ }
24
+ export function listClaudeTranscriptFiles() {
25
+ const root = path.join(os.homedir(), '.claude', 'projects');
26
+ if (!fs.existsSync(root))
27
+ return [];
28
+ const files = [];
29
+ const walk = (dir) => {
30
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ if (dir === root && shouldIgnoreClaudeProjectDir(entry.name))
34
+ continue;
35
+ walk(full);
36
+ }
37
+ else if (entry.isFile() && entry.name.endsWith('.jsonl'))
38
+ files.push(full);
39
+ }
40
+ };
41
+ walk(root);
42
+ return files.sort();
43
+ }
44
+ export function lookupClaudeTranscriptCwd(sourceSessionKey) {
45
+ if (!sourceSessionKey)
46
+ return null;
47
+ const match = listClaudeTranscriptFiles().find((filePath) => path.basename(filePath, '.jsonl') === sourceSessionKey);
48
+ if (!match)
49
+ return null;
50
+ let found = null;
51
+ readJsonlLines(match, 0, (line) => {
52
+ if (found || !line.trim())
53
+ return;
54
+ const parsed = safeParse(line);
55
+ if (!parsed)
56
+ return;
57
+ const cwd = readClaudeEventCwd(parsed);
58
+ if (cwd)
59
+ found = cwd;
60
+ });
61
+ return found;
62
+ }
63
+ export function parseClaudeTranscriptChunk(filePath, startOffset) {
64
+ const fileSize = fs.statSync(filePath).size;
65
+ if (isClaudeSubagentTranscript(filePath) || shouldIgnoreClaudeTranscriptPath(filePath)) {
66
+ return { nextOffset: fileSize, events: [] };
67
+ }
68
+ const events = [];
69
+ const sourceSessionKey = path.basename(filePath, '.jsonl');
70
+ const fallbackWorkDir = relativeProjectDir(path.dirname(filePath).replace(path.join(os.homedir(), '.claude', 'projects') + path.sep, ''));
71
+ if (shouldIgnoreNativeScanPath(fallbackWorkDir)) {
72
+ return { nextOffset: fileSize, events: [] };
73
+ }
74
+ let title = '';
75
+ const nextOffset = readJsonlLines(filePath, startOffset, (line, lineOffset) => {
76
+ const parsed = safeParse(line);
77
+ if (!parsed)
78
+ return;
79
+ const ts = readTimestamp(parsed.timestamp);
80
+ const type = typeof parsed.type === 'string' ? parsed.type : '';
81
+ if (!ts || !type)
82
+ return;
83
+ const eventCwd = readClaudeEventCwd(parsed);
84
+ if (eventCwd && shouldIgnoreNativeScanPath(eventCwd))
85
+ return;
86
+ if (type === 'user') {
87
+ const message = typeof parsed.message === 'object' && parsed.message !== null
88
+ ? parsed.message
89
+ : null;
90
+ const text = typeof message?.content === 'string' ? normalizeText(message.content) : '';
91
+ if (!text)
92
+ return;
93
+ if (!title)
94
+ title = text.slice(0, 80);
95
+ events.push({
96
+ agentType: 'claude',
97
+ sourceMode: 'claude_transcript_import',
98
+ sourceSessionKey,
99
+ sourceEventKey: makeEventKey(filePath, lineOffset, 'user'),
100
+ cursor: makeCursor(filePath, lineOffset),
101
+ role: 'user',
102
+ ts,
103
+ payload: text,
104
+ title,
105
+ titleSource: 'first_user_fallback',
106
+ workDir: eventCwd ?? fallbackWorkDir,
107
+ });
108
+ }
109
+ else if (type === 'assistant') {
110
+ const message = typeof parsed.message === 'object' && parsed.message !== null
111
+ ? parsed.message
112
+ : null;
113
+ const content = Array.isArray(message?.content) ? message.content : [];
114
+ const text = normalizeText(content
115
+ .map((part) => typeof part === 'object' && part !== null && typeof part.text === 'string'
116
+ ? part.text
117
+ : '')
118
+ .filter(Boolean)
119
+ .join('\n\n'));
120
+ if (!text)
121
+ return;
122
+ const modelId = typeof message?.model === 'string' ? message.model : null;
123
+ events.push({
124
+ agentType: 'claude',
125
+ sourceMode: 'claude_transcript_import',
126
+ sourceSessionKey,
127
+ sourceEventKey: makeEventKey(filePath, lineOffset, 'assistant'),
128
+ cursor: makeCursor(filePath, lineOffset),
129
+ role: 'agent',
130
+ ts,
131
+ payload: text,
132
+ title,
133
+ titleSource: 'first_user_fallback',
134
+ modelId,
135
+ workDir: eventCwd ?? fallbackWorkDir,
136
+ });
137
+ }
138
+ });
139
+ return { nextOffset, events };
140
+ }