@myagentroam/node 0.9.0 → 0.9.2
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/connector.d.ts +2 -0
- package/dist/connector.js +38 -12
- package/dist/native-session-history.js +72 -14
- package/dist/runner/abstract-runner.d.ts +1 -1
- package/dist/runner/abstract-runner.js +2 -2
- package/dist/runner/claude/managed-run-controller.js +17 -7
- package/dist/runner/claude-code-runner.js +3 -4
- package/dist/runner/codex/conversation-parser.d.ts +2 -2
- package/dist/runner/codex/conversation-parser.js +30 -12
- package/dist/runner/codex/managed-run-controller.js +7 -6
- package/dist/runner/codex-runner.d.ts +1 -1
- package/dist/runner/codex-runner.js +10 -8
- package/dist/runner/opencode/conversation-parser.d.ts +11 -7
- package/dist/runner/opencode/conversation-parser.js +15 -15
- package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
- package/dist/runner/opencode/managed-run-controller.js +26 -12
- package/dist/runner-command-engine.js +1 -1
- package/dist/runner-profiles.js +25 -17
- package/dist/runner-usage.js +5 -5
- package/dist/service/conversation-history-service.js +55 -24
- package/dist/service/conversation-segment-service.d.ts +20 -0
- package/dist/service/conversation-segment-service.js +155 -0
- package/dist/service/mcp-installation-verifier.js +1 -1
- package/dist/service/native-session-watch-service.d.ts +1 -0
- package/dist/service/native-session-watch-service.js +21 -4
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/run-attachment-service.d.ts +3 -2
- package/dist/service/run-attachment-service.js +21 -11
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +8 -2
- package/dist/service/session-catalog-service.js +4 -4
- package/dist/service/session-presentation-service.d.ts +1 -0
- package/dist/service/session-presentation-service.js +8 -1
- package/dist/service/session-query-service.d.ts +4 -0
- package/dist/service/session-query-service.js +6 -1
- package/dist/service/skill-directory-service.js +3 -26
- package/dist/service/skill-install-service.js +4 -74
- package/dist/service/skill-node-operation-service.js +2 -1
- package/dist/service/workspace-git-exclude-service.d.ts +4 -0
- package/dist/service/workspace-git-exclude-service.js +119 -0
- package/dist/service/workspace-queue-workbench-service.js +3 -2
- package/dist/service/workspace-service.js +2 -0
- package/dist/supervisor.js +2 -1
- package/dist/terminal.js +1 -1
- package/dist/util/node-operation-parsers.js +2 -2
- package/dist/util/runner-native-session-parsers.d.ts +2 -3
- package/dist/util/runner-native-session-parsers.js +5 -14
- package/dist/util/safe-error.d.ts +2 -0
- package/dist/util/safe-error.js +5 -0
- package/package.json +2 -2
package/dist/connector.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class NodeConnector {
|
|
|
27
27
|
private readonly sessionIdentityService;
|
|
28
28
|
private readonly nativeSessionProjectionService;
|
|
29
29
|
private readonly conversationHistoryService;
|
|
30
|
+
private readonly conversationSegmentService;
|
|
30
31
|
private readonly runnerInteractionService;
|
|
31
32
|
private readonly runnerChannelMessages;
|
|
32
33
|
private readonly managedRunnerSessions;
|
|
@@ -78,6 +79,7 @@ export declare class NodeConnector {
|
|
|
78
79
|
private readonly handleMessage;
|
|
79
80
|
private readonly emitRunEvent;
|
|
80
81
|
private readonly emitConversationItem;
|
|
82
|
+
private readonly emitConversationTurn;
|
|
81
83
|
private readonly emitWorkbenchEvent;
|
|
82
84
|
private readonly discoverWorkspaceSessions;
|
|
83
85
|
private readonly invalidateWorkspaceWatchTopics;
|
package/dist/connector.js
CHANGED
|
@@ -44,6 +44,7 @@ import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbe
|
|
|
44
44
|
import { SessionIdentityService } from './service/session-identity-service.js';
|
|
45
45
|
import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
|
|
46
46
|
import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
|
|
47
|
+
import { ConversationSegmentService, conversationSegmentIdentityForItem, conversationSegments } from './service/conversation-segment-service.js';
|
|
47
48
|
import { RunnerInteractionService } from './service/runner-interaction-service.js';
|
|
48
49
|
import { SessionLifecycleService } from './service/session-lifecycle-service.js';
|
|
49
50
|
import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
|
|
@@ -51,7 +52,7 @@ import { SessionMessageService } from './service/session-message-service.js';
|
|
|
51
52
|
import { SessionCatalogService } from './service/session-catalog-service.js';
|
|
52
53
|
import { SessionCommandService } from './service/session-command-service.js';
|
|
53
54
|
import { SessionPresentationService } from './service/session-presentation-service.js';
|
|
54
|
-
import { isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
|
|
55
|
+
import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
|
|
55
56
|
export { nodeConnectEndpoint, validatedCapabilities } from './runner/codex/conversation-parser.js';
|
|
56
57
|
import { NodeControlChannel } from './service/node-control-channel.js';
|
|
57
58
|
import { NodeControlMessageService } from './service/node-control-message-service.js';
|
|
@@ -114,6 +115,7 @@ export class NodeConnector {
|
|
|
114
115
|
sessionIdentityService;
|
|
115
116
|
nativeSessionProjectionService;
|
|
116
117
|
conversationHistoryService;
|
|
118
|
+
conversationSegmentService;
|
|
117
119
|
runnerInteractionService;
|
|
118
120
|
runnerChannelMessages;
|
|
119
121
|
managedRunnerSessions;
|
|
@@ -173,7 +175,12 @@ export class NodeConnector {
|
|
|
173
175
|
isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
|
|
174
176
|
presentSession: (session) => this.sessionPresentationService.present(session),
|
|
175
177
|
rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
|
|
176
|
-
emitWorkbench: (category, payload) =>
|
|
178
|
+
emitWorkbench: (category, payload) => {
|
|
179
|
+
if (category === 'conversation' && isPlainRecord(payload) && 'turn' in payload)
|
|
180
|
+
this.emitConversationTurn(payload.turn);
|
|
181
|
+
else
|
|
182
|
+
this.emitWorkbenchEvent(category, payload);
|
|
183
|
+
},
|
|
177
184
|
cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
|
|
178
185
|
expireImages: (runId) => this.runAttachmentService.expire(runId),
|
|
179
186
|
scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
|
|
@@ -190,8 +197,9 @@ export class NodeConnector {
|
|
|
190
197
|
refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
|
|
191
198
|
present: (session) => this.sessionPresentationService.present(session),
|
|
192
199
|
readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
|
|
200
|
+
projectInitialPage: (session, page, _unit, limit) => this.conversationSegmentService.projectInitial(session, page, limit),
|
|
193
201
|
emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
|
|
194
|
-
emitTurn: (turn) => this.
|
|
202
|
+
emitTurn: (turn) => this.emitConversationTurn(turn)
|
|
195
203
|
});
|
|
196
204
|
constructor(options = {}) {
|
|
197
205
|
this.config = options.config;
|
|
@@ -230,7 +238,7 @@ export class NodeConnector {
|
|
|
230
238
|
this.fakeRunner =
|
|
231
239
|
options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
|
|
232
240
|
this.fakeScript = options.fakeScript;
|
|
233
|
-
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).
|
|
241
|
+
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session));
|
|
234
242
|
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
|
|
235
243
|
this.codexRunController = new CodexManagedRunController(this.codexClient, {
|
|
236
244
|
available: () => this.capabilities.codex.available,
|
|
@@ -252,7 +260,7 @@ export class NodeConnector {
|
|
|
252
260
|
cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
|
|
253
261
|
createApproval: (input) => this.runtime.createApproval(input),
|
|
254
262
|
publishApproval: (input) => this.runEventBridge.publishApproval(input),
|
|
255
|
-
emitItem: (runId, item) => this.
|
|
263
|
+
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
256
264
|
appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
|
|
257
265
|
captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
|
|
258
266
|
syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
|
|
@@ -273,8 +281,9 @@ export class NodeConnector {
|
|
|
273
281
|
intercepted: () => this.fakeRunner !== undefined,
|
|
274
282
|
active: this.runState.active,
|
|
275
283
|
parseAttachments: parseRunnerImageAttachments,
|
|
284
|
+
persistAttachments: (cwd, runId, attachments) => this.runAttachmentService.persistFiles(cwd, `${runId}-opencode-images`, attachments),
|
|
276
285
|
emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
277
|
-
emitItem: (runId, item) => this.
|
|
286
|
+
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
278
287
|
adopt: (input) => this.runtime.adoptRun(input),
|
|
279
288
|
promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
|
|
280
289
|
rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
|
|
@@ -306,7 +315,7 @@ export class NodeConnector {
|
|
|
306
315
|
rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
|
|
307
316
|
promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
|
|
308
317
|
send: (type, payload) => this.send(type, payload),
|
|
309
|
-
emitItem: (runId, item) => this.
|
|
318
|
+
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
310
319
|
syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
|
|
311
320
|
setChannelToken: (sessionId, token) => this.runtime.setChannelToken(sessionId, token),
|
|
312
321
|
commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
|
|
@@ -330,7 +339,7 @@ export class NodeConnector {
|
|
|
330
339
|
claude: () => this.claudeClient,
|
|
331
340
|
runners: this.runners,
|
|
332
341
|
emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
333
|
-
emitItem: (runId, item) => this.
|
|
342
|
+
emitItem: (runId, item) => this.emitConversationItem(runId, item),
|
|
334
343
|
captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
|
|
335
344
|
approvalTimeoutMs: this.claudeApprovalTimeoutMs,
|
|
336
345
|
publishApproval: (input) => this.runEventBridge.publishApproval(input)
|
|
@@ -368,6 +377,7 @@ export class NodeConnector {
|
|
|
368
377
|
hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
|
|
369
378
|
setExternalActivity: (sessionId, activity) => this.sessionState.externalActivity.set(sessionId, activity)
|
|
370
379
|
});
|
|
380
|
+
this.conversationSegmentService = new ConversationSegmentService((session, input) => this.conversationHistoryService.read(session, input));
|
|
371
381
|
this.sessionContextService = new SessionContextService({
|
|
372
382
|
runners: this.runners,
|
|
373
383
|
session: (sessionId) => this.runtime.getAgentSession(sessionId),
|
|
@@ -392,6 +402,7 @@ export class NodeConnector {
|
|
|
392
402
|
resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
|
|
393
403
|
present: (session) => this.sessionPresentationService.present(session),
|
|
394
404
|
history: (session, input) => this.conversationHistoryService.read(session, input),
|
|
405
|
+
segments: (session, input) => this.conversationSegmentService.read(session, input),
|
|
395
406
|
context: this.sessionContextService,
|
|
396
407
|
discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
|
|
397
408
|
workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
|
|
@@ -401,7 +412,7 @@ export class NodeConnector {
|
|
|
401
412
|
emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
|
|
402
413
|
clearImages: (runId) => this.runAttachmentService.clear(runId),
|
|
403
414
|
emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
|
|
404
|
-
emitConversation: (turn) => this.
|
|
415
|
+
emitConversation: (turn) => this.emitConversationTurn(turn),
|
|
405
416
|
control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
|
|
406
417
|
stopped: () => this.stopped,
|
|
407
418
|
markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
|
|
@@ -428,7 +439,7 @@ export class NodeConnector {
|
|
|
428
439
|
emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
|
|
429
440
|
emitRun: (run) => this.emitWorkbenchEvent('run', { run }),
|
|
430
441
|
emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
|
|
431
|
-
emitTurn: (turn) => this.
|
|
442
|
+
emitTurn: (turn) => this.emitConversationTurn(turn)
|
|
432
443
|
});
|
|
433
444
|
this.sessionLifecycleService = new SessionLifecycleService({
|
|
434
445
|
database: () => {
|
|
@@ -596,7 +607,7 @@ export class NodeConnector {
|
|
|
596
607
|
this.terminalChannel.disconnect();
|
|
597
608
|
if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
|
|
598
609
|
nodeLog('node.connection.replaced', {
|
|
599
|
-
message: '
|
|
610
|
+
message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
|
|
600
611
|
});
|
|
601
612
|
if (code === 4003 || code === 1001)
|
|
602
613
|
void this.terminalManager.shutdown();
|
|
@@ -781,7 +792,22 @@ export class NodeConnector {
|
|
|
781
792
|
/** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
|
|
782
793
|
handleMessage = (raw) => this.controlMessages.handle(raw);
|
|
783
794
|
emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
|
|
784
|
-
emitConversationItem = (runId, item) =>
|
|
795
|
+
emitConversationItem = (runId, item) => {
|
|
796
|
+
this.runEventService.flushPendingText(runId);
|
|
797
|
+
const itemId = isPlainRecord(item) && typeof item.itemId === 'string' ? item.itemId : undefined;
|
|
798
|
+
const itemKind = isPlainRecord(item) && typeof item.kind === 'string' ? item.kind : undefined;
|
|
799
|
+
const turn = this.runtime.conversationTurnForRun(runId);
|
|
800
|
+
const segment = itemId === undefined || turn === undefined
|
|
801
|
+
? undefined
|
|
802
|
+
: conversationSegmentIdentityForItem(turn, itemId, itemKind);
|
|
803
|
+
this.runEventBridge.emitRun(runId, 'conversation.item', segment === undefined || !isPlainRecord(item)
|
|
804
|
+
? item
|
|
805
|
+
: { ...item, segmentId: segment.id, segmentIndex: segment.index });
|
|
806
|
+
};
|
|
807
|
+
emitConversationTurn = (turn) => {
|
|
808
|
+
const segments = isPlainRecord(turn) && Array.isArray(turn.items) ? conversationSegments(turn) : [];
|
|
809
|
+
this.emitWorkbenchEvent('conversation', { turn, segments });
|
|
810
|
+
};
|
|
785
811
|
emitWorkbenchEvent = (category, payload) => this.runEventBridge.emitWorkbench(category, payload);
|
|
786
812
|
discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
|
|
787
813
|
invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
|
|
@@ -286,7 +286,7 @@ async function readTranscript(path, runner, byteLimit) {
|
|
|
286
286
|
items.push({
|
|
287
287
|
kind: 'tool_call',
|
|
288
288
|
toolUseId: result.toolUseId,
|
|
289
|
-
toolName: '
|
|
289
|
+
toolName: 'Tool',
|
|
290
290
|
input: null,
|
|
291
291
|
inputSummary: null,
|
|
292
292
|
inputTruncated: false,
|
|
@@ -301,7 +301,7 @@ async function readTranscript(path, runner, byteLimit) {
|
|
|
301
301
|
if (cwd === undefined)
|
|
302
302
|
return undefined;
|
|
303
303
|
const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
|
|
304
|
-
//
|
|
304
|
+
// Tagged Plan Mode instruction labels do not form a session title.
|
|
305
305
|
const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(title);
|
|
306
306
|
return {
|
|
307
307
|
runner,
|
|
@@ -591,7 +591,7 @@ function codexGeneratedImageEvent(record) {
|
|
|
591
591
|
input: null,
|
|
592
592
|
inputSummary: null,
|
|
593
593
|
inputTruncated: false,
|
|
594
|
-
outputSummary: payload['status'] === 'completed' ? '
|
|
594
|
+
outputSummary: payload['status'] === 'completed' ? 'Image generation completed' : 'Image generation failed',
|
|
595
595
|
outputTruncated: false,
|
|
596
596
|
imageAttachments: [{ name: basename(localPath), mime: imageMime(localPath), localPath }],
|
|
597
597
|
failed: payload['status'] !== 'completed',
|
|
@@ -620,7 +620,7 @@ function codexVisibleToolText(value) {
|
|
|
620
620
|
const start = value.indexOf(marker, cursor);
|
|
621
621
|
if (start < 0)
|
|
622
622
|
return visible + value.slice(cursor);
|
|
623
|
-
visible += `${value.slice(cursor, start)}[
|
|
623
|
+
visible += `${value.slice(cursor, start)}[Image content filtered]`;
|
|
624
624
|
const doubleQuote = value.indexOf('"', start);
|
|
625
625
|
const singleQuote = value.indexOf("'", start);
|
|
626
626
|
const end = doubleQuote < 0
|
|
@@ -636,14 +636,20 @@ function codexVisibleToolText(value) {
|
|
|
636
636
|
}
|
|
637
637
|
/** Extract only image placeholder metadata from native Codex output. */
|
|
638
638
|
function codexImagePlaceholders(value) {
|
|
639
|
-
const
|
|
639
|
+
const attachments = [];
|
|
640
640
|
let visited = 0;
|
|
641
641
|
const visit = (current, depth) => {
|
|
642
|
-
if (
|
|
642
|
+
if (attachments.length >= 5 || visited >= 128 || depth > 5 || current == null)
|
|
643
643
|
return;
|
|
644
644
|
visited += 1;
|
|
645
645
|
if (typeof current === 'string') {
|
|
646
|
+
const paths = [];
|
|
646
647
|
collectGeneratedImagePaths(current, paths);
|
|
648
|
+
for (const localPath of paths) {
|
|
649
|
+
if (attachments.some((attachment) => attachment.localPath === localPath))
|
|
650
|
+
continue;
|
|
651
|
+
attachments.push({ name: basename(localPath), mime: imageMime(localPath), localPath });
|
|
652
|
+
}
|
|
647
653
|
return;
|
|
648
654
|
}
|
|
649
655
|
if (Array.isArray(current)) {
|
|
@@ -653,15 +659,28 @@ function codexImagePlaceholders(value) {
|
|
|
653
659
|
}
|
|
654
660
|
if (!isRecord(current))
|
|
655
661
|
return;
|
|
662
|
+
const type = typeof current['type'] === 'string' ? current['type'] : '';
|
|
663
|
+
if (type === 'input_text' && typeof current['text'] === 'string') {
|
|
664
|
+
const mime = /["']image_url["']\s*:\s*["']data:(image\/[^;,"']+)[;,]/.exec(current['text'])?.[1];
|
|
665
|
+
if (mime !== undefined)
|
|
666
|
+
attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
|
|
667
|
+
}
|
|
668
|
+
const imageUrl = isRecord(current['image_url'])
|
|
669
|
+
? firstString(current['image_url'], ['url'])
|
|
670
|
+
: typeof current['image_url'] === 'string'
|
|
671
|
+
? current['image_url']
|
|
672
|
+
: undefined;
|
|
673
|
+
const data = typeof current['data'] === 'string' ? current['data'] : imageUrl;
|
|
674
|
+
if ((type === 'image' || type === 'input_image' || type === 'output_image') &&
|
|
675
|
+
data?.startsWith('data:image/')) {
|
|
676
|
+
const mime = /^data:([^;,]+)[;,]/.exec(data)?.[1] ?? 'image/png';
|
|
677
|
+
attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
|
|
678
|
+
}
|
|
656
679
|
for (const child of Object.values(current))
|
|
657
680
|
visit(child, depth + 1);
|
|
658
681
|
};
|
|
659
682
|
visit(value, 0);
|
|
660
|
-
return
|
|
661
|
-
name: basename(localPath),
|
|
662
|
-
mime: imageMime(localPath),
|
|
663
|
-
localPath
|
|
664
|
-
}));
|
|
683
|
+
return attachments;
|
|
665
684
|
}
|
|
666
685
|
function collectGeneratedImagePaths(value, paths) {
|
|
667
686
|
const marker = '/.codex/generated_images/';
|
|
@@ -690,15 +709,54 @@ function imageMime(path) {
|
|
|
690
709
|
return 'image/webp';
|
|
691
710
|
return 'image/png';
|
|
692
711
|
}
|
|
712
|
+
function imageExtension(mime) {
|
|
713
|
+
if (mime === 'image/jpeg')
|
|
714
|
+
return 'jpg';
|
|
715
|
+
if (mime === 'image/webp')
|
|
716
|
+
return 'webp';
|
|
717
|
+
if (mime === 'image/gif')
|
|
718
|
+
return 'gif';
|
|
719
|
+
return 'png';
|
|
720
|
+
}
|
|
693
721
|
function withToolResult(call, result) {
|
|
722
|
+
const viewedImagePath = codexViewedImagePath(call);
|
|
723
|
+
const imageAttachments = result.imageAttachments?.map((attachment) => attachment.localPath !== undefined || viewedImagePath === undefined
|
|
724
|
+
? attachment
|
|
725
|
+
: {
|
|
726
|
+
name: basename(viewedImagePath),
|
|
727
|
+
mime: imageMime(viewedImagePath),
|
|
728
|
+
localPath: viewedImagePath
|
|
729
|
+
});
|
|
694
730
|
return {
|
|
695
731
|
...call,
|
|
696
732
|
outputSummary: result.output,
|
|
697
733
|
outputTruncated: result.truncated,
|
|
698
|
-
...(
|
|
734
|
+
...(imageAttachments === undefined ? {} : { imageAttachments }),
|
|
699
735
|
failed: result.failed
|
|
700
736
|
};
|
|
701
737
|
}
|
|
738
|
+
function codexViewedImagePath(call) {
|
|
739
|
+
const toolName = call.toolName.toLowerCase();
|
|
740
|
+
if (toolName === 'view_image' || toolName === 'imageview') {
|
|
741
|
+
return isRecord(call.input) && typeof call.input['path'] === 'string'
|
|
742
|
+
? call.input['path']
|
|
743
|
+
: undefined;
|
|
744
|
+
}
|
|
745
|
+
if (toolName !== 'exec' || typeof call.input !== 'string')
|
|
746
|
+
return undefined;
|
|
747
|
+
if (!/tools\.view_image\s*\(/.test(call.input))
|
|
748
|
+
return undefined;
|
|
749
|
+
const encodedPath = /["']path["']\s*:\s*("(?:\\.|[^"\\])*")/.exec(call.input)?.[1];
|
|
750
|
+
if (encodedPath === undefined)
|
|
751
|
+
return undefined;
|
|
752
|
+
try {
|
|
753
|
+
const path = JSON.parse(encodedPath);
|
|
754
|
+
return typeof path === 'string' ? path : undefined;
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
702
760
|
function isInjectedContextPrompt(text) {
|
|
703
761
|
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
704
762
|
if (normalized.startsWith('<codex_internal_context'))
|
|
@@ -776,7 +834,7 @@ function compactValue(value, limit) {
|
|
|
776
834
|
text = JSON.stringify(value);
|
|
777
835
|
}
|
|
778
836
|
catch {
|
|
779
|
-
return { text: '
|
|
837
|
+
return { text: '(Unable to display)', truncated: false };
|
|
780
838
|
}
|
|
781
839
|
}
|
|
782
840
|
return {
|
|
@@ -812,7 +870,7 @@ function nestedString(value, keys) {
|
|
|
812
870
|
function compact(value) {
|
|
813
871
|
return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
|
|
814
872
|
}
|
|
815
|
-
/**
|
|
873
|
+
/** Conversation content can be Markdown, so its internal whitespace must not be collapsed like a title. */
|
|
816
874
|
function messageText(value) {
|
|
817
875
|
return value.trim().slice(0, 16_000);
|
|
818
876
|
}
|
|
@@ -53,7 +53,7 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
|
|
|
53
53
|
}, managedTurn: (nativeTurnId: string) => NodeConversationTurn | undefined): Promise<RunnerOfficialConversationPage | undefined>;
|
|
54
54
|
readExternalActivity(session: NodeAgentSession): Promise<SessionActivityState | undefined>;
|
|
55
55
|
acceptsTruncatedNativeHistory(): boolean;
|
|
56
|
-
|
|
56
|
+
nativeImageRoots(session: NodeAgentSession): readonly string[];
|
|
57
57
|
managedRunForNativeTurn(nativeTurnId: string): string | undefined;
|
|
58
58
|
abstract stop(): void | Promise<void>;
|
|
59
59
|
bindControl(control: RunnerControl): void;
|
|
@@ -106,9 +106,9 @@ export class AbstractRunner {
|
|
|
106
106
|
acceptsTruncatedNativeHistory() {
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
nativeImageRoots(session) {
|
|
110
110
|
void session;
|
|
111
|
-
return
|
|
111
|
+
return [];
|
|
112
112
|
}
|
|
113
113
|
managedRunForNativeTurn(nativeTurnId) {
|
|
114
114
|
void nativeTurnId;
|
|
@@ -4,6 +4,7 @@ import { boundedConversationItemId, claudePlanText, compactRunnerText, compactRu
|
|
|
4
4
|
import { isTerminalRunStatus } from '../../util/node-operation-parsers.js';
|
|
5
5
|
import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
|
|
6
6
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
7
|
+
import { safeErrorCode } from '../../util/safe-error.js';
|
|
7
8
|
export class ClaudeManagedRunController {
|
|
8
9
|
runner;
|
|
9
10
|
host;
|
|
@@ -24,10 +25,10 @@ export class ClaudeManagedRunController {
|
|
|
24
25
|
commandId: 'plan',
|
|
25
26
|
active: true,
|
|
26
27
|
label: 'Plan Mode',
|
|
27
|
-
detail: '
|
|
28
|
+
detail: 'Subsequent messages run in read-only tagged Plan Mode and produce a plan card.',
|
|
28
29
|
closable: true,
|
|
29
30
|
closeInput: '/plan',
|
|
30
|
-
continuation: { label: '
|
|
31
|
+
continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
|
|
31
32
|
});
|
|
32
33
|
return { command: command.id, states: this.host.commandStates(session.id) };
|
|
33
34
|
}
|
|
@@ -83,7 +84,7 @@ export class ClaudeManagedRunController {
|
|
|
83
84
|
secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
86
|
-
this.host.emit(runId, 'run.rejected', { code: error
|
|
87
|
+
this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
this.host.adopt({
|
|
@@ -255,14 +256,23 @@ export class ClaudeManagedRunController {
|
|
|
255
256
|
const kind = toolKinds.get(record.tool_use_id);
|
|
256
257
|
if (kind === 'tool_call') {
|
|
257
258
|
const elapsed = typeof record.elapsed_time_seconds === 'number'
|
|
258
|
-
?
|
|
259
|
+
? ` (${Math.max(0, Math.round(record.elapsed_time_seconds))} seconds)`
|
|
259
260
|
: '';
|
|
260
261
|
this.host.emitItem(runId, {
|
|
261
262
|
itemId: boundedConversationItemId('claude-tool', record.tool_use_id),
|
|
262
263
|
kind,
|
|
263
264
|
status: 'IN_PROGRESS',
|
|
264
265
|
payload: {
|
|
265
|
-
progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : '
|
|
266
|
+
progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : 'Tool'} is running${elapsed}`, 500),
|
|
267
|
+
progressLabelCode: typeof record.elapsed_time_seconds === 'number'
|
|
268
|
+
? 'TOOL_RUNNING_WITH_DURATION'
|
|
269
|
+
: 'TOOL_RUNNING',
|
|
270
|
+
progressLabelParameters: {
|
|
271
|
+
tool: typeof record.tool_name === 'string' ? record.tool_name : 'Tool',
|
|
272
|
+
seconds: typeof record.elapsed_time_seconds === 'number'
|
|
273
|
+
? Math.max(0, Math.round(record.elapsed_time_seconds))
|
|
274
|
+
: 0
|
|
275
|
+
}
|
|
266
276
|
},
|
|
267
277
|
merge: true
|
|
268
278
|
});
|
|
@@ -315,12 +325,12 @@ export class ClaudeManagedRunController {
|
|
|
315
325
|
this.host.send('session.channel.ready', { sessionId, token: gate.token });
|
|
316
326
|
this.host.emit(runId, 'run.running', {}, 'RUNNING');
|
|
317
327
|
void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', {
|
|
318
|
-
code: error
|
|
328
|
+
code: safeErrorCode(error, 'CLAUDE_RUN_FAILED')
|
|
319
329
|
}));
|
|
320
330
|
}
|
|
321
331
|
catch (error) {
|
|
322
332
|
finish('FAILED', 'run.failed', {
|
|
323
|
-
code: error
|
|
333
|
+
code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED')
|
|
324
334
|
});
|
|
325
335
|
}
|
|
326
336
|
}
|
|
@@ -3,7 +3,7 @@ import { AbstractRunner } from './abstract-runner.js';
|
|
|
3
3
|
import { claudeDefaultModel, declaredRunnerProfiles } from '../runner-profiles.js';
|
|
4
4
|
import { withClaudePlanTag } from '../runner-command-engine.js';
|
|
5
5
|
import { discoverNativeSessions, readNativeSession, readClaudeNativeContextUsage } from '../native-session-history.js';
|
|
6
|
-
import { claudeDiscoveredSession, deduplicateDirectSessions,
|
|
6
|
+
import { claudeDiscoveredSession, deduplicateDirectSessions, isNativeHistory, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
|
|
7
7
|
export class ClaudeCodeRunner extends AbstractRunner {
|
|
8
8
|
client;
|
|
9
9
|
environment;
|
|
@@ -102,11 +102,10 @@ export class ClaudeCodeRunner extends AbstractRunner {
|
|
|
102
102
|
context.rememberManagedNative(external.externalSessionId, configured.id);
|
|
103
103
|
return { session: configured, activity: 'IDLE' };
|
|
104
104
|
}
|
|
105
|
-
catch
|
|
105
|
+
catch {
|
|
106
106
|
return {
|
|
107
107
|
failureCode: 'RUNNER_RESUME_FAILED',
|
|
108
|
-
messageFailureCode: 'SESSION_RESUME_REJECTED'
|
|
109
|
-
failureDetail: externalResumeFailureDetail(error)
|
|
108
|
+
messageFailureCode: 'SESSION_RESUME_REJECTED'
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -32,8 +32,8 @@ export declare function mergeCodexCustomToolItems(invocation: NodeConversationIt
|
|
|
32
32
|
export declare function coalesceImageGenerationItems(items: readonly NodeConversationItem[]): readonly NodeConversationItem[];
|
|
33
33
|
export declare function codexPlanText(value: Record<string, unknown>): string | undefined;
|
|
34
34
|
/**
|
|
35
|
-
* Claude
|
|
36
|
-
*
|
|
35
|
+
* A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
|
|
36
|
+
* text with missing or unclosed tags remains a regular assistant message.
|
|
37
37
|
*/
|
|
38
38
|
export declare function codexOfficialItem(session: NodeAgentSession, nativeTurnId: string, value: unknown, turnSequence: number, itemSequence: number, startedAt: number | null, completedAt: number | null): NodeConversationItem | undefined;
|
|
39
39
|
export declare function codexLiveConversationItem(value: Record<string, unknown>, completed: boolean, imageAttachments?: readonly Record<string, unknown>[]): PublicConversationItem | undefined;
|
|
@@ -201,6 +201,11 @@ export function codexOfficialItems(session, nativeTurnId, values, turnSequence,
|
|
|
201
201
|
const item = codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt);
|
|
202
202
|
if (item === undefined)
|
|
203
203
|
continue;
|
|
204
|
+
// Codex persists one compaction as both `compacted` state and a
|
|
205
|
+
// `context_compacted` event. App Server normalizes both records to
|
|
206
|
+
// consecutive `contextCompaction` items without preserving their source.
|
|
207
|
+
if (item.kind === 'context_compaction' && items.at(-1)?.kind === 'context_compaction')
|
|
208
|
+
continue;
|
|
204
209
|
const key = codexPairedToolCallId(value);
|
|
205
210
|
if (key === undefined) {
|
|
206
211
|
items.push(item);
|
|
@@ -295,8 +300,8 @@ export function codexPlanText(value) {
|
|
|
295
300
|
return plan.length > 0 ? plan : undefined;
|
|
296
301
|
}
|
|
297
302
|
/**
|
|
298
|
-
* Claude
|
|
299
|
-
*
|
|
303
|
+
* A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
|
|
304
|
+
* text with missing or unclosed tags remains a regular assistant message.
|
|
300
305
|
*/
|
|
301
306
|
export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
|
|
302
307
|
if (!isPlainRecord(value) || typeof value.type !== 'string')
|
|
@@ -361,7 +366,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
|
|
|
361
366
|
payload: {
|
|
362
367
|
command: typeof value.command === 'string'
|
|
363
368
|
? compactRunnerText(value.command, 10_000)
|
|
364
|
-
: '
|
|
369
|
+
: '(No command provided)',
|
|
365
370
|
cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
|
|
366
371
|
outputPreview: compactRunnerText(output, 10_000),
|
|
367
372
|
outputRef: null,
|
|
@@ -415,7 +420,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
|
|
|
415
420
|
namespace: 'codex',
|
|
416
421
|
toolName: 'imagegen',
|
|
417
422
|
inputSummary: typeof value.revisedPrompt === 'string' ? value.revisedPrompt : null,
|
|
418
|
-
outputSummary: value.status === 'completed' ? '
|
|
423
|
+
outputSummary: value.status === 'completed' ? 'Image generation completed' : 'Image generation failed',
|
|
419
424
|
progressLabel: null,
|
|
420
425
|
errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
|
|
421
426
|
truncated: false
|
|
@@ -456,7 +461,11 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
|
|
|
456
461
|
return {
|
|
457
462
|
...base,
|
|
458
463
|
kind: 'context_compaction',
|
|
459
|
-
payload: {
|
|
464
|
+
payload: {
|
|
465
|
+
phase: 'COMPLETED',
|
|
466
|
+
summary: 'Context compacted',
|
|
467
|
+
summaryCode: 'CONTEXT_COMPACTED'
|
|
468
|
+
}
|
|
460
469
|
};
|
|
461
470
|
return codexUnknownOfficialItem(base, value);
|
|
462
471
|
}
|
|
@@ -501,7 +510,7 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
|
|
|
501
510
|
payload: {
|
|
502
511
|
command: typeof value.command === 'string'
|
|
503
512
|
? compactRunnerText(value.command, 10_000)
|
|
504
|
-
: '
|
|
513
|
+
: '(No command provided)',
|
|
505
514
|
cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
|
|
506
515
|
outputPreview: compactRunnerText(output, 10_000),
|
|
507
516
|
outputRef: null,
|
|
@@ -550,7 +559,8 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
|
|
|
550
559
|
title: 'Web Search',
|
|
551
560
|
inputSummary: typeof value.query === 'string' ? compactRunnerText(value.query, 10_000) : null,
|
|
552
561
|
outputSummary: null,
|
|
553
|
-
progressLabel: completed ? null : '
|
|
562
|
+
progressLabel: completed ? null : 'Searching',
|
|
563
|
+
progressLabelCode: completed ? null : 'SEARCHING',
|
|
554
564
|
errorCode: null,
|
|
555
565
|
truncated: false
|
|
556
566
|
}
|
|
@@ -575,8 +585,10 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
|
|
|
575
585
|
inputSummary: typeof value.revisedPrompt === 'string'
|
|
576
586
|
? compactRunnerText(value.revisedPrompt, 10_000)
|
|
577
587
|
: null,
|
|
578
|
-
outputSummary: completed ? '
|
|
579
|
-
|
|
588
|
+
outputSummary: completed ? 'Image generation completed' : null,
|
|
589
|
+
outputSummaryCode: completed ? 'IMAGE_GENERATION_COMPLETED' : null,
|
|
590
|
+
progressLabel: completed ? null : 'Generating image',
|
|
591
|
+
progressLabelCode: completed ? null : 'GENERATING_IMAGE',
|
|
580
592
|
errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
|
|
581
593
|
truncated: false,
|
|
582
594
|
...(imageAttachments.length === 0 ? {} : { attachments: imageAttachments })
|
|
@@ -704,7 +716,9 @@ export function codexImageAttachments(value) {
|
|
|
704
716
|
totalBytes + Buffer.byteLength(dataBase64, 'base64') <= MAX_COMPOSER_ATTACHMENTS_BYTES) {
|
|
705
717
|
totalBytes += Buffer.byteLength(dataBase64, 'base64');
|
|
706
718
|
found.push({
|
|
707
|
-
name: typeof current.name === 'string'
|
|
719
|
+
name: typeof current.name === 'string'
|
|
720
|
+
? current.name
|
|
721
|
+
: `generated-image-${found.length + 1}.png`,
|
|
708
722
|
mime,
|
|
709
723
|
dataBase64
|
|
710
724
|
});
|
|
@@ -802,10 +816,14 @@ export function codexToolOutput(value) {
|
|
|
802
816
|
return [block.content];
|
|
803
817
|
return [];
|
|
804
818
|
});
|
|
805
|
-
return text.length > 0
|
|
819
|
+
return text.length > 0
|
|
820
|
+
? text.join('')
|
|
821
|
+
: value.some(isCodexImageBlock)
|
|
822
|
+
? 'Image generated'
|
|
823
|
+
: value;
|
|
806
824
|
}
|
|
807
825
|
export function codexVisibleToolText(value) {
|
|
808
|
-
return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[
|
|
826
|
+
return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[Image content extracted]$2');
|
|
809
827
|
}
|
|
810
828
|
export function isCodexImageBlock(value) {
|
|
811
829
|
if (!isPlainRecord(value))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractId } from '../../util/runner-native-session-parsers.js';
|
|
2
2
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
3
|
+
import { safeErrorCode } from '../../util/safe-error.js';
|
|
3
4
|
import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
|
|
4
5
|
export class CodexManagedRunController {
|
|
5
6
|
runner;
|
|
@@ -32,10 +33,10 @@ export class CodexManagedRunController {
|
|
|
32
33
|
commandId: 'plan',
|
|
33
34
|
active: true,
|
|
34
35
|
label: 'Plan Mode',
|
|
35
|
-
detail: '
|
|
36
|
+
detail: 'Subsequent messages run in collaborative Plan Mode.',
|
|
36
37
|
closable: true,
|
|
37
38
|
closeInput: '/plan',
|
|
38
|
-
continuation: { label: '
|
|
39
|
+
continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
|
|
39
40
|
});
|
|
40
41
|
return { command: command.id, states: this.host.commandStates(session.id) };
|
|
41
42
|
}
|
|
@@ -83,7 +84,7 @@ export class CodexManagedRunController {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
86
|
-
this.host.emit(runId, 'run.failed', { code: error
|
|
87
|
+
this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_COMPACT_FAILED') }, 'FAILED');
|
|
87
88
|
this.runner.execution.runs.delete(runId);
|
|
88
89
|
this.runner.releaseEnvironment(runId);
|
|
89
90
|
this.host.removeActive(runId);
|
|
@@ -283,7 +284,7 @@ export class CodexManagedRunController {
|
|
|
283
284
|
secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
|
|
284
285
|
}
|
|
285
286
|
catch (error) {
|
|
286
|
-
this.host.emit(runId, 'run.rejected', { code: error
|
|
287
|
+
this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
290
|
this.host.adopt({
|
|
@@ -311,7 +312,7 @@ export class CodexManagedRunController {
|
|
|
311
312
|
this.runner.prepareEnvironment(secretEnvironment, runId);
|
|
312
313
|
}
|
|
313
314
|
catch (error) {
|
|
314
|
-
this.host.emit(runId, 'run.rejected', { code: error
|
|
315
|
+
this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
|
|
315
316
|
return;
|
|
316
317
|
}
|
|
317
318
|
this.host.addActive(runId);
|
|
@@ -381,7 +382,7 @@ export class CodexManagedRunController {
|
|
|
381
382
|
this.runner.releaseEnvironment(runId);
|
|
382
383
|
return;
|
|
383
384
|
}
|
|
384
|
-
this.host.emit(runId, 'run.failed', { code: error
|
|
385
|
+
this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_RUN_FAILED') }, 'FAILED');
|
|
385
386
|
this.runner.execution.runs.delete(runId);
|
|
386
387
|
this.runner.releaseEnvironment(runId);
|
|
387
388
|
this.host.removeActive(runId);
|