@ai-workbench/remote-agent 0.8.11 → 0.8.12

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.
@@ -0,0 +1,112 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import { LlmError } from '@deepseek-ai/dsh-llm';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { NeedError } from "./remote.js";
5
+ import { ModelConfigurationError } from "./model-configuration.js";
6
+ import { officialEmployeeWorkspace } from "./employee-workspace.js";
7
+ import { AGENTS } from "./agents.js";
8
+ import { validPreflightPayload } from "./preflight-payload.js";
9
+ const unknownSource = () => ({ workspaceId: null, bindingId: null, source: null, status: null });
10
+ export function preflightCode(error) {
11
+ if (typeof error === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error))
12
+ return error;
13
+ if (error instanceof NeedError || error instanceof LlmError || error instanceof ModelConfigurationError)
14
+ return error.code;
15
+ return error instanceof Error && error.name === 'AbortError' ? 'ABORTED' : 'PREFLIGHT_FAILED';
16
+ }
17
+ export function createPreflightDiagnostics(options) {
18
+ const scopes = new WeakMap();
19
+ function capture(sessionId, position) {
20
+ const api = options.getApi();
21
+ let connection, session, header, source = unknownSource(), captureError;
22
+ let context = position ? { ...position } : { turn: null, step: null };
23
+ try {
24
+ connection = api?.getConnectionState();
25
+ session = sessionId ? options.ctx.get('sessions')?.get(SessionId(sessionId)) : undefined;
26
+ header = session && structuredClone(session.header);
27
+ if (!position)
28
+ for (const event of session?.snapshotEvents() ?? []) {
29
+ if (event.type === 'turn/start')
30
+ context = { turn: event.data.turn, step: null };
31
+ else if (event.type === 'turn/end')
32
+ context = { turn: null, step: null };
33
+ else if (event.type === 'step/start' && event.data.turn === context.turn)
34
+ context.step = event.data.step;
35
+ else if (event.type === 'step/end')
36
+ context.step = null;
37
+ }
38
+ if (sessionId && session) {
39
+ let workspace;
40
+ try {
41
+ workspace = officialEmployeeWorkspace(sessionId, options.ctx.get('workspaceRegistry'), session.header);
42
+ }
43
+ catch (error) {
44
+ if (!(error instanceof NeedError && error.code === 'EMPLOYEE_WORKSPACE_UNAVAILABLE'))
45
+ throw error;
46
+ }
47
+ if (workspace) {
48
+ source.workspaceId = workspace.id;
49
+ const state = api?.getWorkspaceLocationState?.();
50
+ const rows = state?.items.filter(row => row.workspaceId === workspace.id) ?? [];
51
+ if (state?.phase === 'ready' && state.instanceId === connection?.instanceId && state.epoch === connection.epoch && rows.length === 1) {
52
+ const row = rows[0];
53
+ source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source, status: row.status };
54
+ }
55
+ }
56
+ }
57
+ }
58
+ catch (error) {
59
+ captureError = error;
60
+ }
61
+ const assertScope = () => {
62
+ if (captureError !== undefined)
63
+ throw Object.assign(new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '连接前诊断上下文无法核对。', '真实官方会话与连接快照'), { cause: captureError });
64
+ if (!sessionId || !session || options.ctx.get('sessions')?.get(SessionId(sessionId)) !== session || !isDeepStrictEqual(session.header, header))
65
+ throw new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '连接前诊断所属会话已变化。', '原官方session/header');
66
+ };
67
+ return { sessionId, binding: { api, connection }, context, source, assertScope };
68
+ }
69
+ const scopeFor = (request) => { let scope = scopes.get(request); if (!scope) {
70
+ scope = capture(request.sessionId);
71
+ scopes.set(request, scope);
72
+ } return scope; };
73
+ const write = (scope, provider, observation) => options.manager().recordPreflightCheck({ sessionId: scope.sessionId, provider, observation, context: scope.context, binding: scope.binding, assertScope: scope.assertScope });
74
+ async function record(request, provider, observation) {
75
+ const scope = scopeFor(request), payload = observation.payload;
76
+ if (validPreflightPayload(payload) && ['failed', 'declined', 'cancelled'].includes(payload.outcome))
77
+ request.preflightFailureRecorded = true;
78
+ try {
79
+ await write(scope, provider, observation);
80
+ if (validPreflightPayload(payload) && payload.outcome === 'confirmed')
81
+ scope.source = { workspaceId: payload.workspaceId, bindingId: payload.bindingId, source: payload.source, status: payload.status };
82
+ }
83
+ catch (error) {
84
+ request.preflightFailureRecorded = true;
85
+ throw error;
86
+ }
87
+ }
88
+ return {
89
+ begin(request, _provider) { scopeFor(request); }, record,
90
+ async failure(request, provider, error, stage) {
91
+ if (request.preflightFailureRecorded)
92
+ return;
93
+ request.preflightFailureRecorded = true;
94
+ const scope = scopeFor(request), code = preflightCode(error);
95
+ try {
96
+ await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。', payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
97
+ }
98
+ catch (diagnostic) {
99
+ throw Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮连接前诊断未能保存,已停止。', '原错误代码 ' + code + ';诊断错误代码 ' + preflightCode(diagnostic)), { cause: new AggregateError([error, diagnostic], 'PREFLIGHT_AND_DIAGNOSTIC_FAILED') });
100
+ }
101
+ },
102
+ forPreparation(sessionId, context) {
103
+ const scope = capture(sessionId, context);
104
+ return async (provider, code) => {
105
+ const brand = AGENTS.find(agent => agent.id === provider)?.loginProvider;
106
+ if (!brand)
107
+ throw new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '不是本插件的远程品牌。', '真实X品牌');
108
+ await write(scope, brand, { kind: 'need', severity: 'action-required', code, summary: '模型准备在派发前失败;没有自动重发任务。', payload: { phase: 'workspace-source-admission', stage: code === 'MODEL_REQUIRED' ? 'input-prepare' : 'model-catalog', ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
109
+ };
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,11 @@
1
+ import type { SourceAdmissionStage } from './workspace-source.ts';
2
+ export interface PreflightPayload {
3
+ phase: 'workspace-source-admission';
4
+ stage: SourceAdmissionStage;
5
+ workspaceId: string | null;
6
+ bindingId: string | null;
7
+ source: 'local' | 'host' | 'legacy-host' | null;
8
+ status: 'unselected' | 'selected' | 'legacy-confirmation-required' | 'invalid' | null;
9
+ outcome: 'confirmation-required' | 'confirmed' | 'declined' | 'cancelled' | 'failed';
10
+ }
11
+ export declare function validPreflightPayload(value: unknown): value is PreflightPayload;
@@ -0,0 +1,11 @@
1
+ export function validPreflightPayload(value) {
2
+ if (!value || typeof value !== 'object' || Array.isArray(value))
3
+ return false;
4
+ const p = value, keys = ['phase', 'stage', 'workspaceId', 'bindingId', 'source', 'status', 'outcome'];
5
+ const id = (v) => v === null || typeof v === 'string' && v.length > 0 && !/[\x00-\x1f\x7f]/.test(v);
6
+ return Object.keys(p).length === keys.length && keys.every(key => Object.hasOwn(p, key)) && p.phase === 'workspace-source-admission'
7
+ && ['source-context', 'source-confirmation', 'input-acceptance', 'login-readiness', 'local-admission', 'input-prepare', 'model-catalog', 'runtime-resolve'].includes(p.stage)
8
+ && id(p.workspaceId) && id(p.bindingId) && [null, 'local', 'host', 'legacy-host'].includes(p.source)
9
+ && [null, 'unselected', 'selected', 'legacy-confirmation-required', 'invalid'].includes(p.status)
10
+ && ['confirmation-required', 'confirmed', 'declined', 'cancelled', 'failed'].includes(p.outcome);
11
+ }
@@ -65,6 +65,7 @@ export interface Runtime {
65
65
  employeePresentation?: string;
66
66
  }
67
67
  export interface RuntimeRequest extends Invocation {
68
+ preflightFailureRecorded?: boolean;
68
69
  recordEarlyFailure?: (error: unknown, stage: SourceAdmissionStage) => Promise<void>;
69
70
  userInputIds?: readonly string[];
70
71
  localAcceptance?: Readonly<LocalUserTurnAcceptance>;
package/dist/provider.js CHANGED
@@ -171,7 +171,7 @@ export function connectProvider(get, options = {}) {
171
171
  throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
172
172
  const confirmedSource = options.workspaceSource ? await confirmLegacyWorkspaceSource(get, api, request, options.workspaceSource) : undefined;
173
173
  let sourceStage = 'input-acceptance';
174
- if (confirmedSource)
174
+ if (confirmedSource && !request.recordEarlyFailure)
175
175
  request.recordEarlyFailure = (error, stage) => confirmedSource.recordFailure(error, stage);
176
176
  try {
177
177
  confirmedSource?.assertCurrent();
@@ -208,8 +208,12 @@ export function connectProvider(get, options = {}) {
208
208
  }
209
209
  catch (error) {
210
210
  admissions.delete(request);
211
+ const record = request.recordEarlyFailure;
211
212
  request.recordEarlyFailure = undefined;
212
- await confirmedSource?.recordFailure(error, sourceStage);
213
+ if (record)
214
+ await record(error, sourceStage);
215
+ else
216
+ await confirmedSource?.recordFailure(error, sourceStage);
213
217
  throw error;
214
218
  }
215
219
  };
@@ -23,6 +23,10 @@ export interface DetailRequest {
23
23
  export declare function privateDirectory(path: string): Promise<void>;
24
24
  export declare function durableWrite(path: string, value: unknown): Promise<void>;
25
25
  export declare function readJournal(path: string): Promise<unknown>;
26
+ export interface DiagnosticBinding {
27
+ api: RemoteDetailsApi | undefined;
28
+ connection: Connection | undefined;
29
+ }
26
30
  export declare function createRemoteDetails(options: {
27
31
  profileDir: string;
28
32
  getApi: () => RemoteDetailsApi | undefined;
@@ -45,5 +49,19 @@ export declare function createRemoteDetails(options: {
45
49
  eventId: string;
46
50
  recorded: true;
47
51
  }>;
52
+ recordPreflightCheck(request: {
53
+ sessionId?: string;
54
+ provider: "claude" | "codex";
55
+ observation: DetailObservation;
56
+ context?: {
57
+ turn: string | number | null;
58
+ step: string | number | null;
59
+ };
60
+ binding?: DiagnosticBinding;
61
+ assertScope?: () => void;
62
+ }): Promise<{
63
+ eventId: string;
64
+ recorded: true;
65
+ }>;
48
66
  open(request: DetailRequest): Promise<DetailChannel>;
49
67
  };
@@ -4,6 +4,7 @@ import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
4
4
  import { isAbsolute, join, parse } from 'node:path';
5
5
  import { isDeepStrictEqual } from 'node:util';
6
6
  import { sanitizeObservation, isSanitizedRedaction } from "./conversation-details.js";
7
+ import { validPreflightPayload } from "./preflight-payload.js";
7
8
  import { NeedError } from "./remote.js";
8
9
  const failure = (code, message = '技术详情未能持久记录,本轮已停止。') => new NeedError(code, message, '可用的远程主机连接技术详情');
9
10
  const hash = (value) => createHash('sha256').update(value).digest('hex');
@@ -90,141 +91,155 @@ export function createRemoteDetails(options) {
90
91
  const root = join(options.profileDir, 'ai-workbench-remote-agent', 'diagnostics');
91
92
  const scopes = new Map(), loginScopes = new Map();
92
93
  const initialize = async () => { await privateDirectory(join(options.profileDir, 'ai-workbench-remote-agent')); await privateDirectory(root); };
93
- return {
94
- async recordLocalFailure(request) {
95
- await initialize();
96
- if (!/^[A-Z][A-Z0-9_]*$/.test(request.code))
97
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
98
- await durableWrite(join(root, randomUUID() + '.json'), { version: 1, kind: 'local-admission', code: request.code, provider: request.provider, sessionHash: request.sessionId ? hash(request.sessionId) : null, ...request.observation ? { observation: sanitizeObservation(request.observation, true) } : {} });
99
- },
100
- async recordLoginFailure(request) {
101
- await initialize();
102
- const eventId = randomUUID(), diagnostic = request.diagnostic;
103
- // The original observed IDs and full approved snapshot are durable before any L call.
104
- await durableWrite(join(root, eventId + '.json'), { version: 1, eventId, kind: 'login-check', code: request.code, sessionHash: request.sessionId ? hash(request.sessionId) : null, diagnostic, delivery: 'pending' });
105
- if (!request.sessionId)
106
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
107
- const sessionId = request.sessionId, provider = diagnostic.provider, api = options.getApi(), connection = diagnostic.connection;
108
- const stable = () => { const now = api?.getConnectionState(); if (!api || options.getApi() !== api || now?.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
109
- throw failure('CONNECTION_LOST'); };
94
+ async function recordConnectionDiagnostic(request) {
95
+ request = { ...request, connection: request.connection && structuredClone(request.connection), payload: structuredClone(request.payload), snapshot: structuredClone(request.snapshot), context: request.context && { ...request.context } };
96
+ await initialize();
97
+ const eventId = randomUUID();
98
+ await durableWrite(join(root, eventId + '.json'), { version: 1, eventId, kind: request.kind, code: request.code, provider: request.provider, sessionHash: request.sessionId ? hash(request.sessionId) : null, ...request.snapshot, delivery: 'pending' });
99
+ if (!request.sessionId)
100
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
101
+ const sessionId = request.sessionId, provider = request.provider, api = request.binding ? request.binding.api : options.getApi(), connection = request.connection;
102
+ if (!connection)
103
+ throw failure('CONNECTION_LOST');
104
+ const stable = () => { request.assertScope?.(); const now = api?.getConnectionState(); if (!api || options.getApi() !== api || connection.phase !== 'connected' || now?.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
105
+ throw failure('CONNECTION_LOST'); };
106
+ stable();
107
+ const info = api.getContractInfo?.();
108
+ if ((request.kind === 'login-check' ? info?.loginDiagnostics : info?.preflightDiagnostics) !== 1 || !['appendLoginDiagnostic', 'getLoginDiagnosticReceipt', 'listLoginDiagnostics'].every(key => typeof api[key] === 'function'))
109
+ throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
110
+ const parent = join(root, 'login');
111
+ await privateDirectory(parent);
112
+ const key = hash(sessionId + '\0' + provider), dir = join(parent, key);
113
+ await privateDirectory(dir);
114
+ const scope = loginScopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
115
+ loginScopes.set(key, scope);
116
+ const operation = scope.tail.then(async () => {
110
117
  stable();
111
- if (api.getContractInfo?.().loginDiagnostics !== 1 || !['appendLoginDiagnostic', 'getLoginDiagnosticReceipt', 'listLoginDiagnostics'].every(key => typeof api[key] === 'function'))
112
- throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
113
- const parent = join(root, 'login');
114
- await privateDirectory(parent);
115
- const key = hash(sessionId + '\0' + provider), dir = join(parent, key);
116
- await privateDirectory(dir);
117
- const scope = loginScopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
118
- loginScopes.set(key, scope);
119
- const operation = scope.tail.then(async () => {
120
- stable();
121
- const generation = connection.instanceId + '\0' + connection.epoch;
122
- if (!scope.loaded || scope.generation !== generation) {
123
- const names = await readdir(dir);
124
- for (const name of names) {
125
- const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
126
- if (!match)
127
- continue;
128
- const sequence = Number(match[1]);
129
- if (!Number.isSafeInteger(sequence))
130
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
131
- scope.maximum = Math.max(scope.maximum, sequence);
132
- const saved = await readJournal(join(dir, name));
133
- if (!object(saved) || !object(saved.detail) || saved.detail.sequence !== sequence || saved.detail.eventId !== match[2] || saved.detail.sessionId !== sessionId || saved.detail.provider !== provider)
118
+ const generation = connection.instanceId + '\0' + connection.epoch;
119
+ if (!scope.loaded || scope.generation !== generation) {
120
+ const names = await readdir(dir);
121
+ for (const name of names) {
122
+ const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
123
+ if (!match)
124
+ continue;
125
+ const sequence = Number(match[1]);
126
+ if (!Number.isSafeInteger(sequence))
127
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
128
+ scope.maximum = Math.max(scope.maximum, sequence);
129
+ const saved = await readJournal(join(dir, name));
130
+ if (!object(saved) || !object(saved.detail) || saved.detail.sequence !== sequence || saved.detail.eventId !== match[2] || saved.detail.sessionId !== sessionId || saved.detail.provider !== provider)
131
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
132
+ const settled = 'settled-' + match[2] + '.json';
133
+ if (names.includes(settled)) {
134
+ const receipt = await readJournal(join(dir, settled));
135
+ if (!object(receipt) || receipt.eventId !== match[2] || !['recorded', 'rejected'].includes(receipt.state))
134
136
  throw failure('REMOTE_DETAIL_STORAGE_FAILED');
135
- const settled = 'settled-' + match[2] + '.json';
136
- if (names.includes(settled)) {
137
- const receipt = await readJournal(join(dir, settled));
138
- if (!object(receipt) || receipt.eventId !== match[2] || !['recorded', 'rejected'].includes(receipt.state))
139
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
140
- continue;
141
- }
142
- stable();
143
- const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId: match[2], sessionId, provider }));
144
- stable();
145
- if (receipt.eventId !== match[2] || !receipt.recorded)
146
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
147
- if (!isDeepStrictEqual(receipt.detail, saved.detail))
148
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
149
- await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
137
+ continue;
150
138
  }
151
- let cursor, previous = -1, count = 0, expected;
152
- const seen = new Set();
153
- do {
154
- stable();
155
- const page = unwrap(await api.listLoginDiagnostics({ sessionId, provider, ...(cursor ? { cursor } : {}) }));
156
- stable();
157
- if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
158
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
159
- const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
160
- if (expected && !isDeepStrictEqual(expected, counts))
161
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
162
- expected = counts;
163
- for (const entry of page.items) {
164
- if (entry.sessionId !== sessionId || entry.provider !== provider || !Number.isSafeInteger(entry.sequence) || entry.sequence < 0 || entry.sequence <= previous)
165
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
166
- previous = entry.sequence;
167
- scope.maximum = Math.max(scope.maximum, entry.sequence);
168
- count++;
169
- }
170
- cursor = page.nextCursor;
171
- if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
172
- throw failure('REMOTE_DETAIL_CURSOR_INVALID');
173
- if (cursor)
174
- seen.add(cursor);
175
- } while (cursor);
176
- if (count !== expected?.total)
177
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
178
- scope.loaded = true;
179
- scope.generation = generation;
180
- }
181
- const mismatch = diagnostic.reason === 'LOGIN_INSTANCE_MISMATCH', unsupported = !!diagnostic.officialReasonRejected;
182
- const reason = mismatch ? 'LOGIN_INSTANCE_MISMATCH' : unsupported ? 'OFFICIAL_REASON_UNSUPPORTED' : diagnostic.reason;
183
- // L 0.7.11: observed evidence never changes the current connection scope.
184
- const login = { phase: diagnostic.login.phase, instanceId: diagnostic.login.instanceId, logins: Object.fromEntries(Object.entries(diagnostic.login.logins).filter(([, v]) => typeof v === 'boolean' || v === 'absent')), queuedProviders: diagnostic.login.queuedProviders, checkingProviders: diagnostic.login.checkingProviders };
185
- const payload = { reason, connection: { ...connection }, login, ...(mismatch ? { observedLoginInstanceId: diagnostic.login.instanceId } : {}), ...(diagnostic.officialReason ? { officialReason: diagnostic.officialReason } : {}), ...(unsupported && !mismatch ? { officialReasonRejected: diagnostic.officialReasonRejected } : {}) };
186
- // The two payload reasons are exclusive in L's contract. Preserve both facts
187
- // using the approved fixed summary and full digest, without inventing a field.
188
- const rejected = diagnostic.officialReasonRejected;
189
- const summary = '登录检查:' + diagnostic.reason + (rejected ? '; OFFICIAL_REASON_UNSUPPORTED; bytes=' + rejected.bytes + '; SHA256=' + rejected.sha256 : '');
190
- const context = request.context ?? { turn: null, step: null };
191
- if (!identifier(context.turn) || !identifier(context.step))
192
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
193
- const sequence = scope.maximum + 1;
194
- if (!Number.isSafeInteger(sequence))
195
- throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
196
- scope.maximum = sequence;
197
- const detail = { version: 1, eventId, instanceId: connection.instanceId, epoch: connection.epoch, sessionId, provider, turn: context.turn, step: context.step, sequence, kind: 'login-check', severity: 'action-required', code: request.code, summary, method: diagnostic.source, payload };
198
- await durableWrite(join(dir, sequence + '-' + eventId + '.json'), { state: 'pending', detail });
199
- stable();
200
- let response;
201
- try {
202
- response = await api.appendLoginDiagnostic(structuredClone(detail));
203
- }
204
- catch {
205
139
  stable();
206
- const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId, sessionId, provider }));
140
+ const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId: match[2], sessionId, provider }));
207
141
  stable();
208
- if (receipt.eventId !== eventId)
209
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
210
- if (!receipt.recorded)
142
+ if (receipt.eventId !== match[2] || !receipt.recorded)
211
143
  throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
212
- if (!isDeepStrictEqual(receipt.detail, detail))
144
+ if (!isDeepStrictEqual(receipt.detail, saved.detail))
213
145
  throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
214
- await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
215
- return;
146
+ await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
216
147
  }
217
- stable();
218
- if (response?.ok === false)
219
- await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'rejected', code: response.error.code });
220
- const ack = unwrap(response);
221
- if (ack.eventId !== eventId || ack.recorded !== true)
148
+ let cursor, previous = -1, count = 0, expected;
149
+ const seen = new Set();
150
+ do {
151
+ stable();
152
+ const page = unwrap(await api.listLoginDiagnostics({ sessionId, provider, ...(cursor ? { cursor } : {}) }));
153
+ stable();
154
+ if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
155
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
156
+ const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
157
+ if (expected && !isDeepStrictEqual(expected, counts))
158
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
159
+ expected = counts;
160
+ for (const entry of page.items) {
161
+ if (entry.sessionId !== sessionId || entry.provider !== provider || !Number.isSafeInteger(entry.sequence) || entry.sequence < 0 || entry.sequence <= previous)
162
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
163
+ previous = entry.sequence;
164
+ scope.maximum = Math.max(scope.maximum, entry.sequence);
165
+ count++;
166
+ }
167
+ cursor = page.nextCursor;
168
+ if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
169
+ throw failure('REMOTE_DETAIL_CURSOR_INVALID');
170
+ if (cursor)
171
+ seen.add(cursor);
172
+ } while (cursor);
173
+ if (count !== expected?.total)
222
174
  throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
175
+ scope.loaded = true;
176
+ scope.generation = generation;
177
+ }
178
+ const context = request.context ?? { turn: null, step: null };
179
+ if (!identifier(context.turn) || !identifier(context.step))
180
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
181
+ const sequence = scope.maximum + 1;
182
+ if (!Number.isSafeInteger(sequence))
183
+ throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
184
+ scope.maximum = sequence;
185
+ const detail = { version: 1, eventId, instanceId: connection.instanceId, epoch: connection.epoch, sessionId, provider, turn: context.turn, step: context.step, sequence, kind: request.kind, severity: request.severity, code: request.code, summary: request.summary, method: request.method, payload: request.payload };
186
+ await durableWrite(join(dir, sequence + '-' + eventId + '.json'), { state: 'pending', detail });
187
+ stable();
188
+ let response;
189
+ try {
190
+ response = await api.appendLoginDiagnostic(structuredClone(detail));
191
+ }
192
+ catch {
193
+ stable();
194
+ const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId, sessionId, provider }));
195
+ stable();
196
+ if (receipt.eventId !== eventId)
197
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
198
+ if (!receipt.recorded)
199
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
200
+ if (!isDeepStrictEqual(receipt.detail, detail))
201
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
223
202
  await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
224
- });
225
- scope.tail = operation.then(() => { }, () => { scope.loaded = false; });
226
- await operation;
227
- return { eventId, recorded: true };
203
+ return;
204
+ }
205
+ stable();
206
+ if (response?.ok === false)
207
+ await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'rejected', code: response.error.code });
208
+ const ack = unwrap(response);
209
+ if (ack.eventId !== eventId || ack.recorded !== true)
210
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
211
+ await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
212
+ });
213
+ scope.tail = operation.then(() => { }, () => { scope.loaded = false; });
214
+ await operation;
215
+ return { eventId, recorded: true };
216
+ }
217
+ return {
218
+ async recordLocalFailure(request) {
219
+ await initialize();
220
+ if (!/^[A-Z][A-Z0-9_]*$/.test(request.code))
221
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
222
+ await durableWrite(join(root, randomUUID() + '.json'), { version: 1, kind: 'local-admission', code: request.code, provider: request.provider, sessionHash: request.sessionId ? hash(request.sessionId) : null, ...request.observation ? { observation: sanitizeObservation(request.observation, true) } : {} });
223
+ },
224
+ async recordLoginFailure(request) {
225
+ const diagnostic = request.diagnostic, connection = diagnostic.connection;
226
+ const mismatch = diagnostic.reason === 'LOGIN_INSTANCE_MISMATCH', unsupported = !!diagnostic.officialReasonRejected;
227
+ const reason = mismatch ? 'LOGIN_INSTANCE_MISMATCH' : unsupported ? 'OFFICIAL_REASON_UNSUPPORTED' : diagnostic.reason;
228
+ // L 0.7.11: observed evidence never changes the current connection scope.
229
+ const login = { phase: diagnostic.login.phase, instanceId: diagnostic.login.instanceId, logins: Object.fromEntries(Object.entries(diagnostic.login.logins).filter(([, v]) => typeof v === 'boolean' || v === 'absent')), queuedProviders: diagnostic.login.queuedProviders, checkingProviders: diagnostic.login.checkingProviders };
230
+ const payload = { reason, connection: { ...connection }, login, ...(mismatch ? { observedLoginInstanceId: diagnostic.login.instanceId } : {}), ...(diagnostic.officialReason ? { officialReason: diagnostic.officialReason } : {}), ...(unsupported && !mismatch ? { officialReasonRejected: diagnostic.officialReasonRejected } : {}) };
231
+ // The two payload reasons are exclusive in L's contract. Preserve both facts
232
+ // using the approved fixed summary and full digest, without inventing a field.
233
+ const rejected = diagnostic.officialReasonRejected;
234
+ const summary = '登录检查:' + diagnostic.reason + (rejected ? '; OFFICIAL_REASON_UNSUPPORTED; bytes=' + rejected.bytes + '; SHA256=' + rejected.sha256 : '');
235
+ return recordConnectionDiagnostic({ ...request, provider: diagnostic.provider, connection: diagnostic.connection, kind: 'login-check', severity: 'action-required', summary, method: diagnostic.source, payload, snapshot: { diagnostic } });
236
+ },
237
+ async recordPreflightCheck(request) {
238
+ const observation = sanitizeObservation(request.observation, true);
239
+ if (!/^[A-Z][A-Z0-9_]*$/.test(observation.code) || !validPreflightPayload(observation.payload))
240
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
241
+ const api = request.binding ? request.binding.api : options.getApi(), connection = request.binding ? request.binding.connection : api?.getConnectionState();
242
+ return recordConnectionDiagnostic({ ...request, connection, binding: { api, connection }, kind: 'preflight-check', code: observation.code, summary: observation.summary, method: observation.payload.stage, severity: observation.severity, payload: observation.payload, snapshot: { observation } });
228
243
  },
229
244
  async open(request) {
230
245
  request = structuredClone(request);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "zod": "4.6.7"
40
40
  },
41
41
  "devDependencies": {
42
- "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.17.tgz",
42
+ "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.18.tgz",
43
43
  "@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.4.tgz",
44
44
  "@deepseek-ai/cordis": "4.0.4",
45
45
  "@deepseek-ai/cordis-plugin-group": "1.0.4",
@@ -68,7 +68,7 @@
68
68
  "zustand": "4.4.7"
69
69
  },
70
70
  "peerDependencies": {
71
- "@ai-workbench/connect": "~0.7.17",
71
+ "@ai-workbench/connect": "~0.7.18",
72
72
  "@ai-workbench/network": "~0.4.4",
73
73
  "@deepseek-ai/cordis": "4.0.4",
74
74
  "@deepseek-ai/dsh-agent": "0.2.0-rc.2",