@kraki/tentacle 0.11.20 → 0.12.1
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/dist/banner.js +1 -1
- package/dist/history-parser.d.ts +37 -0
- package/dist/history-parser.js +190 -0
- package/dist/history-parser.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/relay-client.d.ts +2 -0
- package/dist/relay-client.js +161 -0
- package/dist/relay-client.js.map +1 -1
- package/dist/session-manager.d.ts +31 -0
- package/dist/session-manager.js +75 -0
- package/dist/session-manager.js.map +1 -1
- package/dist/session-scanner.d.ts +27 -0
- package/dist/session-scanner.js +205 -0
- package/dist/session-scanner.js.map +1 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.js +180 -63
- package/dist/update.js.map +1 -1
- package/package.json +2 -2
package/dist/banner.js
CHANGED
|
@@ -7,7 +7,7 @@ import bannerData from './banner-data.json' with { type: 'json' };
|
|
|
7
7
|
const data = bannerData;
|
|
8
8
|
const SCRAMBLE = '!@#$%^&*=+<>~/';
|
|
9
9
|
const TITLE = 'KRAKI';
|
|
10
|
-
const TITLE_COLORS = ['#00c9a7', '#00b4d8', '#
|
|
10
|
+
const TITLE_COLORS = ['#00c9a7', '#00b4d8', '#ea6046', '#0891b2', '#ea6046'];
|
|
11
11
|
const BLOCK_MAP = {
|
|
12
12
|
'.': '░', ':': '░', '-': '▒', '=': '▓', '+': '█', '*': '█', '#': '█', '%': '█', '@': '█',
|
|
13
13
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History parser — converts Copilot SDK events.jsonl into Kraki protocol messages.
|
|
3
|
+
*
|
|
4
|
+
* Used during import to backfill conversation history so the arm can display
|
|
5
|
+
* the full session context from before Kraki attached.
|
|
6
|
+
*
|
|
7
|
+
* Reads events.jsonl line by line (streaming) to handle large files.
|
|
8
|
+
* Caps output at MAX_BACKFILL_MESSAGES most recent messages.
|
|
9
|
+
*/
|
|
10
|
+
export interface BackfilledMessage {
|
|
11
|
+
seq: number;
|
|
12
|
+
type: string;
|
|
13
|
+
payload: string;
|
|
14
|
+
ts: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ParsedSessionMeta {
|
|
17
|
+
model?: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
gitRoot?: string;
|
|
20
|
+
branch?: string;
|
|
21
|
+
repository?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse a Copilot SDK events.jsonl file into Kraki protocol messages.
|
|
25
|
+
* Returns up to MAX_BACKFILL_MESSAGES most recent messages and session metadata.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseEventsFile(eventsPath: string): {
|
|
28
|
+
messages: BackfilledMessage[];
|
|
29
|
+
meta: ParsedSessionMeta;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Parse events.jsonl from a session directory.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSessionHistory(sessionDir: string): {
|
|
35
|
+
messages: BackfilledMessage[];
|
|
36
|
+
meta: ParsedSessionMeta;
|
|
37
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History parser — converts Copilot SDK events.jsonl into Kraki protocol messages.
|
|
3
|
+
*
|
|
4
|
+
* Used during import to backfill conversation history so the arm can display
|
|
5
|
+
* the full session context from before Kraki attached.
|
|
6
|
+
*
|
|
7
|
+
* Reads events.jsonl line by line (streaming) to handle large files.
|
|
8
|
+
* Caps output at MAX_BACKFILL_MESSAGES most recent messages.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createLogger } from './logger.js';
|
|
13
|
+
const logger = createLogger('history-parser');
|
|
14
|
+
/** Max messages to backfill from events.jsonl (most recent kept). */
|
|
15
|
+
const MAX_BACKFILL_MESSAGES = 500;
|
|
16
|
+
// ── Parser ──────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Parse a Copilot SDK events.jsonl file into Kraki protocol messages.
|
|
19
|
+
* Returns up to MAX_BACKFILL_MESSAGES most recent messages and session metadata.
|
|
20
|
+
*/
|
|
21
|
+
export function parseEventsFile(eventsPath) {
|
|
22
|
+
if (!existsSync(eventsPath)) {
|
|
23
|
+
return { messages: [], meta: {} };
|
|
24
|
+
}
|
|
25
|
+
const messages = [];
|
|
26
|
+
const meta = {};
|
|
27
|
+
let seq = 0;
|
|
28
|
+
// Stream line by line using a buffer to handle large files
|
|
29
|
+
const content = readFileChunked(eventsPath);
|
|
30
|
+
for (const line of content.split('\n')) {
|
|
31
|
+
if (!line.trim())
|
|
32
|
+
continue;
|
|
33
|
+
let event;
|
|
34
|
+
try {
|
|
35
|
+
event = JSON.parse(line);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue; // skip corrupt lines
|
|
39
|
+
}
|
|
40
|
+
const ts = event.timestamp ?? new Date().toISOString();
|
|
41
|
+
const converted = convertEvent(event, ts, meta);
|
|
42
|
+
if (converted) {
|
|
43
|
+
seq++;
|
|
44
|
+
messages.push({ seq, ...converted });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Keep only the most recent messages
|
|
48
|
+
if (messages.length > MAX_BACKFILL_MESSAGES) {
|
|
49
|
+
const trimmed = messages.slice(messages.length - MAX_BACKFILL_MESSAGES);
|
|
50
|
+
// Re-number seq starting from 1
|
|
51
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
52
|
+
trimmed[i].seq = i + 1;
|
|
53
|
+
}
|
|
54
|
+
return { messages: trimmed, meta };
|
|
55
|
+
}
|
|
56
|
+
return { messages, meta };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse events.jsonl from a session directory.
|
|
60
|
+
*/
|
|
61
|
+
export function parseSessionHistory(sessionDir) {
|
|
62
|
+
return parseEventsFile(join(sessionDir, 'events.jsonl'));
|
|
63
|
+
}
|
|
64
|
+
// ── Event conversion ────────────────────────────────────
|
|
65
|
+
function convertEvent(event, ts, meta) {
|
|
66
|
+
switch (event.type) {
|
|
67
|
+
case 'session.start': {
|
|
68
|
+
// Extract metadata, don't emit a message
|
|
69
|
+
const ctx = event.data.context;
|
|
70
|
+
if (event.data.selectedModel)
|
|
71
|
+
meta.model = event.data.selectedModel;
|
|
72
|
+
if (ctx?.cwd)
|
|
73
|
+
meta.cwd = ctx.cwd;
|
|
74
|
+
if (ctx?.gitRoot)
|
|
75
|
+
meta.gitRoot = ctx.gitRoot;
|
|
76
|
+
if (ctx?.branch)
|
|
77
|
+
meta.branch = ctx.branch;
|
|
78
|
+
if (ctx?.repository)
|
|
79
|
+
meta.repository = ctx.repository;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
case 'user.message': {
|
|
83
|
+
const content = event.data.content ?? '';
|
|
84
|
+
// Use transformedContent if available (has system context stripped)
|
|
85
|
+
return {
|
|
86
|
+
type: 'user_message',
|
|
87
|
+
payload: JSON.stringify({ content }),
|
|
88
|
+
ts,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case 'assistant.message': {
|
|
92
|
+
const content = event.data.content ?? '';
|
|
93
|
+
if (!content)
|
|
94
|
+
return null; // skip empty messages (SDK sends these before tool calls)
|
|
95
|
+
return {
|
|
96
|
+
type: 'agent_message',
|
|
97
|
+
payload: JSON.stringify({ content }),
|
|
98
|
+
ts,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
case 'tool.execution_start': {
|
|
102
|
+
const toolName = event.data.toolName ?? 'unknown';
|
|
103
|
+
const args = (event.data.arguments ?? event.data.args ?? {});
|
|
104
|
+
const toolCallId = event.data.toolCallId;
|
|
105
|
+
return {
|
|
106
|
+
type: 'tool_start',
|
|
107
|
+
payload: JSON.stringify({ toolName, args, toolCallId }),
|
|
108
|
+
ts,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
case 'tool.execution_complete': {
|
|
112
|
+
const toolName = event.data.toolName ?? 'unknown';
|
|
113
|
+
const toolCallId = event.data.toolCallId;
|
|
114
|
+
const success = event.data.success;
|
|
115
|
+
const rawResult = event.data.result;
|
|
116
|
+
const resultObj = typeof rawResult === 'object' && rawResult !== null
|
|
117
|
+
? rawResult
|
|
118
|
+
: null;
|
|
119
|
+
const result = resultObj?.content
|
|
120
|
+
?? (typeof rawResult === 'string' ? rawResult : (event.data.output ?? ''));
|
|
121
|
+
// Extract model from the first tool completion that has it
|
|
122
|
+
if (!meta.model && event.data.model) {
|
|
123
|
+
meta.model = event.data.model;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
type: 'tool_complete',
|
|
127
|
+
payload: JSON.stringify({
|
|
128
|
+
toolName,
|
|
129
|
+
args: {},
|
|
130
|
+
result: result.slice(0, 5000), // Cap result size for backfill
|
|
131
|
+
toolCallId,
|
|
132
|
+
success,
|
|
133
|
+
}),
|
|
134
|
+
ts,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case 'assistant.turn_end': {
|
|
138
|
+
return {
|
|
139
|
+
type: 'idle',
|
|
140
|
+
payload: JSON.stringify({}),
|
|
141
|
+
ts,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Skip events that don't map to Kraki messages
|
|
145
|
+
case 'assistant.message_delta':
|
|
146
|
+
case 'assistant.turn_start':
|
|
147
|
+
case 'hook.start':
|
|
148
|
+
case 'hook.end':
|
|
149
|
+
case 'subagent.started':
|
|
150
|
+
case 'session.idle':
|
|
151
|
+
case 'session.shutdown':
|
|
152
|
+
case 'session.info':
|
|
153
|
+
case 'session.warning':
|
|
154
|
+
case 'session.task_complete':
|
|
155
|
+
case 'assistant.usage':
|
|
156
|
+
case 'session.title_changed':
|
|
157
|
+
return null;
|
|
158
|
+
default:
|
|
159
|
+
// Unknown event type — skip
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── File reading helper ─────────────────────────────────
|
|
164
|
+
function readFileChunked(filePath) {
|
|
165
|
+
// For files under 10MB, just read the whole thing
|
|
166
|
+
const stat = statSync(filePath);
|
|
167
|
+
if (stat.size <= 10 * 1024 * 1024) {
|
|
168
|
+
const fd = openSync(filePath, 'r');
|
|
169
|
+
const buf = Buffer.alloc(stat.size);
|
|
170
|
+
readSync(fd, buf, 0, stat.size, 0);
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
return buf.toString('utf8');
|
|
173
|
+
}
|
|
174
|
+
// For larger files, read in chunks
|
|
175
|
+
logger.info({ filePath, size: stat.size }, 'Large events.jsonl — reading in chunks');
|
|
176
|
+
const fd = openSync(filePath, 'r');
|
|
177
|
+
const chunks = [];
|
|
178
|
+
const chunkSize = 1024 * 1024; // 1MB chunks
|
|
179
|
+
let offset = 0;
|
|
180
|
+
while (offset < stat.size) {
|
|
181
|
+
const readSize = Math.min(chunkSize, stat.size - offset);
|
|
182
|
+
const buf = Buffer.alloc(readSize);
|
|
183
|
+
readSync(fd, buf, 0, readSize, offset);
|
|
184
|
+
chunks.push(buf);
|
|
185
|
+
offset += readSize;
|
|
186
|
+
}
|
|
187
|
+
closeSync(fd);
|
|
188
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=history-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-parser.js","sourceRoot":"","sources":["../src/history-parser.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,11 @@ export { AgentAdapter, CopilotAdapter, parsePermission } from './adapters/index.
|
|
|
2
2
|
export type { CreateSessionConfig, SessionInfo, PermissionDecision, SessionCreatedEvent, MessageEvent, MessageDeltaEvent, PermissionRequestEvent, QuestionRequestEvent, ToolStartEvent, ToolCompleteEvent, SessionEndedEvent, ErrorEvent, ParsedPermission, } from './adapters/index.js';
|
|
3
3
|
export { loadConfig, saveConfig, configExists, getOrCreateDeviceId } from './config.js';
|
|
4
4
|
export { SessionManager } from './session-manager.js';
|
|
5
|
-
export type { SessionContext, SessionMeta, RunRecord, LoggedMessage } from './session-manager.js';
|
|
5
|
+
export type { SessionContext, SessionMeta, RunRecord, LoggedMessage, SessionLink } from './session-manager.js';
|
|
6
6
|
export { RelayClient } from './relay-client.js';
|
|
7
7
|
export type { RelayClientOptions, RelayClientState } from './relay-client.js';
|
|
8
8
|
export { KeyManager } from './key-manager.js';
|
|
9
|
+
export { scanLocalSessions, filterSessions } from './session-scanner.js';
|
|
10
|
+
export type { ScanOptions, SessionFilter } from './session-scanner.js';
|
|
11
|
+
export { parseEventsFile, parseSessionHistory } from './history-parser.js';
|
|
12
|
+
export type { BackfilledMessage, ParsedSessionMeta } from './history-parser.js';
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,6 @@ export { loadConfig, saveConfig, configExists, getOrCreateDeviceId } from './con
|
|
|
8
8
|
export { SessionManager } from './session-manager.js';
|
|
9
9
|
export { RelayClient } from './relay-client.js';
|
|
10
10
|
export { KeyManager } from './key-manager.js';
|
|
11
|
+
export { scanLocalSessions, filterSessions } from './session-scanner.js';
|
|
12
|
+
export { parseEventsFile, parseSessionHistory } from './history-parser.js';
|
|
11
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,0DAA0D;AAC1D,+DAA+D;AAC/D,0CAA0C;AAE1C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkBpF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,EAAE;AACF,0DAA0D;AAC1D,+DAA+D;AAC/D,0CAA0C;AAE1C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkBpF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/relay-client.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export declare class RelayClient {
|
|
|
90
90
|
private handleConsumerMessage;
|
|
91
91
|
private handleCreateSession;
|
|
92
92
|
private handleForkSession;
|
|
93
|
+
private handleRequestLocalSessions;
|
|
94
|
+
private handleImportSession;
|
|
93
95
|
/**
|
|
94
96
|
* Process queued unicast envelopes delivered by the relay in auth_ok.
|
|
95
97
|
* These are messages sent by arms while this tentacle was offline.
|
package/dist/relay-client.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import { WebSocket } from 'ws';
|
|
9
9
|
import { appendFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
11
12
|
import { importPublicKey, encryptToBlob, decryptFromBlob, signChallenge } from '@kraki/crypto';
|
|
13
|
+
import { scanLocalSessions, filterSessions } from './session-scanner.js';
|
|
14
|
+
import { parseSessionHistory } from './history-parser.js';
|
|
12
15
|
import { createLogger } from './logger.js';
|
|
13
16
|
import { getKrakiHome } from './config.js';
|
|
14
17
|
const logger = createLogger('relay-client');
|
|
@@ -310,6 +313,15 @@ export class RelayClient {
|
|
|
310
313
|
this.handleClientLog(msg.deviceId, payload?.entries);
|
|
311
314
|
return;
|
|
312
315
|
}
|
|
316
|
+
// ── Local session sync (no sessionId) ────────────────
|
|
317
|
+
if (msg.type === 'request_local_sessions') {
|
|
318
|
+
this.handleRequestLocalSessions(msg);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (msg.type === 'import_session') {
|
|
322
|
+
this.handleImportSession(msg);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
313
325
|
const sessionId = msg.sessionId;
|
|
314
326
|
if (!sessionId)
|
|
315
327
|
return;
|
|
@@ -366,6 +378,7 @@ export class RelayClient {
|
|
|
366
378
|
this.adapter.killSession(sessionId)
|
|
367
379
|
.catch((err) => logger.error({ err, sessionId }, 'killSession on delete failed'))
|
|
368
380
|
.finally(() => {
|
|
381
|
+
this.sessionManager.removeLinkByKrakiId(sessionId);
|
|
369
382
|
this.sessionManager.deleteSession(sessionId);
|
|
370
383
|
this.lastAgentContent.delete(sessionId);
|
|
371
384
|
this.send({ type: 'session_deleted', sessionId, payload: {} });
|
|
@@ -512,6 +525,154 @@ export class RelayClient {
|
|
|
512
525
|
});
|
|
513
526
|
}
|
|
514
527
|
}
|
|
528
|
+
// ── Local session sync handlers ───────────────────────
|
|
529
|
+
handleRequestLocalSessions(msg) {
|
|
530
|
+
if (msg.type !== 'request_local_sessions')
|
|
531
|
+
return;
|
|
532
|
+
const { requestId, filter } = msg.payload;
|
|
533
|
+
const requesterDeviceId = msg.deviceId;
|
|
534
|
+
const requesterKey = this.consumerKeys.get(requesterDeviceId);
|
|
535
|
+
try {
|
|
536
|
+
let sessions = scanLocalSessions();
|
|
537
|
+
const linkedIds = this.sessionManager.getLinkedIds();
|
|
538
|
+
// Mark sessions that are already linked
|
|
539
|
+
for (const s of sessions) {
|
|
540
|
+
const link = this.sessionManager.getLink(s.sessionId);
|
|
541
|
+
if (link)
|
|
542
|
+
s.linkedKrakiSessionId = link.krakiSessionId;
|
|
543
|
+
}
|
|
544
|
+
// Exclude sessions that Kraki already manages (created natively, not imported)
|
|
545
|
+
const krakiSessionIds = new Set(this.sessionManager.getSessionList().map(s => s.id));
|
|
546
|
+
sessions = sessions.filter(s => !krakiSessionIds.has(s.sessionId) || s.linkedKrakiSessionId);
|
|
547
|
+
// Apply filters
|
|
548
|
+
if (filter) {
|
|
549
|
+
sessions = filterSessions(sessions, filter, linkedIds);
|
|
550
|
+
}
|
|
551
|
+
const response = {
|
|
552
|
+
type: 'local_sessions_list',
|
|
553
|
+
deviceId: this.authInfo?.deviceId ?? '',
|
|
554
|
+
seq: ++this.seqCounter,
|
|
555
|
+
timestamp: new Date().toISOString(),
|
|
556
|
+
payload: { sessions, requestId },
|
|
557
|
+
};
|
|
558
|
+
if (requesterKey) {
|
|
559
|
+
this.sendUnicastTo(requesterDeviceId, requesterKey, response);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// No encryption key — broadcast (works in open/non-E2E mode)
|
|
563
|
+
this.send(response);
|
|
564
|
+
}
|
|
565
|
+
logger.debug({ count: sessions.length, requestId }, 'Sent local sessions list');
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
logger.error({ err }, 'Failed to scan local sessions');
|
|
569
|
+
const response = {
|
|
570
|
+
type: 'local_sessions_list',
|
|
571
|
+
deviceId: this.authInfo?.deviceId ?? '',
|
|
572
|
+
seq: ++this.seqCounter,
|
|
573
|
+
timestamp: new Date().toISOString(),
|
|
574
|
+
payload: { sessions: [], requestId },
|
|
575
|
+
};
|
|
576
|
+
if (requesterKey) {
|
|
577
|
+
this.sendUnicastTo(requesterDeviceId, requesterKey, response);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
this.send(response);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async handleImportSession(msg) {
|
|
585
|
+
if (msg.type !== 'import_session')
|
|
586
|
+
return;
|
|
587
|
+
const { requestId, localSessionId } = msg.payload;
|
|
588
|
+
// Check if already linked
|
|
589
|
+
const existing = this.sessionManager.getLink(localSessionId);
|
|
590
|
+
if (existing) {
|
|
591
|
+
this.send({
|
|
592
|
+
type: 'error',
|
|
593
|
+
sessionId: '',
|
|
594
|
+
payload: { message: `Session already imported as ${existing.krakiSessionId} (requestId: ${requestId})` },
|
|
595
|
+
});
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
// Use localSessionId as Kraki session ID (shared identity)
|
|
600
|
+
const krakiSessionId = localSessionId;
|
|
601
|
+
// Parse events.jsonl for backfill
|
|
602
|
+
const sessionStateDir = join(homedir(), '.copilot', 'session-state', localSessionId);
|
|
603
|
+
const { messages: backfilledMessages, meta: parsedMeta } = parseSessionHistory(sessionStateDir);
|
|
604
|
+
// Scan to get local session metadata
|
|
605
|
+
const scanResults = scanLocalSessions();
|
|
606
|
+
const localSession = scanResults.find(s => s.sessionId === localSessionId);
|
|
607
|
+
// Create Kraki session
|
|
608
|
+
this.sessionManager.createSession('copilot', parsedMeta.model ?? localSession?.model, krakiSessionId);
|
|
609
|
+
// Persist source, title, model, and original creation time
|
|
610
|
+
this.sessionManager.updateMeta(krakiSessionId, {
|
|
611
|
+
source: localSession?.source ?? 'copilot-cli',
|
|
612
|
+
autoTitle: localSession?.summary?.slice(0, 100),
|
|
613
|
+
model: parsedMeta.model ?? localSession?.model,
|
|
614
|
+
createdAt: localSession?.startTime,
|
|
615
|
+
});
|
|
616
|
+
// Backfill messages
|
|
617
|
+
for (const m of backfilledMessages) {
|
|
618
|
+
this.sessionManager.appendMessage(krakiSessionId, m.type, m.payload);
|
|
619
|
+
}
|
|
620
|
+
// Write link table entry
|
|
621
|
+
this.sessionManager.addLink({
|
|
622
|
+
localSessionId,
|
|
623
|
+
krakiSessionId,
|
|
624
|
+
source: localSession?.source ?? 'copilot-cli',
|
|
625
|
+
cwd: localSession?.cwd,
|
|
626
|
+
branch: localSession?.branch,
|
|
627
|
+
linkedAt: new Date().toISOString(),
|
|
628
|
+
});
|
|
629
|
+
// Map requestId for session_created correlation
|
|
630
|
+
if (requestId) {
|
|
631
|
+
this.pendingRequestIds.set(krakiSessionId, requestId);
|
|
632
|
+
}
|
|
633
|
+
// Resume the session via SDK. Use createSession with the existing sessionId
|
|
634
|
+
// so the CLI server discovers the state on disk. resumeSession only works
|
|
635
|
+
// for sessions the CLI server has already loaded in memory.
|
|
636
|
+
const cwd = localSession?.cwd ?? parsedMeta.cwd ?? '/';
|
|
637
|
+
try {
|
|
638
|
+
await this.adapter.createSession({
|
|
639
|
+
sessionId: krakiSessionId,
|
|
640
|
+
model: parsedMeta.model,
|
|
641
|
+
cwd,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
catch (createErr) {
|
|
645
|
+
// SDK resume failed — still keep backfilled history, but manually
|
|
646
|
+
// send session_created so the web UI knows the session exists.
|
|
647
|
+
logger.warn({ err: createErr.message, krakiSessionId }, 'SDK resume failed — session imported as idle');
|
|
648
|
+
const meta = this.sessionManager.getMeta(krakiSessionId);
|
|
649
|
+
this.send({
|
|
650
|
+
type: 'session_created',
|
|
651
|
+
sessionId: krakiSessionId,
|
|
652
|
+
payload: {
|
|
653
|
+
agent: 'copilot',
|
|
654
|
+
model: parsedMeta.model ?? localSession?.model,
|
|
655
|
+
requestId,
|
|
656
|
+
lastSeq: meta?.lastSeq ?? 0,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
if (requestId)
|
|
660
|
+
this.pendingRequestIds.delete(krakiSessionId);
|
|
661
|
+
}
|
|
662
|
+
// Mark idle — will transition to active when user sends a message
|
|
663
|
+
this.sessionManager.markIdle(krakiSessionId);
|
|
664
|
+
this.send({ type: 'idle', sessionId: krakiSessionId, payload: {} });
|
|
665
|
+
logger.info({ localSessionId, krakiSessionId, backfilled: backfilledMessages.length }, 'Session imported');
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
logger.error({ err, localSessionId }, 'Import session failed');
|
|
669
|
+
this.send({
|
|
670
|
+
type: 'error',
|
|
671
|
+
sessionId: '',
|
|
672
|
+
payload: { message: `Failed to import session: ${err.message} (requestId: ${requestId})` },
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
515
676
|
// ── Pending message processing ─────────────────────
|
|
516
677
|
/**
|
|
517
678
|
* Process queued unicast envelopes delivered by the relay in auth_ok.
|