@ai-workbench/remote-agent 0.8.5 → 0.8.10

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 (180) hide show
  1. package/README.md +33 -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 +148 -28
  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/presentation.d.ts +1 -1
  42. package/dist/presentation.js +46 -2
  43. package/dist/provider.d.ts +36 -2
  44. package/dist/provider.js +33 -0
  45. package/dist/readiness.js +1 -1
  46. package/dist/recovery.d.ts +14 -1
  47. package/dist/recovery.js +11 -2
  48. package/dist/remote-details.d.ts +7 -0
  49. package/dist/remote-details.js +11 -1
  50. package/helpers/ripgrep/LICENSE-vscode +13 -0
  51. package/helpers/ripgrep/PROVENANCE.md +9 -0
  52. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  53. package/helpers/ripgrep/darwin-x64/rg +0 -0
  54. package/helpers/ripgrep/manifest.json +56 -0
  55. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  56. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  57. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  58. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  59. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  60. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  61. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  62. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  63. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  64. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  65. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  66. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  67. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  68. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  69. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  70. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  71. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  72. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  73. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  74. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  75. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  76. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  77. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  78. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  79. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  80. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  81. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  82. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  83. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  84. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  85. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  86. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  87. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  88. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  89. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  90. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  91. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  92. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  93. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  94. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  95. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  96. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  97. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  98. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  99. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  100. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  101. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  102. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  103. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  104. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  105. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  106. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  107. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  108. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  109. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  110. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  112. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  113. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  114. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  115. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  116. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  117. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  118. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  119. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  120. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  121. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  122. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  123. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  124. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  125. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  126. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  127. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  128. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  129. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  130. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  131. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  132. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  133. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  134. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  135. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  136. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  137. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  138. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  139. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  140. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  141. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  142. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  143. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  144. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  145. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  146. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  147. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  148. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  149. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  150. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  151. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  152. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  153. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  154. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  155. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  156. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  157. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  158. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  159. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  160. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  161. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  162. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  163. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  164. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  165. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  166. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  167. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  168. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  169. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  170. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  171. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  172. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  173. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  174. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  175. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  176. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  177. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  178. package/locale/en.json +1 -1
  179. package/locale/zh.json +2 -2
  180. 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,18 @@ 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 { resolveEmployeeNative } from "./employee-native.js";
28
+ import { createLocalUserTurnObserver } from "./local-user-turn.js";
29
+ import { createClaudeLocalProvider } from "./claude-local.js";
30
+ import { readLocalUserTurn } from "./local-user-turn.js";
31
+ import { readEmployeeWorkspace } from "./employee-workspace.js";
26
32
  export const name = 'ai-workbench-remote-agent';
27
33
  export const settingsNamespace = 'ai-workbench-codex';
28
34
  // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
29
35
  export const inject = ['llm'];
30
36
  /** 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' });
37
+ export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000 });
32
38
  export const Config = Schema.object({
33
39
  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
40
  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 +42,13 @@ export const Config = Schema.object({
36
42
  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
43
  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
44
  oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
45
+ 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
46
  });
40
47
  /** DSH volatile settings are handles; adapters freeze their values once per invocation. */
41
48
  export function readConfig(config) {
42
49
  return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
43
50
  }
51
+ function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
44
52
  function readPointer(options, agent) {
45
53
  const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
46
54
  if (!last || last.role !== 'assistant')
@@ -53,6 +61,8 @@ function readPointer(options, agent) {
53
61
  throw new NeedError('SESSION_STATE_INVALID', '原对话缺少新版工作区/模型绑定,不能静默迁移,请新建会话', '有效的新一代远端会话恢复记录');
54
62
  if (options.sessionId && pointer.dshSessionId !== options.sessionId)
55
63
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
64
+ 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))))))
65
+ throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
56
66
  return pointer;
57
67
  }
58
68
  export class CodexRemoteAdapter extends LlmAdapter {
@@ -101,6 +111,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
101
111
  } }
102
112
  async invalidateModelState() { for (const turn of this.turns.values())
103
113
  turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
114
+ async invalidateEmployeeState() { await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
104
115
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
105
116
  cleanupRuntime(runtime) {
106
117
  const existing = this.cleanups.get(runtime);
@@ -165,7 +176,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
165
176
  throw new LlmError(error.message, error.code, { cause: error });
166
177
  if (error instanceof NeedError) {
167
178
  const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
168
- const message = employeeFailure(error.code) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
179
+ const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
169
180
  let diagnostic = error.technical ? JSON.stringify(error.technical) : '';
170
181
  if (error.technical && this.reportLoginFailure) {
171
182
  try {
@@ -200,6 +211,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
200
211
  let live;
201
212
  let succeeded = false;
202
213
  let remoteAttempted = false;
214
+ let preserveRecoveredRuntime = false;
203
215
  let activeMapper;
204
216
  let catalog;
205
217
  let selected;
@@ -221,18 +233,34 @@ export class CodexRemoteAdapter extends LlmAdapter {
221
233
  throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
222
234
  if (live && !previous)
223
235
  throw new NeedError('SESSION_STATE_MISSING', '上一轮回复尚无持久恢复记录,不能继续当前远端会话', '原会话的持久回复记录');
224
- const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes]);
236
+ const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
225
237
  if (live && live.configuration !== configuration)
226
238
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
227
239
  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 }) };
240
+ const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
241
+ request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
242
+ request.userMessageIds = request.userInputIds;
228
243
  await this.resolveRuntime.preflight?.(request);
229
- const input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
244
+ let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
230
245
  const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
231
246
  request.contextSha256 = contextSha256;
247
+ const codex = this.agent.loginProvider === 'codex';
248
+ if (codex && live && previous && live.contextSha256 !== previous.contextSha256.toLowerCase())
249
+ throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
250
+ const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
251
+ // Only identical durable context can prove a legacy context-bound journal.
252
+ // New Codex transport scopes are independent of the native context digest.
253
+ if (contextChanged)
254
+ request.contextSha256 = undefined;
232
255
  if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
233
256
  throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
257
+ if (contextChanged)
258
+ input = await prepareContinuation(options, input, this.readImage);
234
259
  if (live) {
235
- live.session.validateContext({ systemPrompt: input.systemPrompt });
260
+ if (contextChanged)
261
+ live.session.validateNewSessionContext({ systemPrompt: input.systemPrompt });
262
+ else
263
+ live.session.validateContext({ systemPrompt: input.systemPrompt });
236
264
  live.session.validatePrompt(input.prompt);
237
265
  }
238
266
  if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
@@ -241,36 +269,60 @@ export class CodexRemoteAdapter extends LlmAdapter {
241
269
  throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
242
270
  if (mode === 'one-shot')
243
271
  throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
272
+ request.localAdmissionCheck?.();
244
273
  catalog = await this.catalog(signal);
245
274
  selected = catalog.models.find(model => model.id === options.model);
246
275
  if (!selected)
247
276
  throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
248
277
  if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
249
278
  throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
250
- const modelSignal = AbortSignal.any([signal, catalog.signal]);
279
+ let modelSignal = AbortSignal.any([signal, catalog.signal]);
251
280
  options.signal = modelSignal;
252
281
  request.signal = modelSignal;
253
282
  runtime = await this.resolveRuntime(request);
254
283
  signal.throwIfAborted();
284
+ if (runtime.execution?.signal) {
285
+ modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
286
+ options.signal = modelSignal;
287
+ request.signal = modelSignal;
288
+ modelSignal.throwIfAborted();
289
+ }
255
290
  catalog.assertCurrent();
256
291
  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
292
  throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
258
293
  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
294
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
295
+ if (contextChanged && runtime.recoveredSession) {
296
+ const capabilities = runtime.recoveredSession.capabilities;
297
+ // A clean reattach has sent no ACP input. A known policy refusal must
298
+ // detach it, preserving the original native session and checkpoint.
299
+ if (!capabilities.sessionCapabilities?.systemPrompt || !supportsCodexContextPolicy(capabilities)) {
300
+ preserveRecoveredRuntime = true;
301
+ if (!capabilities.sessionCapabilities?.systemPrompt)
302
+ throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
303
+ throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
304
+ }
305
+ }
260
306
  if (runtime.restartNotice) {
261
307
  const ids = runtime.restartInputIds;
262
308
  if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
263
309
  throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
264
- input.prompt = ids.flatMap(id => input.users.find(u => u.id === id).prompt);
310
+ input = { ...input, prompt: ids.flatMap(id => input.users.find(u => u.id === id).prompt) };
311
+ if (contextChanged)
312
+ input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
265
313
  }
266
314
  const details = runtime.details;
267
315
  if (!details)
268
316
  throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
269
317
  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));
318
+ const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
271
319
  activeMapper = mapper;
272
320
  let id;
273
321
  let stop;
322
+ if (runtime.employeePresentation) {
323
+ yield* mapper.text(runtime.employeePresentation + '\n\n');
324
+ yield* mapper.flush();
325
+ }
274
326
  if (runtime.workspaceNotice) {
275
327
  yield* mapper.text(runtime.workspaceNotice.text);
276
328
  yield* mapper.flush();
@@ -279,6 +331,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
279
331
  yield* mapper.text(runtime.restartNotice + '\n\n');
280
332
  yield* mapper.flush();
281
333
  }
334
+ if (runtime.execution) {
335
+ details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: '本机工具路由及未覆盖操作范围', payload: runtime.execution.scope });
336
+ yield* mapper.text(runtime.execution.notice + '\n\n');
337
+ yield* mapper.flush();
338
+ }
282
339
  if (mode === 'session') {
283
340
  if (live && live.runtime !== runtime)
284
341
  throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
@@ -289,7 +346,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
289
346
  let done = false;
290
347
  let failure;
291
348
  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 {
349
+ const hooks = { broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
350
+ const execution = runtime.execution;
351
+ const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
352
+ if (execution) {
353
+ if (toolName && ['Agent', 'Task', 'TaskOutput', 'TaskStop', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'].includes(toolName)) {
354
+ details.record({ kind: 'need', severity: 'warning', code: 'CLAUDE_HOST_TOOL_UNSUPPORTED', summary: '这类原生调用没有经过员工端别名,本机来源不支持;本次审批已取消', payload: { operation: toolName } });
355
+ push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
356
+ return 'cancelled';
357
+ }
358
+ await execution.validate(approvalSignal);
359
+ }
360
+ const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined) ?? 'unavailable';
361
+ if (execution && answer === 'allowed-once')
362
+ await execution.validate(approvalSignal);
363
+ return answer;
364
+ }, onUpdate: notification => { try {
293
365
  push(mapper.acp(notification.update));
294
366
  }
295
367
  catch (error) {
@@ -300,7 +372,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
300
372
  if (fresh) {
301
373
  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
374
  session = new AcpSession(transport, hooks);
303
- live = { session, runtime, configuration, usageSnapshot: mapper.usageSnapshot };
375
+ live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
304
376
  if (key)
305
377
  this.sessions.set(key, live);
306
378
  }
@@ -313,17 +385,30 @@ export class CodexRemoteAdapter extends LlmAdapter {
313
385
  const task = (async () => {
314
386
  remoteAttempted = true;
315
387
  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 });
388
+ if (fresh && runtime.recoveredSession) {
389
+ if (previous && runtime.recoveredSession.sessionId !== previous.sessionId)
390
+ throw new NeedError('SESSION_STATE_INVALID', '已接回的会话与持久标识不一致,本轮未发送', '原会话的恢复标识');
391
+ session.restore(runtime.execution?.hostCwd ?? runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}) }, codex && !!previous && !contextChanged);
392
+ session.validatePrompt(input.prompt);
393
+ if (contextChanged)
394
+ await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
321
395
  }
396
+ else if (fresh)
397
+ 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);
398
+ else if (contextChanged)
399
+ await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
322
400
  else {
323
401
  const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
324
402
  if (updated && this.agent.loginProvider === 'claude')
325
403
  throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
326
404
  }
405
+ if (contextChanged) {
406
+ if (session.id === previous.sessionId)
407
+ throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
408
+ live.contextSha256 = contextSha256;
409
+ live.usageSnapshot = mapper.usageSnapshot;
410
+ 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 } });
411
+ }
327
412
  catalog.assertCurrent();
328
413
  const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
329
414
  catalog.assertCurrent();
@@ -350,11 +435,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
350
435
  details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
351
436
  catalog.assertCurrent();
352
437
  session.assertModelSelection(selected.value, options.reasoningEffort);
438
+ await runtime.execution?.beginTurn(modelSignal);
353
439
  id = session.id;
354
440
  try {
355
- return await session.prompt(input.prompt, modelSignal);
441
+ const result = await session.prompt(input.prompt, modelSignal);
442
+ if (contextChanged)
443
+ push(mapper.text(runtime.restartNotice ? '\n\n会话要求已变化,已把此前可见对话带入新 Codex 会话继续处理。' : '\n\n会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。'));
444
+ return result;
356
445
  }
357
446
  finally {
447
+ runtime.execution?.endTurn();
358
448
  details.endTurn();
359
449
  }
360
450
  })();
@@ -406,7 +496,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
406
496
  if (await runtime.saveCheckpoint(id, session.checkpoint))
407
497
  recoveryCheckpointId = id;
408
498
  }
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 } : {}) };
499
+ const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
500
+ 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
501
  succeeded = true;
411
502
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
412
503
  }
@@ -427,9 +518,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
427
518
  throw error;
428
519
  }
429
520
  finally {
521
+ runtime?.execution?.endTurn();
430
522
  catalog?.release();
431
523
  try {
432
- if (this.shuttingDown || key && this.detached.has(key)) {
524
+ if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
433
525
  session?.close();
434
526
  await runtime?.detach?.();
435
527
  }
@@ -459,6 +551,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
459
551
  listModels(provider) { return this.adapter(provider).listModels(provider); }
460
552
  resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
461
553
  async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
554
+ async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
462
555
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
463
556
  stream(options) { return this.adapter(options.provider).stream(options); }
464
557
  async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
@@ -476,6 +569,7 @@ export function apply(ctx, live) {
476
569
  return attachments.readImage(ref, signal);
477
570
  };
478
571
  const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
572
+ const localTurns = createLocalUserTurnObserver(ctx);
479
573
  const detailManagers = new Map();
480
574
  const managerForProfile = () => {
481
575
  const profile = ctx.get('profileContext');
@@ -490,13 +584,15 @@ export function apply(ctx, live) {
490
584
  };
491
585
  let changed = () => { };
492
586
  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) => {
587
+ const adapter = new RemoteAgentsAdapter(config, agent => {
588
+ const details = async (request, api, connection, target) => {
589
+ return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
590
+ };
591
+ const open = async (...args) => {
496
592
  const profile = ctx.get('profileContext');
497
593
  if (!profile?.dir)
498
594
  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]));
595
+ 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
596
  args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
501
597
  args[2].currentInputIds = await currentInputIds(ctx, args[2]);
502
598
  const runtime = await openByAgent.get(agent.id)(...args);
@@ -520,7 +616,19 @@ export function apply(ctx, live) {
520
616
  return { turn, step };
521
617
  };
522
618
  return runtime;
523
- } }), dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
619
+ };
620
+ 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;
621
+ return connectProvider(get, { agent, ...(agent.loginProvider === 'codex' ? { employee: {
622
+ currentUserTurn: request => localTurns.current(request),
623
+ getNetwork: () => ctx.get('aiWorkbenchNetwork'),
624
+ workspace: async (request) => {
625
+ const services = ctx;
626
+ const registry = services.get('workspaceRegistry');
627
+ const sessions = services.get('sessions');
628
+ return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
629
+ }, native: () => resolveEmployeeNative(),
630
+ } } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
631
+ }, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
524
632
  const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
525
633
  let turn = null, step = null;
526
634
  for (const event of events ?? []) {
@@ -544,8 +652,20 @@ export function apply(ctx, live) {
544
652
  const directory = ctx.llm.registerConfigurableProviders(entries());
545
653
  const registration = ctx.llm.registerAdapter(ids, adapter);
546
654
  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(); });
655
+ const serviceValue = (name) => ctx.get(name);
656
+ let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
657
+ ctx.on('internal/service', service => {
658
+ if (service === 'aiWorkbenchConnect')
659
+ modelDirectory.refresh();
660
+ if (service !== 'aiWorkbenchConnect' && service !== 'aiWorkbenchNetwork')
661
+ return;
662
+ const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
663
+ if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
664
+ return;
665
+ employeeConnect = nextConnect;
666
+ employeeNetwork = nextNetwork;
667
+ void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
668
+ });
549
669
  new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
550
670
  const cleanup = (id) => adapter.detachSession(id);
551
671
  ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
@@ -558,7 +678,7 @@ export function dshApproval(ctx) {
558
678
  const approval = ctx.get('approval');
559
679
  if (!agent || !approval)
560
680
  return 'unavailable';
561
- const label = approvalLabel(request);
681
+ const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host');
562
682
  if (!label)
563
683
  return 'unavailable';
564
684
  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>;
package/dist/input.js CHANGED
@@ -1,5 +1,59 @@
1
1
  import { NeedError } from "./remote.js";
2
2
  import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
3
+ async function publicBlock(block, options, readImage, history = false) {
4
+ options.signal?.throwIfAborted();
5
+ if (block.type === 'text')
6
+ return { type: 'text', text: block.text };
7
+ if (block.type === 'image') {
8
+ if (block.offloaded) {
9
+ return { type: 'text', text: `图片 ${block.attachment.name ?? block.attachment.attachmentId} 已按官方会话策略移出当前上下文。` };
10
+ }
11
+ if (!readImage)
12
+ throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
13
+ const image = await readImage(block.attachment, options.signal);
14
+ options.signal?.throwIfAborted();
15
+ if (!image || !(image.data instanceof Uint8Array) || image.ref?.attachmentId !== block.attachment.attachmentId || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType) || image.ref.bytes !== image.data.byteLength)
16
+ throw new NeedError('ATTACHMENT_INVALID', '图片附件没有返回匹配的已核验内容', '官方核验的图片字节与媒体类型');
17
+ return { type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') };
18
+ }
19
+ throw new NeedError(history ? 'HISTORY_CONTENT_UNSUPPORTED' : 'INPUT_TYPE_REQUIRED', `我需要此${history ? '历史' : '输入'}类型的远程转接能力:${block.type};本轮未新建或发送`, '对应类型的公开ACP输入映射;内部推理及工具记录不可降为用户要求');
20
+ }
21
+ /** Role-labelled public history only; never serialize source/replay/tool internals. */
22
+ export async function prepareContinuation(options, input, readImage) {
23
+ const messages = options.messages.filter(message => !isWorkspaceNoticeMessage(message)).map(withoutWorkspaceNotice);
24
+ for (const message of messages)
25
+ if (!['user', 'assistant', 'system', 'developer'].includes(message.role))
26
+ throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
27
+ const after = messages.findLastIndex(message => message.role === 'assistant');
28
+ const prompt = [];
29
+ const historyTransfer = { scope: 'visible-user-assistant-text-and-images', textBlocks: 0, imageBlocks: 0, offloadedImagePlaceholders: 0, omittedReasoningBlocks: 0, reasoningOmissionReason: 'dsh-internal-reasoning' };
30
+ for (const message of messages.slice(0, after + 1)) {
31
+ if (message.role === 'system' || message.role === 'developer')
32
+ continue;
33
+ if (message.role !== 'user' && message.role !== 'assistant')
34
+ throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
35
+ prompt.push({ type: 'text', text: `此前可见对话(历史角色:${message.role};仅为历史记录):\n` });
36
+ for (const block of message.content) {
37
+ // DSH explicitly distinguishes internal thinking from visible user text.
38
+ if (block.type === 'reasoning') {
39
+ historyTransfer.omittedReasoningBlocks++;
40
+ continue;
41
+ }
42
+ prompt.push(await publicBlock(block, options, readImage, true));
43
+ if (block.type === 'text')
44
+ historyTransfer.textBlocks++;
45
+ else if (block.type === 'image') {
46
+ if (block.offloaded)
47
+ historyTransfer.offloadedImagePlaceholders++;
48
+ else
49
+ historyTransfer.imageBlocks++;
50
+ }
51
+ }
52
+ prompt.push({ type: 'text', text: `\n(历史角色:${message.role} 的记录结束)\n` });
53
+ }
54
+ prompt.push({ type: 'text', text: '此前可见对话结束。以下为当前用户输入:\n' }, ...input.prompt);
55
+ return { ...input, prompt, historyTransfer };
56
+ }
3
57
  /** The attachment service supplies verified bytes; client paths never cross SSH. */
4
58
  export async function prepareInput(options, readImage) {
5
59
  options.signal?.throwIfAborted();
@@ -10,7 +64,11 @@ export async function prepareInput(options, readImage) {
10
64
  for (const message of messages) {
11
65
  if (message.role !== 'system' && message.role !== 'developer')
12
66
  continue;
13
- const parts = message.content.map(block => block.type === 'text' ? block.text : JSON.stringify(block));
67
+ const parts = message.content.map(block => {
68
+ if (block.type !== 'text')
69
+ throw new NeedError('SYSTEM_CONTEXT_TYPE_UNSUPPORTED', '系统或开发者要求包含暂不支持的非文本内容,本轮未新建或发送', '系统及开发者仅支持公开text上下文;非文本内容需要已定义的公开ACP投影');
70
+ return block.text;
71
+ });
14
72
  if (parts.length)
15
73
  context.push((message.role === 'system' ? '系统上下文:' : '开发者上下文:') + '\n' + parts.join('\n'));
16
74
  }
@@ -22,28 +80,8 @@ export async function prepareInput(options, readImage) {
22
80
  const preparedUsers = [];
23
81
  for (const message of users) {
24
82
  const before = prompt.length;
25
- for (const block of message.content) {
26
- options.signal?.throwIfAborted();
27
- if (block.type === 'text') {
28
- prompt.push({ type: 'text', text: block.text });
29
- continue;
30
- }
31
- if (block.type === 'image') {
32
- if (block.offloaded) {
33
- prompt.push({ type: 'text', text: `图片 ${block.attachment.name ?? block.attachment.attachmentId} 已按官方会话策略移出当前上下文。` });
34
- continue;
35
- }
36
- if (!readImage)
37
- throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
38
- const image = await readImage(block.attachment, options.signal);
39
- options.signal?.throwIfAborted();
40
- if (!image || !(image.data instanceof Uint8Array) || image.ref?.attachmentId !== block.attachment.attachmentId || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType) || image.ref.bytes !== image.data.byteLength)
41
- throw new NeedError('ATTACHMENT_INVALID', '图片附件没有返回匹配的已核验内容', '官方核验的图片字节与媒体类型');
42
- prompt.push({ type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') });
43
- continue;
44
- }
45
- throw new NeedError('INPUT_TYPE_REQUIRED', `我需要此输入类型的远程转接能力:${block.type}`, '对应类型的公开ACP输入映射');
46
- }
83
+ for (const block of message.content)
84
+ prompt.push(await publicBlock(block, options, readImage));
47
85
  preparedUsers.push({ id: message.id, prompt: prompt.slice(before) });
48
86
  }
49
87
  if (!prompt.length)
@@ -0,0 +1,24 @@
1
+ import type { RuntimeRequest } from './provider.ts';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export interface LocalTurnProof {
4
+ expectedUserTurn: {
5
+ turn: number;
6
+ userMessageId: string;
7
+ };
8
+ startSequence: number;
9
+ userMessageSequence: number;
10
+ assertCurrent(): void;
11
+ }
12
+ /** CAS expectation only. L independently establishes live human-turn/brand evidence. */
13
+ export declare function readLocalUserTurn(ctx: Context, request: {
14
+ sessionId: string;
15
+ route: string;
16
+ userMessageIds: readonly string[];
17
+ }): Promise<LocalTurnProof>;
18
+ /** Only metadata observed live since this plugin mounted; never seed or persisted tails. */
19
+ export declare function createLocalUserTurnObserver(ctx: Pick<Context, 'get' | 'on'>): {
20
+ current(request: Pick<RuntimeRequest, "sessionId" | "userInputIds">): {
21
+ turn: number;
22
+ userMessageId: string;
23
+ };
24
+ };