@myagentroam/node 0.1.1 → 0.1.3
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/claude-agent-sdk.js +1 -1
- package/dist/codex-app-server.js +8 -5
- package/dist/codex-app-server.js.map +1 -1
- package/dist/connector.d.ts +4 -1
- package/dist/connector.js +315 -70
- package/dist/connector.js.map +1 -1
- package/dist/native-session-history.js +6 -1
- package/dist/native-session-history.js.map +1 -1
- package/dist/runner-command-engine.d.ts +9 -0
- package/dist/runner-command-engine.js +24 -0
- package/dist/runner-command-engine.js.map +1 -1
- package/dist/runner-profiles.js +12 -0
- package/dist/runner-profiles.js.map +1 -1
- package/dist/workspace.d.ts +18 -13
- package/dist/workspace.js +99 -31
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
package/dist/connector.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadNodeConfig, nodeDatabasePath, saveNodeConfig } from './config.js';
|
|
|
9
9
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
10
10
|
import { assertLocalSqlitePath } from './storage.js';
|
|
11
11
|
import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
|
|
12
|
-
import { inspectWorkspace, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, restoreCurrentChange, WorkspaceFileIndex, workspaceUploadTemporaryName } from './workspace.js';
|
|
12
|
+
import { inspectWorkspace, isWorkspaceChangeVisible, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, listInitializedSubmodulePaths, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, resolveGitRepository, restoreCurrentChange, WorkspaceFileIndex, workspaceUploadTemporaryName } from './workspace.js';
|
|
13
13
|
import { ReliableWorkbenchEventBuffer } from './event-buffer.js';
|
|
14
14
|
import { CodexAppServerClient, codexSessionControl } from './codex-app-server.js';
|
|
15
15
|
import { ClaudeAgentSdkAdapter, denyPermission } from './claude-agent-sdk.js';
|
|
@@ -19,7 +19,7 @@ import { TerminalManager } from './terminal.js';
|
|
|
19
19
|
import { discoverAllNativeSessions, discoverNativeSessions, readCodexNativeContextUsage, readCodexNativeTaskActivity, readNativeSession, removeNativeSession } from './native-session-history.js';
|
|
20
20
|
import { declaredRunnerProfiles, supportsRunnerConfiguration } from './runner-profiles.js';
|
|
21
21
|
import { NodeRuntimeState } from './runtime-state.js';
|
|
22
|
-
import { CommandStateStore, parseCommandInvocation } from './runner-command-engine.js';
|
|
22
|
+
import { CommandStateStore, parseCommandInvocation, withClaudePlanTag, stripClaudePlanTag } from './runner-command-engine.js';
|
|
23
23
|
import { RunnerUsageReader } from './runner-usage.js';
|
|
24
24
|
const HEARTBEAT_MS = 15_000;
|
|
25
25
|
const WORKSPACE_FILE_INDEX_CACHE_LIMIT = 8;
|
|
@@ -31,6 +31,7 @@ const CAPABILITY_REFRESH_MS = 60_000;
|
|
|
31
31
|
const SESSION_WATCH_INTERVAL_MS = 2_000;
|
|
32
32
|
const SESSION_WATCH_TTL_MS = 45_000;
|
|
33
33
|
const SESSION_WATCH_MAX_FAILURES = 3;
|
|
34
|
+
const CLAUDE_CONTEXT_CACHE_MS = 30_000;
|
|
34
35
|
const SESSION_WATCH_INITIAL_TURN_LIMIT = 30;
|
|
35
36
|
const RETRY_MIN_MS = 500;
|
|
36
37
|
const RETRY_MAX_MS = 30_000;
|
|
@@ -99,6 +100,7 @@ export class NodeConnector {
|
|
|
99
100
|
reconnectTimer;
|
|
100
101
|
terminalReconnectTimer;
|
|
101
102
|
attempts = 0;
|
|
103
|
+
terminalAttempts = 0;
|
|
102
104
|
stopped = false;
|
|
103
105
|
config;
|
|
104
106
|
capabilitiesProvided;
|
|
@@ -158,6 +160,7 @@ export class NodeConnector {
|
|
|
158
160
|
/** Bounded Runner diagnostic paired with a fixed external resume reason code. */
|
|
159
161
|
externalResumeFailureDetails = new Map();
|
|
160
162
|
claudeApprovals = new Map();
|
|
163
|
+
claudeUserInputs = new Map();
|
|
161
164
|
commandStates = new CommandStateStore();
|
|
162
165
|
runnerUsageReader;
|
|
163
166
|
codexFastEnabled = false;
|
|
@@ -234,6 +237,11 @@ export class NodeConnector {
|
|
|
234
237
|
approval.resolve('REJECTED');
|
|
235
238
|
}
|
|
236
239
|
this.claudeApprovals.clear();
|
|
240
|
+
for (const pending of this.claudeUserInputs.values()) {
|
|
241
|
+
clearTimeout(pending.timer);
|
|
242
|
+
pending.reject('NODE_STOPPED');
|
|
243
|
+
}
|
|
244
|
+
this.claudeUserInputs.clear();
|
|
237
245
|
for (const timer of this.queuePauseWindows.values())
|
|
238
246
|
clearTimeout(timer);
|
|
239
247
|
this.queuePauseWindows.clear();
|
|
@@ -410,7 +418,7 @@ export class NodeConnector {
|
|
|
410
418
|
this.terminalReconnectTimer = setTimeout(() => {
|
|
411
419
|
this.terminalReconnectTimer = undefined;
|
|
412
420
|
this.connectTerminal();
|
|
413
|
-
},
|
|
421
|
+
}, nextReconnectDelay(this.terminalAttempts++));
|
|
414
422
|
}
|
|
415
423
|
handleTerminalMessage(raw) {
|
|
416
424
|
let message;
|
|
@@ -430,6 +438,9 @@ export class NodeConnector {
|
|
|
430
438
|
Object.keys(message).some((key) => key !== 'type' && key !== 'nodeId')) {
|
|
431
439
|
this.terminalSocket?.close(4002, 'TERMINAL_MESSAGE_INVALID');
|
|
432
440
|
}
|
|
441
|
+
else {
|
|
442
|
+
this.terminalAttempts = 0;
|
|
443
|
+
}
|
|
433
444
|
return;
|
|
434
445
|
}
|
|
435
446
|
if (message.type === 'attach') {
|
|
@@ -1077,16 +1088,20 @@ export class NodeConnector {
|
|
|
1077
1088
|
return this.currentChangesForWorkspace(workspace);
|
|
1078
1089
|
}
|
|
1079
1090
|
if (operation === 'workspace.change.diff') {
|
|
1080
|
-
if (typeof input.workspaceId !== 'string' ||
|
|
1091
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
1092
|
+
typeof input.path !== 'string' ||
|
|
1093
|
+
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
|
|
1081
1094
|
throw new Error('GIT_DIFF_INVALID');
|
|
1082
1095
|
const workspace = this.requireWorkspace(input.workspaceId);
|
|
1083
1096
|
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
1084
1097
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
1085
|
-
|
|
1098
|
+
const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
1099
|
+
return { diff: await readCurrentChangeDiff(repository, input.path) };
|
|
1086
1100
|
}
|
|
1087
1101
|
if (operation === 'workspace.change.restore') {
|
|
1088
1102
|
if (typeof input.workspaceId !== 'string' ||
|
|
1089
1103
|
typeof input.path !== 'string' ||
|
|
1104
|
+
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string') ||
|
|
1090
1105
|
(input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
|
|
1091
1106
|
throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
|
|
1092
1107
|
const workspace = this.requireWorkspace(input.workspaceId);
|
|
@@ -1094,7 +1109,8 @@ export class NodeConnector {
|
|
|
1094
1109
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
1095
1110
|
if (this.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
1096
1111
|
throw new Error('WORKSPACE_BUSY');
|
|
1097
|
-
await
|
|
1112
|
+
const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
1113
|
+
await restoreCurrentChange(repository, input.path, input.state);
|
|
1098
1114
|
// A read started before the write must not satisfy the explicit post-write refresh.
|
|
1099
1115
|
this.invalidateWorkspaceWatchTopics(workspace.id);
|
|
1100
1116
|
return { restored: true };
|
|
@@ -1559,12 +1575,16 @@ export class NodeConnector {
|
|
|
1559
1575
|
const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
|
|
1560
1576
|
this.runtime.updateQueuedMessage(created.run.id, runnerInput);
|
|
1561
1577
|
const commandStates = this.commandStates.list(session.id);
|
|
1578
|
+
const planActive = commandStates.some((state) => state.commandId === 'plan');
|
|
1562
1579
|
this.queuedRunStarts.set(created.run.id, {
|
|
1563
1580
|
runId: created.run.id,
|
|
1564
1581
|
sessionId: created.run.sessionId,
|
|
1565
1582
|
workspaceId: created.run.workspaceId,
|
|
1566
1583
|
runner: created.run.runner,
|
|
1567
|
-
|
|
1584
|
+
// Claude 标签式 Plan Mode 只包装发往 Runner 的输入,用户气泡保持原文。
|
|
1585
|
+
input: created.run.runner === 'claude-code' && planActive
|
|
1586
|
+
? withClaudePlanTag(runnerInput)
|
|
1587
|
+
: runnerInput,
|
|
1568
1588
|
attachments: runnerImageAttachments,
|
|
1569
1589
|
attachmentPaths,
|
|
1570
1590
|
cwd: session.cwd,
|
|
@@ -1573,9 +1593,7 @@ export class NodeConnector {
|
|
|
1573
1593
|
effort: session.effort,
|
|
1574
1594
|
// Plan 只切换 Codex 协作模式,不得覆盖会话已选权限。
|
|
1575
1595
|
access: session.access,
|
|
1576
|
-
collaborationMode:
|
|
1577
|
-
? 'plan'
|
|
1578
|
-
: 'default',
|
|
1596
|
+
collaborationMode: planActive ? 'plan' : 'default',
|
|
1579
1597
|
...(this.codexFastEnabled ? { serviceTier: 'fast' } : {})
|
|
1580
1598
|
});
|
|
1581
1599
|
this.runtime.resumeSessionQueue(session.id);
|
|
@@ -1647,7 +1665,12 @@ export class NodeConnector {
|
|
|
1647
1665
|
const runnerInput = withNonImageAttachmentPrompt(input.content.trim(), pending?.attachmentPaths ?? []);
|
|
1648
1666
|
this.runtime.updateQueuedMessage(input.runId, runnerInput);
|
|
1649
1667
|
if (pending !== undefined)
|
|
1650
|
-
this.queuedRunStarts.set(input.runId, {
|
|
1668
|
+
this.queuedRunStarts.set(input.runId, {
|
|
1669
|
+
...pending,
|
|
1670
|
+
input: pending.runner === 'claude-code' && pending.collaborationMode === 'plan'
|
|
1671
|
+
? withClaudePlanTag(runnerInput)
|
|
1672
|
+
: runnerInput
|
|
1673
|
+
});
|
|
1651
1674
|
this.emitWorkspaceQueue(run.workspaceId, run.sessionId);
|
|
1652
1675
|
return { queue: this.presentWorkspaceQueue(run.workspaceId, run.sessionId) };
|
|
1653
1676
|
}
|
|
@@ -1746,7 +1769,7 @@ export class NodeConnector {
|
|
|
1746
1769
|
typeof input.requestId !== 'string' ||
|
|
1747
1770
|
!isPlainRecord(input.answers))
|
|
1748
1771
|
throw new Error('USER_INPUT_INVALID');
|
|
1749
|
-
this.
|
|
1772
|
+
this.respondUserInput(input.runId, input.requestId, input.answers);
|
|
1750
1773
|
return { accepted: true };
|
|
1751
1774
|
}
|
|
1752
1775
|
if (operation === 'session.interrupt') {
|
|
@@ -2006,13 +2029,6 @@ export class NodeConnector {
|
|
|
2006
2029
|
this.workspaceChangesCache.delete(workspaceId);
|
|
2007
2030
|
this.workspaceChangesVersions.set(workspaceId, (this.workspaceChangesVersions.get(workspaceId) ?? 0) + 1);
|
|
2008
2031
|
}
|
|
2009
|
-
fileAccessOptions() {
|
|
2010
|
-
if (this.config === undefined)
|
|
2011
|
-
return {};
|
|
2012
|
-
// The database directory is Node-private even when an operator has
|
|
2013
|
-
// accidentally placed it below an otherwise permitted Workspace root.
|
|
2014
|
-
return { privateRoots: [dirname(nodeDatabasePath(this.config, this.configPath))] };
|
|
2015
|
-
}
|
|
2016
2032
|
async listFilesForWorkspace(workspace, input) {
|
|
2017
2033
|
if ((input.path !== undefined && typeof input.path !== 'string') ||
|
|
2018
2034
|
(input.cursor !== undefined && typeof input.cursor !== 'string') ||
|
|
@@ -2021,7 +2037,7 @@ export class NodeConnector {
|
|
|
2021
2037
|
input.limit < 1 ||
|
|
2022
2038
|
input.limit > 200)))
|
|
2023
2039
|
throw new Error('DIRECTORY_CURSOR_INVALID');
|
|
2024
|
-
return listWorkspaceFiles(workspace.path, typeof input.path === 'string' ? input.path : '.', typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined
|
|
2040
|
+
return listWorkspaceFiles(workspace.path, typeof input.path === 'string' ? input.path : '.', typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined);
|
|
2025
2041
|
}
|
|
2026
2042
|
async readFileForWorkspace(workspace, input) {
|
|
2027
2043
|
if (typeof input.path !== 'string' ||
|
|
@@ -2033,8 +2049,8 @@ export class NodeConnector {
|
|
|
2033
2049
|
input.limit > 512 * 1024)))
|
|
2034
2050
|
throw new Error('FILE_RANGE_INVALID');
|
|
2035
2051
|
if (isAbsolute(input.path))
|
|
2036
|
-
return readAllowedTextFile(input.path, this.config?.allowedRoots ?? [], typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024
|
|
2037
|
-
return readWorkspaceTextFile(workspace.path, input.path, typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024
|
|
2052
|
+
return readAllowedTextFile(input.path, this.config?.allowedRoots ?? [], typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024);
|
|
2053
|
+
return readWorkspaceTextFile(workspace.path, input.path, typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024);
|
|
2038
2054
|
}
|
|
2039
2055
|
async searchFilesForWorkspace(workspace, input) {
|
|
2040
2056
|
if (typeof input.query !== 'string' ||
|
|
@@ -2058,7 +2074,7 @@ export class NodeConnector {
|
|
|
2058
2074
|
this.workspaceFileIndexes.delete(oldestWorkspaceId);
|
|
2059
2075
|
}
|
|
2060
2076
|
}
|
|
2061
|
-
index = new WorkspaceFileIndex(workspace.path,
|
|
2077
|
+
index = new WorkspaceFileIndex(workspace.path, {
|
|
2062
2078
|
onInvalidated: () => this.invalidateWorkspaceWatchTopics(workspace.id)
|
|
2063
2079
|
});
|
|
2064
2080
|
}
|
|
@@ -2078,7 +2094,7 @@ export class NodeConnector {
|
|
|
2078
2094
|
if (seen.has(name))
|
|
2079
2095
|
return { name, status: 'INVALID_NAME', path: null };
|
|
2080
2096
|
seen.add(name);
|
|
2081
|
-
const target = await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', name
|
|
2097
|
+
const target = await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', name);
|
|
2082
2098
|
return { name, status: target.status, path: target.path || null };
|
|
2083
2099
|
}));
|
|
2084
2100
|
}
|
|
@@ -2091,8 +2107,6 @@ export class NodeConnector {
|
|
|
2091
2107
|
typeof input.overwrite !== 'boolean' ||
|
|
2092
2108
|
(composerAttachment !== undefined && input.directory !== undefined))
|
|
2093
2109
|
throw new Error('UPLOAD_INVALID');
|
|
2094
|
-
if (composerAttachment === undefined && this.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
2095
|
-
throw new Error('WORKSPACE_BUSY');
|
|
2096
2110
|
if (composerAttachment !== undefined) {
|
|
2097
2111
|
if (!validComposerAttachmentName(input.name) ||
|
|
2098
2112
|
input.size > MAX_COMPOSER_ATTACHMENT_BYTES ||
|
|
@@ -2101,7 +2115,7 @@ export class NodeConnector {
|
|
|
2101
2115
|
throw new Error('COMPOSER_ATTACHMENT_INVALID');
|
|
2102
2116
|
}
|
|
2103
2117
|
const target = composerAttachment === undefined
|
|
2104
|
-
? await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', input.name
|
|
2118
|
+
? await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', input.name)
|
|
2105
2119
|
: await this.composerAttachmentTarget(workspace.path, composerAttachment.id, input.name);
|
|
2106
2120
|
if (target.status === 'INVALID_NAME')
|
|
2107
2121
|
throw new Error('UPLOAD_NAME_INVALID');
|
|
@@ -2303,18 +2317,56 @@ export class NodeConnector {
|
|
|
2303
2317
|
return existing;
|
|
2304
2318
|
const version = this.workspaceChangesVersions.get(workspace.id) ?? 0;
|
|
2305
2319
|
const request = this.workspaceChangesLimiter.run(async () => {
|
|
2306
|
-
const
|
|
2320
|
+
const repositoryPaths = ['', ...(await listInitializedSubmodulePaths(workspace.path))];
|
|
2321
|
+
const readRepository = async (repositoryPath) => {
|
|
2322
|
+
const repository = repositoryPath === ''
|
|
2323
|
+
? workspace.path
|
|
2324
|
+
: await resolveGitRepository(workspace.path, repositoryPath);
|
|
2325
|
+
const summary = await readCurrentChangesSummary(repository);
|
|
2326
|
+
const visibleChanges = (await Promise.all(summary.changes.map(async (change) => ({
|
|
2327
|
+
change,
|
|
2328
|
+
visible: await isWorkspaceChangeVisible(repository, change.path)
|
|
2329
|
+
}))))
|
|
2330
|
+
.filter(({ visible }) => visible)
|
|
2331
|
+
.map(({ change }) => change);
|
|
2332
|
+
return {
|
|
2333
|
+
repositoryPath,
|
|
2334
|
+
repositoryName: repositoryPath === '' ? '根仓库' : repositoryPath,
|
|
2335
|
+
branch: summary.branch,
|
|
2336
|
+
changes: visibleChanges
|
|
2337
|
+
};
|
|
2338
|
+
};
|
|
2339
|
+
const root = await readRepository('');
|
|
2340
|
+
const submoduleRepositories = (await Promise.all(repositoryPaths.slice(1).map(async (repositoryPath) => {
|
|
2341
|
+
try {
|
|
2342
|
+
return await readRepository(repositoryPath);
|
|
2343
|
+
}
|
|
2344
|
+
catch {
|
|
2345
|
+
return undefined;
|
|
2346
|
+
}
|
|
2347
|
+
}))).filter((repository) => repository !== undefined);
|
|
2348
|
+
const repositories = [root, ...submoduleRepositories];
|
|
2307
2349
|
const value = {
|
|
2308
|
-
branch:
|
|
2309
|
-
changes:
|
|
2350
|
+
branch: root.branch,
|
|
2351
|
+
changes: root.changes.map((change) => ({
|
|
2310
2352
|
...change,
|
|
2311
2353
|
additions: null,
|
|
2312
2354
|
deletions: null,
|
|
2313
|
-
// Current status does not read file bodies. Detailed binary/diff data
|
|
2314
|
-
// remains an explicit on-demand Git request rather than a background
|
|
2315
|
-
// scan of the Workspace.
|
|
2316
2355
|
binary: false,
|
|
2317
2356
|
diffAvailable: change.state !== 'UNTRACKED'
|
|
2357
|
+
})),
|
|
2358
|
+
repositories: repositories.map((repository) => ({
|
|
2359
|
+
...repository,
|
|
2360
|
+
changes: repository.changes.map((change) => ({
|
|
2361
|
+
...change,
|
|
2362
|
+
additions: null,
|
|
2363
|
+
deletions: null,
|
|
2364
|
+
// Current status does not read file bodies. Detailed binary/diff data
|
|
2365
|
+
// remains an explicit on-demand Git request rather than a background
|
|
2366
|
+
// scan of the Workspace.
|
|
2367
|
+
binary: false,
|
|
2368
|
+
diffAvailable: change.state !== 'UNTRACKED'
|
|
2369
|
+
}))
|
|
2318
2370
|
}))
|
|
2319
2371
|
};
|
|
2320
2372
|
if ((this.workspaceChangesVersions.get(workspace.id) ?? 0) === version)
|
|
@@ -3310,6 +3362,8 @@ export class NodeConnector {
|
|
|
3310
3362
|
this.emitRunEvent(runId, 'run.rejected', { code: 'EXTERNAL_THREAD_OBSERVE_ONLY' }, 'FAILED');
|
|
3311
3363
|
return;
|
|
3312
3364
|
}
|
|
3365
|
+
if (typeof payload.externalSessionId === 'string')
|
|
3366
|
+
this.completedClaudeContextReads.delete(`claude:${cwd}:${payload.externalSessionId}`);
|
|
3313
3367
|
this.activeRuns.add(runId);
|
|
3314
3368
|
void (async () => {
|
|
3315
3369
|
try {
|
|
@@ -3449,7 +3503,10 @@ export class NodeConnector {
|
|
|
3449
3503
|
dataBase64: attachment.dataBase64
|
|
3450
3504
|
}))
|
|
3451
3505
|
}),
|
|
3452
|
-
onPermission: (toolName, toolInput) =>
|
|
3506
|
+
onPermission: (toolName, toolInput, options) => toolName === 'AskUserQuestion'
|
|
3507
|
+
? (toolKinds.set(options.toolUseID, 'user_input_request'),
|
|
3508
|
+
this.requestClaudeUserInput(runId, toolInput, options.toolUseID))
|
|
3509
|
+
: this.requestClaudeApproval(runId, toolName, toolInput),
|
|
3453
3510
|
onChannelReply: (content) => this.emitRunEvent(runId, 'channel.reply', { content }),
|
|
3454
3511
|
onMessage: (message) => {
|
|
3455
3512
|
const record = message;
|
|
@@ -3472,7 +3529,15 @@ export class NodeConnector {
|
|
|
3472
3529
|
}
|
|
3473
3530
|
if (record.type === 'assistant') {
|
|
3474
3531
|
const text = extractClaudeText(record);
|
|
3475
|
-
|
|
3532
|
+
const planText = claudePlanText(text);
|
|
3533
|
+
if (planText !== undefined)
|
|
3534
|
+
this.emitConversationItem(runId, {
|
|
3535
|
+
itemId: boundedConversationItemId('claude-plan', typeof record.uuid === 'string' ? record.uuid : `${runId}:plan`),
|
|
3536
|
+
kind: 'plan',
|
|
3537
|
+
status: 'COMPLETED',
|
|
3538
|
+
payload: { explanation: compactRunnerText(planText, 20_000), steps: [] }
|
|
3539
|
+
});
|
|
3540
|
+
else if (text.length > 0)
|
|
3476
3541
|
this.emitRunEvent(runId, 'text.delta', { text });
|
|
3477
3542
|
for (const [index, text] of extractClaudeThinkingSummaries(record).entries()) {
|
|
3478
3543
|
this.emitConversationItem(runId, {
|
|
@@ -3483,6 +3548,10 @@ export class NodeConnector {
|
|
|
3483
3548
|
});
|
|
3484
3549
|
}
|
|
3485
3550
|
for (const tool of extractClaudeToolUses(record)) {
|
|
3551
|
+
if (tool.name === 'AskUserQuestion') {
|
|
3552
|
+
toolKinds.set(tool.id, 'user_input_request');
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3486
3555
|
const itemId = boundedConversationItemId('claude-tool', tool.id);
|
|
3487
3556
|
const kind = isClaudeCommandTool(tool.name) ? 'command_execution' : 'tool_call';
|
|
3488
3557
|
toolKinds.set(tool.id, kind);
|
|
@@ -3533,6 +3602,8 @@ export class NodeConnector {
|
|
|
3533
3602
|
if (record.type === 'user') {
|
|
3534
3603
|
for (const result of extractClaudeToolResults(record)) {
|
|
3535
3604
|
const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
|
|
3605
|
+
if (kind === 'user_input_request')
|
|
3606
|
+
continue;
|
|
3536
3607
|
this.emitConversationItem(runId, {
|
|
3537
3608
|
itemId: boundedConversationItemId('claude-tool', result.toolUseId),
|
|
3538
3609
|
kind,
|
|
@@ -3681,6 +3752,41 @@ export class NodeConnector {
|
|
|
3681
3752
|
merge: true
|
|
3682
3753
|
});
|
|
3683
3754
|
}
|
|
3755
|
+
respondUserInput(runId, requestId, rawAnswers) {
|
|
3756
|
+
if (this.codexUserInputs.has(requestId)) {
|
|
3757
|
+
this.respondCodexUserInput(runId, requestId, rawAnswers);
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
const pending = this.claudeUserInputs.get(requestId);
|
|
3761
|
+
if (pending === undefined || pending.runId !== runId)
|
|
3762
|
+
throw new Error('USER_INPUT_NOT_FOUND');
|
|
3763
|
+
const answers = {};
|
|
3764
|
+
for (const question of pending.questions) {
|
|
3765
|
+
const values = rawAnswers[question.id];
|
|
3766
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > 8)
|
|
3767
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3768
|
+
const normalized = values.map((value) => {
|
|
3769
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 500)
|
|
3770
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3771
|
+
if (!question.allowOther && !question.options.some((option) => option.value === value))
|
|
3772
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3773
|
+
return value;
|
|
3774
|
+
});
|
|
3775
|
+
if (!question.multiSelect && normalized.length !== 1)
|
|
3776
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3777
|
+
answers[question.prompt] = normalized.join(', ');
|
|
3778
|
+
}
|
|
3779
|
+
clearTimeout(pending.timer);
|
|
3780
|
+
this.claudeUserInputs.delete(requestId);
|
|
3781
|
+
pending.resolve(answers);
|
|
3782
|
+
this.emitConversationItem(runId, {
|
|
3783
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
3784
|
+
kind: 'user_input_request',
|
|
3785
|
+
status: 'COMPLETED',
|
|
3786
|
+
payload: { requestId, questions: pending.questions },
|
|
3787
|
+
merge: true
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3684
3790
|
/** Returns true when cancellation already made this Run terminal. */
|
|
3685
3791
|
completeCancelledRun(runId, cwd) {
|
|
3686
3792
|
const run = this.runtime.getRun(runId);
|
|
@@ -3977,7 +4083,10 @@ export class NodeConnector {
|
|
|
3977
4083
|
// Context detail is optional Runner metadata. Cache the miss briefly to
|
|
3978
4084
|
// avoid spawning one Claude process per browser polling tick.
|
|
3979
4085
|
}
|
|
3980
|
-
this.completedClaudeContextReads.set(key, {
|
|
4086
|
+
this.completedClaudeContextReads.set(key, {
|
|
4087
|
+
context,
|
|
4088
|
+
expiresAt: Date.now() + CLAUDE_CONTEXT_CACHE_MS
|
|
4089
|
+
});
|
|
3981
4090
|
this.emitWorkbenchEvent('session', {
|
|
3982
4091
|
sessionId: session.id,
|
|
3983
4092
|
workspaceId: session.workspaceId,
|
|
@@ -3988,9 +4097,27 @@ export class NodeConnector {
|
|
|
3988
4097
|
/** Executes only Node-declared interactions; the browser never supplies behavior. */
|
|
3989
4098
|
async executeRunnerCommand(session, command, input) {
|
|
3990
4099
|
if (session.runner === 'claude-code') {
|
|
3991
|
-
if (command.id
|
|
4100
|
+
if (command.id === 'compact')
|
|
4101
|
+
return this.startClaudeCompaction(session, input);
|
|
4102
|
+
if (command.id !== 'plan')
|
|
4103
|
+
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
4104
|
+
// 标签式 Plan Mode 与 SDK 原生 plan 权限不叠加。
|
|
4105
|
+
if (session.access === 'plan')
|
|
3992
4106
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
3993
|
-
|
|
4107
|
+
const current = this.commandStates.get(session.id, 'plan');
|
|
4108
|
+
if (current?.active)
|
|
4109
|
+
this.commandStates.clear(session.id, 'plan');
|
|
4110
|
+
else
|
|
4111
|
+
this.commandStates.set(session.id, {
|
|
4112
|
+
commandId: 'plan',
|
|
4113
|
+
active: true,
|
|
4114
|
+
label: 'Plan Mode',
|
|
4115
|
+
detail: '后续消息将以标签式规划模式运行,只读规划并输出计划卡。',
|
|
4116
|
+
closable: true,
|
|
4117
|
+
closeInput: '/plan',
|
|
4118
|
+
continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
|
|
4119
|
+
});
|
|
4120
|
+
return { command: command.id, states: this.commandStates.list(session.id) };
|
|
3994
4121
|
}
|
|
3995
4122
|
if (command.id === 'plan') {
|
|
3996
4123
|
await this.codexClient.start();
|
|
@@ -4209,6 +4336,45 @@ export class NodeConnector {
|
|
|
4209
4336
|
});
|
|
4210
4337
|
});
|
|
4211
4338
|
}
|
|
4339
|
+
requestClaudeUserInput(runId, toolInput, toolUseId) {
|
|
4340
|
+
const questions = claudeUserInputQuestions(toolInput);
|
|
4341
|
+
if (questions.length === 0)
|
|
4342
|
+
return Promise.resolve(denyPermission('USER_INPUT_INVALID'));
|
|
4343
|
+
const requestId = `claude:${runId}:${toolUseId}`;
|
|
4344
|
+
return new Promise((resolve) => {
|
|
4345
|
+
const input = { ...toolInput };
|
|
4346
|
+
const settleDeny = (message = 'USER_INPUT_EXPIRED') => resolve(denyPermission(message));
|
|
4347
|
+
const timer = setTimeout(() => {
|
|
4348
|
+
const pending = this.claudeUserInputs.get(requestId);
|
|
4349
|
+
if (pending === undefined)
|
|
4350
|
+
return;
|
|
4351
|
+
this.claudeUserInputs.delete(requestId);
|
|
4352
|
+
pending.reject('USER_INPUT_EXPIRED');
|
|
4353
|
+
this.emitConversationItem(runId, {
|
|
4354
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
4355
|
+
kind: 'user_input_request',
|
|
4356
|
+
status: 'FAILED',
|
|
4357
|
+
payload: { requestId, questions },
|
|
4358
|
+
merge: true
|
|
4359
|
+
});
|
|
4360
|
+
}, this.claudeApprovalTimeoutMs);
|
|
4361
|
+
this.claudeUserInputs.set(requestId, {
|
|
4362
|
+
runId,
|
|
4363
|
+
requestId,
|
|
4364
|
+
questions,
|
|
4365
|
+
input,
|
|
4366
|
+
resolve: (answers) => resolve({ behavior: 'allow', updatedInput: { ...input, answers } }),
|
|
4367
|
+
reject: settleDeny,
|
|
4368
|
+
timer
|
|
4369
|
+
});
|
|
4370
|
+
this.emitConversationItem(runId, {
|
|
4371
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
4372
|
+
kind: 'user_input_request',
|
|
4373
|
+
status: 'PENDING',
|
|
4374
|
+
payload: { requestId, questions }
|
|
4375
|
+
});
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4212
4378
|
/**
|
|
4213
4379
|
* An approval is an individual Runner callback. Keep its exact payload in
|
|
4214
4380
|
* the process-local snapshot and emit a Run event so an already open
|
|
@@ -5042,9 +5208,24 @@ function nativeConversationPage(session, history, input, runtimeTurns = []) {
|
|
|
5042
5208
|
const turnId = `${session.id}:native:${index}`;
|
|
5043
5209
|
const isToolCall = entry.kind === 'tool_call';
|
|
5044
5210
|
const isCommand = isToolCall && isClaudeCommandTool(entry.toolName);
|
|
5211
|
+
const userInputQuestions = session.runner === 'claude-code' &&
|
|
5212
|
+
isToolCall &&
|
|
5213
|
+
entry.toolName === 'AskUserQuestion' &&
|
|
5214
|
+
isPlainRecord(entry.input)
|
|
5215
|
+
? claudeUserInputQuestions(entry.input)
|
|
5216
|
+
: [];
|
|
5217
|
+
const isUserInputRequest = userInputQuestions.length > 0;
|
|
5045
5218
|
const clientMessageId = !isToolCall && entry.role === 'USER'
|
|
5046
5219
|
? nativeUserClientMessageId(entry, time, runtimeTurns)
|
|
5047
5220
|
: null;
|
|
5221
|
+
// Claude 标签式 Plan Mode:用户正文剥离注入的规划指令标签,
|
|
5222
|
+
// 完整的 <proposed_plan> Agent 文本归一化为计划卡。
|
|
5223
|
+
const entryText = entry.kind === 'message'
|
|
5224
|
+
? entry.role === 'USER'
|
|
5225
|
+
? stripClaudePlanTag(entry.text)
|
|
5226
|
+
: entry.text
|
|
5227
|
+
: '';
|
|
5228
|
+
const planText = entry.kind === 'message' && entry.role !== 'USER' ? claudePlanText(entryText) : undefined;
|
|
5048
5229
|
const item = {
|
|
5049
5230
|
id: `${turnId}:item`,
|
|
5050
5231
|
sessionId: session.id,
|
|
@@ -5053,38 +5234,49 @@ function nativeConversationPage(session, history, input, runtimeTurns = []) {
|
|
|
5053
5234
|
parentItemId: null,
|
|
5054
5235
|
sourceSequence: index,
|
|
5055
5236
|
revision: 0,
|
|
5056
|
-
kind:
|
|
5057
|
-
?
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
: '
|
|
5237
|
+
kind: isUserInputRequest
|
|
5238
|
+
? 'user_input_request'
|
|
5239
|
+
: isToolCall
|
|
5240
|
+
? isCommand
|
|
5241
|
+
? 'command_execution'
|
|
5242
|
+
: 'tool_call'
|
|
5243
|
+
: entry.role === 'USER'
|
|
5244
|
+
? 'user_message'
|
|
5245
|
+
: planText !== undefined
|
|
5246
|
+
? 'plan'
|
|
5247
|
+
: 'assistant_message',
|
|
5063
5248
|
status: 'COMPLETED',
|
|
5064
|
-
payload:
|
|
5065
|
-
?
|
|
5066
|
-
? {
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5249
|
+
payload: isUserInputRequest
|
|
5250
|
+
? {
|
|
5251
|
+
requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${index}`}`,
|
|
5252
|
+
questions: userInputQuestions
|
|
5253
|
+
}
|
|
5254
|
+
: isToolCall
|
|
5255
|
+
? isCommand
|
|
5256
|
+
? {
|
|
5257
|
+
command: nativeClaudeCommand(entry),
|
|
5258
|
+
cwd: null,
|
|
5259
|
+
outputPreview: entry.outputSummary ?? '',
|
|
5260
|
+
outputRef: null,
|
|
5261
|
+
exitCode: null,
|
|
5262
|
+
durationMs: null,
|
|
5263
|
+
truncated: entry.inputTruncated || entry.outputTruncated
|
|
5264
|
+
}
|
|
5265
|
+
: {
|
|
5266
|
+
namespace: session.runner,
|
|
5267
|
+
toolName: entry.toolName,
|
|
5268
|
+
title: entry.toolName,
|
|
5269
|
+
inputSummary: entry.inputSummary,
|
|
5270
|
+
outputSummary: entry.outputSummary,
|
|
5271
|
+
progressLabel: null,
|
|
5272
|
+
errorCode: entry.failed ? 'TOOL_EXECUTION_FAILED' : null,
|
|
5273
|
+
truncated: entry.inputTruncated || entry.outputTruncated
|
|
5274
|
+
}
|
|
5275
|
+
: entry.role === 'USER'
|
|
5276
|
+
? { clientMessageId, text: entryText, contexts: [], delivery: 'ACCEPTED' }
|
|
5277
|
+
: planText !== undefined
|
|
5278
|
+
? { explanation: compactRunnerText(planText, 20_000), steps: [] }
|
|
5279
|
+
: { text: entryText, format: 'markdown', source: 'native-file' },
|
|
5088
5280
|
startedAt: time,
|
|
5089
5281
|
completedAt: time
|
|
5090
5282
|
};
|
|
@@ -5121,7 +5313,8 @@ function nativeUserClientMessageId(entry, createdAt, runtimeTurns) {
|
|
|
5121
5313
|
continue;
|
|
5122
5314
|
const payload = user.payload;
|
|
5123
5315
|
const clientMessageId = typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null;
|
|
5124
|
-
|
|
5316
|
+
// 标签式 Plan Mode 的指令标签只存在于原生 transcript,运行态气泡始终是原文。
|
|
5317
|
+
if (clientMessageId === null || payload.text !== stripClaudePlanTag(entry.text))
|
|
5125
5318
|
continue;
|
|
5126
5319
|
const runtimeCreatedAt = user.startedAt ?? turn.startedAt;
|
|
5127
5320
|
if (runtimeCreatedAt !== null &&
|
|
@@ -5335,6 +5528,20 @@ function codexPlanText(value) {
|
|
|
5335
5528
|
.trim();
|
|
5336
5529
|
return plan.length > 0 ? plan : undefined;
|
|
5337
5530
|
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Claude 标签式 Plan Mode 的最终计划以 `<proposed_plan>` 完整包裹输出;
|
|
5533
|
+
* 未闭合或缺失标签的文本保持普通 assistant 消息。
|
|
5534
|
+
*/
|
|
5535
|
+
function claudePlanText(text) {
|
|
5536
|
+
const trimmed = text.trim();
|
|
5537
|
+
if (!trimmed.startsWith('<proposed_plan>') || !/<\/proposed_plan>\s*$/.test(trimmed))
|
|
5538
|
+
return undefined;
|
|
5539
|
+
const plan = trimmed
|
|
5540
|
+
.slice('<proposed_plan>'.length)
|
|
5541
|
+
.replace(/<\/proposed_plan>\s*$/, '')
|
|
5542
|
+
.trim();
|
|
5543
|
+
return plan.length > 0 ? plan : undefined;
|
|
5544
|
+
}
|
|
5338
5545
|
function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
|
|
5339
5546
|
if (!isPlainRecord(value) || typeof value.type !== 'string')
|
|
5340
5547
|
return undefined;
|
|
@@ -5589,6 +5796,44 @@ function codexUserInputQuestions(value) {
|
|
|
5589
5796
|
? []
|
|
5590
5797
|
: questions;
|
|
5591
5798
|
}
|
|
5799
|
+
function claudeUserInputQuestions(value) {
|
|
5800
|
+
if (!Array.isArray(value.questions) || value.questions.length === 0 || value.questions.length > 4)
|
|
5801
|
+
return [];
|
|
5802
|
+
const questions = value.questions.map((raw, index) => {
|
|
5803
|
+
if (!isPlainRecord(raw) || typeof raw.question !== 'string')
|
|
5804
|
+
return undefined;
|
|
5805
|
+
const options = Array.isArray(raw.options)
|
|
5806
|
+
? raw.options.flatMap((option) => {
|
|
5807
|
+
if (!isPlainRecord(option) || typeof option.label !== 'string')
|
|
5808
|
+
return [];
|
|
5809
|
+
const label = compactRunnerText(option.label, 500);
|
|
5810
|
+
return label.length === 0
|
|
5811
|
+
? []
|
|
5812
|
+
: [
|
|
5813
|
+
{
|
|
5814
|
+
value: label,
|
|
5815
|
+
label,
|
|
5816
|
+
description: typeof option.description === 'string'
|
|
5817
|
+
? compactRunnerText(option.description, 1_000)
|
|
5818
|
+
: null
|
|
5819
|
+
}
|
|
5820
|
+
];
|
|
5821
|
+
})
|
|
5822
|
+
: [];
|
|
5823
|
+
const multiSelect = raw.multiSelect === true;
|
|
5824
|
+
return {
|
|
5825
|
+
id: `claude-question-${index}`,
|
|
5826
|
+
prompt: compactRunnerText(raw.question, 5_000),
|
|
5827
|
+
input: options.length === 0 ? 'SHORT_TEXT' : multiSelect ? 'MULTI_SELECT' : 'SINGLE_SELECT',
|
|
5828
|
+
options,
|
|
5829
|
+
allowOther: options.length === 0,
|
|
5830
|
+
multiSelect
|
|
5831
|
+
};
|
|
5832
|
+
});
|
|
5833
|
+
return questions.some((question) => question === undefined)
|
|
5834
|
+
? []
|
|
5835
|
+
: questions;
|
|
5836
|
+
}
|
|
5592
5837
|
function codexItemStatus(value, completed) {
|
|
5593
5838
|
if (value === 'failed')
|
|
5594
5839
|
return 'FAILED';
|