@ai-workbench/remote-agent 0.8.19 → 0.8.23

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 (95) hide show
  1. package/README.md +27 -134
  2. package/dist/acp.d.ts +23 -16
  3. package/dist/acp.js +209 -226
  4. package/dist/agents.d.ts +0 -1
  5. package/dist/agents.js +2 -2
  6. package/dist/broker-runtime.d.ts +3 -5
  7. package/dist/broker-runtime.js +45 -342
  8. package/dist/broker.d.ts +6 -31
  9. package/dist/broker.js +42 -113
  10. package/dist/catalog-ssh.d.ts +2 -31
  11. package/dist/catalog-ssh.js +9 -39
  12. package/dist/claude-local.d.ts +1 -1
  13. package/dist/claude-local.js +41 -105
  14. package/dist/client.js +34 -126
  15. package/dist/close-diagnostics.d.ts +6 -20
  16. package/dist/close-diagnostics.js +13 -57
  17. package/dist/connect-contract.d.ts +4 -8
  18. package/dist/conversation-details.d.ts +0 -29
  19. package/dist/conversation-details.js +8 -99
  20. package/dist/employee-endpoint.d.ts +1 -1
  21. package/dist/employee-endpoint.js +21 -19
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +7 -1
  25. package/dist/employee-info.js +24 -3
  26. package/dist/employee-location.js +3 -4
  27. package/dist/employee-mcp.js +44 -69
  28. package/dist/employee-native.d.ts +1 -3
  29. package/dist/employee-native.js +6 -27
  30. package/dist/employee-presentation.d.ts +0 -5
  31. package/dist/employee-presentation.js +0 -15
  32. package/dist/employee-provider.d.ts +3 -11
  33. package/dist/employee-provider.js +69 -205
  34. package/dist/employee-reasons.js +1 -5
  35. package/dist/employee-shell.d.ts +2 -1
  36. package/dist/employee-shell.js +10 -13
  37. package/dist/employee-ssh.d.ts +1 -3
  38. package/dist/employee-ssh.js +13 -16
  39. package/dist/employee-tools.d.ts +5 -5
  40. package/dist/failure-evidence.d.ts +2 -10
  41. package/dist/failure-evidence.js +12 -61
  42. package/dist/host-workspace.d.ts +4 -2
  43. package/dist/host-workspace.js +6 -2
  44. package/dist/index.d.ts +13 -43
  45. package/dist/index.js +192 -484
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +5 -3
  49. package/dist/mapping.js +31 -36
  50. package/dist/model-catalog.d.ts +2 -41
  51. package/dist/model-catalog.js +24 -282
  52. package/dist/model-configuration.d.ts +0 -3
  53. package/dist/model-configuration.js +8 -47
  54. package/dist/model-presentation.d.ts +1 -0
  55. package/dist/model-presentation.js +2 -2
  56. package/dist/permissions.d.ts +15 -0
  57. package/dist/permissions.js +30 -0
  58. package/dist/preflight-diagnostic.js +11 -22
  59. package/dist/preflight-payload.d.ts +1 -1
  60. package/dist/preflight-payload.js +2 -2
  61. package/dist/presentation.js +21 -41
  62. package/dist/provider.d.ts +12 -34
  63. package/dist/provider.js +17 -92
  64. package/dist/readiness.js +1 -7
  65. package/dist/remote-details.d.ts +41 -17
  66. package/dist/remote-details.js +84 -381
  67. package/dist/workspace-source.d.ts +1 -25
  68. package/dist/workspace-source.js +1 -138
  69. package/package.json +11 -12
  70. package/dist/catalog-diagnostics.d.ts +0 -16
  71. package/dist/catalog-diagnostics.js +0 -108
  72. package/dist/host-layout.d.ts +0 -8
  73. package/dist/host-layout.js +0 -9
  74. package/dist/job.d.ts +0 -17
  75. package/dist/job.js +0 -57
  76. package/dist/model-defaults.d.ts +0 -29
  77. package/dist/model-defaults.js +0 -62
  78. package/dist/oneshot.d.ts +0 -13
  79. package/dist/oneshot.js +0 -78
  80. package/dist/protocol-boundary.d.ts +0 -13
  81. package/dist/protocol-boundary.js +0 -49
  82. package/dist/provider-locale.d.ts +0 -19
  83. package/dist/provider-locale.js +0 -73
  84. package/dist/recovery-checkpoint.d.ts +0 -6
  85. package/dist/recovery-checkpoint.js +0 -71
  86. package/dist/recovery.d.ts +0 -51
  87. package/dist/recovery.js +0 -61
  88. package/dist/sdk-ssh-wrapper.d.ts +0 -2
  89. package/dist/sdk-ssh-wrapper.js +0 -34
  90. package/dist/stage-timing.d.ts +0 -31
  91. package/dist/stage-timing.js +0 -61
  92. package/dist/workspace-notice.d.ts +0 -47
  93. package/dist/workspace-notice.js +0 -273
  94. package/dist/workspace-source-confirmation.d.ts +0 -4
  95. package/dist/workspace-source-confirmation.js +0 -12
@@ -6,18 +6,18 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { CLAUDE_EMPLOYEE_SCOPE_NOTICE as NOTICE } from "./employee-presentation.js";
9
+ import { EMPLOYEE_EXECUTION_NOTICE as NOTICE } from "./employee-presentation.js";
10
10
  import { CloseDiagnostics, closeCode } from "./close-diagnostics.js";
11
11
  import { isDeepStrictEqual } from 'node:util';
12
12
  import { openEmployeeLease } from "./employee-endpoint.js";
13
13
  import { openEmployeeMcp } from "./employee-mcp.js";
14
14
  import { createEmployeeShell } from "./employee-shell.js";
15
- import { sameTarget, unwrap } from "./provider.js";
15
+ import { unwrap } from "./provider.js";
16
16
  import { sshIo } from "./broker.js";
17
17
  import { NeedError, shellQuote } from "./remote.js";
18
18
  import { awaitL } from "./provider.js";
19
19
  const failure = (code) => new NeedError(code, '当前本机执行上下文无法确认,本轮已停止;没有改在主机执行。', 'L当前本地绑定、N员工端点与S同代桥');
20
- /** Local source only. Host and legacy-host remain with the original provider. */
20
+ /** Local source only. Host source remains with the host provider. */
21
21
  export function createClaudeLocalProvider(options) {
22
22
  const entries = new Map();
23
23
  const admitted = new WeakMap();
@@ -38,22 +38,17 @@ export function createClaudeLocalProvider(options) {
38
38
  const item = matches[0];
39
39
  if (item.source !== 'local')
40
40
  return;
41
- if (item.status !== 'selected' || !item.bindingId || api.getContractInfo?.().localUserTurnAcceptance !== 1 || typeof api.acceptLocalWorkspaceUserTurn !== 'function' || typeof local.getLocalWorkspaceContext !== 'function')
41
+ if (item.status !== 'selected' || !item.bindingId || api.getContractInfo?.().localWorkspaceContext !== 1 || typeof local.getLocalWorkspaceContext !== 'function')
42
42
  throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
43
43
  const context = unwrap(await awaitL(() => local.getLocalWorkspaceContext({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.workspaceId, expectedBindingId: item.bindingId }), request.signal));
44
44
  if (context.instanceId !== connection.instanceId || context.epoch !== connection.epoch || context.workspaceId !== workspace.workspaceId || context.bindingId !== item.bindingId || context.source !== 'local' || !isDeepStrictEqual(context.directory, workspace.directory))
45
45
  throw failure('EMPLOYEE_CONTEXT_CHANGED');
46
46
  const proof = await options.readUserTurn(request);
47
47
  proof.assertCurrent();
48
- const accepted = unwrap(await awaitL(() => api.acceptLocalWorkspaceUserTurn({ instanceId: connection.instanceId, epoch: connection.epoch, sessionId: request.sessionId, provider: 'claude', purpose: 'user-turn', workspaceId: workspace.workspaceId, expectedBindingId: item.bindingId, expectedDirectory: { device: workspace.directory.device, inode: workspace.directory.inode }, expectedUserTurn: { ...proof.expectedUserTurn } }), request.signal));
49
- proof.assertCurrent();
50
48
  request.signal?.throwIfAborted();
51
49
  const now = api.getConnectionState();
52
50
  if (options.getConnect() !== api || now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
53
51
  throw failure('CONNECTION_LOST');
54
- if (!accepted || accepted.version !== 1 || typeof accepted.acceptanceId !== 'string' || !accepted.acceptanceId.trim() || accepted.instanceId !== connection.instanceId || accepted.epoch !== connection.epoch || accepted.sessionId !== request.sessionId || accepted.provider !== 'claude' || accepted.workspaceId !== workspace.workspaceId || accepted.bindingId !== item.bindingId || accepted.source !== 'local' || !isDeepStrictEqual(accepted.directory, workspace.directory) || accepted.userTurn?.turn !== proof.expectedUserTurn.turn || accepted.userTurn.userMessageId !== proof.expectedUserTurn.userMessageId || accepted.userTurn.startSequence !== proof.startSequence || accepted.userTurn.userMessageSequence !== proof.userMessageSequence)
55
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
56
- Object.defineProperty(request, 'localAcceptanceId', { value: accepted.acceptanceId, enumerable: true });
57
52
  request.detailChannel = await options.details(request, api, connection, target);
58
53
  admitted.set(request, { api, workspace: structuredClone(workspace), bindingId: item.bindingId, connection: structuredClone(connection) });
59
54
  }
@@ -94,36 +89,17 @@ export function createClaudeLocalProvider(options) {
94
89
  throw failure('SETTINGS_REQUIRED');
95
90
  const scope = { target, workspace, bindingId: item.bindingId, instanceId: connection.instanceId, epoch: connection.epoch, maximum, control, grace };
96
91
  let entry = entries.get(request.sessionId);
97
- if (entry && (entry.closing || !isDeepStrictEqual(entry.scope, scope) || entry.lease.signal.aborted))
98
- throw failure('EMPLOYEE_CONTEXT_CHANGED');
92
+ if (entry) {
93
+ entries.delete(request.sessionId);
94
+ void entry.runtime.dispose?.({ layer: 'runtime', initiator: 'claude.next-round', reason: 'NEXT_ROUND', action: 'dispose' }).catch(() => console.warn('CLAUDE_CLEANUP_FAILED'));
95
+ entry = undefined;
96
+ }
99
97
  const details = request.detailChannel;
100
- const closeDiagnostics = entry?.runtime.closeDiagnostics ?? new CloseDiagnostics(event => details.record(event));
98
+ const closeDiagnostics = new CloseDiagnostics(event => details.record(event));
101
99
  closeDiagnostics.bind(event => details.record(event));
102
100
  request.closeDiagnostics = closeDiagnostics;
103
- request.timing?.bind(observation => details.record(observation));
104
- const trusted = async (ignoreCancellation = false) => {
105
- if (!ignoreCancellation)
106
- request.signal?.throwIfAborted();
107
- if (options.getConnect() !== api)
108
- throw failure('CONNECTION_LOST');
109
- const now = api.getConnectionState();
110
- if (now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || !sameTarget(unwrap(await api.getSshTarget()), target))
111
- throw failure('CONNECTION_LOST');
112
- };
113
- const openRequest = (row, cold = false) => ({ ...request, detailChannel: details, bindingId: item.bindingId, workspaceId: workspace.workspaceId, bindingRevision: state.revision, ...cold ? { recoveryCheckpointId: undefined } : {}, validateContext: async (cleanup = false) => { await trusted(cleanup); if (!cleanup)
114
- await row.lease.validate(request.signal); } });
115
- if (entry) {
116
- entry.details = details;
117
- entry.request = request;
118
- await entry.lease.validate(request.signal);
119
- const base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest(entry));
120
- if (base.acpTransport !== entry.runtime.acpTransport)
121
- throw failure('SESSION_TRANSPORT_MISMATCH');
122
- entry.runtime.details = details;
123
- entry.runtime.restartNotice = base.restartNotice;
124
- entry.runtime.restartInputIds = base.restartInputIds;
125
- return entry.runtime;
126
- }
101
+ details.record({ kind: 'session', severity: 'info', code: 'CLAUDE_EMPLOYEE_VALIDATION_POLICY', summary: '本机执行不可改到主机:每轮开始从L读取一次本机工作区上下文,核验所选物理目录、N端点与主机目标,并新建本轮工具通道。之后不逐消息、审批或工具重复核验;既有连接、位置、网络事件和通道关闭只终止本轮,下一轮重新核验。' });
102
+ const openRequest = () => ({ ...request, detailChannel: details, bindingId: item.bindingId, workspaceId: workspace.workspaceId, bindingRevision: state.revision });
127
103
  let io;
128
104
  const transport = async () => { if (!io) {
129
105
  const name = '@ai-workbench/connect';
@@ -133,61 +109,44 @@ export function createClaudeLocalProvider(options) {
133
109
  io = sshIo(target, l.sshArgs, control);
134
110
  } return io; };
135
111
  let lease, mcp, base;
136
- let privateFailure, privateTail = Promise.resolve();
112
+ let privateTail = Promise.resolve();
137
113
  try {
138
- const unscoped = (observation) => { if (privateFailure)
139
- throw privateFailure; privateTail = privateTail.then(() => options.recordUnscoped(request, observation)).catch(error => { privateFailure ??= error; closeDiagnostics.record({ layer: 'runtime', initiator: 'claude.private-diagnostic-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'failure' }); void lease?.close({ layer: 'endpoint', initiator: 'claude.private-diagnostic-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' }).catch(failure => { privateFailure ??= failure; }); }); };
114
+ const unscoped = (observation) => { privateTail = privateTail.then(() => options.recordUnscoped(request, observation)).catch(() => { console.warn('CLAUDE_DIAGNOSTIC_DROPPED'); }); };
115
+ const network = options.getNetwork();
140
116
  lease = await (options.ports?.lease ?? openEmployeeLease)({ closeDiagnostics, getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
141
- const shell = (options.ports?.shell ?? createEmployeeShell)({ closeDiagnostics, target: lease.target, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
117
+ const shell = (options.ports?.shell ?? createEmployeeShell)({ closeDiagnostics, target: lease.target, sshArgs: lease.sshArgs, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
142
118
  mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ closeDiagnostics, lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
143
119
  throw failure('EMPLOYEE_NO_ACTIVE_TURN'); return observation => sink.record(observation); }, detail: unscoped });
144
120
  entry = { runtime: undefined, scope, lease, details, request };
145
121
  const row = entry;
146
- base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest(row, true));
122
+ base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest());
123
+ // The old per-step identity check runs once when this round finishes opening.
124
+ if (options.getConnect() !== api || options.getNetwork() !== network)
125
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
147
126
  const underlying = base, endpoint = mcp;
148
127
  const close = (detach, cause = { layer: 'runtime', initiator: detach ? 'claude.runtime-detach' : 'claude.runtime-dispose', reason: detach ? 'RUNTIME_DETACH' : 'RUNTIME_DISPOSE', action: detach ? 'detach' : 'dispose' }) => {
128
+ // Unlink synchronously. A slow/failed cleanup must not hold the next round.
129
+ if (entries.get(request.sessionId) === row)
130
+ entries.delete(request.sessionId);
149
131
  closeDiagnostics.record(cause);
150
132
  return row.closing ??= Promise.resolve().then(async () => {
151
133
  row.active = undefined;
152
- const errors = [];
153
- try {
154
- await endpoint.close({ layer: 'relay', initiator: 'claude.cleanup-mcp', reason: 'RUNTIME_CLEANUP', action: 'close' });
155
- }
156
- catch (error) {
157
- errors.push(error);
158
- }
159
- await privateTail;
160
- if (privateFailure)
161
- errors.push(privateFailure);
162
- try {
163
- await (detach ? underlying.detach?.(cause) : underlying.dispose?.(cause));
164
- }
165
- catch (error) {
166
- errors.push(error);
167
- }
168
- try {
169
- await row.details.flush();
134
+ const clean = async (action) => { try {
135
+ await action();
170
136
  }
171
- catch (error) {
172
- errors.push(error);
173
- }
174
- try {
175
- closeDiagnostics.assertRecorded();
176
- }
177
- catch (error) {
178
- errors.push(error);
179
- }
180
- if (errors.length)
181
- throw new AggregateError(errors, 'EMPLOYEE_CLEANUP_UNCONFIRMED');
182
- if (entries.get(request.sessionId) === row)
183
- entries.delete(request.sessionId);
137
+ catch {
138
+ console.warn('CLAUDE_CLEANUP_FAILED');
139
+ } };
140
+ await clean(() => endpoint.close({ layer: 'relay', initiator: 'claude.cleanup-mcp', reason: 'RUNTIME_CLEANUP', action: 'close' }));
141
+ await clean(() => row.lease.close({ layer: 'endpoint', initiator: 'claude.cleanup-lease', reason: 'RUNTIME_CLEANUP', action: 'close' }));
142
+ await clean(() => detach ? underlying.detach?.(cause) : underlying.dispose?.(cause));
184
143
  });
185
144
  };
186
145
  const executionSignal = AbortSignal.any([lease.signal, ...mcp.signal ? [mcp.signal] : []]);
187
- row.runtime = { ...underlying, closeDiagnostics, cwd: workspace.directory.path, details, connection: { ...connection }, binding: { bindingId: item.bindingId, workspaceId: workspace.workspaceId, revision: state.revision }, execution: { hostCwd: target.session.root, routing: mcp.routing, notice: NOTICE, signal: executionSignal, scope: { source: 'local', cwd: workspace.directory.path, hostCwd: target.session.root, uid: lease.target.endpoint.remoteUid, claudeAcpVersion: '0.94.0', sdkVersion: '0.3.303', unsupported: ['subagents', 'internal-direct-calls', 'binary-read', 'background-shell'] }, validate: async (signal) => { executionSignal.throwIfAborted(); await row.lease.validate(signal); }, beginTurn: async (signal) => { executionSignal.throwIfAborted(); if (row.closing || row.active)
188
- throw failure('TURN_BUSY'); await row.lease.validate(signal); executionSignal.throwIfAborted(); row.active = { signal, details: row.details }; }, endTurn: () => { row.active = undefined; } }, dispose: cause => close(false, cause), detach: cause => close(true, cause) };
146
+ row.runtime = { ...underlying, closeDiagnostics, cwd: workspace.directory.path, details, connection: { ...connection }, binding: { bindingId: item.bindingId, workspaceId: workspace.workspaceId, revision: state.revision }, execution: { hostCwd: target.session.root, routing: mcp.routing, notice: NOTICE, signal: executionSignal, scope: { source: 'local', cwd: workspace.directory.path, hostCwd: target.session.root, uid: lease.target.endpoint.remoteUid, claudeAcpVersion: '0.94.0', sdkVersion: '0.3.303', unsupported: ['subagents', 'internal-direct-calls', 'binary-read', 'background-shell'] }, validate: async (signal) => { signal?.throwIfAborted(); executionSignal.throwIfAborted(); }, beginTurn: async (signal) => { signal?.throwIfAborted(); executionSignal.throwIfAborted(); if (row.closing || row.active)
147
+ throw failure('TURN_BUSY'); row.active = { signal, details: row.details }; }, endTurn: () => { row.active = undefined; } }, dispose: cause => close(false, cause), detach: cause => close(true, cause) };
189
148
  entries.set(request.sessionId, row);
190
- lease.signal.addEventListener('abort', () => { void close(false, { layer: 'endpoint', initiator: 'claude.lease-abort', reason: closeCode(lease?.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' }).catch(() => { }); }, { once: true });
149
+ lease.signal.addEventListener('abort', () => { void close(false, { layer: 'endpoint', initiator: 'claude.lease-abort', reason: closeCode(lease?.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' }).catch(() => console.warn('CLAUDE_CLEANUP_FAILED')); }, { once: true });
191
150
  if (lease.signal.aborted) {
192
151
  await close(false, { layer: 'endpoint', initiator: 'claude.open-aborted', reason: closeCode(lease.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' });
193
152
  throw failure('EMPLOYEE_CONTEXT_CHANGED');
@@ -197,37 +156,14 @@ export function createClaudeLocalProvider(options) {
197
156
  catch (error) {
198
157
  closeDiagnostics.record({ layer: 'runtime', initiator: 'claude.open-failure', reason: closeCode(error, 'EMPLOYEE_OPEN_FAILED'), action: 'failure' });
199
158
  const cause = { layer: 'runtime', initiator: 'claude.open-cleanup', reason: 'EMPLOYEE_OPEN_FAILED', action: 'close' };
200
- const cleanup = [];
201
- try {
202
- await mcp?.close(cause);
203
- await lease?.close(cause);
204
- }
205
- catch (e) {
206
- cleanup.push(e);
207
- }
208
- try {
209
- await base?.dispose?.(cause);
210
- }
211
- catch (e) {
212
- cleanup.push(e);
213
- }
214
- await privateTail;
215
- if (privateFailure)
216
- cleanup.push(privateFailure);
217
- try {
218
- await details.flush();
219
- }
220
- catch (e) {
221
- cleanup.push(e);
222
- }
223
- try {
224
- closeDiagnostics.assertRecorded();
225
- }
226
- catch (e) {
227
- cleanup.push(e);
159
+ for (const clean of [() => mcp?.close(cause), () => lease?.close(cause), () => base?.dispose?.(cause)]) {
160
+ try {
161
+ await clean();
162
+ }
163
+ catch {
164
+ console.warn('CLAUDE_CLEANUP_FAILED');
165
+ }
228
166
  }
229
- if (cleanup.length)
230
- throw new AggregateError([error, ...cleanup], 'EMPLOYEE_OPEN_CLEANUP_UNCONFIRMED');
231
167
  throw error;
232
168
  }
233
169
  };
package/dist/client.js CHANGED
@@ -1,145 +1,53 @@
1
- /* Official locale and provider-registration updates only; no custom UI or refresh. */
2
1
  window.__ModuleLoader__.load({id:'@ai-workbench/remote-agent',factory(require){
3
- const messages={"en":{"administratorNote":"Settings → Built-in plugins → 远程助手参数. Eight named settings with defaults, scope and effects.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:设置 → 内置插件 → 远程助手参数。八项设置含默认值、范围、生效和后果说明。","providerName":"Codex(远程主机)"}};
4
- const settingsFields=[{"key":"mode","name":"Codex 运行方式","kind":"select","options":[{"value":"session","name":"会话模式"},{"value":"one-shot","name":"单次模式(当前版本不可运行)"}],"range":"session / one-shot","scope":"仅 Codex;Claude 始终使用会话模式。","when":"当前轮不变;更改后原会话下一轮会拒绝继续,需新建会话。","consequence":"会话模式保留连续对话;当前单次模式会明确拒绝运行,不会发给模型。不提供关闭会话身份核对的选项。","defaultValue":"session"},{"key":"acpMaxMessageBytes","name":"完整消息与工具输出容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"两位远程助手的完整会话消息;也用于模型目录、本机 MCP、员工执行 RPC 和工具输出容量。","when":"当轮快照不变;下一次独立目录查询读取新值;已有会话需新建后使用。","consequence":"调大可接收更大消息、占用更多内存;调小更早拒绝超限。超限明确停止,不截断、不自动重试;不能用0取消完整消息保护。","defaultValue":4194304},{"key":"brokerMaxFrameBytes","name":"主机会话回传单段容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"本插件接收主机会话单段输出的容量;不会替主机修改它自己的输出策略。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更大回传段、占用更多内存;调小可能拒绝主机合法输出。超限停止,不截断;不能关闭协议帧核验。","defaultValue":2097152},{"key":"brokerControlMaxBytes","name":"主机会话管理回复容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"主机会话管理命令的标准输出、标准错误各自容量;也用于本机执行桥的管理回复。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更长管理回复、占用更多内存;调小可能无法核实状态。超限明确停止,不能把不完整回复当成功;0不表示无限。","defaultValue":1048576},{"key":"oneShotSandbox","name":"Codex 单次任务权限模式","kind":"select","options":[{"value":"read-only","name":"只读"},{"value":"workspace-write","name":"工作区内可写"},{"value":"danger-full-access","name":"完全访问(高权限)"}],"range":"read-only / workspace-write / danger-full-access","scope":"仅 Codex 单次通道;不改变会话模式、本机工具或员工账号的系统权限。","when":"仅单次任务启动时适用;当前版本单次任务被拒绝,因此目前不生效。","consequence":"只读不允许单次任务写入;工作区内可写允许该目录写入;完全访问放宽单次沙箱。此项不是员工目录的权限开关,也不会解锁当前不可用的单次模式。","defaultValue":"workspace-write"},{"key":"oneShotApproval","name":"Codex 单次任务审批方式","kind":"fixed","readOnly":true,"range":"固定 never;不能切换","scope":"仅 Codex 单次通道;不改变会话模式的员工审批。","when":"仅单次启动参数;当前单次模式不可运行。","consequence":"单次通道没有中途交互审批能力,所以必须固定为无中途交互;不表示自动批准或允许所有操作。不能切换为自动回答旧审批。","defaultValue":"never"},{"key":"employeeShellKillGraceMs","name":"本机命令外层连接清理宽限(毫秒)","kind":"number","min":0,"max":2147483647,"range":"0~2147483647 的整数","scope":"仅 Claude 本机 Bash 取消后,本插件自己的外层 SSH 进程清理;不控制员工命令或 Codex 执行器。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"大于0时先请求外层连接退出,再等待所设毫秒后强制清理;0表示不留宽限、立即强制清理外层连接,不是关闭取消。没有员工端退出确认时结果仍未知,不能据此自动重放或声称命令已停止。","defaultValue":1000},{"key":"employeeHttpMcpPolicy","name":"本机 HTTP MCP 策略","kind":"select","options":[{"value":"reject","name":"拒绝(默认)"},{"value":"ignore","name":"忽略后继续"}],"range":"reject / ignore","scope":"仅 Codex 员工本机执行器实际会采用的 HTTP MCP;stdio 始终依原生过滤忽略并说明名称。","when":"新连接建立时读取并冻结;修改不停止当轮或已有会话,需新建会话使用新值。","consequence":"默认 reject 会以明确代码拒绝这些 HTTP 条目;ignore 仅剔除会话投影后继续,不改员工配置、不启动 MCP、不新增网络路径。两种模式都显示实际忽略的安全名称。","defaultValue":"reject"}];
5
- /* Public settings.plugins.tab + shared ConfigForms. No private client services. */
2
+ const messages={"en":{"administratorNote":"Settings → Built-in plugins → 远程助手参数. Eight named settings with defaults, scope and effects.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:设置 → 内置插件 → 远程助手参数。八项设置含默认值、范围、生效和后果说明。","providerName":"Codex(远程主机)"}};
3
+ const settingsFields=[{"key":"acpMaxMessageBytes","name":"完整消息与工具输出容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"两位远程助手的完整会话消息;也用于模型目录、本机 MCP、员工执行 RPC 和工具输出容量。","when":"当轮快照不变;下一次独立目录查询读取新值;已有会话需新建后使用。","consequence":"调大可接收更大消息、占用更多内存;调小更早拒绝超限。超限明确停止,不截断、不自动重试;不能用0取消完整消息保护。","defaultValue":4194304},{"key":"brokerMaxFrameBytes","name":"主机会话回传单段容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"本插件接收主机会话单段输出的容量;不会替主机修改它自己的输出策略。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更大回传段、占用更多内存;调小可能拒绝主机合法输出。超限停止,不截断;不能关闭协议帧核验。","defaultValue":2097152},{"key":"brokerControlMaxBytes","name":"主机会话管理回复容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"主机会话管理命令的标准输出、标准错误各自容量;也用于本机执行桥的管理回复。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更长管理回复、占用更多内存;调小可能无法核实状态。超限明确停止,不能把不完整回复当成功;0不表示无限。","defaultValue":1048576},{"key":"employeeShellKillGraceMs","name":"本机命令外层连接清理宽限(毫秒)","kind":"number","min":0,"max":2147483647,"range":"0~2147483647 的整数","scope":"仅 Claude 本机 Bash 取消后,本插件自己的外层 SSH 进程清理;不控制员工命令或 Codex 执行器。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"大于0时先请求外层连接退出,再等待所设毫秒后强制清理;0表示不留宽限、立即强制清理外层连接,不是关闭取消。没有员工端退出确认时结果仍未知,不能据此自动重放或声称命令已停止。","defaultValue":1000}];
4
+ /* Public settings.plugins.tab + shared ConfigForms. No private client services. */
6
5
  function mountRuntimeSettings(ctx,React,fields){
7
6
  const h=React.createElement;
8
7
  const invariants=[
9
- ['员工、主机和文件夹核对','始终开启,不可关闭','每次派发前核对当前连接、员工与工作区,避免操作到别人的文件或旧位置;核对失败即停止。'],
10
- ['本轮输入与技术详情归属','始终开启,不可关闭','真实输入须先接纳,技术详情须确认保存;迟到结果仍属于原轮次,不能借下一轮的记录继续执行。'],
11
- ['模型选择确认','始终开启,不可关闭','主机须完整确认所选模型和推理等级,不能静默换模型继续。'],
12
- ['完整消息与精确整数','始终开启,不可关闭','容量不能用0关闭,超限明确停止、不截断;整数必须能精确表示,防止消息缺失或容量计算失真。'],
13
- ['未知结果与旧审批保护','始终开启,不可关闭','断线或取消不等于员工命令已结束;不自动重放未知操作,不代答旧审批,清理未确认时不擅自重开。'],
14
- ['本机执行位置','始终开启,不可关闭','已提供的本机工具通道不悄悄回退主机;文件夹不是权限隔离根,也不新增目录白名单。Claude子代理及内部直接调用尚未完整覆盖,可能仍在主机,每轮另有范围提示。']
8
+ ['执行位置','始终开启,不可关闭','每轮开始核对文件夹来源和位置。本机执行组件报告就绪后才开通道;本机通道失败就结束本轮,绝不改在主机执行。'],
9
+ ['凭据保护','始终开启,不可关闭','订阅凭据只在主机,不放进命令参数、日志或技术详情。'],
10
+ ['权限跟随当前档位','始终开启,不可关闭','完全权限不问(Claude bypassPermissions / Codex agent-full-access);工作区内修改先问(Claude default / Codex workspace-write);仅可查看只读(Claude plan / Codex read-only)。'],
11
+ ['会话来源','始终开启,不可关闭','同一会话使用一个工作区来源,换来源请新建会话。断线恢复后可在原会话继续;旧资源清理和技术详情记录不会阻止下一轮。']
12
+
15
13
  ];
16
14
  const unavailable={status:'unavailable',mode:'memory',writable:false};
17
15
  const unavailableForm={subscribe:()=>()=>{},getSnapshot:()=>unavailable};
18
- const formIdentities=new WeakMap();let nextFormIdentity=0;
19
- function RuntimePanel({showParameters=true}){
20
- const [snapshot,setSnapshot]=React.useState(null),[failure,setFailure]=React.useState(''),[busy,setBusy]=React.useState(false);
21
- const active=React.useRef(null),serial=React.useRef(0),alive=React.useRef(false);
22
- const load=async()=>{
23
- active.current?.abort();const controller=new AbortController(),mine=++serial.current;active.current=controller;
24
- if(ctx.connection.state.getSnapshot()!=='connected'){setSnapshot(null);setFailure('尚未连上本机服务,暂时无法读取目录诊断。');setBusy(false);return;}
25
- setBusy(true);setFailure('');
26
- try{
27
- const result=await ctx.connection.rpc.call('/api','awRemoteModelDiagnostics/read',{args:{request:null}},controller.signal);
28
- if(!alive.current||controller.signal.aborted||mine!==serial.current)return;
29
- const value=result?.value;
30
- if(result?.ok!==true||!value||!Array.isArray(value.providers)||!Array.isArray(value.records)||!value.providers.every(provider=>provider&&['claude-code-workbench','codex-remote'].includes(provider.provider)&&typeof provider.name==='string'&&Array.isArray(provider.messages)&&provider.messages.every(message=>typeof message==='string'))||!value.records.every(record=>record&&['claude','codex'].includes(record.provider)))throw Error('INVALID_DIAGNOSTICS');
31
- setSnapshot(value);
32
- }catch{if(alive.current&&!controller.signal.aborted&&mine===serial.current){setFailure('目录诊断暂时无法读取。请检查本机服务连接,再点“刷新本机诊断”;这不会重新查询主机或发送原话。');}}
33
- finally{if(alive.current&&mine===serial.current)setBusy(false);}
34
- };
35
- React.useEffect(()=>{alive.current=true;void load();const off=ctx.connection.state.subscribe(()=>{void load();});return()=>{alive.current=false;serial.current++;active.current?.abort();off();};},[]);
36
- const ns=snapshot?.settingsNamespace,form=typeof ns==='string'&&ns?ctx.configForms.get(ns):unavailableForm;
37
- if(!formIdentities.has(form))formIdentities.set(form,++nextFormIdentity);
38
- return h('div',null,
39
- h('section',{'aria-label':'模型目录诊断'},h('h2',null,'模型目录诊断'),
40
- h('p',null,'这里显示本客户端最近的目录查询记录。读取本页只查看本机记录,不会重试主机查询、登录或发送待发送的原话。每次查询都保留本机诊断历史,不自动过期;记录容量沿用下方会话消息容量,超限会明确停止并标明记录不完整。'),
41
- h('button',{type:'button',disabled:busy,onClick:()=>{void load();}},busy?'正在读取本机诊断…':'刷新本机诊断'),
42
- failure&&h('p',{role:'alert'},failure),snapshot?.storageError&&h('p',{role:'alert'},snapshot.storageError),
43
- ...(snapshot?.providers??[]).map(provider=>{
44
- const record=snapshot.records.find(item=>item.provider===(provider.provider==='claude-code-workbench'?'claude':'codex'));
45
- return h('article',{key:provider.provider},h('h3',null,provider.name),
46
- h('p',null,provider.messages.length?'本提供方有目录或模型设置诊断,请展开技术详情。':record?'当前没有新增的模型设置诊断;以下为最近一次查询记录,不代表此刻连接可用。':'尚无本客户端的目录查询记录;旧版本未保存的错误无法还原。'),
47
- record&&h('p',null,'最近查询:'+record.startedAt+';'+(record.stage==='started'?'尚未确认结束':record.failureCode||record.hostErrorCode?'未完成':'已返回主机响应')+'。'),
48
- (record||provider.messages.length>0)&&h('details',null,h('summary',null,'技术详情('+provider.name+')'),
49
- ...provider.messages.map((message,index)=>h('pre',{key:index,style:{whiteSpace:'pre-wrap',overflowWrap:'anywhere'}},message)),
50
- record&&h('pre',{style:{whiteSpace:'pre-wrap',overflowWrap:'anywhere'}},JSON.stringify(record,null,2))));
51
- })),
52
- showParameters&&!ns&&h('p',{role:'status'},'尚未确认本插件的官方配置入口,参数暂不可编辑;上方目录诊断仍可读取。'),
53
- showParameters&&h(Panel,{key:(ns??'unavailable')+':'+formIdentities.get(form),form}));
16
+ function RuntimePanel(){
17
+ const [ns,setNamespace]=React.useState(null);
18
+ React.useEffect(()=>{let active=true;
19
+ void ctx.remote.llm.listConfigurableProviders().then(result=>{
20
+ if(!active)return;
21
+ const rows=result?.ok===true&&Array.isArray(result.value)?result.value.filter(row=>['claude-code-workbench','codex-remote'].includes(row.provider)):[];
22
+ const names=[...new Set(rows.map(row=>row.settingsNs).filter(value=>typeof value==='string'&&value))];
23
+ setNamespace(names.length===1?names[0]:null);
24
+ },()=>{if(active)setNamespace(null);});
25
+ return()=>{active=false;};
26
+ },[]);
27
+ const form=ns?ctx.configForms.get(ns):unavailableForm;
28
+ return h('div',null,!ns&&h('p',{role:'status'},'尚未读取到本插件的官方配置入口,参数当前值暂不可用。'),h(Panel,{key:ns??'unavailable',form}));
54
29
  }
55
30
  function Panel({form}){
56
31
  const snapshot=React.useSyncExternalStore(fn=>form.subscribe(fn),()=>form.getSnapshot(),()=>form.getSnapshot());
57
- const [draft,setDraft]=React.useState({}),[busy,setBusy]=React.useState(false),[notice,setNotice]=React.useState('');
58
- const revision=React.useRef(undefined),saving=React.useRef(false),alive=React.useRef(true),prefix=React.useId();
59
- React.useEffect(()=>{alive.current=true;return()=>{alive.current=false;};},[]);
60
- const ready=snapshot.status==='ready',writable=ready&&snapshot.writable===true&&snapshot.mode==='host'&&snapshot.revision!==undefined;
61
- const changed=Object.keys(draft).length>0,stale=changed&&revision.current!==snapshot.revision;
62
- const edit=(key,value)=>{if(saving.current||!writable)return;if(!changed)revision.current=snapshot.revision;setDraft(previous=>({...previous,[key]:value}));setNotice('修改尚未保存。');};
63
- const discard=()=>{if(saving.current)return;setDraft({});revision.current=undefined;setNotice('已放弃未保存修改,显示当前配置。');};
64
- const save=async()=>{
65
- if(saving.current||!writable||!changed||stale)return;
66
- const ops=[];
67
- for(const field of fields){const entry=draft[field.key];if(!entry)continue;
68
- if(field.readOnly){setNotice('固定约束不能修改。');return;}
69
- if(entry.op==='unset'){ops.push({op:'unset',path:[field.key]});continue;}
70
- const raw=entry.value,value=field.kind==='number'?Number(raw):raw;
71
- if(field.kind==='number'&&(String(raw).trim()===''||!Number.isSafeInteger(value)||value<field.min||value>field.max)||field.kind==='select'&&!field.options.some(option=>option.value===value)){setNotice(field.name+'的值不在所列范围内,未保存。');return;}
72
- ops.push({op:'set',path:[field.key],value});
73
- }
74
- saving.current=true;setBusy(true);setNotice('正在保存,请稍候。');
75
- try{
76
- const accepted=await form.mutate(ops,revision.current);
77
- if(!alive.current)return;
78
- if(accepted===true){setDraft({});revision.current=undefined;setNotice('已保存。运行中的这一轮不变;请按各项生效说明使用新会话。');}
79
- else setNotice('未保存:配置已变化或官方设置服务拒绝了修改。草稿已保留;核对当前值后可放弃草稿重新修改,不会自动重试。');
80
- }catch{if(alive.current)setNotice('保存失败,草稿已保留。请检查连接及官方配置状态;不会自动重试。');}
81
- finally{saving.current=false;if(alive.current)setBusy(false);}
82
- };
83
- const stateMessage=snapshot.mode!=='host'?'当前连接不提供持久配置写入,本页不能保存。':!ready?'尚未取得官方配置,当前不能修改。':!snapshot.writable?'当前配置由更高层覆盖或不可写,本页不能修改。':stale?'其他地方已更新配置;本页草稿保留。请放弃草稿后按最新值重新修改。':'';
32
+ const ready=snapshot.status==='ready';
84
33
  return h('section',{'aria-label':'远程助手参数',style:{maxWidth:850}},
85
34
  h('h2',null,'远程助手参数'),
86
- h('p',null,'这些参数用于本客户端当前配置下的 Claude(远程主机)和 Codex(远程主机),不改变其他模型或主机账号权限。没有总开关;关闭整个插件请使用官方插件管理。'),
87
- stateMessage&&h('p',{role:'status'},stateMessage),
88
- h('form',{onSubmit:event=>{event.preventDefault();void save();}},
89
- ...fields.map(field=>{
90
- const entry=draft[field.key],accepted=ready?snapshot.value?.[field.key]:undefined;
91
- const base=snapshot.base,hasBase=base!==null&&typeof base==='object'&&Object.hasOwn(base,field.key)&&base[field.key]!==undefined;
92
- const value=entry?.op==='set'?entry.value:entry?.op==='unset'&&hasBase?base[field.key]:accepted;
93
- const id=prefix+'-'+field.key,description=id+'-description';
94
- const control=field.readOnly?h('p',{'aria-label':field.name+'当前值'},String(value??'尚未读取')+'(固定,不可切换)'):field.kind==='select'?h('select',{id,value:value??'',disabled:!writable||busy,'aria-describedby':description,onChange:event=>edit(field.key,{op:'set',value:event.target.value})},value===undefined&&h('option',{value:'',disabled:true},'尚未读取'),...field.options.map(option=>h('option',{key:option.value,value:option.value},option.name))):h('input',{id,type:'number',step:1,min:field.min,max:field.max,value:value??'',disabled:!writable||busy,'aria-describedby':description,onChange:event=>edit(field.key,{op:'set',value:event.target.value})});
95
- return h('fieldset',{key:field.key,style:{marginBottom:18,padding:14,border:'1px solid currentColor',borderRadius:6}},
96
- h('legend',null,field.name),h('label',{htmlFor:field.readOnly?undefined:id},field.name+'('+field.key+')'),
97
- h('div',null,control),h('div',{id:description},
98
- h('p',null,'默认值:'+field.defaultValue+';范围:'+field.range),
99
- h('p',null,'生效范围:'+field.scope),h('p',null,'生效时间:'+field.when),h('p',null,'调整或关闭的后果:'+field.consequence)),
100
- entry?.op==='unset'&&h('p',null,hasBase?'将移除本项覆盖,恢复所示继承值;尚未保存。':'将移除本项覆盖;继承层未设此项,保存后由官方配置确定(默认值:'+field.defaultValue+')。输入框仍显示保存前值,尚未保存。'),
101
- !field.readOnly&&h('button',{type:'button',disabled:!writable||busy,onClick:()=>edit(field.key,{op:'unset'})},'恢复继承值:'+field.name));
102
- }),
103
- h('div',null,h('button',{type:'submit',disabled:!writable||!changed||busy||stale},busy?'正在保存…':'保存修改'),h('button',{type:'button',disabled:!changed||busy,onClick:discard},'放弃未保存修改'))),
104
- h('p',{role:'status','aria-live':'polite'},notice),h('h3',null,'必须保持的正确性约束'),
35
+ h('p',null,'这里只显示本客户端当前配置下的 Claude(远程主机)和 Codex(远程主机)参数。请通过 DSH 官方 Config 配置文件入口修改。关闭整个插件请使用官方插件管理。'),
36
+ !ready&&h('p',{role:'status'},'尚未取得官方配置,暂时无法显示当前值。'),
37
+ ...fields.map(field=>h('fieldset',{key:field.key,style:{marginBottom:18,padding:14,border:'1px solid currentColor',borderRadius:6}},
38
+ h('legend',null,field.name),h('p',null,field.key),
39
+ h('p',{'aria-label':field.name+'当前值'},String(ready?snapshot.value?.[field.key]??'尚未读取':'尚未读取')),
40
+ h('p',null,'默认值:'+field.defaultValue+';范围:'+field.range),
41
+ h('p',null,'生效范围:'+field.scope),h('p',null,'生效时间:'+field.when),h('p',null,'调整或关闭的后果:'+field.consequence))),
42
+ h('h3',null,'必须保持的正确性约束'),
105
43
  h('p',null,'以下保护默认开启且不能关闭。关闭会使输入、审批、执行位置或记录失去可信归属,并非可以放宽的偏好。'),
106
44
  h('dl',null,...invariants.flatMap(([name,state,why])=>[h('dt',{key:name},name+':'+state),h('dd',{key:name+'-why'},why)])));
107
45
  }
108
- ctx.effect(()=>ctx.slots.inject('settings.models.footer',()=>ctx.slots.register({name:'settings.models.footer',id:'ai-workbench-remote-model-diagnostics',order:90,inject:()=>({showParameters:false})},RuntimePanel)));
109
46
  ctx.effect(()=>ctx.slots.inject('settings.plugins.tab',()=>ctx.slots.register({name:'settings.plugins.tab',id:'ai-workbench-remote-parameters',order:90,label:()=> '远程助手参数',inject:()=>({})},RuntimePanel)));
110
47
  }
111
48
 
112
- return{inject:['locale','connection'],apply(ctx){
113
- ctx.inject(['slots','configForms'],settings=>mountRuntimeSettings(settings,require('react'),settingsFields));
114
- ctx.effect(()=>ctx.locale.register('ai-workbench-codex',messages),'Codex official translations');
115
- ctx.effect(()=>{
116
- let disposed=false,connected=false,previous,generation=0,request=new AbortController(),queue=Promise.resolve();
117
- const sync=()=>{
118
- if(disposed||!connected)return;
119
- const active=ctx.locale.getSnapshot().active;
120
- const language=active.split('-')[0].toLowerCase()==='zh'?'zh':'en';
121
- if(language===previous)return;
122
- previous=language;
123
- const mine=generation,signal=request.signal;
124
- queue=queue.then(async()=>{
125
- if(disposed||signal.aborted||mine!==generation)return;
126
- const result=await ctx.connection.rpc.call('/api','awCodexPresentation/setLocale',{args:{language}},signal);
127
- signal.throwIfAborted();
128
- if(result?.ok!==true||result.value?.language!==language||result.value?.name!==messages[language].providerName){
129
- throw Object.assign(new Error('Provider-name locale sync was not confirmed'),{code:result?.ok===false&&typeof result.error?.code==='string'?result.error.code:'LOCALE_SYNC_INVALID'});
130
- }
131
- }).catch(error=>{
132
- if(!disposed&&!signal.aborted&&mine===generation)ctx.logger.warn('Codex provider-name locale update failed',typeof error?.code==='string'?error.code:'LOCALE_SYNC_FAILED');
133
- });
134
- };
135
- const link=()=>{
136
- const next=ctx.connection.state.getSnapshot()==='connected';
137
- if(next===connected)return;
138
- connected=next;generation++;previous=undefined;request.abort();request=new AbortController();
139
- if(connected)sync();
140
- };
141
- const offLocale=ctx.locale.subscribe(sync),offConnection=ctx.connection.state.subscribe(link);link();
142
- return()=>{disposed=true;offLocale();offConnection();request.abort();};
143
- },'Codex provider names follow the official locale');
144
- }};
49
+ return{inject:['locale'],apply(ctx){
50
+ ctx.inject(['slots','configForms','remote'],settings=>mountRuntimeSettings(settings,require('react'),settingsFields));
51
+ ctx.effect(()=>ctx.locale.register('ai-workbench-codex',messages),'Remote assistant translations');
52
+ }};
145
53
  }});
@@ -17,35 +17,21 @@ export interface CloseCause {
17
17
  changes?: readonly CloseChange[];
18
18
  }
19
19
  export type CloseObserver = (cause: CloseCause) => void;
20
- export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from './failure-evidence.ts';
21
- /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
20
+ export { closeCode, closeErrorCodes, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from './failure-evidence.ts';
21
+ /** One first-close observation per runtime, including its child resources. */
22
22
  export declare class CloseDiagnostics {
23
23
  private sink;
24
- readonly parentId?: string | undefined;
25
- private state;
26
- readonly id: `${string}-${string}-${string}-${string}-${string}`;
27
- private sequence;
28
- private pending;
29
- private firstValue?;
24
+ private readonly state;
30
25
  private readonly started;
31
26
  stage: string;
32
- constructor(sink: (detail: DetailObservation) => void, parentId?: string | undefined, state?: {
33
- failure?: {
34
- error: unknown;
35
- };
27
+ constructor(sink: (detail: DetailObservation) => void, state?: {
28
+ first?: Readonly<Record<string, unknown>>;
36
29
  });
37
- run<T>(operation: string, work: () => Promise<T>): Promise<T>;
38
- get recordingFailureCode(): string | undefined;
30
+ run<T>(_operation: string, work: () => Promise<T>): Promise<T>;
39
31
  get first(): Readonly<Record<string, unknown>> | undefined;
40
32
  bind(sink: (detail: DetailObservation) => void): void;
41
- /** A delayed resource callback retains its original accepted detail channel. */
42
33
  capture(): CloseObserver;
43
34
  child(sink?: (detail: DetailObservation) => void): CloseDiagnostics;
44
35
  record(cause: CloseCause): void;
45
36
  private recordUsing;
46
- assertRecorded(): void;
47
37
  }
48
- /** Run after owned cleanup; expose both the primary error code and persistence failure. */
49
- export declare function flushCloseDiagnostics(diagnostics: CloseDiagnostics, flush: (() => Promise<void>) | undefined, primary?: {
50
- error: unknown;
51
- }): Promise<void>;
@@ -1,74 +1,30 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { NeedError } from "./remote.js";
3
- import { closeCode, closeErrorCodes, withFailureEvidence } from "./failure-evidence.js";
4
- export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./failure-evidence.js";
5
- /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
1
+ export { closeCode, closeErrorCodes, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./failure-evidence.js";
2
+ /** One first-close observation per runtime, including its child resources. */
6
3
  export class CloseDiagnostics {
7
4
  sink;
8
- parentId;
9
5
  state;
10
- id = randomUUID();
11
- sequence = 0;
12
- pending = new Map();
13
- firstValue;
14
6
  started = performance.now();
15
7
  stage = 'runtime-admission';
16
- constructor(sink, parentId, state = {}) {
8
+ constructor(sink, state = {}) {
17
9
  this.sink = sink;
18
- this.parentId = parentId;
19
10
  this.state = state;
20
11
  }
21
- async run(operation, work) { this.pending.set(operation, (this.pending.get(operation) ?? 0) + 1); try {
22
- return await work();
23
- }
24
- finally {
25
- const count = this.pending.get(operation) - 1;
26
- if (count)
27
- this.pending.set(operation, count);
28
- else
29
- this.pending.delete(operation);
30
- } }
31
- get recordingFailureCode() { return this.state.failure ? closeCode(this.state.failure.error, 'CLOSE_RECORD_FAILED') : undefined; }
32
- get first() { return this.firstValue; }
12
+ run(_operation, work) { return work(); }
13
+ get first() { return this.state.first; }
33
14
  bind(sink) { this.sink = sink; }
34
- /** A delayed resource callback retains its original accepted detail channel. */
35
15
  capture() { const sink = this.sink; return cause => this.recordUsing(sink, cause); }
36
- child(sink = this.sink) { const child = new CloseDiagnostics(sink, this.id, this.state); child.stage = this.stage; return child; }
16
+ child(sink = this.sink) { return new CloseDiagnostics(sink, this.state); }
37
17
  record(cause) { this.recordUsing(this.sink, cause); }
38
18
  recordUsing(sink, cause) {
39
- const first = this.sequence === 0;
40
- const metadata = { ...cause.changedFields ? { changedFields: Object.freeze([...cause.changedFields]) } : {}, changes: Object.freeze((cause.changes ?? [{ field: 'observer.close-sequence', before: this.sequence, after: this.sequence + 1 }]).map(change => Object.freeze({ field: change.field, before: change.before, after: change.after }))), layer: cause.layer, initiator: cause.initiator, reason: cause.reason, ...cause.action ? { action: cause.action } : {}, ...cause.operation ? { operation: cause.operation } : {}, ...cause.exitCode !== undefined ? { exitCode: cause.exitCode } : {}, ...cause.signal !== undefined ? { signal: cause.signal } : {}, ...cause.remoteReason ? { remoteReason: cause.remoteReason } : {} };
41
- const payload = Object.freeze({ ...metadata, pendingOperations: Object.freeze([...this.pending.keys()]), closeId: this.id, ...this.parentId ? { parentCloseId: this.parentId } : {}, closeSequence: ++this.sequence, first, stage: this.stage, clock: 'client-monotonic', elapsedMs: performance.now() - this.started });
42
- this.firstValue ??= payload;
19
+ if (this.state.first)
20
+ return;
21
+ const payload = Object.freeze({ layer: cause.layer, initiator: cause.initiator, reason: cause.reason, ...cause.remoteReason ? { remoteReason: cause.remoteReason } : {}, ...cause.exitCode !== undefined ? { exitCode: cause.exitCode } : {}, ...cause.signal !== undefined ? { signal: cause.signal } : {}, stage: this.stage, clock: 'client-monotonic', elapsedMs: performance.now() - this.started });
22
+ this.state.first = payload;
43
23
  try {
44
- sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: cause.changedFields?.includes('selected-model') ? '最新可信目录已不再提供本轮所选型号,本轮已停止;请在模型菜单核对选择。' : first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
24
+ sink({ kind: 'session', severity: 'warning', code: 'REMOTE_CLOSE_REASON', summary: '首次观察到的关闭原因', payload });
45
25
  }
46
- catch (error) {
47
- this.state.failure ??= { error };
26
+ catch {
27
+ console.warn('[remote-agent] 首次关闭原因未保存');
48
28
  }
49
29
  }
50
- assertRecorded() { if (this.state.failure)
51
- throw new NeedError('CONVERSATION_DETAILS_FAILED', '关闭原因保存未完成;首次观察原因 ' + String(this.firstValue?.reason ?? 'CLOSE_REASON_UNKNOWN') + '。', '关闭诊断保存代码 ' + this.recordingFailureCode + ';不将保存失败当作原始关闭原因'); }
52
- }
53
- /** Run after owned cleanup; expose both the primary error code and persistence failure. */
54
- export async function flushCloseDiagnostics(diagnostics, flush, primary) {
55
- const reporting = [];
56
- try {
57
- await flush?.();
58
- }
59
- catch (error) {
60
- reporting.push(...closeErrorCodes(error, 'CLOSE_FLUSH_FAILED'));
61
- }
62
- try {
63
- diagnostics.assertRecorded();
64
- }
65
- catch (error) {
66
- reporting.push(diagnostics.recordingFailureCode ?? closeCode(error, 'CLOSE_RECORD_FAILED'));
67
- }
68
- if (reporting.length) {
69
- const primaryCodes = primary ? closeErrorCodes(primary.error, 'REMOTE_CLEANUP_UNCONFIRMED') : [];
70
- throw withFailureEvidence(primary?.error ?? new NeedError(primaryCodes[0] ?? 'REMOTE_CLEANUP_UNCONFIRMED', '关闭收尾未确认', '核对实际结果'), reporting.map(code => new NeedError(code, '诊断保存未确认', '只读核对')), 'diagnostic', diagnostics.first?.reason);
71
- }
72
- if (primary)
73
- throw primary.error;
74
30
  }
@@ -1,13 +1,10 @@
1
1
  import type { ConnectNeed } from '@ai-workbench/connect/api';
2
2
  import type { ConnectTarget } from './provider.ts';
3
- import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
3
+ import type { ContractInfo, Provider, WorkspaceUse } from '@ai-workbench/connect/api';
4
4
  /** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
5
- export type { WorkspaceNotice, SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
6
- import type { SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
5
+ export type { HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
6
+ import type { HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
7
7
  export type LoginProvider = Provider;
8
- export type WorkspaceNoticeReceipt = Extract<LWorkspaceNoticeReceipt, {
9
- surface: 'provider';
10
- }>;
11
8
  export type ConnectResult<T> = {
12
9
  ok: true;
13
10
  value: T;
@@ -19,10 +16,9 @@ export type ConnectResult<T> = {
19
16
  requires: string;
20
17
  } & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
21
18
  };
22
- export type SessionProjectResolution = SessionProjectTarget | HostSessionProjectTarget;
19
+ export type SessionProjectResolution = HostSessionProjectTarget;
23
20
  export interface NativeWorkspaceApi {
24
21
  getContractInfo(): ContractInfo;
25
22
  ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
26
23
  resolveSessionProject(sessionId: string, use: WorkspaceUse, location?: HostWorkspaceResolveContext): Promise<ConnectResult<SessionProjectResolution>>;
27
- acknowledgeWorkspaceNotice(receipt: WorkspaceNoticeReceipt): Promise<ConnectResult<void>>;
28
24
  }