@ai-workbench/remote-agent 0.8.5 → 0.8.11

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 (190) hide show
  1. package/README.md +49 -8
  2. package/dist/acp.d.ts +22 -2
  3. package/dist/acp.js +143 -25
  4. package/dist/approval-label.d.ts +1 -1
  5. package/dist/approval-label.js +15 -3
  6. package/dist/broker-runtime.js +40 -8
  7. package/dist/broker.js +5 -1
  8. package/dist/claude-local.d.ts +37 -0
  9. package/dist/claude-local.js +207 -0
  10. package/dist/client.js +71 -2
  11. package/dist/conversation-details.d.ts +0 -1
  12. package/dist/conversation-details.js +37 -7
  13. package/dist/employee-endpoint.d.ts +91 -0
  14. package/dist/employee-endpoint.js +141 -0
  15. package/dist/employee-exec.d.ts +53 -0
  16. package/dist/employee-exec.js +423 -0
  17. package/dist/employee-location.d.ts +66 -0
  18. package/dist/employee-location.js +51 -0
  19. package/dist/employee-mcp.d.ts +42 -0
  20. package/dist/employee-mcp.js +181 -0
  21. package/dist/employee-native.d.ts +18 -0
  22. package/dist/employee-native.js +38 -0
  23. package/dist/employee-provider.d.ts +42 -0
  24. package/dist/employee-provider.js +326 -0
  25. package/dist/employee-relay.d.ts +18 -0
  26. package/dist/employee-relay.js +49 -0
  27. package/dist/employee-shell.d.ts +26 -0
  28. package/dist/employee-shell.js +89 -0
  29. package/dist/employee-ssh.d.ts +22 -0
  30. package/dist/employee-ssh.js +123 -0
  31. package/dist/employee-tools.d.ts +117 -0
  32. package/dist/employee-tools.js +292 -0
  33. package/dist/employee-workspace.d.ts +26 -0
  34. package/dist/employee-workspace.js +61 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +235 -41
  37. package/dist/input.d.ts +11 -0
  38. package/dist/input.js +61 -23
  39. package/dist/local-user-turn.d.ts +24 -0
  40. package/dist/local-user-turn.js +103 -0
  41. package/dist/model-catalog.d.ts +18 -2
  42. package/dist/model-catalog.js +101 -36
  43. package/dist/model-defaults.d.ts +29 -0
  44. package/dist/model-defaults.js +62 -0
  45. package/dist/model-input-retention.d.ts +16 -0
  46. package/dist/model-input-retention.js +129 -0
  47. package/dist/presentation.d.ts +1 -1
  48. package/dist/presentation.js +52 -2
  49. package/dist/provider.d.ts +39 -2
  50. package/dist/provider.js +69 -13
  51. package/dist/readiness.js +1 -1
  52. package/dist/recovery.d.ts +14 -1
  53. package/dist/recovery.js +11 -2
  54. package/dist/remote-details.d.ts +7 -0
  55. package/dist/remote-details.js +11 -1
  56. package/dist/workspace-source-confirmation.d.ts +4 -0
  57. package/dist/workspace-source-confirmation.js +12 -0
  58. package/dist/workspace-source.d.ts +25 -0
  59. package/dist/workspace-source.js +136 -0
  60. package/helpers/ripgrep/LICENSE-vscode +13 -0
  61. package/helpers/ripgrep/PROVENANCE.md +9 -0
  62. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  63. package/helpers/ripgrep/darwin-x64/rg +0 -0
  64. package/helpers/ripgrep/manifest.json +56 -0
  65. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  66. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  67. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  68. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  69. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  70. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  71. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  72. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  73. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  74. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  75. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  76. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  77. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  78. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  79. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  80. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  81. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  82. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  83. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  84. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  85. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  86. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  87. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  88. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  89. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  90. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  91. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  92. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  93. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  94. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  95. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  96. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  97. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  98. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  99. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  100. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  101. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  102. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  103. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  104. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  105. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  106. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  107. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  108. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  109. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  110. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  112. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  113. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  114. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  115. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  116. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  117. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  118. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  119. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  120. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  121. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  122. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  123. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  124. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  125. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  126. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  127. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  128. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  129. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  130. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  131. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  132. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  133. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  134. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  135. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  136. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  137. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  138. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  139. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  140. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  141. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  142. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  143. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  144. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  145. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  146. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  147. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  148. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  149. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  150. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  151. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  152. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  153. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  154. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  155. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  156. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  157. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  158. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  159. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  160. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  161. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  162. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  163. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  164. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  165. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  166. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  167. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  168. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  169. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  170. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  171. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  172. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  173. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  174. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  175. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  176. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  177. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  178. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  179. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  180. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  181. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  182. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  183. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  184. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  185. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  186. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  187. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  188. package/locale/en.json +1 -1
  189. package/locale/zh.json +2 -2
  190. package/package.json +29 -11
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import Schema from '@deepseek-ai/schemastery';
3
3
  import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
- import { recoveryJournal, recoveryKey } from "./recovery.js";
6
+ import { recoveryJournal, runtimeRecoveryKey, supportsCodexContextPolicy } from "./recovery.js";
7
7
  import * as unstable from '@agentclientprotocol/sdk/unstable';
8
8
  import * as acp from '@agentclientprotocol/sdk';
9
9
  import { AcpSession } from "./acp.js";
@@ -12,7 +12,7 @@ import { StreamMapper } from "./mapping.js";
12
12
  import { connectProvider } from "./provider.js";
13
13
  import { brokerFactory } from "./broker.js";
14
14
  import { AGENTS, CODEX_AGENT } from "./agents.js";
15
- import { prepareInput } from "./input.js";
15
+ import { prepareInput, prepareContinuation } from "./input.js";
16
16
  import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
17
17
  import { NeedError } from "./remote.js";
18
18
  import { employeeFailure } from "./presentation.js";
@@ -23,12 +23,21 @@ import { StageTiming } from "./stage-timing.js";
23
23
  import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
24
24
  import { ModelCatalog, modelInfo } from "./model-catalog.js";
25
25
  import { ModelConfigurationError } from "./model-configuration.js";
26
+ import { officialEmployeeWorkspace } from "./employee-workspace.js";
27
+ import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
28
+ import { resolveEmployeeNative } from "./employee-native.js";
29
+ import { createLocalUserTurnObserver } from "./local-user-turn.js";
30
+ import { createClaudeLocalProvider } from "./claude-local.js";
31
+ import { readLocalUserTurn } from "./local-user-turn.js";
32
+ import { readEmployeeWorkspace } from "./employee-workspace.js";
33
+ import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
34
+ import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
26
35
  export const name = 'ai-workbench-remote-agent';
27
36
  export const settingsNamespace = 'ai-workbench-codex';
28
37
  // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
29
38
  export const inject = ['llm'];
30
39
  /** Owner-approved defaults; administrators may override through the existing settings. */
31
- export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never' });
40
+ export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000 });
32
41
  export const Config = Schema.object({
33
42
  mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
34
43
  acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
@@ -36,11 +45,13 @@ export const Config = Schema.object({
36
45
  brokerControlMaxBytes: Schema.number().min(1).step(1).i18n({ en: 'Host session control reply limit; default 1 MiB, adjustable by administrators', zh: '主机会话管理回复容量;默认1 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerControlMaxBytes).volatile(),
37
46
  oneShotSandbox: Schema.union(['read-only', 'workspace-write', 'danger-full-access']).i18n({ en: 'One-shot sandbox; workspace changes are allowed by default', zh: '单次任务沙箱;默认允许工作区内修改' }).default(DEFAULT_CONFIG.oneShotSandbox).volatile(),
38
47
  oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
48
+ employeeShellKillGraceMs: Schema.number().min(0).max(2147483647).step(1).i18n({ en: 'Local command SSH cleanup grace in milliseconds; cancellation keeps the remote result unknown', zh: '本机命令SSH取消后的清理宽限毫秒;默认1000,取消仍保留员工端结果未知' }).default(DEFAULT_CONFIG.employeeShellKillGraceMs).volatile(),
39
49
  });
40
50
  /** DSH volatile settings are handles; adapters freeze their values once per invocation. */
41
51
  export function readConfig(config) {
42
52
  return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
43
53
  }
54
+ function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
44
55
  function readPointer(options, agent) {
45
56
  const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
46
57
  if (!last || last.role !== 'assistant')
@@ -53,6 +64,8 @@ function readPointer(options, agent) {
53
64
  throw new NeedError('SESSION_STATE_INVALID', '原对话缺少新版工作区/模型绑定,不能静默迁移,请新建会话', '有效的新一代远端会话恢复记录');
54
65
  if (options.sessionId && pointer.dshSessionId !== options.sessionId)
55
66
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
67
+ if (agent.loginProvider === 'codex' && (!validContextDigest(pointer.contextSha256) || (pointer.previousRemoteSessions !== undefined && (!Array.isArray(pointer.previousRemoteSessions) || !pointer.previousRemoteSessions.every(ref => ref && typeof ref.sessionId === 'string' && ref.sessionId.length > 0 && validContextDigest(ref.contextSha256))))))
68
+ throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
56
69
  return pointer;
57
70
  }
58
71
  export class CodexRemoteAdapter extends LlmAdapter {
@@ -90,17 +103,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
90
103
  finally {
91
104
  catalog.release();
92
105
  } }
93
- async resolveModel(provider, id, signal) { const catalog = await this.catalog(signal); try {
94
- const model = catalog.models.find(model => model.id === id);
95
- if (!model)
96
- throw new LlmError('无法获取模型列表:所选模型已不在当前主机目录中', 'MODEL_CATALOG_UNAVAILABLE');
97
- return modelInfo(provider, model);
106
+ async resolveModel(provider, id, signal) { try {
107
+ const catalog = await this.catalog(signal);
108
+ try {
109
+ const model = catalog.models.find(model => model.id === id);
110
+ if (!model)
111
+ throw new LlmError(id === 'remote' ? '无法获取模型列表:这个会话保存的是旧版模型选择,请打开本会话的模型菜单,重新选择模型后再发送。' : '所选模型已不在当前主机目录中,请打开本会话的模型菜单重新选择模型。', 'MODEL_CATALOG_UNAVAILABLE');
112
+ return modelInfo(provider, model);
113
+ }
114
+ finally {
115
+ catalog.release();
116
+ }
98
117
  }
99
- finally {
100
- catalog.release();
118
+ catch (error) {
119
+ markModelPreparationFailure(provider, error, signal);
120
+ throw error;
101
121
  } }
102
122
  async invalidateModelState() { for (const turn of this.turns.values())
103
123
  turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
124
+ async invalidateEmployeeState() { await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
104
125
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
105
126
  cleanupRuntime(runtime) {
106
127
  const existing = this.cleanups.get(runtime);
@@ -165,7 +186,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
165
186
  throw new LlmError(error.message, error.code, { cause: error });
166
187
  if (error instanceof NeedError) {
167
188
  const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
168
- const message = employeeFailure(error.code) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
189
+ const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
169
190
  let diagnostic = error.technical ? JSON.stringify(error.technical) : '';
170
191
  if (error.technical && this.reportLoginFailure) {
171
192
  try {
@@ -195,11 +216,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
195
216
  const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
196
217
  const options = { ...original, signal };
197
218
  const timing = new StageTiming();
219
+ let request;
220
+ let earlyStage = 'input-prepare';
198
221
  let runtime;
199
222
  let session;
200
223
  let live;
201
224
  let succeeded = false;
202
225
  let remoteAttempted = false;
226
+ let preserveRecoveredRuntime = false;
203
227
  let activeMapper;
204
228
  let catalog;
205
229
  let selected;
@@ -221,18 +245,34 @@ export class CodexRemoteAdapter extends LlmAdapter {
221
245
  throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
222
246
  if (live && !previous)
223
247
  throw new NeedError('SESSION_STATE_MISSING', '上一轮回复尚无持久恢复记录,不能继续当前远端会话', '原会话的持久回复记录');
224
- const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes]);
248
+ const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
225
249
  if (live && live.configuration !== configuration)
226
250
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
227
- const request = { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
251
+ request = { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
252
+ const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
253
+ request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
254
+ request.userMessageIds = request.userInputIds;
228
255
  await this.resolveRuntime.preflight?.(request);
229
- const input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
256
+ let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
230
257
  const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
231
258
  request.contextSha256 = contextSha256;
259
+ const codex = this.agent.loginProvider === 'codex';
260
+ if (codex && live && previous && live.contextSha256 !== previous.contextSha256.toLowerCase())
261
+ throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
262
+ const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
263
+ // Only identical durable context can prove a legacy context-bound journal.
264
+ // New Codex transport scopes are independent of the native context digest.
265
+ if (contextChanged)
266
+ request.contextSha256 = undefined;
232
267
  if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
233
268
  throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
269
+ if (contextChanged)
270
+ input = await prepareContinuation(options, input, this.readImage);
234
271
  if (live) {
235
- live.session.validateContext({ systemPrompt: input.systemPrompt });
272
+ if (contextChanged)
273
+ live.session.validateNewSessionContext({ systemPrompt: input.systemPrompt });
274
+ else
275
+ live.session.validateContext({ systemPrompt: input.systemPrompt });
236
276
  live.session.validatePrompt(input.prompt);
237
277
  }
238
278
  if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
@@ -241,36 +281,63 @@ export class CodexRemoteAdapter extends LlmAdapter {
241
281
  throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
242
282
  if (mode === 'one-shot')
243
283
  throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
284
+ request.localAdmissionCheck?.();
285
+ earlyStage = 'model-catalog';
244
286
  catalog = await this.catalog(signal);
245
287
  selected = catalog.models.find(model => model.id === options.model);
246
288
  if (!selected)
247
289
  throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
248
290
  if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
249
291
  throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
250
- const modelSignal = AbortSignal.any([signal, catalog.signal]);
292
+ let modelSignal = AbortSignal.any([signal, catalog.signal]);
251
293
  options.signal = modelSignal;
252
294
  request.signal = modelSignal;
295
+ earlyStage = 'runtime-resolve';
253
296
  runtime = await this.resolveRuntime(request);
254
297
  signal.throwIfAborted();
298
+ if (runtime.execution?.signal) {
299
+ modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
300
+ options.signal = modelSignal;
301
+ request.signal = modelSignal;
302
+ modelSignal.throwIfAborted();
303
+ }
255
304
  catalog.assertCurrent();
256
305
  if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
257
306
  throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
258
307
  if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
259
308
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
309
+ if (contextChanged && runtime.recoveredSession) {
310
+ const capabilities = runtime.recoveredSession.capabilities;
311
+ // A clean reattach has sent no ACP input. A known policy refusal must
312
+ // detach it, preserving the original native session and checkpoint.
313
+ if (!capabilities.sessionCapabilities?.systemPrompt || !supportsCodexContextPolicy(capabilities)) {
314
+ preserveRecoveredRuntime = true;
315
+ if (!capabilities.sessionCapabilities?.systemPrompt)
316
+ throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
317
+ throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
318
+ }
319
+ }
260
320
  if (runtime.restartNotice) {
261
321
  const ids = runtime.restartInputIds;
262
322
  if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
263
323
  throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
264
- input.prompt = ids.flatMap(id => input.users.find(u => u.id === id).prompt);
324
+ input = { ...input, prompt: ids.flatMap(id => input.users.find(u => u.id === id).prompt) };
325
+ if (contextChanged)
326
+ input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
265
327
  }
266
328
  const details = runtime.details;
267
329
  if (!details)
268
330
  throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
269
331
  timing.bind(detail => details.record(detail));
270
- const mapper = new StreamMapper(mode === 'session' ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
332
+ request.recordEarlyFailure = undefined;
333
+ const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
271
334
  activeMapper = mapper;
272
335
  let id;
273
336
  let stop;
337
+ if (runtime.employeePresentation) {
338
+ yield* mapper.text(runtime.employeePresentation + '\n\n');
339
+ yield* mapper.flush();
340
+ }
274
341
  if (runtime.workspaceNotice) {
275
342
  yield* mapper.text(runtime.workspaceNotice.text);
276
343
  yield* mapper.flush();
@@ -279,6 +346,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
279
346
  yield* mapper.text(runtime.restartNotice + '\n\n');
280
347
  yield* mapper.flush();
281
348
  }
349
+ if (runtime.execution) {
350
+ details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: '本机工具路由及未覆盖操作范围', payload: runtime.execution.scope });
351
+ yield* mapper.text(runtime.execution.notice + '\n\n');
352
+ yield* mapper.flush();
353
+ }
282
354
  if (mode === 'session') {
283
355
  if (live && live.runtime !== runtime)
284
356
  throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
@@ -289,7 +361,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
289
361
  let done = false;
290
362
  let failure;
291
363
  const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
292
- const hooks = { broker: runtime.broker, timing, approve: (permission, approvalSignal) => this.approve?.(permission, approvalSignal, options) ?? Promise.resolve('unavailable'), onUpdate: notification => { try {
364
+ const hooks = { broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
365
+ const execution = runtime.execution;
366
+ const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
367
+ if (execution) {
368
+ if (toolName && ['Agent', 'Task', 'TaskOutput', 'TaskStop', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'].includes(toolName)) {
369
+ details.record({ kind: 'need', severity: 'warning', code: 'CLAUDE_HOST_TOOL_UNSUPPORTED', summary: '这类原生调用没有经过员工端别名,本机来源不支持;本次审批已取消', payload: { operation: toolName } });
370
+ push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
371
+ return 'cancelled';
372
+ }
373
+ await execution.validate(approvalSignal);
374
+ }
375
+ const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined) ?? 'unavailable';
376
+ if (execution && answer === 'allowed-once')
377
+ await execution.validate(approvalSignal);
378
+ return answer;
379
+ }, onUpdate: notification => { try {
293
380
  push(mapper.acp(notification.update));
294
381
  }
295
382
  catch (error) {
@@ -300,7 +387,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
300
387
  if (fresh) {
301
388
  const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
302
389
  session = new AcpSession(transport, hooks);
303
- live = { session, runtime, configuration, usageSnapshot: mapper.usageSnapshot };
390
+ live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
304
391
  if (key)
305
392
  this.sessions.set(key, live);
306
393
  }
@@ -313,17 +400,30 @@ export class CodexRemoteAdapter extends LlmAdapter {
313
400
  const task = (async () => {
314
401
  remoteAttempted = true;
315
402
  try {
316
- if (fresh) {
317
- if (runtime.recoveredSession)
318
- session.restore(runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt });
319
- else
320
- await session.start(runtime.cwd, runtime.restartNotice ? undefined : previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt });
403
+ if (fresh && runtime.recoveredSession) {
404
+ if (previous && runtime.recoveredSession.sessionId !== previous.sessionId)
405
+ throw new NeedError('SESSION_STATE_INVALID', '已接回的会话与持久标识不一致,本轮未发送', '原会话的恢复标识');
406
+ session.restore(runtime.execution?.hostCwd ?? runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}) }, codex && !!previous && !contextChanged);
407
+ session.validatePrompt(input.prompt);
408
+ if (contextChanged)
409
+ await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
321
410
  }
411
+ else if (fresh)
412
+ await session.start(runtime.execution?.hostCwd ?? runtime.cwd, contextChanged || runtime.restartNotice ? undefined : previous?.sessionId, modelSignal, { systemPrompt: contextChanged ? (input.systemPrompt ?? '') : input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
413
+ else if (contextChanged)
414
+ await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
322
415
  else {
323
416
  const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
324
417
  if (updated && this.agent.loginProvider === 'claude')
325
418
  throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
326
419
  }
420
+ if (contextChanged) {
421
+ if (session.id === previous.sessionId)
422
+ throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
423
+ live.contextSha256 = contextSha256;
424
+ live.usageSnapshot = mapper.usageSnapshot;
425
+ details.record({ kind: 'session', severity: 'info', code: 'CODEX_CONTEXT_SESSION_REPLACED', summary: 'Codex要求仅在新建生效,已保留原会话;新会话承接可见用户及助手正文,内部推理不转发', payload: { reason: 'system-context-changed', oldSessionId: previous.sessionId, newSessionId: session.id, oldContextSha256: previous.contextSha256, newContextSha256: contextSha256, history: input.historyTransfer } });
426
+ }
327
427
  catalog.assertCurrent();
328
428
  const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
329
429
  catalog.assertCurrent();
@@ -350,11 +450,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
350
450
  details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
351
451
  catalog.assertCurrent();
352
452
  session.assertModelSelection(selected.value, options.reasoningEffort);
453
+ await runtime.execution?.beginTurn(modelSignal);
353
454
  id = session.id;
354
455
  try {
355
- return await session.prompt(input.prompt, modelSignal);
456
+ const result = await session.prompt(input.prompt, modelSignal);
457
+ if (contextChanged)
458
+ push(mapper.text(runtime.restartNotice ? '\n\n会话要求已变化,已把此前可见对话带入新 Codex 会话继续处理。' : '\n\n会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。'));
459
+ return result;
356
460
  }
357
461
  finally {
462
+ runtime.execution?.endTurn();
358
463
  details.endTurn();
359
464
  }
360
465
  })();
@@ -406,7 +511,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
406
511
  if (await runtime.saveCheckpoint(id, session.checkpoint))
407
512
  recoveryCheckpointId = id;
408
513
  }
409
- const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
514
+ const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
515
+ const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
410
516
  succeeded = true;
411
517
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
412
518
  }
@@ -414,11 +520,29 @@ export class CodexRemoteAdapter extends LlmAdapter {
414
520
  if (live && (remoteAttempted || runtime === live.runtime))
415
521
  live.failure = { cause: error };
416
522
  let reportingFailure;
523
+ if (request?.recordEarlyFailure) {
524
+ const record = request.recordEarlyFailure;
525
+ request.recordEarlyFailure = undefined;
526
+ const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
527
+ try {
528
+ await record(new NeedError(code, '来源确认后本轮在执行前失败。', '白名单阶段与错误代码'), earlyStage);
529
+ }
530
+ catch (failure) {
531
+ reportingFailure = { cause: failure };
532
+ }
533
+ }
417
534
  try {
418
535
  await runtime?.details?.flush();
419
536
  }
420
537
  catch (failure) {
421
- reportingFailure = { cause: failure };
538
+ if (reportingFailure === undefined)
539
+ reportingFailure = { cause: failure };
540
+ else {
541
+ const first = reportingFailure.cause;
542
+ const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
543
+ const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
544
+ reportingFailure = { cause: Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮诊断保存及详情收尾失败,已停止。', '原错误代码 ' + originalCode + ';' + (first instanceof NeedError ? first.requires : '早期诊断保存失败') + ';详情收尾代码 ' + flushCode), { cause: new AggregateError([error, first, failure], 'ADMISSION_DIAGNOSTIC_AND_FLUSH_FAILED') }) };
545
+ }
422
546
  }
423
547
  if (activeMapper)
424
548
  yield* activeMapper.flush();
@@ -427,9 +551,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
427
551
  throw error;
428
552
  }
429
553
  finally {
554
+ runtime?.execution?.endTurn();
430
555
  catalog?.release();
431
556
  try {
432
- if (this.shuttingDown || key && this.detached.has(key)) {
557
+ if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
433
558
  session?.close();
434
559
  await runtime?.detach?.();
435
560
  }
@@ -459,6 +584,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
459
584
  listModels(provider) { return this.adapter(provider).listModels(provider); }
460
585
  resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
461
586
  async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
587
+ async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
462
588
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
463
589
  stream(options) { return this.adapter(options.provider).stream(options); }
464
590
  async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
@@ -476,6 +602,7 @@ export function apply(ctx, live) {
476
602
  return attachments.readImage(ref, signal);
477
603
  };
478
604
  const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
605
+ const localTurns = createLocalUserTurnObserver(ctx);
479
606
  const detailManagers = new Map();
480
607
  const managerForProfile = () => {
481
608
  const profile = ctx.get('profileContext');
@@ -488,15 +615,35 @@ export function apply(ctx, live) {
488
615
  }
489
616
  return manager;
490
617
  };
491
- let changed = () => { };
492
- const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed());
493
- const adapter = new RemoteAgentsAdapter(config, agent => connectProvider(get, { agent, notice: createWorkspaceNoticeConsumer(ctx, agent), details: async (request, api, connection, target) => {
494
- return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
495
- }, open: async (...args) => {
618
+ let changed = () => { }, publishDiagnostics = () => { };
619
+ const diagnostics = new Map();
620
+ const diagnostic = (scope, provider, message) => {
621
+ let entry = diagnostics.get(provider);
622
+ if (!entry) {
623
+ entry = new Map();
624
+ diagnostics.set(provider, entry);
625
+ }
626
+ if (entry.get(scope) === message)
627
+ return;
628
+ if (message === undefined)
629
+ entry.delete(scope);
630
+ else
631
+ entry.set(scope, message);
632
+ publishDiagnostics();
633
+ };
634
+ let defaults;
635
+ const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed(), (brand, message) => {
636
+ diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
637
+ }, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
638
+ const adapter = new RemoteAgentsAdapter(config, agent => {
639
+ const details = async (request, api, connection, target) => {
640
+ return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
641
+ };
642
+ const open = async (...args) => {
496
643
  const profile = ctx.get('profileContext');
497
644
  if (!profile?.dir)
498
645
  throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
499
- args[2].recoveryJournal = recoveryJournal(profile.dir, recoveryKey([`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId]));
646
+ args[2].recoveryJournal = recoveryJournal(profile.dir, runtimeRecoveryKey(`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId, args[2].employeeScope));
500
647
  args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
501
648
  args[2].currentInputIds = await currentInputIds(ctx, args[2]);
502
649
  const runtime = await openByAgent.get(agent.id)(...args);
@@ -520,7 +667,23 @@ export function apply(ctx, live) {
520
667
  return { turn, step };
521
668
  };
522
669
  return runtime;
523
- } }), dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
670
+ };
671
+ const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code }), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
672
+ const workspace = async (request) => {
673
+ const services = ctx;
674
+ const registry = services.get('workspaceRegistry');
675
+ const sessions = services.get('sessions');
676
+ return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
677
+ };
678
+ return connectProvider(get, { agent, workspaceSource: {
679
+ workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
680
+ record: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: agent.loginProvider, code: observation.code, observation }),
681
+ }, ...(agent.loginProvider === 'codex' ? { employee: {
682
+ currentUserTurn: request => localTurns.current(request),
683
+ getNetwork: () => ctx.get('aiWorkbenchNetwork'),
684
+ workspace, native: () => resolveEmployeeNative(),
685
+ } } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
686
+ }, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
524
687
  const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
525
688
  let turn = null, step = null;
526
689
  for (const event of events ?? []) {
@@ -540,12 +703,43 @@ export function apply(ctx, live) {
540
703
  return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
541
704
  }, modelDirectory);
542
705
  const ids = AGENTS.map(agent => agent.id);
543
- const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
706
+ const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [], ...(diagnostics.get(agent.id)?.size ? { error: [...diagnostics.get(agent.id).values()].join('\n') } : {}) }));
544
707
  const directory = ctx.llm.registerConfigurableProviders(entries());
545
708
  const registration = ctx.llm.registerAdapter(ids, adapter);
546
- changed = () => { registration.replace(ids); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
547
- ctx.on('internal/service', service => { if (service === 'aiWorkbenchConnect')
548
- modelDirectory.refresh(); });
709
+ installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnostic: (provider, message) => diagnostic('input', provider, message) });
710
+ const serviceValue = (name) => ctx.get(name);
711
+ publishDiagnostics = () => directory.replace(entries());
712
+ const refreshDefault = () => {
713
+ const service = serviceValue('agentDefaultModel');
714
+ const selected = service?.currentSelection();
715
+ const agent = AGENTS.find(agent => agent.id === selected?.provider);
716
+ if (!defaults || !agent)
717
+ return;
718
+ // Read through the same trusted, fresh catalog. Failures are already recorded there.
719
+ void modelDirectory.read(agent).then(lease => lease.release(), () => { });
720
+ };
721
+ ctx.inject(['agentDefaultModel'], child => {
722
+ const service = child.get('agentDefaultModel');
723
+ const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
724
+ defaults = current;
725
+ refreshDefault();
726
+ child.effect(() => () => { current.dispose(); if (defaults === current)
727
+ defaults = undefined; });
728
+ });
729
+ changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
730
+ let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
731
+ ctx.on('internal/service', service => {
732
+ if (service === 'aiWorkbenchConnect')
733
+ modelDirectory.refresh();
734
+ if (service !== 'aiWorkbenchConnect' && service !== 'aiWorkbenchNetwork')
735
+ return;
736
+ const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
737
+ if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
738
+ return;
739
+ employeeConnect = nextConnect;
740
+ employeeNetwork = nextNetwork;
741
+ void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
742
+ });
549
743
  new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
550
744
  const cleanup = (id) => adapter.detachSession(id);
551
745
  ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
@@ -558,7 +752,7 @@ export function dshApproval(ctx) {
558
752
  const approval = ctx.get('approval');
559
753
  if (!agent || !approval)
560
754
  return 'unavailable';
561
- const label = approvalLabel(request);
755
+ const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host');
562
756
  if (!label)
563
757
  return 'unavailable';
564
758
  return approval.request({ agent, ...label, signal });
package/dist/input.d.ts CHANGED
@@ -4,6 +4,14 @@ export type ImageReader = (ref: ImageBlock['attachment'], signal?: AbortSignal)
4
4
  ref: ImageBlock['attachment'];
5
5
  data: Uint8Array;
6
6
  }>;
7
+ export interface HistoryTransfer {
8
+ scope: 'visible-user-assistant-text-and-images';
9
+ textBlocks: number;
10
+ imageBlocks: number;
11
+ offloadedImagePlaceholders: number;
12
+ omittedReasoningBlocks: number;
13
+ reasoningOmissionReason: 'dsh-internal-reasoning';
14
+ }
7
15
  export interface PreparedInput {
8
16
  prompt: AcpContentBlock[];
9
17
  users: {
@@ -11,6 +19,9 @@ export interface PreparedInput {
11
19
  prompt: AcpContentBlock[];
12
20
  }[];
13
21
  systemPrompt?: string;
22
+ historyTransfer?: HistoryTransfer;
14
23
  }
24
+ /** Role-labelled public history only; never serialize source/replay/tool internals. */
25
+ export declare function prepareContinuation(options: GenerateOptions, input: PreparedInput, readImage?: ImageReader): Promise<PreparedInput>;
15
26
  /** The attachment service supplies verified bytes; client paths never cross SSH. */
16
27
  export declare function prepareInput(options: GenerateOptions, readImage?: ImageReader): Promise<PreparedInput>;