@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.
- package/README.md +27 -134
- package/dist/acp.d.ts +23 -16
- package/dist/acp.js +209 -226
- package/dist/agents.d.ts +0 -1
- package/dist/agents.js +2 -2
- package/dist/broker-runtime.d.ts +3 -5
- package/dist/broker-runtime.js +45 -342
- package/dist/broker.d.ts +6 -31
- package/dist/broker.js +42 -113
- package/dist/catalog-ssh.d.ts +2 -31
- package/dist/catalog-ssh.js +9 -39
- package/dist/claude-local.d.ts +1 -1
- package/dist/claude-local.js +41 -105
- package/dist/client.js +34 -126
- package/dist/close-diagnostics.d.ts +6 -20
- package/dist/close-diagnostics.js +13 -57
- package/dist/connect-contract.d.ts +4 -8
- package/dist/conversation-details.d.ts +0 -29
- package/dist/conversation-details.js +8 -99
- package/dist/employee-endpoint.d.ts +1 -1
- package/dist/employee-endpoint.js +21 -19
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +7 -1
- package/dist/employee-info.js +24 -3
- package/dist/employee-location.js +3 -4
- package/dist/employee-mcp.js +44 -69
- package/dist/employee-native.d.ts +1 -3
- package/dist/employee-native.js +6 -27
- package/dist/employee-presentation.d.ts +0 -5
- package/dist/employee-presentation.js +0 -15
- package/dist/employee-provider.d.ts +3 -11
- package/dist/employee-provider.js +69 -205
- package/dist/employee-reasons.js +1 -5
- package/dist/employee-shell.d.ts +2 -1
- package/dist/employee-shell.js +10 -13
- package/dist/employee-ssh.d.ts +1 -3
- package/dist/employee-ssh.js +13 -16
- package/dist/employee-tools.d.ts +5 -5
- package/dist/failure-evidence.d.ts +2 -10
- package/dist/failure-evidence.js +12 -61
- package/dist/host-workspace.d.ts +4 -2
- package/dist/host-workspace.js +6 -2
- package/dist/index.d.ts +13 -43
- package/dist/index.js +192 -484
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +5 -3
- package/dist/mapping.js +31 -36
- package/dist/model-catalog.d.ts +2 -41
- package/dist/model-catalog.js +24 -282
- package/dist/model-configuration.d.ts +0 -3
- package/dist/model-configuration.js +8 -47
- package/dist/model-presentation.d.ts +1 -0
- package/dist/model-presentation.js +2 -2
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.js +30 -0
- package/dist/preflight-diagnostic.js +11 -22
- package/dist/preflight-payload.d.ts +1 -1
- package/dist/preflight-payload.js +2 -2
- package/dist/presentation.js +21 -41
- package/dist/provider.d.ts +12 -34
- package/dist/provider.js +17 -92
- package/dist/readiness.js +1 -7
- package/dist/remote-details.d.ts +41 -17
- package/dist/remote-details.js +84 -381
- package/dist/workspace-source.d.ts +1 -25
- package/dist/workspace-source.js +1 -138
- package/package.json +11 -12
- package/dist/catalog-diagnostics.d.ts +0 -16
- package/dist/catalog-diagnostics.js +0 -108
- package/dist/host-layout.d.ts +0 -8
- package/dist/host-layout.js +0 -9
- package/dist/job.d.ts +0 -17
- package/dist/job.js +0 -57
- package/dist/model-defaults.d.ts +0 -29
- package/dist/model-defaults.js +0 -62
- package/dist/oneshot.d.ts +0 -13
- package/dist/oneshot.js +0 -78
- package/dist/protocol-boundary.d.ts +0 -13
- package/dist/protocol-boundary.js +0 -49
- package/dist/provider-locale.d.ts +0 -19
- package/dist/provider-locale.js +0 -73
- package/dist/recovery-checkpoint.d.ts +0 -6
- package/dist/recovery-checkpoint.js +0 -71
- package/dist/recovery.d.ts +0 -51
- package/dist/recovery.js +0 -61
- package/dist/sdk-ssh-wrapper.d.ts +0 -2
- package/dist/sdk-ssh-wrapper.js +0 -34
- package/dist/stage-timing.d.ts +0 -31
- package/dist/stage-timing.js +0 -61
- package/dist/workspace-notice.d.ts +0 -47
- package/dist/workspace-notice.js +0 -273
- package/dist/workspace-source-confirmation.d.ts +0 -4
- package/dist/workspace-source-confirmation.js +0 -12
package/dist/index.js
CHANGED
|
@@ -1,59 +1,45 @@
|
|
|
1
1
|
import { connectNeedMessage } from "./connect-need.js";
|
|
2
|
-
import { CloseDiagnostics,
|
|
3
|
-
import {
|
|
4
|
-
import { runCatalog } from "./catalog-ssh.js";
|
|
5
|
-
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
2
|
+
import { CloseDiagnostics, withFirstFailureEvidence, failureEvidenceSummary, closeCode } from "./close-diagnostics.js";
|
|
3
|
+
import { withRemoteFailureEvidence } from "./failure-evidence.js";
|
|
6
4
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
7
5
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
6
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
9
|
-
import {
|
|
10
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
-
import { recoveryJournal, runtimeRecoveryKey, supportsCodexContextPolicy } from "./recovery.js";
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
12
8
|
import * as unstable from '@agentclientprotocol/sdk/unstable';
|
|
13
9
|
import * as acp from '@agentclientprotocol/sdk';
|
|
14
10
|
import { AcpSession } from "./acp.js";
|
|
15
|
-
import { runOneShot } from "./oneshot.js";
|
|
16
11
|
import { StreamMapper } from "./mapping.js";
|
|
17
12
|
import { connectProvider } from "./provider.js";
|
|
18
13
|
import { brokerFactory } from "./broker.js";
|
|
19
14
|
import { AGENTS, CODEX_AGENT } from "./agents.js";
|
|
20
|
-
import { prepareInput,
|
|
21
|
-
import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
|
|
15
|
+
import { prepareInput, projectHostWorkspaceInput } from "./input.js";
|
|
22
16
|
import { NeedError } from "./remote.js";
|
|
23
17
|
import { employeeFailure } from "./presentation.js";
|
|
24
18
|
import { createRemoteDetails } from "./remote-details.js";
|
|
25
|
-
import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
|
|
26
19
|
import { approvalLabel } from "./approval-label.js";
|
|
27
|
-
import { StageTiming } from "./stage-timing.js";
|
|
28
|
-
import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
|
|
29
20
|
import { ModelCatalog, modelInfo } from "./model-catalog.js";
|
|
30
|
-
import { ModelPresentation
|
|
31
|
-
import { EMPLOYEE_EXECUTION_NOTICE
|
|
21
|
+
import { ModelPresentation } from "./model-presentation.js";
|
|
22
|
+
import { EMPLOYEE_EXECUTION_NOTICE } from "./employee-presentation.js";
|
|
32
23
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
33
24
|
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
34
|
-
import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
|
|
35
25
|
import { resolveEmployeeNative } from "./employee-native.js";
|
|
36
|
-
import { createLocalUserTurnObserver } from "./local-user-turn.js";
|
|
37
26
|
import { createClaudeLocalProvider } from "./claude-local.js";
|
|
38
27
|
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
39
28
|
import { readEmployeeWorkspace } from "./employee-workspace.js";
|
|
40
|
-
import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
|
|
41
29
|
import { createPreflightDiagnostics } from "./preflight-diagnostic.js";
|
|
42
|
-
import { installModelInputRetention
|
|
30
|
+
import { installModelInputRetention } from "./model-input-retention.js";
|
|
31
|
+
import { dshPermissionSource, permissionMode } from "./permissions.js";
|
|
32
|
+
export { readDshPermissions } from "./permissions.js";
|
|
43
33
|
export const name = 'ai-workbench-remote-agent';
|
|
44
34
|
export const settingsNamespace = 'ai-workbench-codex';
|
|
45
35
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
46
36
|
export const inject = ['llm'];
|
|
47
37
|
/** Owner-approved defaults; administrators may override through the existing settings. */
|
|
48
|
-
export const DEFAULT_CONFIG = Object.freeze({
|
|
38
|
+
export const DEFAULT_CONFIG = Object.freeze({ acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, employeeShellKillGraceMs: 1000 });
|
|
49
39
|
export const Config = Schema.object({
|
|
50
|
-
employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP(默认)', en: '拒绝本机 HTTP MCP(默认)' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续', en: '忽略本机 HTTP MCP 后继续' })]).i18n({ zh: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称', en: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
|
|
51
|
-
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(),
|
|
52
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(),
|
|
53
41
|
brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
|
|
54
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(),
|
|
55
|
-
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(),
|
|
56
|
-
oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
|
|
57
43
|
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(),
|
|
58
44
|
});
|
|
59
45
|
/** DSH volatile settings are handles; adapters freeze their values once per invocation. */
|
|
@@ -65,23 +51,16 @@ function modelResolutionLabel(configuration) {
|
|
|
65
51
|
? `型号:${configuration.resolvedModel}(来源:主机目录映射 ${configuration.selected.value};会话未单独回报具体型号)`
|
|
66
52
|
: `具体型号:${configuration.resolvedModel}`;
|
|
67
53
|
}
|
|
68
|
-
function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
|
|
69
54
|
function readPointer(options, agent) {
|
|
70
|
-
const last = options.messages.findLast(message => message.role === 'assistant'
|
|
55
|
+
const last = options.messages.findLast(message => message.role === 'assistant');
|
|
71
56
|
if (!last || last.role !== 'assistant')
|
|
72
57
|
return;
|
|
73
58
|
if (last.source.provider !== options.provider)
|
|
74
|
-
throw new NeedError('SESSION_STATE_MISSING', '已有对话属于其他模型通道,不能沿用其远端进程', '当前模型的远端恢复记录');
|
|
75
|
-
if (last.source.replayState === undefined)
|
|
76
59
|
return;
|
|
77
60
|
const envelope = last.source.replayState;
|
|
78
61
|
const pointer = envelope?.response;
|
|
79
|
-
if (pointer?.version !== 2 || pointer.agent !== agent.id ||
|
|
80
|
-
|
|
81
|
-
if (options.sessionId && pointer.dshSessionId !== options.sessionId)
|
|
82
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
|
|
83
|
-
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))))))
|
|
84
|
-
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
|
|
62
|
+
if (pointer?.version !== 2 || pointer.agent !== agent.id || pointer.dshSessionId !== options.sessionId || ![pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0))
|
|
63
|
+
return;
|
|
85
64
|
return pointer;
|
|
86
65
|
}
|
|
87
66
|
// A reporting failure can wrap the original admission error. Preserve its
|
|
@@ -110,17 +89,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
110
89
|
reportLoginFailure;
|
|
111
90
|
directory;
|
|
112
91
|
preflightDiagnostics;
|
|
92
|
+
permissions;
|
|
113
93
|
config;
|
|
114
94
|
sessions = new Map();
|
|
115
95
|
turns = new Map();
|
|
116
96
|
lifetime = new AbortController();
|
|
117
97
|
shuttingDown = false;
|
|
118
98
|
detached = new Set();
|
|
119
|
-
cleanups = new WeakMap();
|
|
120
|
-
preparationFailures = new Map();
|
|
121
99
|
modelPresentation = new ModelPresentation();
|
|
122
100
|
employeeNoticeSessions = new Set();
|
|
123
|
-
constructor(config, resolveRuntime, approve, providerName = () =>
|
|
101
|
+
constructor(config, resolveRuntime, approve, providerName = () => CODEX_AGENT.name.zh, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) {
|
|
124
102
|
super();
|
|
125
103
|
this.resolveRuntime = resolveRuntime;
|
|
126
104
|
this.approve = approve;
|
|
@@ -130,6 +108,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
130
108
|
this.reportLoginFailure = reportLoginFailure;
|
|
131
109
|
this.directory = directory;
|
|
132
110
|
this.preflightDiagnostics = preflightDiagnostics;
|
|
111
|
+
this.permissions = permissions;
|
|
133
112
|
this.config = typeof config === 'function' ? config : () => config;
|
|
134
113
|
}
|
|
135
114
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
@@ -142,112 +121,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
142
121
|
finally {
|
|
143
122
|
catalog.release();
|
|
144
123
|
} }
|
|
145
|
-
async resolveModel(provider, id,
|
|
146
|
-
|
|
147
|
-
const displayed = this.modelPresentation.resolve(provider, id);
|
|
148
|
-
if (displayed)
|
|
149
|
-
return displayed;
|
|
150
|
-
const catalog = await this.catalog(signal);
|
|
151
|
-
try {
|
|
152
|
-
const model = catalog.models.find(model => model.id === id);
|
|
153
|
-
return model ? modelInfo(provider, model) : unavailableModelInfo(provider, id);
|
|
154
|
-
}
|
|
155
|
-
finally {
|
|
156
|
-
catalog.release();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
markModelPreparationFailure(provider, error, signal);
|
|
161
|
-
throw error;
|
|
162
|
-
} }
|
|
163
|
-
async prepareCall(provider, id, signal) {
|
|
164
|
-
try {
|
|
165
|
-
const catalog = await this.catalog(signal);
|
|
166
|
-
try {
|
|
167
|
-
catalog.assertCurrent();
|
|
168
|
-
const selected = catalog.models.find(model => model.id === id);
|
|
169
|
-
if (!selected)
|
|
170
|
-
throw new LlmError('所选模型已不在当前主机目录中,请在模型菜单重新选择后再发送;本次输入尚未发送。', 'MODEL_CATALOG_UNAVAILABLE');
|
|
171
|
-
return { model: modelInfo(provider, selected), stream: (options) => this.stream(options) };
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
catalog.release();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
markModelPreparationFailure(provider, error, signal);
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
observeModelInvalidation(fields, changes = []) { for (const live of this.sessions.values())
|
|
183
|
-
live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'catalog.signature-change', reason: 'MODEL_CATALOG_SIGNATURE_CHANGED', action: 'invalidate', changedFields: fields, changes }); }
|
|
184
|
-
async invalidateModelState() { for (const [id, turn] of this.turns) {
|
|
185
|
-
this.sessions.get(id)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.model-invalidation', reason: 'MODEL_STATE_INVALIDATED', action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: turn.signal.aborted, after: true }] });
|
|
186
|
-
turn.abort();
|
|
187
|
-
} await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
|
|
188
|
-
async invalidateEmployeeState() { for (const live of this.sessions.values())
|
|
189
|
-
if (live.runtime.employeeExecutionMeta || live.runtime.execution)
|
|
190
|
-
live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.employee-invalidation', reason: 'EMPLOYEE_SERVICE_CHANGED', action: 'invalidate' }); await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
|
|
124
|
+
async resolveModel(provider, id, _signal) { return this.modelPresentation.resolve(provider, id) ?? { provider, id, name: id }; }
|
|
125
|
+
async prepareCall(provider, id, _signal) { return { model: await this.resolveModel(provider, id), stream: (options) => this.stream(options) }; }
|
|
191
126
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
192
127
|
cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
|
|
193
128
|
runtime.closeDiagnostics?.record(cause);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return existing;
|
|
197
|
-
const cleanup = Promise.resolve().then(() => runtime.dispose?.(cause)).catch(error => {
|
|
198
|
-
const codes = closeErrorCodes(error, 'REMOTE_CLEANUP_UNCONFIRMED');
|
|
199
|
-
const failure = closeFailure('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对原主机会话;收尾代码 ' + codes.join(','), codes);
|
|
200
|
-
for (const live of this.sessions.values())
|
|
201
|
-
if (live.runtime === runtime) {
|
|
202
|
-
live.cleanupFailure = failure;
|
|
203
|
-
live.failure ??= { cause: failure };
|
|
204
|
-
}
|
|
205
|
-
throw failure;
|
|
206
|
-
});
|
|
207
|
-
this.cleanups.set(runtime, cleanup);
|
|
208
|
-
return cleanup;
|
|
209
|
-
}
|
|
210
|
-
/** Publish an already known failure without coupling the official stream to cleanup. */
|
|
211
|
-
finishFailedTurn(key, live, primary, code, firstReason) {
|
|
212
|
-
withPendingCleanupEvidence(primary, firstReason, code);
|
|
213
|
-
const runtime = live.runtime;
|
|
214
|
-
const disposal = this.cleanupRuntime(runtime).then(() => {
|
|
215
|
-
if (this.sessions.get(key) !== live || live.disposal !== disposal)
|
|
216
|
-
return;
|
|
217
|
-
live.disposal = undefined;
|
|
218
|
-
if (!live.failure) {
|
|
219
|
-
if (typeof runtime.dispose === 'function') {
|
|
220
|
-
this.sessions.delete(key);
|
|
221
|
-
this.preparationFailures.set(key, code);
|
|
222
|
-
}
|
|
223
|
-
else
|
|
224
|
-
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
225
|
-
}
|
|
226
|
-
}, error => {
|
|
227
|
-
if (this.sessions.get(key) === live && live.disposal === disposal)
|
|
228
|
-
live.cleanupFailure = withFailureEvidence(primary, [error], 'cleanup', firstReason, code);
|
|
229
|
-
throw error;
|
|
230
|
-
});
|
|
231
|
-
live.disposal = disposal;
|
|
232
|
-
const report = (reason) => {
|
|
233
|
-
runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup', reason, action: 'dispose' });
|
|
234
|
-
// State settles before reporting. A delayed D19 write is not another release gate.
|
|
235
|
-
void Promise.resolve().then(() => runtime.details?.flush()).catch(error => runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup-report', reason: closeCode(error, 'CLOSE_FLUSH_FAILED'), action: 'failure' }));
|
|
236
|
-
};
|
|
237
|
-
void disposal.then(() => report(typeof runtime.dispose === 'function' ? 'RUNTIME_CLEANUP_CONFIRMED' : 'REMOTE_CLEANUP_UNCONFIRMED'), () => report('REMOTE_CLEANUP_UNCONFIRMED'));
|
|
129
|
+
void Promise.resolve().then(() => runtime.dispose?.(cause)).catch(() => console.warn('[remote-agent] 旧主机会话暂未清理,将由主机自动回收'));
|
|
130
|
+
return Promise.resolve();
|
|
238
131
|
}
|
|
239
132
|
async disposeSession(sessionId) {
|
|
240
|
-
this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
|
|
241
133
|
this.turns.get(sessionId)?.abort();
|
|
242
134
|
const current = this.sessions.get(sessionId);
|
|
243
135
|
if (!current)
|
|
244
136
|
return;
|
|
245
|
-
|
|
246
|
-
return current.disposal;
|
|
137
|
+
this.sessions.delete(sessionId);
|
|
247
138
|
current.session.close({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'close' });
|
|
248
|
-
|
|
249
|
-
this.sessions.delete(sessionId); });
|
|
250
|
-
return current.disposal;
|
|
139
|
+
await this.cleanupRuntime(current.runtime);
|
|
251
140
|
}
|
|
252
141
|
async detachSession(sessionId) {
|
|
253
142
|
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
@@ -286,11 +175,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
286
175
|
}
|
|
287
176
|
catch (error) {
|
|
288
177
|
if (error instanceof ModelConfigurationError)
|
|
289
|
-
throw new LlmError(error.
|
|
178
|
+
throw new LlmError(employeeFailure(error.code, this.agent.loginProvider), error.code, { cause: error });
|
|
290
179
|
if (error instanceof NeedError) {
|
|
291
180
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
292
181
|
const evidence = failureEvidenceSummary(error);
|
|
293
|
-
const message =
|
|
182
|
+
const message = connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider);
|
|
294
183
|
const login = originalLoginFailure(error);
|
|
295
184
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
296
185
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -302,11 +191,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
302
191
|
diagnostic += ';诊断保存失败:' + (failure instanceof NeedError ? failure.code : 'DIAGNOSTIC_WRITE_FAILED');
|
|
303
192
|
}
|
|
304
193
|
}
|
|
305
|
-
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
|
|
194
|
+
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (evidence ? ';' + evidence : '') + (diagnostic ? ';' + diagnostic : '')) });
|
|
306
195
|
}
|
|
307
|
-
|
|
308
|
-
throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
|
|
309
|
-
throw error;
|
|
196
|
+
throw new LlmError(employeeFailure('TURN_FAILED', this.agent.loginProvider), 'TURN_FAILED', { cause: error });
|
|
310
197
|
}
|
|
311
198
|
}
|
|
312
199
|
async *generate(original) {
|
|
@@ -322,7 +209,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
322
209
|
this.turns.set(key, turn);
|
|
323
210
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
324
211
|
const options = { ...original, signal };
|
|
325
|
-
const timing = new StageTiming();
|
|
326
212
|
const abortOffs = [];
|
|
327
213
|
let request;
|
|
328
214
|
let earlyStage = 'input-prepare';
|
|
@@ -335,18 +221,19 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
335
221
|
let failureCode;
|
|
336
222
|
let preserveRecoveredRuntime = false;
|
|
337
223
|
let activeMapper;
|
|
338
|
-
let catalog;
|
|
339
224
|
let selected;
|
|
340
225
|
let modelConfiguration;
|
|
341
226
|
let turnFailure;
|
|
342
227
|
let employeeNoticeShown = false;
|
|
228
|
+
let permissionOff;
|
|
229
|
+
let permissionEpoch = new AbortController();
|
|
343
230
|
const information = [];
|
|
344
|
-
const recordInformation = () => {
|
|
231
|
+
const recordInformation = () => { if (runtime?.details)
|
|
345
232
|
for (const entry of information.splice(0))
|
|
346
233
|
runtime.details.record(entry); };
|
|
347
234
|
const config = this.config();
|
|
348
|
-
const mode =
|
|
349
|
-
request = { sessionId: options.sessionId, mode: mode ?? 'session',
|
|
235
|
+
const mode = 'session';
|
|
236
|
+
request = { sessionId: options.sessionId, mode: mode ?? 'session', signal };
|
|
350
237
|
const observeAbort = (source, initiator, reason) => { if (!source)
|
|
351
238
|
return; const record = () => { (request?.closeDiagnostics ?? runtime?.closeDiagnostics)?.record({ layer: 'adapter', initiator, reason, action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', record, { once: true }); abortOffs.push(() => source.removeEventListener('abort', record)); };
|
|
352
239
|
observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
|
|
@@ -356,93 +243,48 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
356
243
|
if (this.preflightDiagnostics)
|
|
357
244
|
request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
|
|
358
245
|
try {
|
|
359
|
-
if (!
|
|
360
|
-
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的
|
|
246
|
+
if (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)
|
|
247
|
+
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的acpMaxMessageBytes');
|
|
361
248
|
const previous = readPointer(options, this.agent);
|
|
362
|
-
employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) ||
|
|
249
|
+
employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || false;
|
|
363
250
|
if (employeeNoticeShown && key)
|
|
364
251
|
this.employeeNoticeSessions.add(key);
|
|
365
|
-
const last = options.messages.findLast(message => message.role === 'assistant'
|
|
252
|
+
const last = options.messages.findLast(message => message.role === 'assistant');
|
|
366
253
|
const missingPointer = !!last && !previous;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (previous && !previous.modelConfiguration)
|
|
371
|
-
information.push({ kind: 'session', severity: 'info', code: 'TURN_GATE_INFORMATION', summary: '历史回复没有模型配置资料;本轮会重新确认你当前选择的模型。', payload: { reason: 'HISTORICAL_MODEL_CONFIGURATION_ABSENT' } });
|
|
372
|
-
live = key ? this.sessions.get(key) : undefined;
|
|
373
|
-
if (live?.disposal || live?.cleanupFailure)
|
|
374
|
-
throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
|
|
375
|
-
if (live?.failure)
|
|
376
|
-
throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '上一轮输入已经交给远端,但结果未确认,不能自动重发', '核对已发送输入及原主机会话状态');
|
|
377
|
-
if (live && !previous) {
|
|
378
|
-
if (typeof live.runtime.dispose !== 'function')
|
|
379
|
-
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原会话不能确认已安全结束,本轮尚未发送', '可确认结束原实例的通道');
|
|
380
|
-
live.session.close({ layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'close' });
|
|
381
|
-
await this.cleanupRuntime(live.runtime, { layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
|
|
382
|
-
if (key && this.sessions.get(key) === live)
|
|
383
|
-
this.sessions.delete(key);
|
|
384
|
-
live = undefined;
|
|
385
|
-
requireFreshSession = true;
|
|
386
|
-
}
|
|
387
|
-
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
388
|
-
if (live && live.configuration !== configuration)
|
|
389
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
390
|
-
Object.assign(request, { timing, employeeHttpMcpPolicy: config.employeeHttpMcpPolicy ?? DEFAULT_CONFIG.employeeHttpMcpPolicy, 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 }) });
|
|
391
|
-
if (requireFreshSession) {
|
|
392
|
-
request.requireFreshSession = true;
|
|
393
|
-
request.resumeSessionId = undefined;
|
|
394
|
-
request.recoveryCheckpointId = undefined;
|
|
395
|
-
information.push({ kind: 'session', severity: 'warning', code: 'TURN_GATE_INFORMATION', summary: '缺少可用的旧会话恢复资料,或上次在发送前已失败;将核对旧实例安全结束后,只处理本次接纳的输入。', payload: { reason: missingPointer ? 'PRIVATE_RECOVERY_POINTER_ABSENT' : 'PRIOR_PREPARATION_FAILED' } });
|
|
396
|
-
}
|
|
254
|
+
live = undefined;
|
|
255
|
+
const configuration = JSON.stringify([config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
256
|
+
Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
397
257
|
request.previousContextSha256 = previous?.contextSha256;
|
|
398
|
-
const after = options.messages.findLastIndex(message => message.role === 'assistant'
|
|
399
|
-
|
|
258
|
+
const after = options.messages.findLastIndex(message => message.role === 'assistant');
|
|
259
|
+
const tail = options.messages.slice(after + 1);
|
|
260
|
+
const humanIds = tail.flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []);
|
|
261
|
+
// An official approval may continue the same native turn without another
|
|
262
|
+
// human message. This is a CAS candidate only: live turn/L proof must still
|
|
263
|
+
// bind it. The actual input delta remains prepareInput's unchanged tail.
|
|
264
|
+
if (!humanIds.length && tail.some(message => message.role === 'user' && message.source?.kind === 'user-approval')) {
|
|
265
|
+
const original = options.messages.slice(0, after + 1).findLast(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string');
|
|
266
|
+
if (typeof original?.id === 'string')
|
|
267
|
+
humanIds.push(original.id);
|
|
268
|
+
}
|
|
269
|
+
request.userInputIds = Object.freeze(humanIds);
|
|
400
270
|
request.userMessageIds = request.userInputIds;
|
|
401
271
|
earlyStage = 'source-context';
|
|
402
272
|
await this.resolveRuntime.preflight?.(request);
|
|
403
273
|
earlyStage = 'input-prepare';
|
|
404
|
-
let input = await
|
|
274
|
+
let input = await prepareInput(options, this.readImage);
|
|
405
275
|
const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
|
|
406
276
|
request.contextSha256 = contextSha256;
|
|
407
277
|
const codex = this.agent.loginProvider === 'codex';
|
|
408
|
-
|
|
409
|
-
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
|
|
410
|
-
const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
|
|
278
|
+
const contextChanged = !!previous && codex && (previous.contextSha256 ?? '').toLowerCase() !== contextSha256;
|
|
411
279
|
// Only identical durable context can prove a legacy context-bound journal.
|
|
412
280
|
// New Codex transport scopes are independent of the native context digest.
|
|
413
281
|
if (contextChanged)
|
|
414
282
|
request.contextSha256 = undefined;
|
|
415
283
|
const claudeContextChanged = !!previous && !codex && previous.contextSha256 !== contextSha256;
|
|
416
|
-
if (contextChanged)
|
|
417
|
-
input = await prepareContinuation(options, input, this.readImage);
|
|
418
|
-
if (live) {
|
|
419
|
-
if (contextChanged)
|
|
420
|
-
live.session.validateNewSessionContext({ systemPrompt: input.systemPrompt });
|
|
421
|
-
else
|
|
422
|
-
live.session.validateContext({ systemPrompt: input.systemPrompt });
|
|
423
|
-
live.session.validatePrompt(input.prompt);
|
|
424
|
-
}
|
|
425
|
-
if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
|
|
426
|
-
throw new NeedError('ONESHOT_CONTEXT_REQUIRED', '首版单次模式不支持图片或系统/开发者上下文,请使用会话模式', 'Codex会话模式');
|
|
427
284
|
if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
428
285
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
request.localAdmissionCheck?.();
|
|
432
|
-
earlyStage = 'model-catalog';
|
|
433
|
-
catalog = await this.catalog(signal);
|
|
434
|
-
selected = catalog.models.find(model => model.id === options.model);
|
|
435
|
-
catalog.onInformation?.(entry => information.push(entry));
|
|
436
|
-
if (!selected)
|
|
437
|
-
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
438
|
-
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
439
|
-
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
440
|
-
catalog.bindSelection?.(selected.id, options.reasoningEffort);
|
|
441
|
-
const observeSignal = (source, initiator, reason) => { const observed = () => { const diagnostic = request?.closeDiagnostics ?? runtime?.closeDiagnostics; if (diagnostic)
|
|
442
|
-
diagnostic.record({ layer: 'adapter', initiator, reason: catalog?.signal === source ? catalog.revocation?.()?.reason ?? reason : reason, action: 'abort', changedFields: catalog?.signal === source ? catalog.revocation?.()?.fields : undefined, changes: catalog?.signal === source ? catalog.revocation?.()?.changes ?? [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] : [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', observed, { once: true }); abortOffs.push(() => source.removeEventListener('abort', observed)); };
|
|
443
|
-
observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
|
|
444
|
-
observeSignal(signal, 'signal.turn-composite', 'ABORTED');
|
|
445
|
-
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
286
|
+
selected = this.modelPresentation.find(options.model) ?? { id: options.model, value: options.model, name: options.model, resolvedModel: null, reasoning: { options: [], defaultValue: null, defaultKnown: false }, source: null };
|
|
287
|
+
let modelSignal = signal;
|
|
446
288
|
options.signal = modelSignal;
|
|
447
289
|
request.signal = modelSignal;
|
|
448
290
|
earlyStage = 'runtime-resolve';
|
|
@@ -450,8 +292,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
450
292
|
const closeSink = runtime.details;
|
|
451
293
|
runtime.closeDiagnostics ??= new CloseDiagnostics(event => closeSink?.record(event));
|
|
452
294
|
signal.throwIfAborted();
|
|
453
|
-
if (requireFreshSession && (!runtime.freshSessionConfirmed || !runtime.restartNotice))
|
|
454
|
-
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '未确认原实例安全结束或原位置为空,本轮没有发送', '同UUID条件结束与list消失证明,或原slot不存在证明');
|
|
455
295
|
if (runtime.execution?.signal) {
|
|
456
296
|
modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
|
|
457
297
|
options.signal = modelSignal;
|
|
@@ -464,42 +304,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
464
304
|
request.signal = modelSignal;
|
|
465
305
|
modelSignal.throwIfAborted();
|
|
466
306
|
}
|
|
467
|
-
catalog.assertCurrent();
|
|
468
|
-
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}`)
|
|
469
|
-
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
470
307
|
if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
471
308
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
|
|
472
|
-
if (contextChanged && runtime.recoveredSession) {
|
|
473
|
-
const capabilities = runtime.recoveredSession.capabilities;
|
|
474
|
-
// A clean reattach has sent no ACP input. A known policy refusal must
|
|
475
|
-
// detach it, preserving the original native session and checkpoint.
|
|
476
|
-
if (!capabilities.sessionCapabilities?.systemPrompt || !supportsCodexContextPolicy(capabilities)) {
|
|
477
|
-
preserveRecoveredRuntime = true;
|
|
478
|
-
if (!capabilities.sessionCapabilities?.systemPrompt)
|
|
479
|
-
throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
|
|
480
|
-
throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (runtime.restartNotice) {
|
|
484
|
-
const ids = runtime.restartInputIds;
|
|
485
|
-
if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
|
|
486
|
-
throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
|
|
487
|
-
input = { ...input, prompt: ids.flatMap(id => input.users.find(u => u.id === id).prompt) };
|
|
488
|
-
if (contextChanged)
|
|
489
|
-
input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
|
|
490
|
-
}
|
|
491
309
|
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
492
310
|
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
493
311
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
494
|
-
const details = runtime.details;
|
|
495
|
-
if (!details)
|
|
496
|
-
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
312
|
+
const details = runtime.details ?? await createRemoteDetails({ warn: line => console.warn(line) }).open({ sessionId: options.sessionId ?? 'unscoped', provider: this.agent.loginProvider, identity: runtime.identity, connection: runtime.connection });
|
|
497
313
|
recordInformation();
|
|
498
|
-
timing.bind(detail => details.record(detail));
|
|
499
314
|
request.recordEarlyFailure = undefined;
|
|
500
315
|
if (runtime.hostWorkspace)
|
|
501
316
|
details.record({ kind: 'session', severity: 'info', code: 'HOST_WORKSPACE_CONTEXT', summary: '本轮模型工作区上下文及审批会话目录使用已确认的主机目录。', payload: { source: 'host', cwd: runtime.hostWorkspace.cwd } });
|
|
502
|
-
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (
|
|
317
|
+
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
503
318
|
activeMapper = mapper;
|
|
504
319
|
let id;
|
|
505
320
|
let stop;
|
|
@@ -518,17 +333,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
518
333
|
}
|
|
519
334
|
yield* mapper.flush();
|
|
520
335
|
}
|
|
521
|
-
if (runtime.workspaceNotice) {
|
|
522
|
-
yield* mapper.text(runtime.workspaceNotice.text);
|
|
523
|
-
yield* mapper.flush();
|
|
524
|
-
}
|
|
525
|
-
if (runtime.restartNotice) {
|
|
526
|
-
yield* mapper.text(runtime.restartNotice + '\n\n');
|
|
527
|
-
yield* mapper.flush();
|
|
528
|
-
}
|
|
529
336
|
if (mode === 'session') {
|
|
530
|
-
if (live && live.runtime !== runtime)
|
|
531
|
-
throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
|
|
532
337
|
if (!runtime.acpTransport)
|
|
533
338
|
throw new NeedError('DEPENDENCY_REQUIRED', '我需要该员工主机上的ACP通道', 'S提供的ACP命令与aw-session');
|
|
534
339
|
const queue = [];
|
|
@@ -536,7 +341,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
536
341
|
let done = false;
|
|
537
342
|
let failure;
|
|
538
343
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
539
|
-
|
|
344
|
+
let promptPermit;
|
|
345
|
+
const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, ...this.permissions ? { promptGuard: () => {
|
|
346
|
+
if (!promptPermit)
|
|
347
|
+
throw new NeedError('PERMISSION_POLICY_CHANGED', '当前权限没有保持确认,本次没有继续派发。', '当前权限代次和同值回执');
|
|
348
|
+
return promptPermit;
|
|
349
|
+
}, approvalGuard: () => {
|
|
350
|
+
const source = this.permissions, epoch = permissionEpoch, policy = JSON.stringify(source.read(options.sessionId));
|
|
351
|
+
return { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && JSON.stringify(source.read(options.sessionId)) === policy };
|
|
352
|
+
} } : {}, approve: async (permission, approvalSignal) => {
|
|
540
353
|
const execution = runtime.execution;
|
|
541
354
|
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
542
355
|
if (execution) {
|
|
@@ -545,99 +358,120 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
545
358
|
push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
|
|
546
359
|
return 'cancelled';
|
|
547
360
|
}
|
|
548
|
-
await execution.validate(approvalSignal);
|
|
549
361
|
}
|
|
550
362
|
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined, runtime.hostWorkspace) ?? 'unavailable';
|
|
551
|
-
if (execution && answer === 'allowed-once')
|
|
552
|
-
await execution.validate(approvalSignal);
|
|
553
363
|
return answer;
|
|
554
|
-
}, onUpdate: notification => { try {
|
|
364
|
+
}, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
|
|
555
365
|
push(mapper.acp(notification.update));
|
|
556
366
|
}
|
|
557
367
|
catch (error) {
|
|
558
368
|
failure = error;
|
|
559
369
|
session?.close({ layer: 'adapter', initiator: 'adapter.mapper-failure', reason: closeCode(error, 'MAPPING_FAILED'), action: 'close' });
|
|
560
370
|
} }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (key)
|
|
567
|
-
this.sessions.set(key, live);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
session = live.session;
|
|
571
|
-
session.setHooks(hooks);
|
|
572
|
-
if (previous?.sessionId !== session.id)
|
|
573
|
-
throw new NeedError('SESSION_STATE_INVALID', '持久会话标识与当前进程不一致', '原会话的恢复标识');
|
|
574
|
-
}
|
|
371
|
+
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 });
|
|
372
|
+
session = new AcpSession(transport, hooks);
|
|
373
|
+
live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
|
|
374
|
+
if (key)
|
|
375
|
+
this.sessions.set(key, live);
|
|
575
376
|
promptBaseline = session.promptDispatchCount;
|
|
377
|
+
let permissionTail = Promise.resolve(), permissionKey, observedPermissionKey, permissionQueued = false;
|
|
378
|
+
const permissionFailed = (error) => {
|
|
379
|
+
failure ??= error;
|
|
380
|
+
runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
|
|
381
|
+
session.close({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
|
|
382
|
+
wake?.();
|
|
383
|
+
};
|
|
384
|
+
const syncPermissions = () => {
|
|
385
|
+
if (!this.permissions)
|
|
386
|
+
return Promise.resolve(); // Explicit component-test port; production always supplies the official source.
|
|
387
|
+
return permissionTail = permissionTail.then(async () => {
|
|
388
|
+
const policy = this.permissions.read(options.sessionId), next = JSON.stringify(policy);
|
|
389
|
+
if (next === permissionKey)
|
|
390
|
+
return;
|
|
391
|
+
const remoteMode = permissionMode(this.agent.loginProvider, policy);
|
|
392
|
+
details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_POLICY', summary: '按DSH当前会话权限同步远程工具;不会把关闭审批提示解释为自动批准。', payload: { ...policy, remoteMode } });
|
|
393
|
+
await session.selectPermissionMode(remoteMode, modelSignal, this.agent.loginProvider);
|
|
394
|
+
permissionKey = next;
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
const dispatchPrompt = async () => {
|
|
398
|
+
if (!this.permissions)
|
|
399
|
+
return session.prompt(input.prompt, modelSignal);
|
|
400
|
+
// New official policy events can append work while an ACK is awaited.
|
|
401
|
+
// Setters for those distinct policy changes are not retries. Dispatch
|
|
402
|
+
// only from one synchronous, stable epoch/ACK boundary.
|
|
403
|
+
for (;;) {
|
|
404
|
+
const barrier = syncPermissions();
|
|
405
|
+
await barrier;
|
|
406
|
+
if (barrier !== permissionTail || permissionQueued)
|
|
407
|
+
continue;
|
|
408
|
+
const source = this.permissions, policy = JSON.stringify(source.read(options.sessionId));
|
|
409
|
+
if (policy !== permissionKey)
|
|
410
|
+
continue;
|
|
411
|
+
const epoch = permissionEpoch;
|
|
412
|
+
promptPermit = { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && permissionKey === policy && JSON.stringify(source.read(options.sessionId)) === policy };
|
|
413
|
+
return session.prompt(input.prompt, modelSignal);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
576
416
|
const task = (async () => {
|
|
577
417
|
remoteAttempted = true;
|
|
578
418
|
try {
|
|
579
|
-
|
|
580
|
-
if (previous && runtime.recoveredSession.sessionId !== previous.sessionId)
|
|
581
|
-
throw new NeedError('SESSION_STATE_INVALID', '已接回的会话与持久标识不一致,本轮未发送', '原会话的恢复标识');
|
|
582
|
-
session.restore(runtime.execution?.hostCwd ?? runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}) }, codex && !!previous && !contextChanged);
|
|
583
|
-
session.validatePrompt(input.prompt);
|
|
584
|
-
if (contextChanged)
|
|
585
|
-
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
586
|
-
else if (claudeContextChanged)
|
|
587
|
-
await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal, true);
|
|
588
|
-
}
|
|
589
|
-
else if (fresh)
|
|
590
|
-
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);
|
|
591
|
-
else if (contextChanged)
|
|
592
|
-
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
593
|
-
else {
|
|
594
|
-
await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
595
|
-
}
|
|
596
|
-
if (fresh && claudeContextChanged && !runtime.recoveredSession && !runtime.restartNotice)
|
|
597
|
-
details.record({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '新要求已通过原会话恢复接口传送;官方仅承诺下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
|
|
598
|
-
if (!codex)
|
|
599
|
-
live.contextSha256 = contextSha256;
|
|
600
|
-
if (contextChanged) {
|
|
601
|
-
if (session.id === previous.sessionId)
|
|
602
|
-
throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
|
|
603
|
-
live.contextSha256 = contextSha256;
|
|
604
|
-
live.usageSnapshot = mapper.usageSnapshot;
|
|
605
|
-
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 } });
|
|
606
|
-
}
|
|
607
|
-
catalog.assertCurrent();
|
|
419
|
+
await session.start(runtime.execution?.hostCwd ?? runtime.cwd, previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
|
|
608
420
|
const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
|
|
609
|
-
catalog.assertCurrent();
|
|
610
|
-
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
611
421
|
// This lease is tied to the trusted host-user and instance/epoch.
|
|
612
422
|
// Its mapping and this turn's exact setter ACK are different facts.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型在当前主机目录中没有唯一对应型号,本轮未发送', 'CATALOG_VALUE_MAPPING_NOT_UNIQUE');
|
|
616
|
-
const resolved = mapping[0].resolvedModel;
|
|
617
|
-
if (typeof resolved !== 'string' || !resolved.trim())
|
|
618
|
-
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
|
|
619
|
-
modelConfiguration = { selected: { ...mapping[0], ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
|
|
620
|
-
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
423
|
+
modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: selected.resolvedModel ?? selected.value, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
|
|
424
|
+
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮模型设置回应;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
621
425
|
}
|
|
622
426
|
catch (error) {
|
|
623
427
|
details.record({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_FAILED', summary: '本轮模型配置未获完整确认,未发送用户输入', payload: { selected, requestedEffort: options.reasoningEffort ?? null, actual: session.modelConfiguration ?? null, reason: error instanceof NeedError ? (error.code === 'MODEL_RESOLUTION_UNCONFIRMED' ? error.requires : error.code) : error instanceof ModelConfigurationError ? error.message : 'MODEL_CONFIGURATION_FAILED' } });
|
|
624
428
|
throw error;
|
|
625
429
|
}
|
|
626
|
-
|
|
430
|
+
if (this.permissions && options.sessionId) {
|
|
431
|
+
observedPermissionKey = JSON.stringify(this.permissions.read(options.sessionId));
|
|
432
|
+
permissionOff = this.permissions.subscribe(options.sessionId, () => {
|
|
433
|
+
let next;
|
|
434
|
+
try {
|
|
435
|
+
next = JSON.stringify(this.permissions.read(options.sessionId));
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
permissionEpoch.abort();
|
|
439
|
+
permissionFailed(error);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (next === observedPermissionKey) {
|
|
443
|
+
details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_EVENT_UNCHANGED', summary: '设置事件没有改变有效权限,原审批与执行许可保持有效。' });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Compare observed values, not the possibly lagging remote ACK.
|
|
447
|
+
// A real A→B→A still revokes the original permit twice.
|
|
448
|
+
observedPermissionKey = next;
|
|
449
|
+
permissionEpoch.abort();
|
|
450
|
+
permissionEpoch = new AbortController();
|
|
451
|
+
// One official preset writes identity and two knobs synchronously.
|
|
452
|
+
// Read the complete committed bundle after that append stack.
|
|
453
|
+
if (permissionQueued)
|
|
454
|
+
return;
|
|
455
|
+
permissionQueued = true;
|
|
456
|
+
queueMicrotask(() => { permissionQueued = false; if (!permissionOff)
|
|
457
|
+
return; void syncPermissions().catch(permissionFailed); });
|
|
458
|
+
});
|
|
459
|
+
await syncPermissions();
|
|
460
|
+
}
|
|
627
461
|
recordInformation();
|
|
628
|
-
await details.flush();
|
|
629
462
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
630
|
-
catalog.assertCurrent();
|
|
631
|
-
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
632
463
|
await runtime.execution?.beginTurn(modelSignal);
|
|
633
464
|
id = session.id;
|
|
634
465
|
try {
|
|
635
|
-
const result = await
|
|
636
|
-
|
|
637
|
-
|
|
466
|
+
const result = await dispatchPrompt();
|
|
467
|
+
permissionOff?.();
|
|
468
|
+
permissionOff = undefined;
|
|
469
|
+
await permissionTail;
|
|
638
470
|
return result;
|
|
639
471
|
}
|
|
640
472
|
finally {
|
|
473
|
+
permissionOff?.();
|
|
474
|
+
permissionOff = undefined;
|
|
641
475
|
runtime.execution?.endTurn();
|
|
642
476
|
details.endTurn();
|
|
643
477
|
}
|
|
@@ -657,22 +491,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
657
491
|
for (const chunk of mapper.text('\n[远端拒绝] 此请求未执行。\n'))
|
|
658
492
|
yield chunk;
|
|
659
493
|
}
|
|
660
|
-
|
|
661
|
-
if (!runtime.oneShot)
|
|
662
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要主机单次运行助手', 'S安装的aw-codex-once');
|
|
663
|
-
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
664
|
-
const text = input.prompt.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
665
|
-
for await (const event of runOneShot(runtime.oneShot, { wrapperPath: fileURLToPath(new URL('./sdk-ssh-wrapper.js', import.meta.url)), sandboxMode: config.oneShotSandbox, approvalPolicy: config.oneShotApproval }, text, previous?.sessionId, signal)) {
|
|
666
|
-
if (event.type === 'thread.started')
|
|
667
|
-
id = event.thread_id;
|
|
668
|
-
for (const chunk of mapper.sdk(event))
|
|
669
|
-
yield chunk;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
494
|
+
mapper.flushMetadata();
|
|
672
495
|
recordInformation();
|
|
673
496
|
details.endTurn();
|
|
674
|
-
await timing.run('details-flush', () => details.flush());
|
|
675
|
-
await details.flush(); // The timing record is a new ordered write, not a retry.
|
|
676
497
|
if (!id)
|
|
677
498
|
throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
|
|
678
499
|
if (live)
|
|
@@ -680,22 +501,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
680
501
|
if (modelConfiguration) {
|
|
681
502
|
const actual = session.modelConfiguration;
|
|
682
503
|
const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
|
|
683
|
-
catalog.assertCurrent();
|
|
684
504
|
if (changed) {
|
|
685
505
|
details.record({ kind: 'session', severity: 'warning', code: 'MODEL_CONFIGURATION_CHANGED_DURING_TURN', summary: '本轮回复已经完成,期间远端配置发生变化;保留真实回复,具体执行型号未单独回报。', payload: { reason: 'POST_PROMPT_CONFIGURATION_CHANGED', configured: modelConfiguration.configured, actual: actual ?? null } });
|
|
686
|
-
await details.flush();
|
|
687
506
|
}
|
|
688
507
|
}
|
|
689
508
|
yield* mapper.flush();
|
|
690
|
-
|
|
691
|
-
if (session?.checkpoint && runtime.saveCheckpoint) {
|
|
692
|
-
const id = randomUUID();
|
|
693
|
-
if (await runtime.saveCheckpoint(id, session.checkpoint))
|
|
694
|
-
recoveryCheckpointId = id;
|
|
695
|
-
}
|
|
696
|
-
const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
|
|
697
|
-
const presentation = { ...(runtime.workspaceNotice ? { workspaceNotice: runtime.workspaceNotice } : {}), ...(employeeNoticeShown ? { employeeExecutionNotice: 'local-v1' } : {}) };
|
|
698
|
-
const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(Object.keys(presentation).length ? { presentation } : {}), 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 } : {}) };
|
|
509
|
+
const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
699
510
|
succeeded = true;
|
|
700
511
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
701
512
|
}
|
|
@@ -711,104 +522,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
711
522
|
recordInformation();
|
|
712
523
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
713
524
|
withRemoteFailureEvidence(error, runtime?.employeeRemoteCloseReason?.(), failureCode);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const reportFailure = async () => {
|
|
718
|
-
if (request?.recordEarlyFailure) {
|
|
719
|
-
const record = request.recordEarlyFailure;
|
|
720
|
-
request.recordEarlyFailure = undefined;
|
|
721
|
-
try {
|
|
722
|
-
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
723
|
-
}
|
|
724
|
-
catch (failure) {
|
|
725
|
-
reporting.push(failure);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
try {
|
|
729
|
-
await runtime?.details?.flush();
|
|
730
|
-
}
|
|
731
|
-
catch (failure) {
|
|
732
|
-
reporting.push(failure);
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
if (runtime && key && live && live.runtime === runtime && this.sessions.get(key) === live) {
|
|
736
|
-
const captured = runtime;
|
|
737
|
-
void reportFailure().then(() => { for (const failure of reporting)
|
|
738
|
-
for (const code of closeErrorCodes(failure, 'DIAGNOSTIC_WRITE_FAILED'))
|
|
739
|
-
captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: code, action: 'failure' }); }, failure => captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: closeCode(failure, 'DIAGNOSTIC_WRITE_FAILED'), action: 'failure' }));
|
|
740
|
-
}
|
|
741
|
-
else
|
|
742
|
-
await reportFailure();
|
|
525
|
+
activeMapper?.flushMetadata();
|
|
526
|
+
if (request?.recordEarlyFailure)
|
|
527
|
+
void request.recordEarlyFailure(error, earlyStage).catch(() => console.warn('[remote-agent] 预检诊断未保存'));
|
|
743
528
|
if (activeMapper)
|
|
744
529
|
yield* activeMapper.flush();
|
|
745
|
-
|
|
746
|
-
if (reporting.length) {
|
|
747
|
-
turnFailure = withFailureEvidence(error, reporting, 'diagnostic', firstReason, failureCode);
|
|
748
|
-
throw turnFailure;
|
|
749
|
-
}
|
|
750
|
-
turnFailure = firstReason && !(error instanceof NeedError || error instanceof ModelConfigurationError || error instanceof LlmError) ? withFailureEvidence(error, [], 'diagnostic', firstReason, failureCode) : withFirstFailureEvidence(error, firstReason, failureCode);
|
|
751
|
-
throw turnFailure;
|
|
530
|
+
throw withFirstFailureEvidence(error, runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
752
531
|
}
|
|
753
532
|
finally {
|
|
533
|
+
permissionOff?.();
|
|
534
|
+
permissionOff = undefined;
|
|
535
|
+
permissionEpoch.abort();
|
|
754
536
|
for (const off of abortOffs)
|
|
755
537
|
off();
|
|
756
538
|
runtime?.execution?.endTurn();
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
else if (!succeeded || !key || mode !== 'session') {
|
|
764
|
-
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
765
|
-
if (runtime) {
|
|
766
|
-
if (!succeeded && turnFailure && key && live && live.runtime === runtime && this.sessions.get(key) === live)
|
|
767
|
-
this.finishFailedTurn(key, live, turnFailure, failureCode ?? 'TURN_PREPARATION_FAILED', request?.closeDiagnostics?.first?.reason ?? runtime.closeDiagnostics?.first?.reason);
|
|
768
|
-
else {
|
|
769
|
-
await this.cleanupRuntime(runtime);
|
|
770
|
-
if (key && live && !live.failure && this.sessions.get(key) === live) {
|
|
771
|
-
if (typeof runtime.dispose === 'function') {
|
|
772
|
-
this.sessions.delete(key);
|
|
773
|
-
if (!succeeded)
|
|
774
|
-
this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
|
|
775
|
-
}
|
|
776
|
-
else
|
|
777
|
-
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (succeeded && key)
|
|
783
|
-
this.preparationFailures.delete(key);
|
|
784
|
-
}
|
|
785
|
-
catch (cleanup) {
|
|
786
|
-
throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
787
|
-
}
|
|
788
|
-
finally {
|
|
789
|
-
if (key && this.turns.get(key) === turn)
|
|
790
|
-
this.turns.delete(key);
|
|
791
|
-
if (key)
|
|
792
|
-
this.detached.delete(key);
|
|
539
|
+
if (runtime) {
|
|
540
|
+
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
541
|
+
if (key && this.sessions.get(key) === live)
|
|
542
|
+
this.sessions.delete(key);
|
|
543
|
+
void this.cleanupRuntime(runtime);
|
|
793
544
|
}
|
|
545
|
+
if (key && this.turns.get(key) === turn)
|
|
546
|
+
this.turns.delete(key);
|
|
547
|
+
if (key)
|
|
548
|
+
this.detached.delete(key);
|
|
794
549
|
}
|
|
795
550
|
}
|
|
796
551
|
}
|
|
797
552
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
798
553
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
799
554
|
adapters = new Map();
|
|
800
|
-
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics) { super(); for (const agent of AGENTS)
|
|
801
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics)); }
|
|
555
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) { super(); for (const agent of AGENTS)
|
|
556
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions)); }
|
|
802
557
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
803
558
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
804
559
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
805
560
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
806
561
|
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
807
562
|
prepareCall(provider, model, signal) { return this.adapter(provider).prepareCall(provider, model, signal); }
|
|
808
|
-
observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
|
|
809
|
-
adapter.observeModelInvalidation(fields, changes); }
|
|
810
|
-
async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
|
|
811
|
-
async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
|
|
812
563
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
813
564
|
stream(options) { return this.adapter(options.provider).stream(options); }
|
|
814
565
|
async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
|
|
@@ -818,7 +569,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
818
569
|
export function apply(ctx, live) {
|
|
819
570
|
const config = () => readConfig(live);
|
|
820
571
|
const get = () => ctx.get('aiWorkbenchConnect');
|
|
821
|
-
|
|
572
|
+
const language = 'zh';
|
|
822
573
|
const readImage = async (ref, signal) => {
|
|
823
574
|
const attachments = ctx.get('attachments');
|
|
824
575
|
if (typeof attachments?.readImage !== 'function')
|
|
@@ -826,22 +577,19 @@ export function apply(ctx, live) {
|
|
|
826
577
|
return attachments.readImage(ref, signal);
|
|
827
578
|
};
|
|
828
579
|
const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
|
|
829
|
-
const localTurns = createLocalUserTurnObserver(ctx);
|
|
830
580
|
const detailManagers = new Map();
|
|
831
581
|
const managerForProfile = () => {
|
|
832
582
|
const profile = ctx.get('profileContext');
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
let manager = detailManagers.get(profile.dir);
|
|
583
|
+
const profileDir = typeof profile?.dir === 'string' ? profile.dir : undefined;
|
|
584
|
+
let manager = detailManagers.get(profileDir ?? '');
|
|
836
585
|
if (!manager) {
|
|
837
|
-
manager = createRemoteDetails({ profileDir
|
|
838
|
-
detailManagers.set(
|
|
586
|
+
manager = createRemoteDetails({ profileDir, warn: line => ctx.logger('remote-agent').warn(line) });
|
|
587
|
+
detailManagers.set(profileDir ?? '', manager);
|
|
839
588
|
}
|
|
840
589
|
return manager;
|
|
841
590
|
};
|
|
591
|
+
ctx.provide('aiWorkbenchRemoteDiagnostics', { read: (query) => managerForProfile().read(query) });
|
|
842
592
|
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
843
|
-
let changed = () => { };
|
|
844
|
-
let observeCatalogRevocation = () => { };
|
|
845
593
|
const diagnostics = new Map();
|
|
846
594
|
const diagnostic = (scope, provider, message) => {
|
|
847
595
|
let entry = diagnostics.get(provider);
|
|
@@ -856,25 +604,14 @@ export function apply(ctx, live) {
|
|
|
856
604
|
else
|
|
857
605
|
entry.set(scope, message);
|
|
858
606
|
};
|
|
859
|
-
let defaults;
|
|
860
|
-
const catalogDiagnostics = createCatalogDiagnostics(() => { const profile = ctx.get('profileContext'); return profile?.dir; });
|
|
861
607
|
// Public Loader Entry.options.id; optional when mounted without Loader.
|
|
862
608
|
const actualSettingsNamespace = ctx.fiber.entry?.options.id;
|
|
863
|
-
new
|
|
864
|
-
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, (...args) => { const state = get()?.getConnectionState(); const connection = state ? { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase } : null; return runCatalog(...args, event => catalogDiagnostics.record({ ...event, connection })); }, () => changed(), (brand, message) => {
|
|
865
|
-
diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
|
|
866
|
-
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent), (fields, changes) => observeCatalogRevocation(fields, changes));
|
|
609
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes);
|
|
867
610
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
868
611
|
const details = async (request, api, connection, target) => {
|
|
869
|
-
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection
|
|
612
|
+
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
|
|
870
613
|
};
|
|
871
614
|
const open = async (...args) => {
|
|
872
|
-
const profile = ctx.get('profileContext');
|
|
873
|
-
if (!profile?.dir)
|
|
874
|
-
throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
|
|
875
|
-
args[2].recoveryJournal = recoveryJournal(profile.dir, runtimeRecoveryKey(`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId, args[2].employeeScope));
|
|
876
|
-
args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
|
|
877
|
-
args[2].currentInputIds = await currentInputIds(ctx, args[2]);
|
|
878
615
|
const runtime = await openByAgent.get(agent.id)(...args);
|
|
879
616
|
runtime.detailContext = () => {
|
|
880
617
|
const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
|
|
@@ -904,14 +641,11 @@ export function apply(ctx, live) {
|
|
|
904
641
|
const sessions = services.get('sessions');
|
|
905
642
|
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
906
643
|
};
|
|
907
|
-
|
|
908
|
-
workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
|
|
909
|
-
record: (request, observation) => preflightDiagnostics.record(request, agent.loginProvider, observation),
|
|
910
|
-
}, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
911
|
-
currentUserTurn: request => localTurns.current(request),
|
|
644
|
+
const provider = connectProvider(get, { agent, workspace, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
912
645
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
913
646
|
workspace, native: () => resolveEmployeeNative(),
|
|
914
|
-
} } : {}),
|
|
647
|
+
} } : {}), details, open, local });
|
|
648
|
+
return provider;
|
|
915
649
|
}, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
|
|
916
650
|
const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
|
|
917
651
|
let turn = null, step = null;
|
|
@@ -930,50 +664,24 @@ export function apply(ctx, live) {
|
|
|
930
664
|
step = null;
|
|
931
665
|
}
|
|
932
666
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
933
|
-
}, modelDirectory, preflightDiagnostics);
|
|
667
|
+
}, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx));
|
|
668
|
+
for (const agent of AGENTS)
|
|
669
|
+
void adapter.listModels(agent.id).catch(() => ctx.logger('remote-agent').warn('模型列表暂不可用,打开菜单时可重新读取'));
|
|
934
670
|
const ids = AGENTS.map(agent => agent.id);
|
|
935
671
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
|
|
936
672
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
937
673
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
938
674
|
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
// Read through the same trusted, fresh catalog. Failures are already recorded there.
|
|
947
|
-
void modelDirectory.read(agent).then(lease => lease.release(), () => { });
|
|
948
|
-
};
|
|
949
|
-
ctx.inject(['agentDefaultModel'], child => {
|
|
950
|
-
const service = child.get('agentDefaultModel');
|
|
951
|
-
const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
|
|
952
|
-
defaults = current;
|
|
953
|
-
refreshDefault();
|
|
954
|
-
child.effect(() => () => { current.dispose(); if (defaults === current)
|
|
955
|
-
defaults = undefined; });
|
|
956
|
-
});
|
|
957
|
-
observeCatalogRevocation = (fields, changes) => adapter.observeModelInvalidation(fields, changes);
|
|
958
|
-
changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
|
|
959
|
-
let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
|
|
960
|
-
ctx.on('internal/service', service => {
|
|
961
|
-
if (service === 'aiWorkbenchConnect')
|
|
962
|
-
modelDirectory.refresh();
|
|
963
|
-
if (service !== 'aiWorkbenchConnect' && service !== 'aiWorkbenchNetwork')
|
|
964
|
-
return;
|
|
965
|
-
const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
|
|
966
|
-
if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
|
|
967
|
-
return;
|
|
968
|
-
employeeConnect = nextConnect;
|
|
969
|
-
employeeNetwork = nextNetwork;
|
|
970
|
-
void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
|
|
971
|
-
});
|
|
972
|
-
new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
|
|
973
|
-
const cleanup = (id) => adapter.detachSession(id);
|
|
675
|
+
const cleanup = async (id) => { try {
|
|
676
|
+
await adapter.detachSession(id);
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
for (const manager of detailManagers.values())
|
|
680
|
+
manager.releaseSession(id);
|
|
681
|
+
} };
|
|
974
682
|
ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
|
|
975
683
|
ctx.on('session/disposed', session => cleanup(session.id));
|
|
976
|
-
ctx.effect(() => () => { modelDirectory.dispose();
|
|
684
|
+
ctx.effect(() => async () => { modelDirectory.dispose(); await adapter.dispose(); await Promise.all([...detailManagers.values()].map(manager => manager.dispose())); });
|
|
977
685
|
}
|
|
978
686
|
export function dshApproval(ctx) {
|
|
979
687
|
return async (request, signal, options, context, hostWorkspace) => {
|