neoctl 0.2.36 → 0.2.37

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 (149) hide show
  1. package/dist/agents/agent-report-security.test.d.ts +1 -0
  2. package/dist/agents/agent-report-security.test.js +301 -0
  3. package/dist/agents/agent-report-security.test.js.map +1 -0
  4. package/dist/agents/agent-tool-persistence.test.js +9 -3
  5. package/dist/agents/agent-tool-persistence.test.js.map +1 -1
  6. package/dist/agents/agent-tool.d.ts +3 -0
  7. package/dist/agents/agent-tool.js +127 -34
  8. package/dist/agents/agent-tool.js.map +1 -1
  9. package/dist/agents/live-preview-redaction-security.test.d.ts +1 -0
  10. package/dist/agents/live-preview-redaction-security.test.js +286 -0
  11. package/dist/agents/live-preview-redaction-security.test.js.map +1 -0
  12. package/dist/agents/local-agent-task.d.ts +27 -1
  13. package/dist/agents/local-agent-task.js +75 -5
  14. package/dist/agents/local-agent-task.js.map +1 -1
  15. package/dist/agents/obs07-11-run-facts.test.d.ts +1 -0
  16. package/dist/agents/obs07-11-run-facts.test.js +459 -0
  17. package/dist/agents/obs07-11-run-facts.test.js.map +1 -0
  18. package/dist/agents/smoke-agent-lifecycle.js +8 -4
  19. package/dist/agents/smoke-agent-lifecycle.js.map +1 -1
  20. package/dist/agents/smoke-agents.js +7 -3
  21. package/dist/agents/smoke-agents.js.map +1 -1
  22. package/dist/context/context-manager.d.ts +1 -0
  23. package/dist/context/context-manager.js +10 -6
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/smoke-context.js +46 -3
  26. package/dist/context/smoke-context.js.map +1 -1
  27. package/dist/core/image-registry.js +1 -1
  28. package/dist/core/image-registry.js.map +1 -1
  29. package/dist/core/obs04-visible-data.test.d.ts +1 -0
  30. package/dist/core/obs04-visible-data.test.js +147 -0
  31. package/dist/core/obs04-visible-data.test.js.map +1 -0
  32. package/dist/core/query-engine.d.ts +5 -0
  33. package/dist/core/query-engine.js +35 -2
  34. package/dist/core/query-engine.js.map +1 -1
  35. package/dist/core/query.d.ts +2 -0
  36. package/dist/core/query.js +104 -15
  37. package/dist/core/query.js.map +1 -1
  38. package/dist/core/run-agent.d.ts +2 -0
  39. package/dist/core/run-agent.js +20 -10
  40. package/dist/core/run-agent.js.map +1 -1
  41. package/dist/model/model-gateway.d.ts +1 -0
  42. package/dist/model/model-metadata.json +99 -99
  43. package/dist/model/openai-native-image-guard.test.d.ts +1 -0
  44. package/dist/model/openai-native-image-guard.test.js +101 -0
  45. package/dist/model/openai-native-image-guard.test.js.map +1 -0
  46. package/dist/model/openai-responses-mapper.js +91 -16
  47. package/dist/model/openai-responses-mapper.js.map +1 -1
  48. package/dist/plugins/smoke-plugin-system.js +14 -14
  49. package/dist/repl/index.js +2 -2
  50. package/dist/repl/index.js.map +1 -1
  51. package/dist/secrets/secret-redaction.d.ts +6 -5
  52. package/dist/secrets/secret-redaction.js +7 -79
  53. package/dist/secrets/secret-redaction.js.map +1 -1
  54. package/dist/secrets/secret-types.d.ts +4 -1
  55. package/dist/secrets/secret-types.js.map +1 -1
  56. package/dist/session/session-store.d.ts +4 -1
  57. package/dist/session/session-store.js +5 -2
  58. package/dist/session/session-store.js.map +1 -1
  59. package/dist/tasks/subagent-tools.test.js +1 -0
  60. package/dist/tasks/subagent-tools.test.js.map +1 -1
  61. package/dist/tasks/task-ack-size.test.js +6 -5
  62. package/dist/tasks/task-ack-size.test.js.map +1 -1
  63. package/dist/tasks/task-persistence.d.ts +1 -0
  64. package/dist/tasks/task-persistence.js +31 -9
  65. package/dist/tasks/task-persistence.js.map +1 -1
  66. package/dist/tasks/task-store.d.ts +6 -4
  67. package/dist/tasks/task-store.js +46 -20
  68. package/dist/tasks/task-store.js.map +1 -1
  69. package/dist/tools/builtins/exec-process-manager.d.ts +97 -2
  70. package/dist/tools/builtins/exec-process-manager.js +218 -19
  71. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  72. package/dist/tools/builtins/exec-tool.js +14 -1
  73. package/dist/tools/builtins/exec-tool.js.map +1 -1
  74. package/dist/tools/builtins/image-generation-tool.js +24 -24
  75. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  76. package/dist/tools/smoke-exec-process.js +2 -1
  77. package/dist/tools/smoke-exec-process.js.map +1 -1
  78. package/dist/tools/smoke-terminal-output-chain.d.ts +1 -0
  79. package/dist/tools/smoke-terminal-output-chain.js +245 -0
  80. package/dist/tools/smoke-terminal-output-chain.js.map +1 -0
  81. package/dist/tools/smoke-tool-system.js +6 -6
  82. package/dist/tools/smoke-tool-system.js.map +1 -1
  83. package/dist/tools/terminal-output-persistence.test.d.ts +1 -0
  84. package/dist/tools/terminal-output-persistence.test.js +50 -0
  85. package/dist/tools/terminal-output-persistence.test.js.map +1 -0
  86. package/dist/tools/terminal-output-store.d.ts +150 -0
  87. package/dist/tools/terminal-output-store.js +562 -0
  88. package/dist/tools/terminal-output-store.js.map +1 -0
  89. package/dist/tools/terminal-output-store.test.d.ts +1 -0
  90. package/dist/tools/terminal-output-store.test.js +364 -0
  91. package/dist/tools/terminal-output-store.test.js.map +1 -0
  92. package/dist/types/events.d.ts +1 -0
  93. package/dist/types/messages.d.ts +1 -0
  94. package/dist/web/agent-content-detail.d.ts +78 -0
  95. package/dist/web/agent-content-detail.js +495 -0
  96. package/dist/web/agent-content-detail.js.map +1 -0
  97. package/dist/web/agent-content-detail.test.d.ts +1 -0
  98. package/dist/web/agent-content-detail.test.js +307 -0
  99. package/dist/web/agent-content-detail.test.js.map +1 -0
  100. package/dist/web/agent-parent-messages.test.d.ts +1 -0
  101. package/dist/web/agent-parent-messages.test.js +58 -0
  102. package/dist/web/agent-parent-messages.test.js.map +1 -0
  103. package/dist/web/agent-tool-payload-security.test.d.ts +1 -0
  104. package/dist/web/agent-tool-payload-security.test.js +120 -0
  105. package/dist/web/agent-tool-payload-security.test.js.map +1 -0
  106. package/dist/web/index.d.ts +114 -3
  107. package/dist/web/index.js +182 -67
  108. package/dist/web/index.js.map +1 -1
  109. package/dist/web/smoke-agent-content-http.d.ts +1 -0
  110. package/dist/web/smoke-agent-content-http.js +446 -0
  111. package/dist/web/smoke-agent-content-http.js.map +1 -0
  112. package/dist/web/smoke-status-semantics.d.ts +1 -0
  113. package/dist/web/smoke-status-semantics.js +90 -0
  114. package/dist/web/smoke-status-semantics.js.map +1 -0
  115. package/dist/web/smoke-task-session-entrypoints.js +22 -1
  116. package/dist/web/smoke-task-session-entrypoints.js.map +1 -1
  117. package/dist/web/smoke-terminal-output-http.d.ts +1 -0
  118. package/dist/web/smoke-terminal-output-http.js +372 -0
  119. package/dist/web/smoke-terminal-output-http.js.map +1 -0
  120. package/dist/web/smoke-tool-call-detail.d.ts +1 -0
  121. package/dist/web/smoke-tool-call-detail.js +106 -0
  122. package/dist/web/smoke-tool-call-detail.js.map +1 -0
  123. package/dist/web/smoke-tool-call-http.d.ts +1 -0
  124. package/dist/web/smoke-tool-call-http.js +100 -0
  125. package/dist/web/smoke-tool-call-http.js.map +1 -0
  126. package/dist/web/smoke-tool-detail-fields-http.d.ts +1 -0
  127. package/dist/web/smoke-tool-detail-fields-http.js +121 -0
  128. package/dist/web/smoke-tool-detail-fields-http.js.map +1 -0
  129. package/dist/web/smoke-web-terminal-tasks.js +126 -76
  130. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  131. package/dist/web/status-semantics.d.ts +11 -0
  132. package/dist/web/status-semantics.js +59 -0
  133. package/dist/web/status-semantics.js.map +1 -0
  134. package/dist/web/terminal-presentation.d.ts +15 -0
  135. package/dist/web/terminal-presentation.js +61 -0
  136. package/dist/web/terminal-presentation.js.map +1 -0
  137. package/dist/web/terminal-presentation.test.d.ts +1 -0
  138. package/dist/web/terminal-presentation.test.js +33 -0
  139. package/dist/web/terminal-presentation.test.js.map +1 -0
  140. package/dist/web/tool-call-detail.d.ts +40 -0
  141. package/dist/web/tool-call-detail.js +167 -0
  142. package/dist/web/tool-call-detail.js.map +1 -0
  143. package/dist/web/tool-detail-fields.d.ts +59 -0
  144. package/dist/web/tool-detail-fields.js +119 -0
  145. package/dist/web/tool-detail-fields.js.map +1 -0
  146. package/dist/web/tool-detail-fields.test.d.ts +1 -0
  147. package/dist/web/tool-detail-fields.test.js +121 -0
  148. package/dist/web/tool-detail-fields.test.js.map +1 -0
  149. package/package.json +93 -93
package/dist/web/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { subagentHeader, subagentStatusFacts } from "./status-semantics.js";
2
3
  import http from "node:http";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
@@ -31,6 +32,10 @@ import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/comm
31
32
  import { writeSessionMarkdownExport } from "../session/session-export.js";
32
33
  import { DefaultContextManager } from "../context/context-manager.js";
33
34
  import { createImageId } from "../types/messages.js";
35
+ import { readToolCallDetail, toolErrorText, allowToolDetailRequest, sanitizeAgentToolPayload, redactToolDetail } from "./tool-call-detail.js";
36
+ import { terminalFacts, terminalPreviews, terminalResultText } from "./terminal-presentation.js";
37
+ import { createAgentContentDetailResolver } from "./agent-content-detail.js";
38
+ import { toolDetailFieldsFromDetail } from "./tool-detail-fields.js";
34
39
  import { WEB_HTML } from "./html.js";
35
40
  import { openDirectory } from "../open-directory.js";
36
41
  import { resolveImageBlockDataResultSync, resolveImageBlockDataSync } from "../core/image-storage.js";
@@ -274,7 +279,15 @@ function applyToolOverrides(registry, catalog, globalOverrides, sessionOverrides
274
279
  }
275
280
  /** Bind only the foreground view; never stop work owned by another session. */
276
281
  export function activateSession(runtime) {
277
- runtime.taskStore.bindSession?.(runtime.engine.snapshot().session?.sessionDir || undefined);
282
+ const sessionDir = runtime.engine.snapshot().session?.sessionDir || undefined;
283
+ const summary = runtime.taskStore.bindSession?.(sessionDir);
284
+ const interruptedTasks = summary?.loaded ? runtime.taskStore.recoverableInterruptedTasks?.(sessionDir) ?? [] : [];
285
+ if (interruptedTasks.length) {
286
+ runtime.engine.noteRecoverableSubagents(() => {
287
+ for (const task of interruptedTasks)
288
+ runtime.taskStore.markNotified(task.taskId);
289
+ });
290
+ }
278
291
  }
279
292
  export function sessionAgentTasks(runtime) {
280
293
  const dir = runtime.engine.snapshot().session?.sessionDir || undefined;
@@ -390,6 +403,7 @@ function webRuntimeScopeKey(scope) {
390
403
  }
391
404
  export class WebRepl {
392
405
  runtime;
406
+ agentContentReader = createAgentContentDetailResolver();
393
407
  subscribers = new Set();
394
408
  eventSequence = 0;
395
409
  pendingDeltaOperations = [];
@@ -458,19 +472,53 @@ export class WebRepl {
458
472
  this.send(subscriber, "client.reload", payload);
459
473
  return subscribers.length;
460
474
  }
475
+ async toolCallDetail(sessionId, toolUseId, messageId) {
476
+ const session = this.runtime.engine.snapshot().session;
477
+ const detail = await readToolCallDetail({ sessionId: session?.sessionId ?? "", expectedSessionId: sessionId, sessionDir: session?.sessionDir, toolUseId, messageId, redact: value => this.runtime.engine.redactDisplayValue(value), entries: this.runtime.engine.getDisplayEntries() });
478
+ return detail ? this.runtime.engine.redactDisplayValue({ ...detail, fields: toolDetailFieldsFromDetail(detail) }) : undefined;
479
+ }
480
+ async agentContent(sessionId, view, request) {
481
+ const session = this.runtime.engine.snapshot().session;
482
+ if (!session?.sessionDir || sessionId !== session.sessionId || !["timeline", "delegation", "report", "messages"].includes(view))
483
+ return undefined;
484
+ // Loading/recovery belongs to activateSession, never this read-only route.
485
+ if (!this.runtime.taskStore.getInSession(request.taskId, session.sessionDir))
486
+ return undefined;
487
+ return this.agentContentReader[view]({
488
+ ownerSessionId: sessionId, ownerSessionDir: session.sessionDir, taskStore: this.runtime.taskStore,
489
+ redact: value => this.runtime.engine.redactDisplayValue(value),
490
+ }, request);
491
+ }
492
+ terminalOutput(sessionId, runId, stream, offset, limitBytes) {
493
+ const session = this.runtime.engine.snapshot().session;
494
+ if (!session?.sessionDir || !sessionId || sessionId !== session.sessionId || !runId || !["stdout", "stderr"].includes(stream))
495
+ return undefined;
496
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limitBytes) || limitBytes < 4 || limitBytes > 262144)
497
+ return undefined;
498
+ const manager = this.runtime.execProcessManager;
499
+ const registered = manager.registerOwnerSession(sessionId, session.sessionDir);
500
+ if (!registered.ok)
501
+ return undefined;
502
+ const page = manager.readOutput(sessionId, runId, { stream: stream, offset, limitBytes });
503
+ // Cursor addresses stored bytes, not the possibly additionally redacted response length.
504
+ return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
505
+ }
461
506
  snapshot(includeCatalog = false) {
462
507
  const backgroundTasks = this.backgroundTasks();
508
+ const engineSnapshot = this.runtime.engine.snapshot();
463
509
  return {
464
- lines: this.lines,
510
+ lines: this.lines.map(line => line.toolError ? { ...line, toolError: this.runtime.engine.redactDisplayValue?.(line.toolError) ?? line.toolError } : line),
465
511
  status: this.status,
466
512
  busy: this.busy,
467
513
  queuedInput: this.queuedInput,
468
514
  backgroundTaskCount: backgroundTasks.length,
469
515
  backgroundTasks,
470
- agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 20).map((task) => this.agentTaskSnapshot(task)),
516
+ terminalTaskHistory: this.terminalTaskHistory(),
517
+ agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((task) => this.agentTaskSnapshot(task)),
471
518
  backgroundSessionRunCount: this.backgroundSessionRuns.size,
472
519
  runningSessionIds: [...this.backgroundSessionRuns.keys()],
473
- session: this.runtime.engine.snapshot().session,
520
+ session: engineSnapshot.session,
521
+ modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
474
522
  fastMode: this.runtime.engine.isFastMode(),
475
523
  appPrompt: this.runtime.engine.getAppPrompt(),
476
524
  catalog: includeCatalog ? webCatalog(this.runtime) : undefined,
@@ -1042,27 +1090,75 @@ export class WebRepl {
1042
1090
  }
1043
1091
  agentTaskSnapshot(task) {
1044
1092
  const terminal = this.runtime.taskStore.isTerminal(task);
1045
- const result = (value, limit = 1500) => value ? { content: value.content.slice(0, limit), truncated: value.content.length > limit, status: value.status } : undefined;
1046
- return {
1093
+ // Redact complete values before any public preview boundary: clipping first can expose
1094
+ // a credential prefix which no longer matches the runtime registry's full value.
1095
+ const safeText = (value) => value === undefined ? undefined : this.runtime.engine.redactDisplayValue(value);
1096
+ const result = (value, limit = 1500) => {
1097
+ if (!value)
1098
+ return undefined;
1099
+ if (value.displaySource !== "agent_report" && value.displaySource !== "visible_text") {
1100
+ return { content: "", truncated: false, status: value.status, availability: "unavailable", reason: "旧报告缺少获准展示来源;不会公开未验证的文本" };
1101
+ }
1102
+ const content = safeText(value.content);
1103
+ return { content: content.slice(0, limit), truncated: content.length > limit, status: value.status };
1104
+ };
1105
+ const error = safeText(task.error);
1106
+ return this.runtime.engine.redactDisplayValue({
1047
1107
  kind: "agent", taskId: task.taskId, agentId: task.agentId,
1048
1108
  agentType: task.agentType, type: task.type, status: task.status,
1049
1109
  description: task.description, runGeneration: task.runGeneration ?? 1,
1050
- progress: { lastActivity: task.progress.lastActivity, currentAction: task.progress.currentAction, totalToolUseCount: task.progress.totalToolUseCount, steps: task.progress.steps?.slice(-8).map((step) => ({ id: step.id, title: step.title, status: step.status })) },
1110
+ progress: { visibleText: task.progress.visibleText?.channel === "visible" && task.progress.visibleText.redactionVersion === 1 && task.progress.visibleText.runGeneration === task.runGeneration ? this.runtime.engine.redactDisplayValue(task.progress.visibleText) : undefined, lastActivity: task.progress.lastActivity, currentAction: task.progress.currentAction, totalToolUseCount: task.progress.totalToolUseCount, steps: task.progress.steps?.slice(-8).map((step) => ({ id: step.id, title: step.title, status: step.status })) },
1051
1111
  pendingMessageCount: task.pendingMessages.length,
1052
1112
  deliveredRetainedThisRun: task.messageReceipts?.filter((receipt) => receipt.status === "delivered" && receipt.runGeneration === task.runGeneration).length,
1053
1113
  messageReceipts: (task.messageReceipts ?? []).slice(-20).map(({ id, status, queuedAt, deliveredAt, runGeneration }) => ({ id, status, queuedAt, deliveredAt, runGeneration })),
1054
1114
  result: terminal ? result(task.result) : undefined,
1055
- error: terminal ? task.error?.slice(0, 1500) : undefined,
1056
- errorTruncated: terminal && (task.error?.length ?? 0) > 1500,
1057
- runHistory: (task.runHistory ?? []).slice(-3).map((run) => ({ runGeneration: run.runGeneration, status: run.status, completedAt: run.completedAt, result: result(run.result, 600), error: run.error?.slice(0, 600), errorTruncated: (run.error?.length ?? 0) > 600 })),
1058
- createdAt: task.createdAt, updatedAt: task.updatedAt, completedAt: task.completedAt,
1059
- };
1115
+ error: terminal ? error?.slice(0, 1500) : undefined,
1116
+ errorTruncated: terminal && (error?.length ?? 0) > 1500,
1117
+ runHistory: (task.runHistory ?? []).map((run) => {
1118
+ const archivedError = safeText(run.error);
1119
+ return { runGeneration: run.runGeneration, status: run.status, startedAt: run.startedAt, durationMs: run.durationMs, completedAt: run.completedAt, result: result(run.result, 600), error: archivedError?.slice(0, 600), errorTruncated: (archivedError?.length ?? 0) > 600 };
1120
+ }),
1121
+ createdAt: task.createdAt, updatedAt: task.updatedAt, startedAt: task.startedAt, completedAt: task.completedAt, durationMs: task.durationMs,
1122
+ });
1123
+ }
1124
+ terminalTaskHistory() {
1125
+ const session = this.runtime.engine.snapshot().session;
1126
+ if (!session?.sessionId || !session.sessionDir)
1127
+ return [];
1128
+ const manager = this.runtime.execProcessManager;
1129
+ if (!manager.registerOwnerSession?.(session.sessionId, session.sessionDir).ok)
1130
+ return [];
1131
+ const records = [];
1132
+ let offset = 0;
1133
+ do {
1134
+ const history = manager.listHistory(session.sessionId, { offset, limit: 200 });
1135
+ if (!history.ok)
1136
+ break;
1137
+ records.push(...history.value.records);
1138
+ if (history.value.nextOffset === null)
1139
+ break;
1140
+ offset = history.value.nextOffset;
1141
+ } while (true);
1142
+ return records.reverse().map(({ record }) => ({
1143
+ kind: "terminal", taskId: `terminal:${record.runId}`, type: "终端",
1144
+ ownerSessionId: session.sessionId, sessionId: record.runId,
1145
+ status: record.lifecycle === "lost" ? "lost" : record.exit?.status ?? "unknown",
1146
+ description: record.metadata.description || record.metadata.command || "终端运行",
1147
+ command: record.metadata.command, cwd: record.metadata.cwd, shell: record.metadata.shell,
1148
+ tty: record.metadata.tty, processId: record.metadata.processId,
1149
+ createdAt: record.metadata.startedAt, completedAt: record.exit?.finishedAt,
1150
+ durationMs: record.exit?.durationMs, exitCode: record.exit?.exitCode,
1151
+ signal: record.exit?.signal, terminationReason: record.exit?.terminationReason,
1152
+ outputAvailability: record.availability, outputTruncated: record.truncated,
1153
+ metadataTruncated: record.metadata.truncatedFields,
1154
+ })).map(task => this.runtime.engine.redactDisplayValue(task));
1060
1155
  }
1061
1156
  backgroundTasks() {
1062
1157
  const agentTasks = sessionAgentTasks(this.runtime)
1063
1158
  .filter((task) => !this.runtime.taskStore.isTerminal(task))
1064
1159
  .map((task) => this.agentTaskSnapshot(task));
1065
- const terminalTasks = this.runtime.execProcessManager.list()
1160
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1161
+ const terminalTasks = (ownerSessionId ? this.runtime.execProcessManager.list(ownerSessionId) : [])
1066
1162
  .filter((terminal) => terminal.status === "running" && terminal.backgrounded)
1067
1163
  .map((terminal) => ({
1068
1164
  kind: "terminal",
@@ -1070,7 +1166,8 @@ export class WebRepl {
1070
1166
  type: "终端",
1071
1167
  status: terminal.status,
1072
1168
  description: terminal.description || terminal.command,
1073
- createdAt: Date.now() - terminal.duration_ms,
1169
+ createdAt: terminal.started_at,
1170
+ ownerSessionId,
1074
1171
  sessionId: terminal.session_id,
1075
1172
  processId: terminal.process_id,
1076
1173
  command: terminal.command,
@@ -1078,8 +1175,11 @@ export class WebRepl {
1078
1175
  shell: terminal.shell,
1079
1176
  tty: terminal.tty,
1080
1177
  durationMs: terminal.duration_ms,
1081
- output: terminal.output,
1082
- outputEnd: terminal.outputEnd,
1178
+ // Output is loaded from the owner-bound reader, never copied into state snapshots.
1179
+ exitCode: terminal.exit_code,
1180
+ signal: terminal.signal,
1181
+ terminationReason: terminal.termination_reason,
1182
+ outputAvailability: terminal.output_ref.availability,
1083
1183
  }));
1084
1184
  const sessionTasks = [...this.backgroundSessionRuns.values()].map((run) => ({
1085
1185
  kind: "session",
@@ -1289,7 +1389,7 @@ export class WebRepl {
1289
1389
  return liveLineId;
1290
1390
  }, runtimeToolResultPresenter(this.runtime));
1291
1391
  }
1292
- const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: event.ok ? "completed" : "failed" } : step);
1392
+ const steps = (liveLine.toolStream?.steps ?? []).map((step) => step.status === "running" ? { ...step, status: "unknown" } : step);
1293
1393
  if (replacement) {
1294
1394
  this.replaceLine(liveLineId, {
1295
1395
  ...replacement,
@@ -1714,6 +1814,10 @@ export class WebRepl {
1714
1814
  this.queueDeltaOperation(operation);
1715
1815
  }
1716
1816
  queueDeltaOperation(operation) {
1817
+ if (operation.type === "line.append" && operation.line.toolError)
1818
+ operation = { ...operation, line: { ...operation.line, toolError: this.runtime.engine.redactDisplayValue?.(operation.line.toolError) ?? operation.line.toolError } };
1819
+ if (operation.type === "line.patch" && operation.patch.toolError)
1820
+ operation = { ...operation, patch: { ...operation.patch, toolError: this.runtime.engine.redactDisplayValue?.(operation.patch.toolError) ?? operation.patch.toolError } };
1717
1821
  this.pendingDeltaOperations.push(operation);
1718
1822
  this.scheduleDeltaFlush();
1719
1823
  }
@@ -1757,18 +1861,11 @@ export class WebRepl {
1757
1861
  }
1758
1862
  }
1759
1863
  queueTerminalOutput(delta) {
1760
- if (!this.subscribers.size || !this.runtime.execProcessManager.isBackgroundRunning(delta.sessionId))
1864
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1865
+ if (!this.subscribers.size || !ownerSessionId || delta.ownerSessionId !== ownerSessionId)
1761
1866
  return;
1762
- const outputStart = Number(delta.outputStart ?? 0);
1763
- const outputEnd = Number(delta.outputEnd ?? outputStart + delta.text.length);
1764
- const pending = this.pendingTerminalOutput.get(delta.sessionId);
1765
- if (pending && pending.outputEnd === outputStart) {
1766
- pending.text = appendBoundedText(pending.text, delta.text, TERMINAL_OUTPUT_EVENT_MAX_CHARS);
1767
- pending.outputEnd = outputEnd;
1768
- }
1769
- else {
1770
- this.pendingTerminalOutput.set(delta.sessionId, { text: delta.text.slice(-TERMINAL_OUTPUT_EVENT_MAX_CHARS), outputStart, outputEnd });
1771
- }
1867
+ // Invalidation only. The reader owns independent stream cursors; SSE is not a full-output source.
1868
+ this.pendingTerminalOutput.set(delta.sessionId, { ownerSessionId });
1772
1869
  if (this.terminalOutputTimer)
1773
1870
  return;
1774
1871
  this.terminalOutputTimer = setTimeout(() => this.flushTerminalOutput(), TERMINAL_OUTPUT_FLUSH_MS);
@@ -1776,12 +1873,12 @@ export class WebRepl {
1776
1873
  }
1777
1874
  flushTerminalOutput() {
1778
1875
  this.terminalOutputTimer = undefined;
1779
- if (!this.pendingTerminalOutput.size)
1780
- return;
1781
- const updates = [...this.pendingTerminalOutput.entries()].map(([sessionId, output]) => ({ sessionId, ...output }));
1876
+ const ownerSessionId = this.runtime.engine.snapshot().session?.sessionId;
1877
+ const updates = [...this.pendingTerminalOutput.entries()].filter(([, output]) => output.ownerSessionId === ownerSessionId).map(([sessionId, output]) => ({ sessionId, ...output, invalidated: true }));
1782
1878
  this.pendingTerminalOutput.clear();
1783
- for (const subscriber of this.subscribers)
1784
- this.send(subscriber, "terminal.output", { updates });
1879
+ if (updates.length)
1880
+ for (const subscriber of this.subscribers)
1881
+ this.send(subscriber, "terminal.output", { updates });
1785
1882
  }
1786
1883
  broadcastSync() {
1787
1884
  this.discardPendingDeltaOperations();
@@ -1832,10 +1929,38 @@ async function route(req, res, router) {
1832
1929
  const body = await readJsonBody(req);
1833
1930
  return sendJson(res, await router.requestClientReload(body));
1834
1931
  }
1932
+ if (["/api/tool-call-detail", "/api/terminal-output", "/api/agent-content"].includes(url.pathname) && !allowToolDetailRequest(req.headers.origin, req.headers.host, req.headers["sec-fetch-site"])) {
1933
+ return sendJson(res, { error: "详情访问被拒绝" }, 403);
1934
+ }
1835
1935
  const scope = webRuntimeScopeFromUrl(url);
1836
1936
  const repl = await router.get(scope);
1837
1937
  if (req.method === "GET" && url.pathname === "/events")
1838
1938
  return repl.subscribe(res, router.clientInfo());
1939
+ if (req.method === "GET" && url.pathname === "/api/tool-call-detail") {
1940
+ res.setHeader("Cache-Control", "no-store");
1941
+ const detail = await repl.toolCallDetail(url.searchParams.get("sessionId") ?? "", url.searchParams.get("toolUseId") ?? "", url.searchParams.get("messageId") ?? undefined);
1942
+ if (!detail)
1943
+ return sendJson(res, { error: "调用详情不可获取或无权访问" }, 404);
1944
+ return sendJson(res, detail);
1945
+ }
1946
+ if (req.method === "GET" && url.pathname === "/api/agent-content") {
1947
+ res.setHeader("Cache-Control", "no-store");
1948
+ const refresh = url.searchParams.get("refresh");
1949
+ if (refresh !== null && refresh !== "true" && refresh !== "false")
1950
+ return sendJson(res, { error: "代理详情参数无效" }, 404);
1951
+ const page = await repl.agentContent(url.searchParams.get("sessionId") ?? "", url.searchParams.get("view") ?? "timeline", {
1952
+ taskId: url.searchParams.get("taskId") ?? "", runGeneration: Number(url.searchParams.get("runGeneration")),
1953
+ ...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
1954
+ ...(url.searchParams.has("pageChars") ? { pageChars: Number(url.searchParams.get("pageChars")) } : {}),
1955
+ ...(url.searchParams.has("refresh") ? { refresh: url.searchParams.get("refresh") === "true" } : {}),
1956
+ });
1957
+ return page ? sendJson(res, page) : sendJson(res, { error: "代理内容不可获取或无权访问" }, 404);
1958
+ }
1959
+ if (req.method === "GET" && url.pathname === "/api/terminal-output") {
1960
+ res.setHeader("Cache-Control", "no-store");
1961
+ const page = repl.terminalOutput(url.searchParams.get("sessionId") ?? "", url.searchParams.get("runId") ?? "", url.searchParams.get("stream") ?? "stdout", Number(url.searchParams.get("offset") ?? 0), Number(url.searchParams.get("limitBytes") ?? 65536));
1962
+ return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
1963
+ }
1839
1964
  if (req.method === "GET" && url.pathname === "/api/state")
1840
1965
  return sendJson(res, repl.snapshot(true));
1841
1966
  if (req.method === "GET" && url.pathname === "/api/runtime-context")
@@ -2757,13 +2882,19 @@ function formatToolUse(toolUse) {
2757
2882
  return { kind: "tool", toolName: toolUse.name, toolPresentation, toolUseId: toolUse.id, title: toolPresentation.label, bodyTitle: summary.bodyTitle, text: summary.text, toolDisplay: buildToolUseDisplay(toolUse.name, toolUse.input), previewStyle: "summary", presentationLevel, collapsible: true };
2758
2883
  }
2759
2884
  function formatToolResultLine(toolName, output, ok, presentToolResult) {
2885
+ // Hidden fallback reports in historical agent payloads must not enter line.text
2886
+ // even when the frontend suppresses the ordinary result preview.
2887
+ const authorized = sanitizeAgentToolPayload(toolName, output);
2888
+ if (toolName.startsWith("subagent_"))
2889
+ output = authorized.value;
2760
2890
  const presentation = normalizeToolResultPresentation(presentToolResult?.(toolName, output, ok));
2761
2891
  const resources = presentation?.resources ?? toolResources(output);
2762
2892
  const formatted = resources?.length
2763
2893
  ? { text: ok ? "资源已准备好。" : "资源准备失败。", full: true, bodyTitle: ok ? "资源已就绪" : "资源准备失败" }
2764
- : formatToolResult(toolName, output, ok);
2894
+ : formatToolResult(toolName, toolName.startsWith("subagent_") ? redactToolDetail(output) : output, ok);
2765
2895
  return {
2766
2896
  kind: ok ? "tool" : "error",
2897
+ toolError: ok ? undefined : toolErrorText(output, ok).slice(0, 2000),
2767
2898
  toolName,
2768
2899
  toolPresentation: toolPresentationForSource(toolName),
2769
2900
  title: presentation?.title ?? toolTitle(toolName, "finished"),
@@ -2950,12 +3081,16 @@ function normalizeToolDisplay(display) {
2950
3081
  const visibleText = new Set([subject, purpose].filter((value) => Boolean(value)).map(normalizeDisplayText));
2951
3082
  const facts = display.facts.filter((fact) => {
2952
3083
  const normalized = normalizeDisplayText(fact.value);
2953
- if (!normalized || visibleText.has(normalized))
3084
+ const identity = `${fact.label}\0${normalized}`;
3085
+ if (!normalized || visibleText.has(identity))
2954
3086
  return false;
2955
- visibleText.add(normalized);
3087
+ visibleText.add(identity);
2956
3088
  return true;
2957
3089
  });
2958
3090
  const previews = display.previews.filter((preview) => {
3091
+ // Keep identical nonempty streams, but never render empty output panels.
3092
+ if (/^(stdout|stderr)(?:$|[( ·])/.test(preview.label ?? ""))
3093
+ return !!preview.content.trim();
2959
3094
  const normalized = normalizeDisplayText(preview.content);
2960
3095
  if (!normalized || visibleText.has(normalized))
2961
3096
  return false;
@@ -2989,8 +3124,10 @@ function buildToolUseDisplay(toolName, input) {
2989
3124
  }
2990
3125
  else if (toolName === "expose_downloads")
2991
3126
  pushToolFact(facts, "文件", arrayLabel(data.paths), true);
2992
- else if (toolName === "subagent_message")
2993
- pushToolFact(facts, "接收方", data.target);
3127
+ else if (toolName === "subagent_message") {
3128
+ if (typeof data.message === "string" && data.message.trim())
3129
+ previews.push({ kind: "text", content: data.message });
3130
+ }
2994
3131
  else if (toolName.startsWith("subagent_"))
2995
3132
  pushToolFact(facts, "任务", data.task_id, true);
2996
3133
  else if (toolName.startsWith("secret_")) {
@@ -3004,7 +3141,7 @@ function buildToolUseDisplay(toolName, input) {
3004
3141
  return normalizeToolDisplay({ subject, purpose, facts, previews });
3005
3142
  }
3006
3143
  function buildToolResultDisplay(toolName, output, ok) {
3007
- const data = isRecord(output) ? output : {};
3144
+ const data = toolName.startsWith("subagent_") ? subagentHeader(output) : isRecord(output) ? output : {};
3008
3145
  const facts = [];
3009
3146
  const previews = [];
3010
3147
  let subject = toolDisplaySubject(toolName, data);
@@ -3017,13 +3154,8 @@ function buildToolResultDisplay(toolName, output, ok) {
3017
3154
  subject = undefined;
3018
3155
  purpose = stringValue(data.description);
3019
3156
  pushToolPreview(previews, "命令", "code", data.command);
3020
- const outputText = [stringValue(data.stdout), stringValue(data.stderr)].filter(Boolean).join("\n");
3021
- if (outputText)
3022
- pushToolPreview(previews, data.stderr ? "输出 / 错误" : "输出", "code", outputText);
3023
- if (data.timed_out && !outputText)
3024
- pushToolFact(facts, "状态", "命令执行超时", false, "warning");
3025
- else if ((!ok || (typeof data.exit_code === "number" && data.exit_code !== 0)) && !outputText)
3026
- pushToolFact(facts, "状态", `退出码 ${data.exit_code ?? "未知"}`, false, "danger");
3157
+ previews.push(...terminalPreviews(data));
3158
+ facts.push(...terminalFacts(data));
3027
3159
  }
3028
3160
  else if (toolName === "file_search") {
3029
3161
  subject = stringValue(data.query) || stringValue(data.grepPath) || stringValue(data.path);
@@ -3046,7 +3178,7 @@ function buildToolResultDisplay(toolName, output, ok) {
3046
3178
  else if (toolName === "image_create") {
3047
3179
  subject = stringValue(data.semanticName);
3048
3180
  pushToolFact(facts, "图片", `${numberValue(data.returnedImages) ?? arrayLength(data.images)} 张`);
3049
- pushToolFact(facts, "尺寸", data.size);
3181
+ pushToolFact(facts, "请求尺寸", data.size === "auto" ? "auto(自动策略)" : data.size);
3050
3182
  }
3051
3183
  else if (toolName === "image_inspect") {
3052
3184
  subject = arrayLabel(data.imageRefs);
@@ -3056,13 +3188,9 @@ function buildToolResultDisplay(toolName, output, ok) {
3056
3188
  }
3057
3189
  else if (toolName.startsWith("subagent_") || toolName === "subagent_run") {
3058
3190
  subject = stringValue(data.description) || stringValue(data.task_id) || stringValue(data.agent_id);
3059
- pushToolFact(facts, "任务状态", data.status);
3191
+ facts.push(...subagentStatusFacts(toolName, output, ok));
3060
3192
  pushToolFact(facts, "轮次", data.run_generation);
3061
3193
  pushToolFact(facts, "待交付", data.pending_messages);
3062
- if (toolName === "subagent_message")
3063
- pushToolFact(facts, "交付说明", "仅入队,不代表已交付或完成");
3064
- if (data.requires_resume === true)
3065
- pushToolFact(facts, "后续", "需要显式续跑");
3066
3194
  }
3067
3195
  else if (toolName === "subagent_message") {
3068
3196
  subject = stringValue(data.target);
@@ -3246,21 +3374,8 @@ function diffLineMarker(line) {
3246
3374
  function isExecOutput(value) {
3247
3375
  return isRecord(value) && typeof value.command === "string" && typeof value.duration_ms === "number";
3248
3376
  }
3249
- function formatExecToolResult(output, ok) {
3250
- const status = output.status === "running" ? "运行中" : output.timed_out ? "已超时" : ok ? "已完成" : "执行失败";
3251
- const description = typeof output.description === "string" ? output.description.trim() : "";
3252
- const lines = [description ? `目的:${description}` : "执行命令", `状态:${status}`, `耗时:${output.duration_ms}ms`];
3253
- if (output.status === "running" && typeof output.session_id === "string")
3254
- lines.push(`会话:${output.session_id}`);
3255
- const stdout = typeof output.stdout === "string" ? output.stdout.replace(/\s+$/u, "") : "";
3256
- const stderr = typeof output.stderr === "string" ? output.stderr.replace(/\s+$/u, "") : "";
3257
- if (stdout)
3258
- lines.push("输出:", stdout);
3259
- if (stderr)
3260
- lines.push("错误:", stderr);
3261
- if (!stdout && !stderr)
3262
- lines.push(ok ? "无输出。" : "没有捕获到输出。");
3263
- return lines.join("\n");
3377
+ function formatExecToolResult(output, _ok) {
3378
+ return terminalResultText(output);
3264
3379
  }
3265
3380
  function formatImageGenerationToolResult(output, ok) {
3266
3381
  const error = typeof output.error === "string" ? output.error : undefined;