linkshell-cli 0.5.1 → 0.5.4

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 (47) hide show
  1. package/dist/cli/src/index.js +23 -1
  2. package/dist/cli/src/index.js.map +1 -1
  3. package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
  4. package/dist/cli/src/runtime/acp/acp-client.js +81 -21
  5. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  6. package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
  7. package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
  8. package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
  9. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +14 -0
  10. package/dist/cli/src/runtime/acp/agent-workspace.js +845 -88
  11. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  12. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
  13. package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
  14. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  15. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -0
  16. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +87 -14
  17. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  18. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
  19. package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
  20. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  21. package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
  22. package/dist/cli/src/runtime/acp-relay.js +73 -0
  23. package/dist/cli/src/runtime/acp-relay.js.map +1 -0
  24. package/dist/cli/src/runtime/bridge-session.d.ts +37 -0
  25. package/dist/cli/src/runtime/bridge-session.js +318 -69
  26. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  27. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  28. package/dist/shared-protocol/src/index.d.ts +5436 -3886
  29. package/dist/shared-protocol/src/index.js +127 -13
  30. package/dist/shared-protocol/src/index.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/index.ts +29 -1
  33. package/src/runtime/acp/acp-client.ts +96 -21
  34. package/src/runtime/acp/agent-workspace.ts +916 -83
  35. package/src/runtime/acp/claude-sdk-client.ts +352 -31
  36. package/src/runtime/acp/claude-stream-json-client.ts +89 -16
  37. package/src/runtime/acp/json-rpc.ts +5 -0
  38. package/src/runtime/bridge-session.ts +343 -75
  39. package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
  40. package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
  41. package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
  42. package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
  43. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
  44. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
  45. package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
  46. package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
  47. package/dist/cli/src/runtime/acp/codex-sessions.js.map +0 -1
@@ -11,6 +11,12 @@ const MAX_TIMELINE_ITEMS = 200;
11
11
  function id(prefix) {
12
12
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
13
13
  }
14
+ function normalizeAgentSessionIdSegment(value) {
15
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
16
+ }
17
+ export function makeAgentV2RemoteConversationId(provider, agentSessionId) {
18
+ return `agent-remote-${provider}-${normalizeAgentSessionIdSegment(agentSessionId)}`;
19
+ }
14
20
  function stringify(value) {
15
21
  if (typeof value === "string")
16
22
  return value;
@@ -66,6 +72,12 @@ function extractItem(value) {
66
72
  return undefined;
67
73
  return asRecord(raw.item) ?? raw;
68
74
  }
75
+ function extractTurn(value) {
76
+ const raw = asRecord(value);
77
+ if (!raw)
78
+ return undefined;
79
+ return asRecord(raw.turn) ?? raw;
80
+ }
69
81
  function stringifyDefined(value) {
70
82
  if (value === undefined || value === null || value === "")
71
83
  return undefined;
@@ -107,6 +119,29 @@ function normalizePlanStatus(value) {
107
119
  return "in_progress";
108
120
  return "pending";
109
121
  }
122
+ function agentStatusFromThreadStatus(value) {
123
+ const raw = asRecord(value);
124
+ const status = typeof value === "string"
125
+ ? value
126
+ : firstString(raw, ["status", "state", "phase", "type"]) ??
127
+ firstString(asRecord(raw?.status), ["status", "state", "phase", "type"]);
128
+ const normalized = normalizedIdentifier(status);
129
+ if (!normalized)
130
+ return undefined;
131
+ if (["active", "running", "inprogress", "busy", "working", "streaming"].includes(normalized)) {
132
+ return "running";
133
+ }
134
+ if (["idle", "ready", "completed", "complete", "done", "finished"].includes(normalized)) {
135
+ return "idle";
136
+ }
137
+ if (["waitingpermission", "waitingforpermission", "requirespermission", "needspermission"].includes(normalized)) {
138
+ return "waiting_permission";
139
+ }
140
+ if (["error", "failed", "systemerror", "fatal"].includes(normalized)) {
141
+ return "error";
142
+ }
143
+ return undefined;
144
+ }
110
145
  function nameFromToolMethod(method) {
111
146
  if (method.includes("commandExecution"))
112
147
  return "命令";
@@ -473,7 +508,9 @@ function parseStructuredInputQuestion(value, index) {
473
508
  question,
474
509
  isOther: raw.isOther === true,
475
510
  isSecret: raw.isSecret === true || raw.secret === true,
476
- selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
511
+ selectionLimit: raw.multiSelect === true
512
+ ? options.length || undefined
513
+ : firstNumber(raw, ["selectionLimit", "maxSelections"]),
477
514
  options: options.length > 0 ? options : undefined,
478
515
  };
479
516
  }
@@ -1004,10 +1041,11 @@ function parseTimestamp(value) {
1004
1041
  function parseRemoteSessions(value) {
1005
1042
  const raw = asRecord(value);
1006
1043
  const sessionsValue = Array.isArray(value) ? value :
1007
- Array.isArray(raw?.threads) ? raw.threads :
1008
- Array.isArray(raw?.sessions) ? raw.sessions :
1009
- Array.isArray(raw?.items) ? raw.items :
1010
- [];
1044
+ Array.isArray(raw?.data) ? raw.data :
1045
+ Array.isArray(raw?.threads) ? raw.threads :
1046
+ Array.isArray(raw?.sessions) ? raw.sessions :
1047
+ Array.isArray(raw?.items) ? raw.items :
1048
+ [];
1011
1049
  const result = [];
1012
1050
  for (const entry of sessionsValue) {
1013
1051
  const session = asRecord(entry);
@@ -1024,7 +1062,7 @@ function parseRemoteSessions(value) {
1024
1062
  result.push({
1025
1063
  id,
1026
1064
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
1027
- title: firstString(source, ["title", "name", "summary"]),
1065
+ title: firstString(source, ["title", "name", "summary", "preview"]),
1028
1066
  model: firstString(source, ["model", "modelId"]),
1029
1067
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1030
1068
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
@@ -1032,11 +1070,265 @@ function parseRemoteSessions(value) {
1032
1070
  }
1033
1071
  return result;
1034
1072
  }
1073
+ function threadFromProviderResult(value) {
1074
+ const raw = asRecord(value);
1075
+ return asRecord(raw?.thread) ?? raw;
1076
+ }
1077
+ function threadFromTurnsListResult(sessionId, value) {
1078
+ const raw = asRecord(value);
1079
+ const order = firstString(raw, ["sortDirection", "order", "sortOrder", "sort"]);
1080
+ const turns = Array.isArray(raw?.turns) ? raw.turns :
1081
+ Array.isArray(raw?.data) ? raw.data :
1082
+ Array.isArray(raw?.items) ? raw.items :
1083
+ Array.isArray(value) ? value :
1084
+ undefined;
1085
+ if (!turns)
1086
+ return undefined;
1087
+ const extracted = turns
1088
+ .map(extractTurn)
1089
+ .filter((turn) => Boolean(turn));
1090
+ const chronological = order && /asc/i.test(order)
1091
+ ? extracted
1092
+ : [...extracted].reverse();
1093
+ return { id: sessionId, turns: chronological };
1094
+ }
1095
+ function turnItemsFromThread(thread) {
1096
+ if (!thread)
1097
+ return [];
1098
+ const turnsValue = Array.isArray(thread.turns)
1099
+ ? thread.turns
1100
+ : Array.isArray(thread.items)
1101
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items }]
1102
+ : [];
1103
+ const result = [];
1104
+ for (const turnEntry of turnsValue) {
1105
+ const turn = extractTurn(turnEntry);
1106
+ if (!turn)
1107
+ continue;
1108
+ const turnId = firstString(turn, ["id", "turnId"]);
1109
+ const createdAt = parseTimestamp(turn.createdAt ??
1110
+ turn.created_at ??
1111
+ turn.startedAt ??
1112
+ turn.started_at ??
1113
+ turn.completedAt ??
1114
+ turn.completed_at ??
1115
+ turn.updatedAt ??
1116
+ turn.updated_at);
1117
+ const itemsRecord = asRecord(turn.items);
1118
+ const items = Array.isArray(turn.items)
1119
+ ? turn.items
1120
+ : Array.isArray(itemsRecord?.data)
1121
+ ? itemsRecord.data
1122
+ : Array.isArray(itemsRecord?.items)
1123
+ ? itemsRecord.items
1124
+ : Array.isArray(turn.item)
1125
+ ? turn.item
1126
+ : Array.isArray(turn.events)
1127
+ ? turn.events
1128
+ : [];
1129
+ for (const itemEntry of items) {
1130
+ const item = extractItem(itemEntry);
1131
+ if (!item)
1132
+ continue;
1133
+ result.push({
1134
+ ...item,
1135
+ ...(turnId && !firstString(item, ["turnId"]) ? { turnId } : {}),
1136
+ __turnCreatedAt: createdAt,
1137
+ });
1138
+ }
1139
+ }
1140
+ return result;
1141
+ }
1142
+ function activeTurnIdFromThread(thread) {
1143
+ if (!thread)
1144
+ return undefined;
1145
+ const turnsValue = Array.isArray(thread.turns)
1146
+ ? thread.turns
1147
+ : Array.isArray(thread.items)
1148
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items, status: thread.status }]
1149
+ : [];
1150
+ let activeTurnId;
1151
+ for (const turnEntry of turnsValue) {
1152
+ const turn = extractTurn(turnEntry);
1153
+ if (!turn)
1154
+ continue;
1155
+ const turnId = firstString(turn, ["id", "turnId"]);
1156
+ if (!turnId)
1157
+ continue;
1158
+ const status = firstString(turn, ["status", "state", "phase"]) ??
1159
+ firstString(asRecord(turn.status), ["status", "state", "phase", "type"]);
1160
+ const normalized = normalizedIdentifier(status);
1161
+ if ([
1162
+ "active",
1163
+ "running",
1164
+ "inprogress",
1165
+ "busy",
1166
+ "working",
1167
+ "streaming",
1168
+ "waitingpermission",
1169
+ "waitingforpermission",
1170
+ "requirespermission",
1171
+ "needspermission",
1172
+ ].includes(normalized)) {
1173
+ activeTurnId = turnId;
1174
+ }
1175
+ }
1176
+ return activeTurnId;
1177
+ }
1178
+ function timelineItemFromProviderItem(item, conversationId, index) {
1179
+ const itemType = firstString(item, ["type"]);
1180
+ const normalized = normalizedIdentifier(itemType);
1181
+ const itemId = firstString(item, ["id", "itemId", "messageId", "toolCallId"]) ?? `history-${index + 1}`;
1182
+ const createdAt = parseTimestamp(item.createdAt ?? item.created_at ?? item.timestamp ?? item.__turnCreatedAt) ??
1183
+ Date.now() + index;
1184
+ const base = {
1185
+ id: itemId,
1186
+ conversationId,
1187
+ turnId: firstString(item, ["turnId"]),
1188
+ itemId,
1189
+ createdAt,
1190
+ updatedAt: parseTimestamp(item.updatedAt ?? item.updated_at) ?? createdAt,
1191
+ isStreaming: false,
1192
+ };
1193
+ if (normalized === "usermessage" ||
1194
+ normalized === "userinput" ||
1195
+ (normalized === "message" && item.role === "user")) {
1196
+ const content = contentBlocksFromItem(item);
1197
+ const text = textFromBlocks(content);
1198
+ if (!content.length && !text)
1199
+ return undefined;
1200
+ return {
1201
+ ...base,
1202
+ type: "message",
1203
+ kind: "chat",
1204
+ role: "user",
1205
+ content,
1206
+ text,
1207
+ };
1208
+ }
1209
+ if (normalized === "agentmessage" ||
1210
+ normalized === "assistantmessage" ||
1211
+ (normalized === "message" && item.role !== "user")) {
1212
+ const content = contentBlocksFromItem(item);
1213
+ const text = textFromBlocks(content);
1214
+ if (!content.length && !text)
1215
+ return undefined;
1216
+ return {
1217
+ ...base,
1218
+ type: "message",
1219
+ kind: "chat",
1220
+ role: item.role === "system" ? "system" : "assistant",
1221
+ content,
1222
+ text,
1223
+ };
1224
+ }
1225
+ if (normalized === "reasoning" || normalized === "thinking") {
1226
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1227
+ stringifyDefined(item.contentItems ?? item.summary);
1228
+ if (!text)
1229
+ return undefined;
1230
+ return {
1231
+ ...base,
1232
+ type: "status",
1233
+ kind: "thinking",
1234
+ role: "system",
1235
+ text,
1236
+ };
1237
+ }
1238
+ if (normalized === "enteredreviewmode" || normalized === "exitedreviewmode") {
1239
+ const text = firstString(item, ["review", "text", "content", "summary", "message"]);
1240
+ return {
1241
+ ...base,
1242
+ type: "status",
1243
+ kind: "review",
1244
+ role: "system",
1245
+ text: text ?? (normalized === "enteredreviewmode" ? "正在审查" : "审查已完成"),
1246
+ };
1247
+ }
1248
+ if (normalized === "contextcompaction") {
1249
+ return {
1250
+ ...base,
1251
+ type: "status",
1252
+ kind: "context_compaction",
1253
+ role: "system",
1254
+ text: firstString(item, ["text", "summary", "message"]) ?? "上下文已压缩",
1255
+ };
1256
+ }
1257
+ if (isSubagentItemType(itemType)) {
1258
+ const subagent = decodeSubagentAction(item, normalizeToolStatus(item.status, true));
1259
+ if (!subagent)
1260
+ return undefined;
1261
+ return {
1262
+ ...base,
1263
+ type: "status",
1264
+ kind: "subagent_action",
1265
+ role: "system",
1266
+ text: summarizeSubagentAction(subagent),
1267
+ subagent,
1268
+ };
1269
+ }
1270
+ if (isToolItemType(itemType) || toolNameFromItem(item)) {
1271
+ const toolId = firstString(item, ["id", "itemId", "toolCallId"]) ?? itemId;
1272
+ const toolCall = {
1273
+ id: toolId,
1274
+ name: toolNameFromItem(item) ?? "工具",
1275
+ input: toolInputFromItem(item),
1276
+ output: normalized === "filechange" || normalized === "diff"
1277
+ ? extractDiffText(item) ?? firstString(item, ["output", "summary"])
1278
+ : firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
1279
+ stringifyDefined(item.result ?? item.error ?? item.contentItems),
1280
+ createdAt,
1281
+ status: normalizeToolStatus(item.status, true),
1282
+ };
1283
+ const kind = toolCall.name.includes("文件") || normalized === "filechange" || normalized === "diff"
1284
+ ? "file_change"
1285
+ : toolCall.name.includes("命令") || normalized === "commandexecution"
1286
+ ? "command_execution"
1287
+ : "tool_activity";
1288
+ return {
1289
+ ...base,
1290
+ id: `tool:${toolId}`,
1291
+ type: "tool_call",
1292
+ kind,
1293
+ itemId: toolId,
1294
+ toolCall,
1295
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(toolCall) : undefined,
1296
+ fileChange: kind === "file_change" ? fileChangeFromTool(toolCall) : undefined,
1297
+ };
1298
+ }
1299
+ return undefined;
1300
+ }
1301
+ function timelineItemsFromProviderThread(value, conversationId) {
1302
+ const items = turnItemsFromThread(threadFromProviderResult(value))
1303
+ .map((item, index) => timelineItemFromProviderItem(item, conversationId, index))
1304
+ .filter((item) => Boolean(item));
1305
+ const byId = new Map();
1306
+ for (const item of items) {
1307
+ byId.set(item.id, item);
1308
+ }
1309
+ return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).slice(-MAX_TIMELINE_ITEMS);
1310
+ }
1311
+ function previewFromTimelineItem(item) {
1312
+ if (item.error)
1313
+ return item.error;
1314
+ if (item.text)
1315
+ return item.text;
1316
+ if (item.content?.length)
1317
+ return textFromBlocks(item.content);
1318
+ if (item.toolCall)
1319
+ return `${item.toolCall.name} · ${item.toolCall.status}`;
1320
+ if (item.permission)
1321
+ return `需要授权 ${item.permission.toolName ?? ""}`.trim();
1322
+ if (item.subagent)
1323
+ return summarizeSubagentAction(item.subagent);
1324
+ return undefined;
1325
+ }
1035
1326
  export class AgentWorkspaceProxy {
1036
1327
  input;
1037
1328
  clients = new Map();
1038
1329
  agentProtocols = new Map();
1039
1330
  providerCapabilities = new Map();
1331
+ providerErrors = new Map();
1040
1332
  initialized = false;
1041
1333
  status = "unavailable";
1042
1334
  error;
@@ -1046,6 +1338,9 @@ export class AgentWorkspaceProxy {
1046
1338
  conversations = new Map();
1047
1339
  conversationByAgentSessionId = new Map();
1048
1340
  timelines = new Map();
1341
+ // Opaque codex turns cursor per conversation, pointing at OLDER history to
1342
+ // page through (captured at hydration, advanced on each history.request).
1343
+ historyCursors = new Map();
1049
1344
  toolOutputBuffers = new Map();
1050
1345
  pendingPermissions = new Map();
1051
1346
  permissionWaiters = new Map();
@@ -1085,6 +1380,11 @@ export class AgentWorkspaceProxy {
1085
1380
  this.sendSnapshot(payload.conversationId);
1086
1381
  break;
1087
1382
  }
1383
+ case "agent.v2.history.request": {
1384
+ const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
1385
+ await this.loadOlderHistory(payload);
1386
+ break;
1387
+ }
1088
1388
  case "agent.v2.prompt": {
1089
1389
  const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
1090
1390
  await this.sendPrompt(payload);
@@ -1098,11 +1398,18 @@ export class AgentWorkspaceProxy {
1098
1398
  case "agent.v2.cancel": {
1099
1399
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
1100
1400
  const conversation = this.conversations.get(payload.conversationId);
1401
+ if (!conversation)
1402
+ break;
1403
+ const turnId = this.currentTurnIds.get(payload.conversationId);
1404
+ if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
1405
+ this.rejectAgentAction(conversation, "无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。", conversation.status);
1406
+ break;
1407
+ }
1101
1408
  this.cancelPendingPermissions(payload.conversationId);
1102
- const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1409
+ const cancelClient = this.clientForProvider(conversation.provider);
1103
1410
  cancelClient?.cancel({
1104
- sessionId: conversation?.agentSessionId,
1105
- turnId: this.currentTurnIds.get(payload.conversationId),
1411
+ sessionId: conversation.agentSessionId,
1412
+ turnId,
1106
1413
  });
1107
1414
  this.forgetCurrentTurn(payload.conversationId);
1108
1415
  this.updateConversationStatus(payload.conversationId, "idle");
@@ -1130,9 +1437,38 @@ export class AgentWorkspaceProxy {
1130
1437
  clientForProvider(provider) {
1131
1438
  return this.clients.get(provider);
1132
1439
  }
1440
+ shouldRecycleProviderClient(provider, error) {
1441
+ if (provider !== "codex")
1442
+ return false;
1443
+ const message = error instanceof Error ? error.message : String(error);
1444
+ return /ACP agent is not running|ACP request timed out|Transport channel closed|TokenRefreshFailed|Failed to parse server response|channel closed|EPIPE|ECONNRESET/i.test(message);
1445
+ }
1446
+ async recycleProviderClient(provider, error) {
1447
+ if (!this.shouldRecycleProviderClient(provider, error))
1448
+ return undefined;
1449
+ const message = error instanceof Error ? error.message : String(error);
1450
+ const existing = this.clients.get(provider);
1451
+ if (existing) {
1452
+ try {
1453
+ existing.stop();
1454
+ }
1455
+ catch { }
1456
+ }
1457
+ this.clients.delete(provider);
1458
+ this.agentProtocols.delete(provider);
1459
+ this.providerErrors.set(provider, message);
1460
+ return this.ensureProviderClient(provider);
1461
+ }
1133
1462
  protocolForProvider(provider) {
1134
1463
  return this.agentProtocols.get(provider);
1135
1464
  }
1465
+ defaultModelForProvider(provider) {
1466
+ const capabilities = this.providerCapabilities.get(provider);
1467
+ const defaultModel = capabilities?.defaultModel?.trim();
1468
+ if (defaultModel)
1469
+ return defaultModel;
1470
+ return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
1471
+ }
1136
1472
  async initialize() {
1137
1473
  if (this.initialized)
1138
1474
  return;
@@ -1197,6 +1533,7 @@ export class AgentWorkspaceProxy {
1197
1533
  try {
1198
1534
  const client = await tryCreateClient(resolved);
1199
1535
  this.clients.set(provider, client);
1536
+ this.providerErrors.delete(provider);
1200
1537
  await this.refreshProviderCapabilities(provider, client, resolved.protocol);
1201
1538
  this.status = "idle";
1202
1539
  this.error = undefined;
@@ -1217,6 +1554,7 @@ export class AgentWorkspaceProxy {
1217
1554
  };
1218
1555
  const client = await tryCreateClient(fallback);
1219
1556
  this.clients.set(provider, client);
1557
+ this.providerErrors.delete(provider);
1220
1558
  await this.refreshProviderCapabilities(provider, client, fallback.protocol);
1221
1559
  this.status = "idle";
1222
1560
  this.error = undefined;
@@ -1232,6 +1570,8 @@ export class AgentWorkspaceProxy {
1232
1570
  if (this.input.verbose) {
1233
1571
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1234
1572
  }
1573
+ this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
1574
+ this.sendCapabilities();
1235
1575
  return undefined;
1236
1576
  }
1237
1577
  }
@@ -1276,12 +1616,23 @@ export class AgentWorkspaceProxy {
1276
1616
  await this.initialize();
1277
1617
  for (const [provider, client] of this.clients) {
1278
1618
  try {
1279
- const result = await client.listSessions();
1619
+ let activeClient = client;
1620
+ let result;
1621
+ try {
1622
+ result = await activeClient.listSessions();
1623
+ }
1624
+ catch (error) {
1625
+ const recovered = await this.recycleProviderClient(provider, error);
1626
+ if (!recovered)
1627
+ throw error;
1628
+ activeClient = recovered;
1629
+ result = await activeClient.listSessions();
1630
+ }
1280
1631
  for (const remote of parseRemoteSessions(result)) {
1281
1632
  const agentSessionId = remote.id;
1282
1633
  const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1283
1634
  const now = Date.now();
1284
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1635
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1285
1636
  const existing = this.conversations.get(conversationId);
1286
1637
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1287
1638
  const conversation = {
@@ -1312,6 +1663,103 @@ export class AgentWorkspaceProxy {
1312
1663
  }
1313
1664
  }
1314
1665
  }
1666
+ async hydrateConversationFromProvider(conversation, seedResult) {
1667
+ let client = this.clientForProvider(conversation.provider);
1668
+ if (!client || !conversation.agentSessionId)
1669
+ return;
1670
+ let source = seedResult;
1671
+ const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
1672
+ if (!hasSeedTurns) {
1673
+ const readSession = client.readSession;
1674
+ if (typeof readSession === "function") {
1675
+ try {
1676
+ source = await readSession.call(client, {
1677
+ sessionId: conversation.agentSessionId,
1678
+ includeTurns: true,
1679
+ });
1680
+ }
1681
+ catch (error) {
1682
+ const recovered = await this.recycleProviderClient(conversation.provider, error);
1683
+ if (recovered)
1684
+ client = recovered;
1685
+ if (this.input.verbose) {
1686
+ process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1687
+ }
1688
+ }
1689
+ }
1690
+ if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
1691
+ const listTurns = client.listTurns;
1692
+ if (typeof listTurns === "function") {
1693
+ try {
1694
+ let turnsResult;
1695
+ try {
1696
+ turnsResult = await listTurns.call(client, {
1697
+ sessionId: conversation.agentSessionId,
1698
+ limit: MAX_TIMELINE_ITEMS,
1699
+ sortDirection: "desc",
1700
+ itemsView: "full",
1701
+ });
1702
+ }
1703
+ catch (error) {
1704
+ const recovered = await this.recycleProviderClient(conversation.provider, error);
1705
+ if (!recovered)
1706
+ throw error;
1707
+ client = recovered;
1708
+ const recoveredListTurns = client.listTurns;
1709
+ if (typeof recoveredListTurns !== "function")
1710
+ throw error;
1711
+ turnsResult = await recoveredListTurns.call(client, {
1712
+ sessionId: conversation.agentSessionId,
1713
+ limit: MAX_TIMELINE_ITEMS,
1714
+ sortDirection: "desc",
1715
+ itemsView: "full",
1716
+ });
1717
+ }
1718
+ const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
1719
+ if (turnsThread)
1720
+ source = { thread: turnsThread };
1721
+ // Capture the opaque cursor pointing at OLDER turns so the client
1722
+ // can page further back via agent.v2.history.request.
1723
+ const olderCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
1724
+ this.historyCursors.set(conversation.id, olderCursor);
1725
+ }
1726
+ catch (error) {
1727
+ if (this.input.verbose) {
1728
+ process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+ const thread = threadFromProviderResult(source);
1735
+ const activeTurnId = activeTurnIdFromThread(thread);
1736
+ if (activeTurnId)
1737
+ this.rememberTurnConversationId(conversation.id, activeTurnId);
1738
+ const model = firstString(thread, ["model", "modelId", "currentModel"]);
1739
+ if (model && !conversation.model)
1740
+ conversation.model = model;
1741
+ const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
1742
+ if (hydratedItems.length === 0)
1743
+ return;
1744
+ const existing = this.timelines.get(conversation.id) ?? [];
1745
+ const merged = new Map();
1746
+ for (const item of [...existing, ...hydratedItems]) {
1747
+ merged.set(item.id, item);
1748
+ }
1749
+ const nextItems = [...merged.values()]
1750
+ .sort((a, b) => a.createdAt - b.createdAt)
1751
+ .slice(-MAX_TIMELINE_ITEMS);
1752
+ this.timelines.set(conversation.id, nextItems);
1753
+ for (const item of nextItems) {
1754
+ this.rememberItemConversationId(conversation.id, item);
1755
+ }
1756
+ const lastPreview = [...nextItems].reverse()
1757
+ .map((item) => previewText(previewFromTimelineItem(item) ?? ""))
1758
+ .find(Boolean);
1759
+ if (lastPreview && !conversation.lastMessagePreview) {
1760
+ conversation.lastMessagePreview = lastPreview;
1761
+ }
1762
+ }
1315
1763
  sendCapabilities() {
1316
1764
  const providers = this.input.availableProviders.map((provider) => {
1317
1765
  const client = this.clients.get(provider);
@@ -1328,7 +1776,7 @@ export class AgentWorkspaceProxy {
1328
1776
  id: provider,
1329
1777
  label: providerLabel(provider),
1330
1778
  enabled,
1331
- reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1779
+ reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
1332
1780
  supportsImages,
1333
1781
  supportsPermission,
1334
1782
  supportsPlan: enabled,
@@ -1385,18 +1833,70 @@ export class AgentWorkspaceProxy {
1385
1833
  if (!this.input.availableProviders.includes(provider)) {
1386
1834
  return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1387
1835
  }
1388
- const client = await this.ensureProviderClient(provider);
1389
- if (!client) {
1390
- return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1391
- }
1392
- const cwd = payload.cwd ?? this.input.cwd;
1836
+ let cwd = payload.cwd ?? this.input.cwd;
1393
1837
  let agentSessionId = payload.agentSessionId;
1394
1838
  let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
1395
1839
  (agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
1840
+ if (!payload.cwd && existingConversation?.cwd)
1841
+ cwd = existingConversation.cwd;
1842
+ let client = await this.ensureProviderClient(provider);
1843
+ if (!client) {
1844
+ const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
1845
+ if (existingConversation) {
1846
+ this.activeConversationId = existingConversation.id;
1847
+ this.input.send(createEnvelope({
1848
+ type: "agent.v2.conversation.opened",
1849
+ sessionId: this.input.sessionId,
1850
+ payload: {
1851
+ conversation: existingConversation,
1852
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1853
+ requestedConversationId: payload.conversationId,
1854
+ providerError: message,
1855
+ },
1856
+ }));
1857
+ this.sendCapabilities();
1858
+ return existingConversation;
1859
+ }
1860
+ if (payload.conversationId) {
1861
+ return this.openOfflineHistory(payload, message, cwd);
1862
+ }
1863
+ return this.openFailure(payload, message, cwd);
1864
+ }
1396
1865
  if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
1397
- if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1866
+ const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
1867
+ if (payload.conversationId &&
1868
+ payload.conversationId === requestedCanonicalId &&
1869
+ existingConversation.id !== payload.conversationId) {
1398
1870
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1399
1871
  }
1872
+ if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
1873
+ try {
1874
+ const existingAgentSessionId = existingConversation.agentSessionId;
1875
+ let result;
1876
+ try {
1877
+ result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
1878
+ }
1879
+ catch (error) {
1880
+ const recovered = await this.recycleProviderClient(provider, error);
1881
+ if (!recovered)
1882
+ throw error;
1883
+ client = recovered;
1884
+ result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
1885
+ }
1886
+ const nextAgentSessionId = this.extractSessionId(result);
1887
+ if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
1888
+ this.conversationByAgentSessionId.delete(existingAgentSessionId);
1889
+ existingConversation.agentSessionId = nextAgentSessionId;
1890
+ this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
1891
+ }
1892
+ await this.hydrateConversationFromProvider(existingConversation, result);
1893
+ }
1894
+ catch (error) {
1895
+ if (this.input.verbose) {
1896
+ process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1897
+ }
1898
+ }
1899
+ }
1400
1900
  this.activeConversationId = existingConversation.id;
1401
1901
  this.input.send(createEnvelope({
1402
1902
  type: "agent.v2.conversation.opened",
@@ -1404,17 +1904,30 @@ export class AgentWorkspaceProxy {
1404
1904
  payload: {
1405
1905
  conversation: existingConversation,
1406
1906
  snapshot: this.timelines.get(existingConversation.id) ?? [],
1907
+ requestedConversationId: payload.conversationId,
1407
1908
  },
1408
1909
  }));
1409
1910
  return existingConversation;
1410
1911
  }
1411
1912
  try {
1412
- const result = agentSessionId
1413
- ? await client.loadSession({ sessionId: agentSessionId, cwd })
1414
- : await client.newSession({ cwd });
1913
+ let result;
1914
+ try {
1915
+ result = agentSessionId
1916
+ ? await client.loadSession({ sessionId: agentSessionId, cwd })
1917
+ : await client.newSession({ cwd });
1918
+ }
1919
+ catch (error) {
1920
+ const recovered = await this.recycleProviderClient(provider, error);
1921
+ if (!recovered)
1922
+ throw error;
1923
+ client = recovered;
1924
+ result = agentSessionId
1925
+ ? await client.loadSession({ sessionId: agentSessionId, cwd })
1926
+ : await client.newSession({ cwd });
1927
+ }
1415
1928
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
1416
1929
  const now = Date.now();
1417
- const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
1930
+ const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
1418
1931
  const conversation = {
1419
1932
  ...existingConversation,
1420
1933
  id: conversationId,
@@ -1436,10 +1949,15 @@ export class AgentWorkspaceProxy {
1436
1949
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1437
1950
  this.activeConversationId = conversation.id;
1438
1951
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1952
+ await this.hydrateConversationFromProvider(conversation, result);
1439
1953
  this.input.send(createEnvelope({
1440
1954
  type: "agent.v2.conversation.opened",
1441
1955
  sessionId: this.input.sessionId,
1442
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1956
+ payload: {
1957
+ conversation,
1958
+ snapshot: this.timelines.get(conversation.id) ?? [],
1959
+ requestedConversationId: payload.conversationId,
1960
+ },
1443
1961
  }));
1444
1962
  return conversation;
1445
1963
  }
@@ -1478,8 +1996,47 @@ export class AgentWorkspaceProxy {
1478
1996
  this.input.send(createEnvelope({
1479
1997
  type: "agent.v2.conversation.opened",
1480
1998
  sessionId: this.input.sessionId,
1481
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1999
+ payload: {
2000
+ conversation,
2001
+ snapshot: this.timelines.get(conversation.id) ?? [],
2002
+ requestedConversationId: payload.conversationId,
2003
+ },
2004
+ }));
2005
+ return conversation;
2006
+ }
2007
+ openOfflineHistory(payload, message, cwd = payload.cwd ?? this.input.cwd) {
2008
+ const now = Date.now();
2009
+ const conversation = {
2010
+ id: payload.conversationId,
2011
+ agentSessionId: payload.agentSessionId,
2012
+ provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
2013
+ cwd,
2014
+ title: payload.title ?? titleFromCwd(cwd),
2015
+ model: payload.model,
2016
+ reasoningEffort: payload.reasoningEffort,
2017
+ permissionMode: payload.permissionMode,
2018
+ collaborationMode: payload.collaborationMode,
2019
+ status: "idle",
2020
+ archived: false,
2021
+ lastActivityAt: now,
2022
+ createdAt: now,
2023
+ };
2024
+ this.conversations.set(conversation.id, conversation);
2025
+ if (conversation.agentSessionId) {
2026
+ this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
2027
+ }
2028
+ this.activeConversationId = conversation.id;
2029
+ this.input.send(createEnvelope({
2030
+ type: "agent.v2.conversation.opened",
2031
+ sessionId: this.input.sessionId,
2032
+ payload: {
2033
+ conversation,
2034
+ snapshot: this.timelines.get(conversation.id) ?? [],
2035
+ requestedConversationId: payload.conversationId,
2036
+ providerError: message,
2037
+ },
1482
2038
  }));
2039
+ this.sendCapabilities();
1483
2040
  return conversation;
1484
2041
  }
1485
2042
  async sendPrompt(payload) {
@@ -1488,60 +2045,62 @@ export class AgentWorkspaceProxy {
1488
2045
  if (!conversation)
1489
2046
  return;
1490
2047
  if (!conversation.agentSessionId) {
1491
- this.addItem(payload.conversationId, {
1492
- id: id("error"),
1493
- conversationId: payload.conversationId,
1494
- type: "error",
1495
- error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
1496
- createdAt: Date.now(),
1497
- });
2048
+ this.rejectAgentAction(conversation, "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
1498
2049
  return;
1499
2050
  }
1500
- const client = this.clientForProvider(conversation.provider);
2051
+ const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
1501
2052
  if (!client) {
1502
- this.addItem(conversation.id, {
1503
- id: id("error"),
1504
- conversationId: conversation.id,
1505
- type: "error",
1506
- error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
1507
- createdAt: Date.now(),
1508
- });
2053
+ this.rejectAgentAction(conversation, this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
1509
2054
  return;
1510
2055
  }
1511
2056
  const protocol = this.protocolForProvider(conversation.provider);
1512
2057
  if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1513
- conversation.status = "idle";
1514
- conversation.lastActivityAt = Date.now();
1515
- this.emitConversation(conversation);
2058
+ this.rejectAgentAction(conversation, "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。");
2059
+ return;
2060
+ }
2061
+ const priorSettings = {
2062
+ model: conversation.model,
2063
+ reasoningEffort: conversation.reasoningEffort,
2064
+ permissionMode: conversation.permissionMode,
2065
+ };
2066
+ const effectiveModel = payload.model ??
2067
+ conversation.model ??
2068
+ (protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
2069
+ const wasRunning = conversation.status === "running";
2070
+ const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
2071
+ const shouldSteer = Boolean(payload.delivery !== "new_turn" &&
2072
+ wasRunning &&
2073
+ protocol === "codex-app-server" &&
2074
+ activeTurnId);
2075
+ if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
1516
2076
  this.addItem(conversation.id, {
1517
2077
  id: id("error"),
1518
2078
  conversationId: conversation.id,
1519
2079
  type: "error",
1520
- error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
2080
+ error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
1521
2081
  createdAt: Date.now(),
1522
2082
  });
1523
2083
  return;
1524
2084
  }
1525
- const priorSettings = {
1526
- model: conversation.model,
1527
- reasoningEffort: conversation.reasoningEffort,
1528
- permissionMode: conversation.permissionMode,
1529
- };
1530
- conversation.model = payload.model ?? conversation.model;
2085
+ if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
2086
+ this.rejectAgentAction(conversation, "当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。");
2087
+ return;
2088
+ }
2089
+ conversation.model = effectiveModel ?? conversation.model;
1531
2090
  conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1532
2091
  conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1533
2092
  conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
1534
2093
  conversation.status = "running";
1535
2094
  conversation.lastActivityAt = Date.now();
1536
2095
  this.activeConversationId = conversation.id;
1537
- if (payload.model && payload.model !== priorSettings.model) {
2096
+ if (effectiveModel && effectiveModel !== priorSettings.model) {
1538
2097
  const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1539
- const label = runtimeCapabilities?.models?.find((m) => m.id === payload.model)?.label ?? payload.model;
2098
+ const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
1540
2099
  this.emitNotice({
1541
2100
  conversationId: conversation.id,
1542
2101
  kind: "model_changed",
1543
2102
  title: `已切换模型 · ${label}`,
1544
- detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
2103
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
1545
2104
  });
1546
2105
  }
1547
2106
  if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
@@ -1566,20 +2125,61 @@ export class AgentWorkspaceProxy {
1566
2125
  role: "user",
1567
2126
  content: payload.contentBlocks,
1568
2127
  text: userText,
2128
+ metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
1569
2129
  createdAt: Date.now(),
1570
2130
  });
1571
2131
  this.emitConversation(conversation);
2132
+ const promptAsNewTurn = () => client.prompt({
2133
+ sessionId: conversation.agentSessionId,
2134
+ content: payload.contentBlocks,
2135
+ clientMessageId: payload.clientMessageId,
2136
+ model: effectiveModel,
2137
+ reasoningEffort: payload.reasoningEffort,
2138
+ permissionMode: payload.permissionMode,
2139
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
2140
+ cwd: conversation.cwd,
2141
+ });
1572
2142
  try {
1573
- const result = await client.prompt({
1574
- sessionId: conversation.agentSessionId,
1575
- content: payload.contentBlocks,
1576
- clientMessageId: payload.clientMessageId,
1577
- model: payload.model,
1578
- reasoningEffort: payload.reasoningEffort,
1579
- permissionMode: payload.permissionMode,
1580
- collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1581
- cwd: conversation.cwd,
1582
- });
2143
+ const steer = client.steer;
2144
+ let result;
2145
+ if (shouldSteer && typeof steer === "function") {
2146
+ try {
2147
+ result = await steer.call(client, {
2148
+ sessionId: conversation.agentSessionId,
2149
+ turnId: activeTurnId,
2150
+ content: payload.contentBlocks,
2151
+ });
2152
+ }
2153
+ catch (steerError) {
2154
+ this.forgetCurrentTurn(conversation.id, activeTurnId);
2155
+ const userItem = this.findItem(conversation.id, payload.clientMessageId);
2156
+ if (userItem) {
2157
+ this.upsertItem(conversation.id, {
2158
+ ...userItem,
2159
+ metadata: {
2160
+ ...(userItem.metadata ?? {}),
2161
+ delivery: "new_turn",
2162
+ fallbackFrom: "steer",
2163
+ failedTargetTurnId: activeTurnId,
2164
+ },
2165
+ updatedAt: Date.now(),
2166
+ });
2167
+ }
2168
+ this.addItem(conversation.id, {
2169
+ id: id("status"),
2170
+ conversationId: conversation.id,
2171
+ type: "status",
2172
+ role: "system",
2173
+ text: "Codex 未接收运行中追加输入,已改为发送新消息。",
2174
+ status: "running",
2175
+ createdAt: Date.now(),
2176
+ });
2177
+ result = await promptAsNewTurn();
2178
+ }
2179
+ }
2180
+ else {
2181
+ result = await promptAsNewTurn();
2182
+ }
1583
2183
  const nextAgentSessionId = this.extractSessionId(result);
1584
2184
  if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1585
2185
  if (conversation.agentSessionId)
@@ -1784,6 +2384,9 @@ export class AgentWorkspaceProxy {
1784
2384
  }
1785
2385
  }
1786
2386
  handleRequest(method, params) {
2387
+ if (method === "claude/askUserQuestion") {
2388
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
2389
+ }
1787
2390
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1788
2391
  return this.handleStructuredInput(params, true);
1789
2392
  }
@@ -1829,13 +2432,30 @@ export class AgentWorkspaceProxy {
1829
2432
  }
1830
2433
  if (method.startsWith("account/") ||
1831
2434
  method.startsWith("mcpServer/startupStatus/") ||
1832
- method === "thread/status/changed" ||
1833
2435
  method === "thread/tokenUsage/updated" ||
1834
2436
  method === "serverRequest/resolved" ||
1835
2437
  method === "mcpServer/oauthLogin/completed") {
1836
2438
  return;
1837
2439
  }
1838
2440
  const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
2441
+ if (method === "thread/status/changed") {
2442
+ const status = agentStatusFromThreadStatus(params);
2443
+ if (conversationId && status) {
2444
+ const raw = asRecord(params);
2445
+ const turnId = this.extractTurnId(raw);
2446
+ if (turnId && (status === "running" || status === "waiting_permission")) {
2447
+ this.rememberTurnConversationId(conversationId, turnId);
2448
+ }
2449
+ if (status === "idle" || status === "error") {
2450
+ this.forgetCurrentTurn(conversationId, turnId);
2451
+ }
2452
+ const message = status === "error"
2453
+ ? firstString(raw, ["message", "error", "reason"])
2454
+ : undefined;
2455
+ this.updateConversationStatus(conversationId, status, message);
2456
+ }
2457
+ return;
2458
+ }
1839
2459
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1840
2460
  this.handleStructuredInput(params);
1841
2461
  return;
@@ -2345,9 +2965,7 @@ export class AgentWorkspaceProxy {
2345
2965
  const raw = asRecord(params) ?? {};
2346
2966
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2347
2967
  const source = firstString(raw, ["method", "source", "requestMethod"]);
2348
- const formatResponse = source === "mcpServer/elicitation/request"
2349
- ? formatMcpElicitationResponse
2350
- : formatStructuredInputResponse;
2968
+ const formatResponse = (answers, input) => formatStructuredInputResponseForSource(source, answers, input);
2351
2969
  if (!conversationId)
2352
2970
  return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
2353
2971
  const structuredInput = decodeStructuredInput(raw);
@@ -2374,13 +2992,13 @@ export class AgentWorkspaceProxy {
2374
2992
  const timer = setTimeout(() => {
2375
2993
  this.pendingStructuredInputs.delete(structuredInput.requestId);
2376
2994
  this.structuredInputWaiters.delete(structuredInput.requestId);
2377
- resolve(formatResponse({}));
2995
+ resolve(formatResponse({}, structuredInput));
2378
2996
  this.markStructuredInput(conversationId, structuredInput.requestId, {
2379
2997
  inputPending: false,
2380
2998
  inputError: "等待用户输入超时",
2381
2999
  });
2382
3000
  }, PERMISSION_TIMEOUT_MS);
2383
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
3001
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
2384
3002
  });
2385
3003
  }
2386
3004
  toolCallFromItem(item, fallbackStatus) {
@@ -2414,11 +3032,19 @@ export class AgentWorkspaceProxy {
2414
3032
  return waitForResponse ? Promise.resolve({ outcome: { outcome: "cancelled" } }) : undefined;
2415
3033
  const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
2416
3034
  const rawToolCall = asRecord(raw.toolCall) ?? raw;
3035
+ // Codex app-server command/file approvals carry `command`/`reason`/`cwd`
3036
+ // (not toolCall/context). Surface them so the card actually shows what's
3037
+ // being approved; fall back to the Claude/MCP shape otherwise.
3038
+ const codexCommand = firstString(raw, ["command"]);
3039
+ const codexCwd = firstString(raw, ["cwd"]);
2417
3040
  const permission = {
2418
3041
  requestId,
2419
- toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]),
2420
- toolInput: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
2421
- context: firstString(raw, ["context", "description", "message", "title"]),
3042
+ toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]) ??
3043
+ (codexCommand ? "shell" : undefined),
3044
+ toolInput: codexCommand
3045
+ ? (codexCwd ? `$ ${codexCommand}\n# cwd: ${codexCwd}` : `$ ${codexCommand}`)
3046
+ : stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
3047
+ context: firstString(raw, ["context", "description", "message", "title", "reason"]),
2422
3048
  options: parsePermissionOptions(raw.options),
2423
3049
  };
2424
3050
  this.pendingPermissions.set(requestId, permission);
@@ -2430,6 +3056,12 @@ export class AgentWorkspaceProxy {
2430
3056
  conversationId,
2431
3057
  type: "permission",
2432
3058
  permission,
3059
+ metadata: {
3060
+ protocol: "v2",
3061
+ permissionLive: true,
3062
+ permissionExpired: false,
3063
+ permissionPending: false,
3064
+ },
2433
3065
  createdAt: Date.now(),
2434
3066
  updatedAt: Date.now(),
2435
3067
  };
@@ -2454,9 +3086,13 @@ export class AgentWorkspaceProxy {
2454
3086
  }
2455
3087
  respondPermission(payload) {
2456
3088
  const permission = this.pendingPermissions.get(payload.requestId);
3089
+ const waiter = this.permissionWaiters.get(payload.requestId);
3090
+ const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
3091
+ const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
3092
+ if (!permission && !waiter && alreadyResolved)
3093
+ return;
2457
3094
  this.pendingPermissions.delete(payload.requestId);
2458
3095
  const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
2459
- const waiter = this.permissionWaiters.get(payload.requestId);
2460
3096
  if (waiter) {
2461
3097
  clearTimeout(waiter.timer);
2462
3098
  this.permissionWaiters.delete(payload.requestId);
@@ -2479,19 +3115,22 @@ export class AgentWorkspaceProxy {
2479
3115
  permissionError: undefined,
2480
3116
  permissionPending: false,
2481
3117
  });
2482
- this.updateConversationStatus(payload.conversationId, "running");
3118
+ if (permission || waiter || existingItem) {
3119
+ this.updateConversationStatus(payload.conversationId, "running");
3120
+ }
2483
3121
  }
2484
3122
  respondStructuredInput(payload) {
2485
3123
  const pending = this.pendingStructuredInputs.get(payload.requestId);
2486
- this.pendingStructuredInputs.delete(payload.requestId);
2487
3124
  const waiter = this.structuredInputWaiters.get(payload.requestId);
3125
+ const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
3126
+ const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
3127
+ if (!pending && !waiter && alreadySubmitted)
3128
+ return;
3129
+ this.pendingStructuredInputs.delete(payload.requestId);
2488
3130
  if (waiter) {
2489
3131
  clearTimeout(waiter.timer);
2490
3132
  this.structuredInputWaiters.delete(payload.requestId);
2491
- const formatResponse = waiter.source === "mcpServer/elicitation/request"
2492
- ? formatMcpElicitationResponse
2493
- : formatStructuredInputResponse;
2494
- waiter.resolve(formatResponse(payload.answers));
3133
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
2495
3134
  }
2496
3135
  this.markStructuredInput(payload.conversationId, payload.requestId, {
2497
3136
  inputPending: false,
@@ -2500,7 +3139,9 @@ export class AgentWorkspaceProxy {
2500
3139
  inputError: undefined,
2501
3140
  answers: payload.answers,
2502
3141
  });
2503
- this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3142
+ if (pending || waiter || existingItem) {
3143
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3144
+ }
2504
3145
  }
2505
3146
  markPermission(conversationId, requestId, metadata) {
2506
3147
  const item = this.findItem(conversationId, `permission:${requestId}`);
@@ -2675,14 +3316,14 @@ export class AgentWorkspaceProxy {
2675
3316
  this.input.send(createEnvelope({
2676
3317
  type: "agent.v2.event",
2677
3318
  sessionId: this.input.sessionId,
2678
- payload: { conversationId, conversation, item },
3319
+ payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
2679
3320
  }));
2680
3321
  }
2681
3322
  emitConversation(conversation) {
2682
3323
  this.input.send(createEnvelope({
2683
3324
  type: "agent.v2.event",
2684
3325
  sessionId: this.input.sessionId,
2685
- payload: { conversationId: conversation.id, conversation },
3326
+ payload: { conversationId: conversation.id, conversation: { ...conversation } },
2686
3327
  }));
2687
3328
  }
2688
3329
  emitStatus(conversationId, status, text) {
@@ -2702,6 +3343,16 @@ export class AgentWorkspaceProxy {
2702
3343
  payload: input,
2703
3344
  }));
2704
3345
  }
3346
+ rejectAgentAction(conversation, message, status = "idle") {
3347
+ this.updateConversationStatus(conversation.id, status, message);
3348
+ this.addItem(conversation.id, {
3349
+ id: id("error"),
3350
+ conversationId: conversation.id,
3351
+ type: "error",
3352
+ error: message,
3353
+ createdAt: Date.now(),
3354
+ });
3355
+ }
2705
3356
  updateConversationPreview(conversationId, text, status) {
2706
3357
  const conversation = this.conversations.get(conversationId);
2707
3358
  if (!conversation)
@@ -2737,6 +3388,58 @@ export class AgentWorkspaceProxy {
2737
3388
  },
2738
3389
  }));
2739
3390
  }
3391
+ /** Page OLDER transcript history for a conversation (client scrolling up).
3392
+ * Only Codex's app-server exposes paginated reads; other providers report
3393
+ * no more history. Pure pass-through — does not touch the bounded live
3394
+ * timeline. The client prepends the returned (ascending) items. */
3395
+ async loadOlderHistory(payload) {
3396
+ const sendResult = (items, nextCursor) => {
3397
+ this.input.send(createEnvelope({
3398
+ type: "agent.v2.history.result",
3399
+ sessionId: this.input.sessionId,
3400
+ payload: {
3401
+ conversationId: payload.conversationId,
3402
+ items,
3403
+ nextCursor,
3404
+ hasMore: Boolean(nextCursor),
3405
+ },
3406
+ }));
3407
+ };
3408
+ const conversation = this.conversations.get(payload.conversationId);
3409
+ if (!conversation?.agentSessionId)
3410
+ return sendResult([], undefined);
3411
+ const client = this.clientForProvider(conversation.provider);
3412
+ const listTurns = client?.listTurns;
3413
+ if (typeof listTurns !== "function")
3414
+ return sendResult([], undefined);
3415
+ // Client-supplied cursor wins; else the one captured at hydration. Absent
3416
+ // cursor means we're already at the start of history.
3417
+ const cursor = payload.cursor ?? this.historyCursors.get(payload.conversationId);
3418
+ if (!cursor)
3419
+ return sendResult([], undefined);
3420
+ try {
3421
+ const turnsResult = await listTurns.call(client, {
3422
+ sessionId: conversation.agentSessionId,
3423
+ limit: payload.limit,
3424
+ cursor,
3425
+ sortDirection: "desc",
3426
+ itemsView: "full",
3427
+ });
3428
+ const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
3429
+ const items = turnsThread
3430
+ ? timelineItemsFromProviderThread({ thread: turnsThread }, payload.conversationId)
3431
+ : [];
3432
+ const nextCursor = firstString(asRecord(turnsResult), ["nextCursor", "next_cursor", "cursor"]);
3433
+ this.historyCursors.set(payload.conversationId, nextCursor);
3434
+ sendResult(items, nextCursor);
3435
+ }
3436
+ catch (error) {
3437
+ if (this.input.verbose) {
3438
+ process.stderr.write(`[agent:v2] history page failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
3439
+ }
3440
+ sendResult([], undefined);
3441
+ }
3442
+ }
2740
3443
  conversationIdFromParams(params) {
2741
3444
  const raw = asRecord(params);
2742
3445
  if (!raw)
@@ -2823,13 +3526,23 @@ export class AgentWorkspaceProxy {
2823
3526
  handleProviderExit(provider, message) {
2824
3527
  this.clients.delete(provider);
2825
3528
  this.agentProtocols.delete(provider);
2826
- this.cancelPendingPermissions();
3529
+ this.providerErrors.set(provider, message);
3530
+ for (const conversation of this.conversations.values()) {
3531
+ if (conversation.provider === provider && conversation.status === "waiting_permission") {
3532
+ this.cancelPendingPermissions(conversation.id, false);
3533
+ }
3534
+ }
2827
3535
  for (const conversation of this.conversations.values()) {
2828
3536
  if (conversation.provider !== provider)
2829
3537
  continue;
3538
+ if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
3539
+ this.emitConversation(conversation);
3540
+ continue;
3541
+ }
2830
3542
  conversation.status = "error";
2831
3543
  conversation.lastMessagePreview = message;
2832
3544
  conversation.lastActivityAt = Date.now();
3545
+ this.forgetCurrentTurn(conversation.id);
2833
3546
  this.emitConversation(conversation);
2834
3547
  this.addItem(conversation.id, {
2835
3548
  id: id("error"),
@@ -2841,18 +3554,23 @@ export class AgentWorkspaceProxy {
2841
3554
  }
2842
3555
  this.sendCapabilities();
2843
3556
  }
2844
- cancelPendingPermissions(conversationId) {
3557
+ cancelPendingPermissions(conversationId, updateStatus = true) {
2845
3558
  for (const [requestId, waiter] of this.permissionWaiters) {
3559
+ if (conversationId && this.itemConversationIds.get(requestId) !== conversationId)
3560
+ continue;
2846
3561
  clearTimeout(waiter.timer);
2847
3562
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
2848
3563
  this.pendingPermissions.delete(requestId);
2849
3564
  this.permissionSources.delete(requestId);
2850
3565
  }
2851
- this.permissionWaiters.clear();
3566
+ if (!conversationId)
3567
+ this.permissionWaiters.clear();
2852
3568
  for (const [requestId, waiter] of this.structuredInputWaiters) {
2853
- clearTimeout(waiter.timer);
2854
- waiter.resolve(formatStructuredInputResponse({}));
2855
3569
  const pending = this.pendingStructuredInputs.get(requestId);
3570
+ if (conversationId && pending?.conversationId !== conversationId)
3571
+ continue;
3572
+ clearTimeout(waiter.timer);
3573
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
2856
3574
  if (pending) {
2857
3575
  this.markStructuredInput(pending.conversationId, requestId, {
2858
3576
  inputPending: false,
@@ -2860,9 +3578,11 @@ export class AgentWorkspaceProxy {
2860
3578
  });
2861
3579
  }
2862
3580
  this.pendingStructuredInputs.delete(requestId);
3581
+ this.structuredInputWaiters.delete(requestId);
2863
3582
  }
2864
- this.structuredInputWaiters.clear();
2865
- if (conversationId)
3583
+ if (!conversationId)
3584
+ this.structuredInputWaiters.clear();
3585
+ if (conversationId && updateStatus)
2866
3586
  this.updateConversationStatus(conversationId, "idle");
2867
3587
  }
2868
3588
  extractSessionId(value) {
@@ -2940,6 +3660,34 @@ function formatStructuredInputResponse(answers) {
2940
3660
  ])),
2941
3661
  };
2942
3662
  }
3663
+ function formatClaudeAskUserQuestionResponse(answers, input) {
3664
+ const questions = input?.questions ?? [];
3665
+ const byQuestionText = {};
3666
+ for (const question of questions) {
3667
+ const values = answers[question.id] ?? answers[question.question] ?? [];
3668
+ const cleaned = values.map((value) => value.trim()).filter(Boolean);
3669
+ if (cleaned.length === 0)
3670
+ continue;
3671
+ byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
3672
+ ? cleaned
3673
+ : cleaned[0] ?? "";
3674
+ }
3675
+ return {
3676
+ behavior: "allow",
3677
+ updatedInput: {
3678
+ questions: questions.map((question) => ({
3679
+ question: question.question,
3680
+ header: question.header,
3681
+ options: question.options?.map((option) => ({
3682
+ label: option.label,
3683
+ description: option.description,
3684
+ })) ?? [],
3685
+ multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
3686
+ })),
3687
+ answers: byQuestionText,
3688
+ },
3689
+ };
3690
+ }
2943
3691
  function formatMcpElicitationResponse(answers) {
2944
3692
  const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
2945
3693
  questionId,
@@ -2951,6 +3699,15 @@ function formatMcpElicitationResponse(answers) {
2951
3699
  _meta: { source: "linkshell" },
2952
3700
  };
2953
3701
  }
3702
+ function formatStructuredInputResponseForSource(source, answers, input) {
3703
+ if (source === "mcpServer/elicitation/request") {
3704
+ return formatMcpElicitationResponse(answers);
3705
+ }
3706
+ if (source === "claude/askUserQuestion") {
3707
+ return formatClaudeAskUserQuestionResponse(answers, input);
3708
+ }
3709
+ return formatStructuredInputResponse(answers);
3710
+ }
2954
3711
  function formatPermissionResponse(source, outcome, optionId) {
2955
3712
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
2956
3713
  return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };