@ai-workbench/remote-agent 0.8.21 → 0.8.24
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 +28 -155
- package/dist/acp.d.ts +20 -15
- package/dist/acp.js +197 -224
- 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 +40 -105
- package/dist/client.js +34 -127
- 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 -3
- package/dist/employee-endpoint.js +8 -39
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +5 -1
- package/dist/employee-info.js +16 -5
- 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 +1 -1
- package/dist/index.d.ts +12 -42
- package/dist/index.js +203 -471
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +0 -3
- package/dist/mapping.js +0 -34
- 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.d.ts +0 -1
- package/dist/presentation.js +21 -42
- package/dist/provider.d.ts +12 -48
- package/dist/provider.js +18 -106
- 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
30
|
import { installModelInputRetention, markModelPreparationFailure } 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 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示', en: '本机 HTTP MCP 策略;默认忽略;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示' }).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,47 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
142
121
|
finally {
|
|
143
122
|
catalog.release();
|
|
144
123
|
} }
|
|
145
|
-
async resolveModel(provider, id, signal) {
|
|
146
|
-
signal?.throwIfAborted();
|
|
124
|
+
async resolveModel(provider, id, signal) {
|
|
147
125
|
const displayed = this.modelPresentation.resolve(provider, id);
|
|
148
126
|
if (displayed)
|
|
149
127
|
return displayed;
|
|
150
|
-
const
|
|
128
|
+
const active = signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
129
|
+
const catalog = await this.catalog(active);
|
|
151
130
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
131
|
+
active.throwIfAborted();
|
|
132
|
+
this.modelPresentation.remember(catalog);
|
|
133
|
+
const resolved = this.modelPresentation.resolve(provider, id);
|
|
134
|
+
if (!resolved)
|
|
135
|
+
throw new LlmError('所选模型当前不可用,请打开模型菜单重新选择。', 'MODEL_CATALOG_UNAVAILABLE');
|
|
136
|
+
return resolved;
|
|
154
137
|
}
|
|
155
138
|
finally {
|
|
156
139
|
catalog.release();
|
|
157
140
|
}
|
|
158
141
|
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
markModelPreparationFailure(provider, error, signal);
|
|
161
|
-
throw error;
|
|
162
|
-
} }
|
|
163
142
|
async prepareCall(provider, id, signal) {
|
|
164
143
|
try {
|
|
165
|
-
|
|
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
|
-
}
|
|
144
|
+
return { model: await this.resolveModel(provider, id, signal), stream: (options) => this.stream(options) };
|
|
176
145
|
}
|
|
177
146
|
catch (error) {
|
|
178
147
|
markModelPreparationFailure(provider, error, signal);
|
|
179
148
|
throw error;
|
|
180
149
|
}
|
|
181
150
|
}
|
|
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))); }
|
|
191
151
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
192
152
|
cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
|
|
193
153
|
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'));
|
|
154
|
+
void Promise.resolve().then(() => runtime.dispose?.(cause)).catch(() => console.warn('[remote-agent] 旧主机会话暂未清理,将由主机自动回收'));
|
|
155
|
+
return Promise.resolve();
|
|
238
156
|
}
|
|
239
157
|
async disposeSession(sessionId) {
|
|
240
|
-
this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
|
|
241
158
|
this.turns.get(sessionId)?.abort();
|
|
242
159
|
const current = this.sessions.get(sessionId);
|
|
243
160
|
if (!current)
|
|
244
161
|
return;
|
|
245
|
-
|
|
246
|
-
return current.disposal;
|
|
162
|
+
this.sessions.delete(sessionId);
|
|
247
163
|
current.session.close({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'close' });
|
|
248
|
-
|
|
249
|
-
this.sessions.delete(sessionId); });
|
|
250
|
-
return current.disposal;
|
|
164
|
+
await this.cleanupRuntime(current.runtime);
|
|
251
165
|
}
|
|
252
166
|
async detachSession(sessionId) {
|
|
253
167
|
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
@@ -286,11 +200,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
286
200
|
}
|
|
287
201
|
catch (error) {
|
|
288
202
|
if (error instanceof ModelConfigurationError)
|
|
289
|
-
throw new LlmError(error.
|
|
203
|
+
throw new LlmError(employeeFailure(error.code, this.agent.loginProvider), error.code, { cause: error });
|
|
290
204
|
if (error instanceof NeedError) {
|
|
291
205
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
292
206
|
const evidence = failureEvidenceSummary(error);
|
|
293
|
-
const message =
|
|
207
|
+
const message = connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider);
|
|
294
208
|
const login = originalLoginFailure(error);
|
|
295
209
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
296
210
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -302,11 +216,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
302
216
|
diagnostic += ';诊断保存失败:' + (failure instanceof NeedError ? failure.code : 'DIAGNOSTIC_WRITE_FAILED');
|
|
303
217
|
}
|
|
304
218
|
}
|
|
305
|
-
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
|
|
219
|
+
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (evidence ? ';' + evidence : '') + (diagnostic ? ';' + diagnostic : '')) });
|
|
306
220
|
}
|
|
307
|
-
|
|
308
|
-
throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
|
|
309
|
-
throw error;
|
|
221
|
+
throw new LlmError(employeeFailure('TURN_FAILED', this.agent.loginProvider), 'TURN_FAILED', { cause: error });
|
|
310
222
|
}
|
|
311
223
|
}
|
|
312
224
|
async *generate(original) {
|
|
@@ -322,7 +234,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
322
234
|
this.turns.set(key, turn);
|
|
323
235
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
324
236
|
const options = { ...original, signal };
|
|
325
|
-
const timing = new StageTiming();
|
|
326
237
|
const abortOffs = [];
|
|
327
238
|
let request;
|
|
328
239
|
let earlyStage = 'input-prepare';
|
|
@@ -335,18 +246,19 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
335
246
|
let failureCode;
|
|
336
247
|
let preserveRecoveredRuntime = false;
|
|
337
248
|
let activeMapper;
|
|
338
|
-
let catalog;
|
|
339
249
|
let selected;
|
|
340
250
|
let modelConfiguration;
|
|
341
251
|
let turnFailure;
|
|
342
252
|
let employeeNoticeShown = false;
|
|
253
|
+
let permissionOff;
|
|
254
|
+
let permissionEpoch = new AbortController();
|
|
343
255
|
const information = [];
|
|
344
|
-
const recordInformation = () => {
|
|
256
|
+
const recordInformation = () => { if (runtime?.details)
|
|
345
257
|
for (const entry of information.splice(0))
|
|
346
258
|
runtime.details.record(entry); };
|
|
347
259
|
const config = this.config();
|
|
348
|
-
const mode =
|
|
349
|
-
request = { sessionId: options.sessionId, mode: mode ?? 'session',
|
|
260
|
+
const mode = 'session';
|
|
261
|
+
request = { sessionId: options.sessionId, mode: mode ?? 'session', signal };
|
|
350
262
|
const observeAbort = (source, initiator, reason) => { if (!source)
|
|
351
263
|
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
264
|
observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
|
|
@@ -356,93 +268,48 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
356
268
|
if (this.preflightDiagnostics)
|
|
357
269
|
request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
|
|
358
270
|
try {
|
|
359
|
-
if (!
|
|
360
|
-
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的
|
|
271
|
+
if (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)
|
|
272
|
+
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的acpMaxMessageBytes');
|
|
361
273
|
const previous = readPointer(options, this.agent);
|
|
362
|
-
employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) ||
|
|
274
|
+
employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || false;
|
|
363
275
|
if (employeeNoticeShown && key)
|
|
364
276
|
this.employeeNoticeSessions.add(key);
|
|
365
|
-
const last = options.messages.findLast(message => message.role === 'assistant'
|
|
277
|
+
const last = options.messages.findLast(message => message.role === 'assistant');
|
|
366
278
|
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
|
-
}
|
|
279
|
+
live = undefined;
|
|
280
|
+
const configuration = JSON.stringify([config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
281
|
+
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
282
|
request.previousContextSha256 = previous?.contextSha256;
|
|
398
|
-
const after = options.messages.findLastIndex(message => message.role === 'assistant'
|
|
399
|
-
|
|
283
|
+
const after = options.messages.findLastIndex(message => message.role === 'assistant');
|
|
284
|
+
const tail = options.messages.slice(after + 1);
|
|
285
|
+
const humanIds = tail.flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []);
|
|
286
|
+
// An official approval may continue the same native turn without another
|
|
287
|
+
// human message. This is a CAS candidate only: live turn/L proof must still
|
|
288
|
+
// bind it. The actual input delta remains prepareInput's unchanged tail.
|
|
289
|
+
if (!humanIds.length && tail.some(message => message.role === 'user' && message.source?.kind === 'user-approval')) {
|
|
290
|
+
const original = options.messages.slice(0, after + 1).findLast(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string');
|
|
291
|
+
if (typeof original?.id === 'string')
|
|
292
|
+
humanIds.push(original.id);
|
|
293
|
+
}
|
|
294
|
+
request.userInputIds = Object.freeze(humanIds);
|
|
400
295
|
request.userMessageIds = request.userInputIds;
|
|
401
296
|
earlyStage = 'source-context';
|
|
402
297
|
await this.resolveRuntime.preflight?.(request);
|
|
403
298
|
earlyStage = 'input-prepare';
|
|
404
|
-
let input = await
|
|
299
|
+
let input = await prepareInput(options, this.readImage);
|
|
405
300
|
const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
|
|
406
301
|
request.contextSha256 = contextSha256;
|
|
407
302
|
const codex = this.agent.loginProvider === 'codex';
|
|
408
|
-
|
|
409
|
-
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
|
|
410
|
-
const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
|
|
303
|
+
const contextChanged = !!previous && codex && (previous.contextSha256 ?? '').toLowerCase() !== contextSha256;
|
|
411
304
|
// Only identical durable context can prove a legacy context-bound journal.
|
|
412
305
|
// New Codex transport scopes are independent of the native context digest.
|
|
413
306
|
if (contextChanged)
|
|
414
307
|
request.contextSha256 = undefined;
|
|
415
308
|
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
309
|
if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
428
310
|
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]);
|
|
311
|
+
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 };
|
|
312
|
+
let modelSignal = signal;
|
|
446
313
|
options.signal = modelSignal;
|
|
447
314
|
request.signal = modelSignal;
|
|
448
315
|
earlyStage = 'runtime-resolve';
|
|
@@ -450,8 +317,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
450
317
|
const closeSink = runtime.details;
|
|
451
318
|
runtime.closeDiagnostics ??= new CloseDiagnostics(event => closeSink?.record(event));
|
|
452
319
|
signal.throwIfAborted();
|
|
453
|
-
if (requireFreshSession && (!runtime.freshSessionConfirmed || !runtime.restartNotice))
|
|
454
|
-
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '未确认原实例安全结束或原位置为空,本轮没有发送', '同UUID条件结束与list消失证明,或原slot不存在证明');
|
|
455
320
|
if (runtime.execution?.signal) {
|
|
456
321
|
modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
|
|
457
322
|
options.signal = modelSignal;
|
|
@@ -464,42 +329,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
464
329
|
request.signal = modelSignal;
|
|
465
330
|
modelSignal.throwIfAborted();
|
|
466
331
|
}
|
|
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
332
|
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
333
|
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
334
|
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
492
335
|
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
493
336
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
494
|
-
const details = runtime.details;
|
|
495
|
-
if (!details)
|
|
496
|
-
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
337
|
+
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
338
|
recordInformation();
|
|
498
|
-
timing.bind(detail => details.record(detail));
|
|
499
339
|
request.recordEarlyFailure = undefined;
|
|
500
340
|
if (runtime.hostWorkspace)
|
|
501
341
|
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 ? (
|
|
342
|
+
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
503
343
|
activeMapper = mapper;
|
|
504
344
|
let id;
|
|
505
345
|
let stop;
|
|
@@ -518,17 +358,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
518
358
|
}
|
|
519
359
|
yield* mapper.flush();
|
|
520
360
|
}
|
|
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
361
|
if (mode === 'session') {
|
|
530
|
-
if (live && live.runtime !== runtime)
|
|
531
|
-
throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
|
|
532
362
|
if (!runtime.acpTransport)
|
|
533
363
|
throw new NeedError('DEPENDENCY_REQUIRED', '我需要该员工主机上的ACP通道', 'S提供的ACP命令与aw-session');
|
|
534
364
|
const queue = [];
|
|
@@ -536,7 +366,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
536
366
|
let done = false;
|
|
537
367
|
let failure;
|
|
538
368
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
539
|
-
|
|
369
|
+
let promptPermit;
|
|
370
|
+
const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, ...this.permissions ? { promptGuard: () => {
|
|
371
|
+
if (!promptPermit)
|
|
372
|
+
throw new NeedError('PERMISSION_POLICY_CHANGED', '当前权限没有保持确认,本次没有继续派发。', '当前权限代次和同值回执');
|
|
373
|
+
return promptPermit;
|
|
374
|
+
}, approvalGuard: () => {
|
|
375
|
+
const source = this.permissions, epoch = permissionEpoch, policy = JSON.stringify(source.read(options.sessionId));
|
|
376
|
+
return { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && JSON.stringify(source.read(options.sessionId)) === policy };
|
|
377
|
+
} } : {}, approve: async (permission, approvalSignal) => {
|
|
540
378
|
const execution = runtime.execution;
|
|
541
379
|
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
542
380
|
if (execution) {
|
|
@@ -545,11 +383,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
545
383
|
push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
|
|
546
384
|
return 'cancelled';
|
|
547
385
|
}
|
|
548
|
-
await execution.validate(approvalSignal);
|
|
549
386
|
}
|
|
550
387
|
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
388
|
return answer;
|
|
554
389
|
}, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
|
|
555
390
|
push(mapper.acp(notification.update));
|
|
@@ -558,86 +393,110 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
558
393
|
failure = error;
|
|
559
394
|
session?.close({ layer: 'adapter', initiator: 'adapter.mapper-failure', reason: closeCode(error, 'MAPPING_FAILED'), action: 'close' });
|
|
560
395
|
} }, 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
|
-
}
|
|
396
|
+
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 });
|
|
397
|
+
session = new AcpSession(transport, hooks);
|
|
398
|
+
live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
|
|
399
|
+
if (key)
|
|
400
|
+
this.sessions.set(key, live);
|
|
575
401
|
promptBaseline = session.promptDispatchCount;
|
|
402
|
+
let permissionTail = Promise.resolve(), permissionKey, observedPermissionKey, permissionQueued = false;
|
|
403
|
+
const permissionFailed = (error) => {
|
|
404
|
+
failure ??= error;
|
|
405
|
+
runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
|
|
406
|
+
session.close({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
|
|
407
|
+
wake?.();
|
|
408
|
+
};
|
|
409
|
+
const syncPermissions = () => {
|
|
410
|
+
if (!this.permissions)
|
|
411
|
+
return Promise.resolve(); // Explicit component-test port; production always supplies the official source.
|
|
412
|
+
return permissionTail = permissionTail.then(async () => {
|
|
413
|
+
const policy = this.permissions.read(options.sessionId), next = JSON.stringify(policy);
|
|
414
|
+
if (next === permissionKey)
|
|
415
|
+
return;
|
|
416
|
+
const remoteMode = permissionMode(this.agent.loginProvider, policy);
|
|
417
|
+
details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_POLICY', summary: '按DSH当前会话权限同步远程工具;不会把关闭审批提示解释为自动批准。', payload: { ...policy, remoteMode } });
|
|
418
|
+
await session.selectPermissionMode(remoteMode, modelSignal, this.agent.loginProvider);
|
|
419
|
+
permissionKey = next;
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
const dispatchPrompt = async () => {
|
|
423
|
+
if (!this.permissions)
|
|
424
|
+
return session.prompt(input.prompt, modelSignal);
|
|
425
|
+
// New official policy events can append work while an ACK is awaited.
|
|
426
|
+
// Setters for those distinct policy changes are not retries. Dispatch
|
|
427
|
+
// only from one synchronous, stable epoch/ACK boundary.
|
|
428
|
+
for (;;) {
|
|
429
|
+
const barrier = syncPermissions();
|
|
430
|
+
await barrier;
|
|
431
|
+
if (barrier !== permissionTail || permissionQueued)
|
|
432
|
+
continue;
|
|
433
|
+
const source = this.permissions, policy = JSON.stringify(source.read(options.sessionId));
|
|
434
|
+
if (policy !== permissionKey)
|
|
435
|
+
continue;
|
|
436
|
+
const epoch = permissionEpoch;
|
|
437
|
+
promptPermit = { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && permissionKey === policy && JSON.stringify(source.read(options.sessionId)) === policy };
|
|
438
|
+
return session.prompt(input.prompt, modelSignal);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
576
441
|
const task = (async () => {
|
|
577
442
|
remoteAttempted = true;
|
|
578
443
|
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();
|
|
444
|
+
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
445
|
const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
|
|
609
|
-
catalog.assertCurrent();
|
|
610
|
-
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
611
446
|
// This lease is tied to the trusted host-user and instance/epoch.
|
|
612
447
|
// 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 });
|
|
448
|
+
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 } : {}) };
|
|
449
|
+
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮模型设置回应;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
621
450
|
}
|
|
622
451
|
catch (error) {
|
|
623
452
|
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
453
|
throw error;
|
|
625
454
|
}
|
|
626
|
-
|
|
455
|
+
if (this.permissions && options.sessionId) {
|
|
456
|
+
observedPermissionKey = JSON.stringify(this.permissions.read(options.sessionId));
|
|
457
|
+
permissionOff = this.permissions.subscribe(options.sessionId, () => {
|
|
458
|
+
let next;
|
|
459
|
+
try {
|
|
460
|
+
next = JSON.stringify(this.permissions.read(options.sessionId));
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
permissionEpoch.abort();
|
|
464
|
+
permissionFailed(error);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (next === observedPermissionKey) {
|
|
468
|
+
details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_EVENT_UNCHANGED', summary: '设置事件没有改变有效权限,原审批与执行许可保持有效。' });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Compare observed values, not the possibly lagging remote ACK.
|
|
472
|
+
// A real A→B→A still revokes the original permit twice.
|
|
473
|
+
observedPermissionKey = next;
|
|
474
|
+
permissionEpoch.abort();
|
|
475
|
+
permissionEpoch = new AbortController();
|
|
476
|
+
// One official preset writes identity and two knobs synchronously.
|
|
477
|
+
// Read the complete committed bundle after that append stack.
|
|
478
|
+
if (permissionQueued)
|
|
479
|
+
return;
|
|
480
|
+
permissionQueued = true;
|
|
481
|
+
queueMicrotask(() => { permissionQueued = false; if (!permissionOff)
|
|
482
|
+
return; void syncPermissions().catch(permissionFailed); });
|
|
483
|
+
});
|
|
484
|
+
await syncPermissions();
|
|
485
|
+
}
|
|
627
486
|
recordInformation();
|
|
628
|
-
await details.flush();
|
|
629
487
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
630
|
-
catalog.assertCurrent();
|
|
631
|
-
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
632
488
|
await runtime.execution?.beginTurn(modelSignal);
|
|
633
489
|
id = session.id;
|
|
634
490
|
try {
|
|
635
|
-
const result = await
|
|
636
|
-
|
|
637
|
-
|
|
491
|
+
const result = await dispatchPrompt();
|
|
492
|
+
permissionOff?.();
|
|
493
|
+
permissionOff = undefined;
|
|
494
|
+
await permissionTail;
|
|
638
495
|
return result;
|
|
639
496
|
}
|
|
640
497
|
finally {
|
|
498
|
+
permissionOff?.();
|
|
499
|
+
permissionOff = undefined;
|
|
641
500
|
runtime.execution?.endTurn();
|
|
642
501
|
details.endTurn();
|
|
643
502
|
}
|
|
@@ -657,23 +516,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
657
516
|
for (const chunk of mapper.text('\n[远端拒绝] 此请求未执行。\n'))
|
|
658
517
|
yield chunk;
|
|
659
518
|
}
|
|
660
|
-
else {
|
|
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
|
-
}
|
|
672
519
|
mapper.flushMetadata();
|
|
673
520
|
recordInformation();
|
|
674
521
|
details.endTurn();
|
|
675
|
-
await timing.run('details-flush', () => details.flush());
|
|
676
|
-
await details.flush(); // The timing record is a new ordered write, not a retry.
|
|
677
522
|
if (!id)
|
|
678
523
|
throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
|
|
679
524
|
if (live)
|
|
@@ -681,22 +526,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
681
526
|
if (modelConfiguration) {
|
|
682
527
|
const actual = session.modelConfiguration;
|
|
683
528
|
const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
|
|
684
|
-
catalog.assertCurrent();
|
|
685
529
|
if (changed) {
|
|
686
530
|
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 } });
|
|
687
|
-
await details.flush();
|
|
688
531
|
}
|
|
689
532
|
}
|
|
690
533
|
yield* mapper.flush();
|
|
691
|
-
|
|
692
|
-
if (session?.checkpoint && runtime.saveCheckpoint) {
|
|
693
|
-
const id = randomUUID();
|
|
694
|
-
if (await runtime.saveCheckpoint(id, session.checkpoint))
|
|
695
|
-
recoveryCheckpointId = id;
|
|
696
|
-
}
|
|
697
|
-
const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
|
|
698
|
-
const presentation = { ...(runtime.workspaceNotice ? { workspaceNotice: runtime.workspaceNotice } : {}), ...(employeeNoticeShown ? { employeeExecutionNotice: 'local-v1' } : {}) };
|
|
699
|
-
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 } : {}) };
|
|
534
|
+
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 } : {}) };
|
|
700
535
|
succeeded = true;
|
|
701
536
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
702
537
|
}
|
|
@@ -712,110 +547,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
712
547
|
recordInformation();
|
|
713
548
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
714
549
|
withRemoteFailureEvidence(error, runtime?.employeeRemoteCloseReason?.(), failureCode);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
activeMapper?.flushMetadata();
|
|
720
|
-
}
|
|
721
|
-
catch (failure) {
|
|
722
|
-
reporting.push(failure);
|
|
723
|
-
}
|
|
724
|
-
const reportFailure = async () => {
|
|
725
|
-
if (request?.recordEarlyFailure) {
|
|
726
|
-
const record = request.recordEarlyFailure;
|
|
727
|
-
request.recordEarlyFailure = undefined;
|
|
728
|
-
try {
|
|
729
|
-
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
730
|
-
}
|
|
731
|
-
catch (failure) {
|
|
732
|
-
reporting.push(failure);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
try {
|
|
736
|
-
await runtime?.details?.flush();
|
|
737
|
-
}
|
|
738
|
-
catch (failure) {
|
|
739
|
-
reporting.push(failure);
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
if (runtime && key && live && live.runtime === runtime && this.sessions.get(key) === live) {
|
|
743
|
-
const captured = runtime;
|
|
744
|
-
void reportFailure().then(() => { for (const failure of reporting)
|
|
745
|
-
for (const code of closeErrorCodes(failure, 'DIAGNOSTIC_WRITE_FAILED'))
|
|
746
|
-
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' }));
|
|
747
|
-
}
|
|
748
|
-
else
|
|
749
|
-
await reportFailure();
|
|
550
|
+
activeMapper?.flushMetadata();
|
|
551
|
+
if (request?.recordEarlyFailure)
|
|
552
|
+
void request.recordEarlyFailure(error, earlyStage).catch(() => console.warn('[remote-agent] 预检诊断未保存'));
|
|
750
553
|
if (activeMapper)
|
|
751
554
|
yield* activeMapper.flush();
|
|
752
|
-
|
|
753
|
-
if (reporting.length) {
|
|
754
|
-
turnFailure = withFailureEvidence(error, reporting, 'diagnostic', firstReason, failureCode);
|
|
755
|
-
throw turnFailure;
|
|
756
|
-
}
|
|
757
|
-
turnFailure = firstReason && !(error instanceof NeedError || error instanceof ModelConfigurationError || error instanceof LlmError) ? withFailureEvidence(error, [], 'diagnostic', firstReason, failureCode) : withFirstFailureEvidence(error, firstReason, failureCode);
|
|
758
|
-
throw turnFailure;
|
|
555
|
+
throw withFirstFailureEvidence(error, runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
759
556
|
}
|
|
760
557
|
finally {
|
|
558
|
+
permissionOff?.();
|
|
559
|
+
permissionOff = undefined;
|
|
560
|
+
permissionEpoch.abort();
|
|
761
561
|
for (const off of abortOffs)
|
|
762
562
|
off();
|
|
763
563
|
runtime?.execution?.endTurn();
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
else if (!succeeded || !key || mode !== 'session') {
|
|
771
|
-
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
772
|
-
if (runtime) {
|
|
773
|
-
if (!succeeded && turnFailure && key && live && live.runtime === runtime && this.sessions.get(key) === live)
|
|
774
|
-
this.finishFailedTurn(key, live, turnFailure, failureCode ?? 'TURN_PREPARATION_FAILED', request?.closeDiagnostics?.first?.reason ?? runtime.closeDiagnostics?.first?.reason);
|
|
775
|
-
else {
|
|
776
|
-
await this.cleanupRuntime(runtime);
|
|
777
|
-
if (key && live && !live.failure && this.sessions.get(key) === live) {
|
|
778
|
-
if (typeof runtime.dispose === 'function') {
|
|
779
|
-
this.sessions.delete(key);
|
|
780
|
-
if (!succeeded)
|
|
781
|
-
this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
|
|
782
|
-
}
|
|
783
|
-
else
|
|
784
|
-
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (succeeded && key)
|
|
790
|
-
this.preparationFailures.delete(key);
|
|
791
|
-
}
|
|
792
|
-
catch (cleanup) {
|
|
793
|
-
throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
794
|
-
}
|
|
795
|
-
finally {
|
|
796
|
-
if (key && this.turns.get(key) === turn)
|
|
797
|
-
this.turns.delete(key);
|
|
798
|
-
if (key)
|
|
799
|
-
this.detached.delete(key);
|
|
564
|
+
if (runtime) {
|
|
565
|
+
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
566
|
+
if (key && this.sessions.get(key) === live)
|
|
567
|
+
this.sessions.delete(key);
|
|
568
|
+
void this.cleanupRuntime(runtime);
|
|
800
569
|
}
|
|
570
|
+
if (key && this.turns.get(key) === turn)
|
|
571
|
+
this.turns.delete(key);
|
|
572
|
+
if (key)
|
|
573
|
+
this.detached.delete(key);
|
|
801
574
|
}
|
|
802
575
|
}
|
|
803
576
|
}
|
|
804
577
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
805
578
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
806
579
|
adapters = new Map();
|
|
807
|
-
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics) { super(); for (const agent of AGENTS)
|
|
808
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics)); }
|
|
580
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) { super(); for (const agent of AGENTS)
|
|
581
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions)); }
|
|
809
582
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
810
583
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
811
584
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
812
585
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
813
586
|
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
814
587
|
prepareCall(provider, model, signal) { return this.adapter(provider).prepareCall(provider, model, signal); }
|
|
815
|
-
observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
|
|
816
|
-
adapter.observeModelInvalidation(fields, changes); }
|
|
817
|
-
async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
|
|
818
|
-
async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
|
|
819
588
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
820
589
|
stream(options) { return this.adapter(options.provider).stream(options); }
|
|
821
590
|
async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
|
|
@@ -825,7 +594,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
825
594
|
export function apply(ctx, live) {
|
|
826
595
|
const config = () => readConfig(live);
|
|
827
596
|
const get = () => ctx.get('aiWorkbenchConnect');
|
|
828
|
-
|
|
597
|
+
const language = 'zh';
|
|
829
598
|
const readImage = async (ref, signal) => {
|
|
830
599
|
const attachments = ctx.get('attachments');
|
|
831
600
|
if (typeof attachments?.readImage !== 'function')
|
|
@@ -833,22 +602,19 @@ export function apply(ctx, live) {
|
|
|
833
602
|
return attachments.readImage(ref, signal);
|
|
834
603
|
};
|
|
835
604
|
const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
|
|
836
|
-
const localTurns = createLocalUserTurnObserver(ctx);
|
|
837
605
|
const detailManagers = new Map();
|
|
838
606
|
const managerForProfile = () => {
|
|
839
607
|
const profile = ctx.get('profileContext');
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
let manager = detailManagers.get(profile.dir);
|
|
608
|
+
const profileDir = typeof profile?.dir === 'string' ? profile.dir : undefined;
|
|
609
|
+
let manager = detailManagers.get(profileDir ?? '');
|
|
843
610
|
if (!manager) {
|
|
844
|
-
manager = createRemoteDetails({ profileDir
|
|
845
|
-
detailManagers.set(
|
|
611
|
+
manager = createRemoteDetails({ profileDir, warn: line => ctx.logger('remote-agent').warn(line) });
|
|
612
|
+
detailManagers.set(profileDir ?? '', manager);
|
|
846
613
|
}
|
|
847
614
|
return manager;
|
|
848
615
|
};
|
|
616
|
+
ctx.provide('aiWorkbenchRemoteDiagnostics', { read: (query) => managerForProfile().read(query) });
|
|
849
617
|
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
850
|
-
let changed = () => { };
|
|
851
|
-
let observeCatalogRevocation = () => { };
|
|
852
618
|
const diagnostics = new Map();
|
|
853
619
|
const diagnostic = (scope, provider, message) => {
|
|
854
620
|
let entry = diagnostics.get(provider);
|
|
@@ -863,25 +629,14 @@ export function apply(ctx, live) {
|
|
|
863
629
|
else
|
|
864
630
|
entry.set(scope, message);
|
|
865
631
|
};
|
|
866
|
-
let defaults;
|
|
867
|
-
const catalogDiagnostics = createCatalogDiagnostics(() => { const profile = ctx.get('profileContext'); return profile?.dir; });
|
|
868
632
|
// Public Loader Entry.options.id; optional when mounted without Loader.
|
|
869
633
|
const actualSettingsNamespace = ctx.fiber.entry?.options.id;
|
|
870
|
-
new
|
|
871
|
-
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) => {
|
|
872
|
-
diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
|
|
873
|
-
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent), (fields, changes) => observeCatalogRevocation(fields, changes));
|
|
634
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes);
|
|
874
635
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
875
636
|
const details = async (request, api, connection, target) => {
|
|
876
|
-
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection
|
|
637
|
+
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
|
|
877
638
|
};
|
|
878
639
|
const open = async (...args) => {
|
|
879
|
-
const profile = ctx.get('profileContext');
|
|
880
|
-
if (!profile?.dir)
|
|
881
|
-
throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
|
|
882
|
-
args[2].recoveryJournal = recoveryJournal(profile.dir, runtimeRecoveryKey(`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId, args[2].employeeScope));
|
|
883
|
-
args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
|
|
884
|
-
args[2].currentInputIds = await currentInputIds(ctx, args[2]);
|
|
885
640
|
const runtime = await openByAgent.get(agent.id)(...args);
|
|
886
641
|
runtime.detailContext = () => {
|
|
887
642
|
const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
|
|
@@ -911,14 +666,11 @@ export function apply(ctx, live) {
|
|
|
911
666
|
const sessions = services.get('sessions');
|
|
912
667
|
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
913
668
|
};
|
|
914
|
-
|
|
915
|
-
workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
|
|
916
|
-
record: (request, observation) => preflightDiagnostics.record(request, agent.loginProvider, observation),
|
|
917
|
-
}, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
918
|
-
currentUserTurn: request => localTurns.current(request),
|
|
669
|
+
const provider = connectProvider(get, { agent, workspace, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
919
670
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
920
671
|
workspace, native: () => resolveEmployeeNative(),
|
|
921
|
-
} } : {}),
|
|
672
|
+
} } : {}), details, open, local });
|
|
673
|
+
return provider;
|
|
922
674
|
}, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
|
|
923
675
|
const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
|
|
924
676
|
let turn = null, step = null;
|
|
@@ -937,50 +689,30 @@ export function apply(ctx, live) {
|
|
|
937
689
|
step = null;
|
|
938
690
|
}
|
|
939
691
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
940
|
-
}, modelDirectory, preflightDiagnostics);
|
|
692
|
+
}, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx));
|
|
941
693
|
const ids = AGENTS.map(agent => agent.id);
|
|
942
694
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
|
|
943
695
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
944
696
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return;
|
|
953
|
-
// Read through the same trusted, fresh catalog. Failures are already recorded there.
|
|
954
|
-
void modelDirectory.read(agent).then(lease => lease.release(), () => { });
|
|
955
|
-
};
|
|
956
|
-
ctx.inject(['agentDefaultModel'], child => {
|
|
957
|
-
const service = child.get('agentDefaultModel');
|
|
958
|
-
const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
|
|
959
|
-
defaults = current;
|
|
960
|
-
refreshDefault();
|
|
961
|
-
child.effect(() => () => { current.dispose(); if (defaults === current)
|
|
962
|
-
defaults = undefined; });
|
|
697
|
+
let connection;
|
|
698
|
+
ctx.on('ai-workbench/connection-state', state => {
|
|
699
|
+
const previous = connection;
|
|
700
|
+
connection = state;
|
|
701
|
+
// Official replacement invalidates the browser's shared catalog and loads it.
|
|
702
|
+
if (state.phase === 'connected' && (previous?.phase !== 'connected' || previous.instanceId !== state.instanceId || previous.epoch !== state.epoch))
|
|
703
|
+
registration.replace(ids);
|
|
963
704
|
});
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
|
|
973
|
-
if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
|
|
974
|
-
return;
|
|
975
|
-
employeeConnect = nextConnect;
|
|
976
|
-
employeeNetwork = nextNetwork;
|
|
977
|
-
void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
|
|
978
|
-
});
|
|
979
|
-
new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
|
|
980
|
-
const cleanup = (id) => adapter.detachSession(id);
|
|
705
|
+
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) });
|
|
706
|
+
const cleanup = async (id) => { try {
|
|
707
|
+
await adapter.detachSession(id);
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
for (const manager of detailManagers.values())
|
|
711
|
+
manager.releaseSession(id);
|
|
712
|
+
} };
|
|
981
713
|
ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
|
|
982
714
|
ctx.on('session/disposed', session => cleanup(session.id));
|
|
983
|
-
ctx.effect(() => () => { modelDirectory.dispose();
|
|
715
|
+
ctx.effect(() => async () => { modelDirectory.dispose(); await adapter.dispose(); await Promise.all([...detailManagers.values()].map(manager => manager.dispose())); });
|
|
984
716
|
}
|
|
985
717
|
export function dshApproval(ctx) {
|
|
986
718
|
return async (request, signal, options, context, hostWorkspace) => {
|