@nordbyte/nordrelay 0.3.0 → 0.4.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.
Files changed (52) hide show
  1. package/.env.example +45 -2
  2. package/README.md +227 -47
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +333 -161
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +15 -2
  17. package/dist/config.js +113 -9
  18. package/dist/context-key.js +23 -0
  19. package/dist/hermes-api.js +150 -0
  20. package/dist/hermes-auth.js +96 -0
  21. package/dist/hermes-cli.js +19 -0
  22. package/dist/hermes-launch.js +57 -0
  23. package/dist/hermes-session.js +477 -0
  24. package/dist/hermes-state.js +609 -0
  25. package/dist/index.js +51 -8
  26. package/dist/openclaw-auth.js +27 -0
  27. package/dist/openclaw-cli.js +19 -0
  28. package/dist/openclaw-gateway.js +285 -0
  29. package/dist/openclaw-launch.js +65 -0
  30. package/dist/openclaw-session.js +549 -0
  31. package/dist/openclaw-state.js +409 -0
  32. package/dist/operations.js +84 -3
  33. package/dist/pi-auth.js +59 -0
  34. package/dist/pi-launch.js +61 -0
  35. package/dist/pi-rpc.js +18 -0
  36. package/dist/pi-session.js +103 -15
  37. package/dist/pi-state.js +253 -0
  38. package/dist/relay-runtime.js +1073 -22
  39. package/dist/session-format.js +28 -18
  40. package/dist/session-registry.js +43 -18
  41. package/dist/settings-service.js +80 -26
  42. package/dist/state-backend.js +17 -8
  43. package/dist/web-dashboard-ui.js +18 -0
  44. package/dist/web-dashboard.js +463 -55
  45. package/dist/web-state.js +131 -0
  46. package/docker-compose.yml +1 -1
  47. package/package.json +8 -3
  48. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  49. package/plugins/nordrelay/commands/remote.md +2 -2
  50. package/plugins/nordrelay/scripts/nordrelay.mjs +173 -20
  51. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  52. package/CHANGELOG.md +0 -17
@@ -0,0 +1,590 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function getDefaultClaudeCodeHome() {
5
+ return path.join(os.homedir(), ".claude");
6
+ }
7
+ export function resolveClaudeCodeProjectsDir(options = {}) {
8
+ const configDir = options.configDir ?? process.env.CLAUDE_CONFIG_DIR;
9
+ const home = options.claudeHome ?? getDefaultClaudeCodeHome();
10
+ return path.join(configDir || home, "projects");
11
+ }
12
+ export function listClaudeCodeSessions(limit = 20, options = {}) {
13
+ const projectsDir = resolveClaudeCodeProjectsDir(options);
14
+ if (!existsSync(projectsDir)) {
15
+ return [];
16
+ }
17
+ const records = [];
18
+ for (const projectKey of safeReadDir(projectsDir)) {
19
+ const projectPath = path.join(projectsDir, projectKey);
20
+ if (!safeStat(projectPath)?.isDirectory()) {
21
+ continue;
22
+ }
23
+ for (const fileName of safeReadDir(projectPath)) {
24
+ if (!fileName.endsWith(".jsonl")) {
25
+ continue;
26
+ }
27
+ const sessionPath = path.join(projectPath, fileName);
28
+ const record = readClaudeCodeSessionRecord(sessionPath, projectKey, options);
29
+ if (record) {
30
+ records.push(record);
31
+ }
32
+ }
33
+ }
34
+ return records
35
+ .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())
36
+ .slice(0, Math.max(1, limit));
37
+ }
38
+ export function getClaudeCodeSession(idOrPath, options = {}) {
39
+ const normalized = idOrPath.trim();
40
+ if (!normalized) {
41
+ return null;
42
+ }
43
+ if (existsSync(normalized)) {
44
+ return readClaudeCodeSessionRecord(normalized, path.basename(path.dirname(normalized)), options);
45
+ }
46
+ return listClaudeCodeSessions(500, options).find((record) => record.id === normalized ||
47
+ record.id.startsWith(normalized) ||
48
+ path.basename(record.sessionPath, ".jsonl") === normalized ||
49
+ record.sessionPath === normalized) ?? null;
50
+ }
51
+ export function getClaudeCodeSessionActivity(idOrPath, options = {}) {
52
+ return getClaudeCodeSessionSnapshot(idOrPath, { ...options, maxEvents: 0 })?.activity ?? null;
53
+ }
54
+ export function getClaudeCodeSessionActivityLog(idOrPath, limit = 50, options = {}) {
55
+ return getClaudeCodeSessionSnapshot(idOrPath, { ...options, maxEvents: Math.max(1, limit) })?.events ?? [];
56
+ }
57
+ export function getClaudeCodeSessionSnapshot(idOrPath, options = {}) {
58
+ const record = getClaudeCodeSession(idOrPath, options);
59
+ if (!record) {
60
+ return null;
61
+ }
62
+ return readClaudeCodeSessionSnapshot(record, options);
63
+ }
64
+ export function getClaudeCodeSessionDiagnostics(idOrPath, options = {}) {
65
+ const projectsDir = resolveClaudeCodeProjectsDir(options);
66
+ if (!idOrPath) {
67
+ return {
68
+ projectsDir,
69
+ sessionPath: null,
70
+ lineCount: 0,
71
+ status: "unavailable",
72
+ reason: "no active Claude Code session",
73
+ updatedAt: null,
74
+ };
75
+ }
76
+ const snapshot = getClaudeCodeSessionSnapshot(idOrPath, { ...options, maxEvents: 0 });
77
+ if (!snapshot) {
78
+ return {
79
+ projectsDir,
80
+ sessionPath: null,
81
+ lineCount: 0,
82
+ status: "unavailable",
83
+ reason: "Claude Code session file not found or unreadable",
84
+ updatedAt: null,
85
+ };
86
+ }
87
+ const status = snapshot.activity.active ? "active" : snapshot.activity.stale ? "stale" : "idle";
88
+ return {
89
+ projectsDir,
90
+ sessionPath: snapshot.sourcePath,
91
+ lineCount: snapshot.lineCount,
92
+ status,
93
+ reason: snapshot.activity.active
94
+ ? "latest Claude Code user turn has no terminal response yet"
95
+ : snapshot.activity.stale
96
+ ? "open Claude Code turn exceeded stale timeout"
97
+ : "latest Claude Code turn has a terminal response",
98
+ updatedAt: snapshot.activity.updatedAt,
99
+ };
100
+ }
101
+ export function listClaudeCodeWorkspaces(options = {}) {
102
+ const workspaces = new Set();
103
+ if (options.workspace) {
104
+ workspaces.add(options.workspace);
105
+ }
106
+ for (const record of listClaudeCodeSessions(500, options)) {
107
+ if (record.cwd) {
108
+ workspaces.add(record.cwd);
109
+ }
110
+ }
111
+ return [...workspaces].sort((left, right) => left.localeCompare(right));
112
+ }
113
+ export function readClaudeCodeSessionRecord(sessionPath, projectKey, options = {}) {
114
+ try {
115
+ const fileStat = statSync(sessionPath);
116
+ const lines = readJsonlLines(sessionPath);
117
+ const fileSessionId = path.basename(sessionPath, ".jsonl");
118
+ let id = fileSessionId;
119
+ let createdAt = fileStat.birthtimeMs > 0 ? fileStat.birthtime : fileStat.mtime;
120
+ let updatedAt = fileStat.mtime;
121
+ let cwd = options.workspace ?? (projectKey ? decodeClaudeCodeProjectKey(projectKey) : path.dirname(sessionPath));
122
+ let model = null;
123
+ let reasoningEffort = null;
124
+ let firstUserMessage = null;
125
+ let lastAssistantText = null;
126
+ let title = null;
127
+ let messageCount = 0;
128
+ let usage;
129
+ for (const { entry } of lines) {
130
+ id = stringValue(entry.session_id) ?? stringValue(entry.sessionId) ?? id;
131
+ cwd = stringValue(entry.cwd) ?? stringValue(objectValue(entry.message)?.cwd) ?? cwd;
132
+ title = stringValue(entry.customTitle) ?? stringValue(entry.summary) ?? title;
133
+ model = stringValue(entry.model) ?? stringValue(objectValue(entry.message)?.model) ?? model;
134
+ reasoningEffort = reasoningValue(entry) ?? reasoningEffort;
135
+ const timestamp = dateValue(entry.timestamp) ?? dateValue(entry.created_at) ?? dateValue(entry.createdAt);
136
+ if (timestamp) {
137
+ if (timestamp < createdAt) {
138
+ createdAt = timestamp;
139
+ }
140
+ if (timestamp > updatedAt) {
141
+ updatedAt = timestamp;
142
+ }
143
+ }
144
+ const type = stringValue(entry.type);
145
+ if (type === "summary") {
146
+ title = stringValue(entry.summary) ?? title;
147
+ }
148
+ if (type === "user" || type === "assistant") {
149
+ messageCount += 1;
150
+ }
151
+ if (type === "user" && !isToolResultEntry(entry) && !firstUserMessage) {
152
+ firstUserMessage = extractEntryText(entry);
153
+ }
154
+ else if (type === "assistant") {
155
+ const assistantText = extractEntryText(entry);
156
+ if (assistantText) {
157
+ lastAssistantText = assistantText;
158
+ }
159
+ }
160
+ else if (type === "result") {
161
+ usage = usageFromObject(entry.usage) ?? usage;
162
+ }
163
+ usage = usageFromObject(objectValue(entry.message)?.usage) ?? usage;
164
+ }
165
+ return {
166
+ id,
167
+ title: title ?? summarizeTitle(firstUserMessage ?? lastAssistantText),
168
+ cwd,
169
+ model,
170
+ reasoningEffort,
171
+ createdAt,
172
+ updatedAt,
173
+ firstUserMessage,
174
+ agentId: "claude-code",
175
+ sessionPath,
176
+ projectKey: projectKey ?? path.basename(path.dirname(sessionPath)),
177
+ messageCount,
178
+ usage,
179
+ };
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ function readClaudeCodeSessionSnapshot(record, options = {}) {
186
+ try {
187
+ const fileStat = statSync(record.sessionPath);
188
+ const rows = readJsonlLines(record.sessionPath);
189
+ const parsed = parseClaudeCodeActivityEvents(rows, record.id, options.afterLine ?? 0);
190
+ const latestUser = [...parsed.events].reverse().find((event) => event.kind === "user");
191
+ const latestAgent = [...parsed.events].reverse().find((event) => event.kind === "agent");
192
+ const latestTerminal = [...parsed.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
193
+ const latestTool = [...parsed.events].reverse().find((event) => event.kind === "tool" && event.toolName);
194
+ const latestTimestamp = parsed.latestTimestamp ?? fileStat.mtime;
195
+ const hasAssistantAfterUser = Boolean(latestUser && latestAgent && latestAgent.lineNumber > latestUser.lineNumber);
196
+ const terminalAfterUser = Boolean(latestUser && latestTerminal && latestTerminal.lineNumber > latestUser.lineNumber);
197
+ const openTurn = Boolean(latestUser && !hasAssistantAfterUser && !terminalAfterUser);
198
+ const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
199
+ const nowMs = options.nowMs ?? Date.now();
200
+ const stale = openTurn && nowMs - latestTimestamp.getTime() > staleAfterMs;
201
+ const active = openTurn && !stale;
202
+ const maxEvents = options.maxEvents ?? 50;
203
+ const events = maxEvents <= 0 ? [] : parsed.events.slice(-maxEvents);
204
+ return {
205
+ agentId: "claude-code",
206
+ agentLabel: "Claude Code",
207
+ threadId: record.id,
208
+ sourcePath: record.sessionPath,
209
+ sourceLabel: "Claude Code transcript",
210
+ lineCount: rows.length,
211
+ activity: {
212
+ agentId: "claude-code",
213
+ agentLabel: "Claude Code",
214
+ threadId: record.id,
215
+ sourcePath: record.sessionPath,
216
+ sourceLabel: "Claude Code transcript",
217
+ active,
218
+ stale,
219
+ turnId: latestUser?.turnId ?? latestTerminal?.turnId ?? null,
220
+ startedAt: latestUser?.timestamp ?? null,
221
+ updatedAt: latestTimestamp,
222
+ },
223
+ events,
224
+ latestAgentMessage: latestAgent?.text ?? null,
225
+ latestUserMessage: latestUser?.text ?? null,
226
+ latestToolName: latestTool?.toolName ?? null,
227
+ };
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ function parseClaudeCodeActivityEvents(rows, sessionId, afterLine) {
234
+ const events = [];
235
+ let latestTimestamp = null;
236
+ let currentTurnId = null;
237
+ const toolNamesById = new Map();
238
+ for (const { lineNumber, entry } of rows) {
239
+ const timestamp = dateValue(entry.timestamp) ?? dateValue(entry.createdAt) ?? dateValue(entry.created_at);
240
+ if (timestamp && (!latestTimestamp || timestamp > latestTimestamp)) {
241
+ latestTimestamp = timestamp;
242
+ }
243
+ const type = stringValue(entry.type) ?? "entry";
244
+ const subtype = stringValue(entry.subtype);
245
+ if (type === "user") {
246
+ if (isToolResultEntry(entry)) {
247
+ for (const tool of extractToolResults(entry)) {
248
+ pushEvent(events, afterLine, {
249
+ lineNumber,
250
+ kind: "tool",
251
+ timestamp,
252
+ type: "tool_result",
253
+ turnId: currentTurnId,
254
+ status: tool.isError ? "failed" : "finished",
255
+ text: tool.text,
256
+ toolName: tool.name ?? (tool.id ? toolNamesById.get(tool.id) : undefined) ?? "tool",
257
+ phase: null,
258
+ });
259
+ }
260
+ continue;
261
+ }
262
+ currentTurnId = `claude-code-${sessionId}-${lineNumber}`;
263
+ const text = extractEntryText(entry);
264
+ pushEvent(events, afterLine, {
265
+ lineNumber,
266
+ kind: "task",
267
+ timestamp,
268
+ type: "turn",
269
+ turnId: currentTurnId,
270
+ status: "started",
271
+ text,
272
+ toolName: null,
273
+ phase: null,
274
+ });
275
+ pushEvent(events, afterLine, {
276
+ lineNumber,
277
+ kind: "user",
278
+ timestamp,
279
+ type,
280
+ turnId: currentTurnId,
281
+ status: null,
282
+ text,
283
+ toolName: null,
284
+ phase: null,
285
+ });
286
+ continue;
287
+ }
288
+ if (type === "assistant") {
289
+ for (const tool of extractToolUses(entry)) {
290
+ if (tool.id && tool.name) {
291
+ toolNamesById.set(tool.id, tool.name);
292
+ }
293
+ pushEvent(events, afterLine, {
294
+ lineNumber,
295
+ kind: "tool",
296
+ timestamp,
297
+ type: "tool_use",
298
+ turnId: currentTurnId,
299
+ status: "started",
300
+ text: tool.text,
301
+ toolName: tool.name ?? "tool",
302
+ phase: null,
303
+ });
304
+ }
305
+ const text = extractEntryText(entry);
306
+ if (text) {
307
+ pushEvent(events, afterLine, {
308
+ lineNumber,
309
+ kind: "agent",
310
+ timestamp,
311
+ type,
312
+ turnId: currentTurnId,
313
+ status: "completed",
314
+ text,
315
+ toolName: null,
316
+ phase: null,
317
+ });
318
+ pushEvent(events, afterLine, {
319
+ lineNumber,
320
+ kind: "task",
321
+ timestamp,
322
+ type: "turn",
323
+ turnId: currentTurnId,
324
+ status: "completed",
325
+ text: null,
326
+ toolName: null,
327
+ phase: null,
328
+ });
329
+ }
330
+ continue;
331
+ }
332
+ if (type === "result") {
333
+ pushEvent(events, afterLine, {
334
+ lineNumber,
335
+ kind: "task",
336
+ timestamp,
337
+ type: subtype ?? type,
338
+ turnId: currentTurnId,
339
+ status: subtype && subtype !== "success" ? "failed" : "completed",
340
+ text: stringValue(entry.result) ?? firstString(entry.errors),
341
+ toolName: null,
342
+ phase: null,
343
+ });
344
+ continue;
345
+ }
346
+ if (type === "system" && subtype === "session_state_changed") {
347
+ const state = stringValue(entry.state);
348
+ if (state === "running" || state === "requires_action") {
349
+ pushEvent(events, afterLine, {
350
+ lineNumber,
351
+ kind: "task",
352
+ timestamp,
353
+ type: subtype,
354
+ turnId: currentTurnId,
355
+ status: "started",
356
+ text: state,
357
+ toolName: null,
358
+ phase: state,
359
+ });
360
+ }
361
+ else if (state === "idle") {
362
+ pushEvent(events, afterLine, {
363
+ lineNumber,
364
+ kind: "task",
365
+ timestamp,
366
+ type: subtype,
367
+ turnId: currentTurnId,
368
+ status: "completed",
369
+ text: state,
370
+ toolName: null,
371
+ phase: state,
372
+ });
373
+ }
374
+ continue;
375
+ }
376
+ if (type === "tool_progress" || subtype === "task_progress" || subtype === "task_started" || subtype === "task_notification") {
377
+ pushEvent(events, afterLine, {
378
+ lineNumber,
379
+ kind: "tool",
380
+ timestamp,
381
+ type: subtype ?? type,
382
+ turnId: currentTurnId,
383
+ status: subtype === "task_notification" ? "finished" : "started",
384
+ text: stringValue(entry.description) ?? stringValue(entry.summary) ?? stringValue(entry.output_file),
385
+ toolName: stringValue(entry.tool_name) ?? stringValue(entry.last_tool_name) ?? stringValue(entry.task_type) ?? "task",
386
+ phase: null,
387
+ });
388
+ }
389
+ }
390
+ return { events, latestTimestamp };
391
+ }
392
+ function readJsonlLines(filePath) {
393
+ return readFileSync(filePath, "utf8")
394
+ .split(/\r?\n/)
395
+ .map((line, index) => ({ lineNumber: index + 1, line }))
396
+ .filter(({ line }) => line.trim().length > 0)
397
+ .map(({ lineNumber, line }) => ({ lineNumber, entry: safeJsonParse(line) }))
398
+ .filter((row) => Boolean(row.entry));
399
+ }
400
+ function isToolResultEntry(entry) {
401
+ const message = objectValue(entry.message);
402
+ const content = message?.content ?? entry.content;
403
+ return Array.isArray(content) && content.some((part) => stringValue(objectValue(part)?.type) === "tool_result");
404
+ }
405
+ function extractToolUses(entry) {
406
+ const content = contentArray(entry);
407
+ return content
408
+ .map((part) => objectValue(part))
409
+ .filter((part) => part !== null && stringValue(part.type) === "tool_use")
410
+ .map((part) => ({
411
+ id: stringValue(part.id),
412
+ name: stringValue(part.name),
413
+ text: stringifyPreview(part.input),
414
+ }));
415
+ }
416
+ function extractToolResults(entry) {
417
+ const content = contentArray(entry);
418
+ return content
419
+ .map((part) => objectValue(part))
420
+ .filter((part) => part !== null && stringValue(part.type) === "tool_result")
421
+ .map((part) => ({
422
+ id: stringValue(part.tool_use_id) ?? stringValue(part.toolUseId),
423
+ name: stringValue(part.name),
424
+ text: extractContentText(part),
425
+ isError: booleanValue(part.is_error) ?? booleanValue(part.isError) ?? false,
426
+ }));
427
+ }
428
+ function extractEntryText(entry) {
429
+ const message = objectValue(entry.message);
430
+ const direct = stringValue(entry.text) ?? stringValue(entry.result);
431
+ if (direct) {
432
+ return direct;
433
+ }
434
+ return extractContentText(message ?? entry);
435
+ }
436
+ function extractContentText(container) {
437
+ if (!container) {
438
+ return null;
439
+ }
440
+ const direct = stringValue(container.text) ?? stringValue(container.content) ?? stringValue(container.summary);
441
+ if (direct) {
442
+ return direct;
443
+ }
444
+ const content = container.content;
445
+ if (!Array.isArray(content)) {
446
+ return null;
447
+ }
448
+ const parts = content
449
+ .map((part) => {
450
+ const block = objectValue(part);
451
+ if (!block) {
452
+ return "";
453
+ }
454
+ if (stringValue(block.type) === "tool_use" || stringValue(block.type) === "tool_result") {
455
+ return "";
456
+ }
457
+ return stringValue(block.text) ?? stringValue(block.thinking) ?? "";
458
+ })
459
+ .filter(Boolean);
460
+ return parts.join("\n").trim() || null;
461
+ }
462
+ function contentArray(entry) {
463
+ const message = objectValue(entry.message);
464
+ const content = message?.content ?? entry.content;
465
+ return Array.isArray(content) ? content : [];
466
+ }
467
+ function reasoningValue(entry) {
468
+ const effort = objectValue(entry.effort) ?? objectValue(entry.message);
469
+ return stringValue(objectValue(effort?.effort)?.level)
470
+ ?? stringValue(objectValue(effort?.thinking)?.level)
471
+ ?? stringValue(effort?.reasoningEffort)
472
+ ?? stringValue(effort?.reasoning_effort);
473
+ }
474
+ function usageFromObject(value) {
475
+ const usage = objectValue(value);
476
+ if (!usage) {
477
+ return undefined;
478
+ }
479
+ const input = numberValue(usage.input_tokens) ?? numberValue(usage.inputTokens) ?? 0;
480
+ const output = numberValue(usage.output_tokens) ?? numberValue(usage.outputTokens) ?? 0;
481
+ const cacheRead = numberValue(usage.cache_read_input_tokens) ?? numberValue(usage.cacheReadInputTokens) ?? numberValue(usage.cache_read_tokens) ?? 0;
482
+ const cacheWrite = numberValue(usage.cache_creation_input_tokens) ?? numberValue(usage.cacheCreationInputTokens) ?? numberValue(usage.cache_write_tokens) ?? 0;
483
+ const total = input + output + cacheRead + cacheWrite;
484
+ if (total <= 0) {
485
+ return undefined;
486
+ }
487
+ return { input, output, cacheRead, cacheWrite, total, cost: numberValue(usage.cost) ?? undefined };
488
+ }
489
+ function decodeClaudeCodeProjectKey(projectKey) {
490
+ if (!projectKey) {
491
+ return process.cwd();
492
+ }
493
+ if (projectKey.startsWith("file-")) {
494
+ try {
495
+ return decodeURIComponent(projectKey.slice("file-".length));
496
+ }
497
+ catch {
498
+ return projectKey;
499
+ }
500
+ }
501
+ if (projectKey.startsWith("-")) {
502
+ return path.normalize(`/${projectKey.slice(1).replace(/-/g, "/")}`);
503
+ }
504
+ return path.normalize(projectKey.replace(/-/g, path.sep));
505
+ }
506
+ function safeReadDir(directory) {
507
+ try {
508
+ return readdirSync(directory);
509
+ }
510
+ catch {
511
+ return [];
512
+ }
513
+ }
514
+ function safeStat(targetPath) {
515
+ try {
516
+ return statSync(targetPath);
517
+ }
518
+ catch {
519
+ return null;
520
+ }
521
+ }
522
+ function safeJsonParse(line) {
523
+ try {
524
+ return objectValue(JSON.parse(line));
525
+ }
526
+ catch {
527
+ return null;
528
+ }
529
+ }
530
+ function pushEvent(events, afterLine, event) {
531
+ if (event.lineNumber > afterLine) {
532
+ events.push(event);
533
+ }
534
+ }
535
+ function objectValue(value) {
536
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
537
+ }
538
+ function stringValue(value) {
539
+ if (typeof value === "string" && value.trim()) {
540
+ return value;
541
+ }
542
+ if (typeof value === "number" && Number.isFinite(value)) {
543
+ return String(value);
544
+ }
545
+ return null;
546
+ }
547
+ function numberValue(value) {
548
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
549
+ }
550
+ function booleanValue(value) {
551
+ return typeof value === "boolean" ? value : null;
552
+ }
553
+ function dateValue(value) {
554
+ if (typeof value === "number" && Number.isFinite(value)) {
555
+ return new Date(value > 10_000_000_000 ? value : value * 1000);
556
+ }
557
+ if (typeof value === "string" && value.trim()) {
558
+ const timestamp = Date.parse(value);
559
+ return Number.isNaN(timestamp) ? null : new Date(timestamp);
560
+ }
561
+ return null;
562
+ }
563
+ function firstString(value) {
564
+ if (!Array.isArray(value)) {
565
+ return stringValue(value);
566
+ }
567
+ return value.map(stringValue).find(Boolean) ?? null;
568
+ }
569
+ function stringifyPreview(value) {
570
+ if (value === undefined || value === null) {
571
+ return null;
572
+ }
573
+ if (typeof value === "string") {
574
+ return value.trim() || null;
575
+ }
576
+ try {
577
+ const text = JSON.stringify(value);
578
+ return text.length <= 500 ? text : `${text.slice(0, 497)}...`;
579
+ }
580
+ catch {
581
+ return null;
582
+ }
583
+ }
584
+ function summarizeTitle(text) {
585
+ if (!text) {
586
+ return null;
587
+ }
588
+ const normalized = text.replace(/\s+/g, " ").trim();
589
+ return normalized.length <= 60 ? normalized : `${normalized.slice(0, 57)}...`;
590
+ }
@@ -47,6 +47,8 @@ export class CodexSessionService {
47
47
  const effectiveLaunchProfile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
48
48
  const codexFastMode = readCodexFastMode();
49
49
  this.lastObservedFastMode = codexFastMode;
50
+ const attachedThreadFastMode = effectiveLaunchProfile.id === "attached-thread" &&
51
+ effectiveLaunchProfile.approvalPolicy === "never";
50
52
  const info = {
51
53
  agentId: "codex",
52
54
  agentLabel: "Codex",
@@ -58,7 +60,7 @@ export class CodexSessionService {
58
60
  launchProfileBehavior: formatLaunchProfileBehavior(effectiveLaunchProfile),
59
61
  sandboxMode: effectiveLaunchProfile.sandboxMode,
60
62
  approvalPolicy: effectiveLaunchProfile.approvalPolicy,
61
- fastMode: codexFastMode ?? (effectiveLaunchProfile.approvalPolicy === "never"),
63
+ fastMode: attachedThreadFastMode || (codexFastMode ?? (effectiveLaunchProfile.approvalPolicy === "never")),
62
64
  unsafeLaunch: effectiveLaunchProfile.unsafe,
63
65
  capabilities: CODEX_AGENT_CAPABILITIES,
64
66
  };
@@ -282,9 +284,20 @@ export class CodexSessionService {
282
284
  listWorkspaces() {
283
285
  return listWorkspaces();
284
286
  }
287
+ async refreshModels() {
288
+ // Codex models are read from local state on each listModels() call.
289
+ }
285
290
  listModels() {
286
291
  return listModels();
287
292
  }
293
+ listLaunchProfiles() {
294
+ return this.config.launchProfiles.map((profile) => ({
295
+ id: profile.id,
296
+ label: profile.label,
297
+ behavior: formatLaunchProfileBehavior(profile),
298
+ unsafe: profile.unsafe,
299
+ }));
300
+ }
288
301
  getSessionRecord(threadId) {
289
302
  const record = getThread(threadId);
290
303
  return record ? toAgentThreadRecord(record) : null;
@@ -336,7 +349,7 @@ export class CodexSessionService {
336
349
  getSelectedLaunchProfile() {
337
350
  return this.currentLaunchProfile;
338
351
  }
339
- syncFromCodexState(options = {}) {
352
+ syncFromAgentState(options = {}) {
340
353
  const activeThreadId = this.thread?.id ?? this.currentThreadId;
341
354
  const before = {
342
355
  workspace: this.currentWorkspace,