@ai-workbench/remote-agent 0.8.0 → 0.8.2

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/dist/provider.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
1
2
  import { NeedError } from "./remote.js";
2
3
  import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
3
4
  import { CODEX_AGENT } from "./agents.js";
@@ -8,6 +9,8 @@ export function requireLoginCoordinator(api) {
8
9
  export function requireLoginState(state) {
9
10
  const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
10
11
  const need = (v) => object(v) && typeof v.code === 'string' && v.code.length > 0 && ['enrollment', 'network', 'ssh', 'auth', 'integration', 'settings', 'projects'].includes(v.layer) && typeof v.message === 'string' && typeof v.requires === 'string' && ['employee', 'administrator', 'ai-administrator', 'plugin'].includes(v.actor) && object(v.action) && typeof v.action.id === 'string' && typeof v.action.label === 'string' && typeof v.action.available === 'boolean';
12
+ if (state.queuedProviders !== undefined && (!Array.isArray(state.queuedProviders) || state.queuedProviders.some(p => p !== 'codex' && p !== 'claude') || new Set(state.queuedProviders).size !== state.queuedProviders.length || state.queuedProviders.some(p => state.checkingProviders?.includes(p))))
13
+ throw new NeedError('CONNECT_API_MISMATCH', '我需要真实的品牌排队与进行中状态', 'L >=0.7.10');
11
14
  if (!Array.isArray(state.checkingProviders) || state.checkingProviders.some(p => p !== 'codex' && p !== 'claude') || !object(state.providerNeeds) || Object.entries(state.providerNeeds).some(([p, v]) => !['codex', 'claude'].includes(p) || !need(v)))
12
15
  throw new NeedError('CONNECT_API_MISMATCH', '我需要按助手区分的真实登录状态', 'L >=0.6.1');
13
16
  }
@@ -32,15 +35,77 @@ function verifiedLogin(api, brand, expected) {
32
35
  if (!connection.instanceId || !Number.isSafeInteger(connection.epoch) || connection.epoch < 0 || connection.phase !== 'connected' || expected && (connection.instanceId !== expected.instanceId || connection.epoch !== expected.epoch))
33
36
  throw new NeedError('CONNECTION_LOST', 'connection binding changed', 'current employee connection');
34
37
  requireLoginState(login);
35
- if (login.instanceId !== connection.instanceId || login.checkingProviders.includes(brand) || login.providerNeeds[brand] || login.phase === 'blocked' || login.phase === 'disposed' || login.phase === 'authorizing' && login.provider === brand || typeof login.logins?.[brand] !== 'boolean')
36
- throw new NeedError('AUTH_STATUS_INVALID', '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态');
37
- if (!login.logins[brand])
38
- throw new NeedError('LOGIN_REQUIRED', '需要登录,请到设置 → 远程主机连接 点登录', '本人登录');
38
+ const reason = loginReason(login, connection, brand);
39
+ if (reason)
40
+ throw Object.assign(new NeedError(reason === 'LOGIN_REQUIRED' ? 'LOGIN_REQUIRED' : 'AUTH_STATUS_INVALID', reason === 'LOGIN_REQUIRED' ? '需要登录,请到设置 → 远程主机连接 点登录' : '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态'), { technical: loginDiagnostic('verified-login', reason, brand, connection, login), ...(typeof login.providerNeeds[brand]?.officialReason === 'string' ? { officialReason: login.providerNeeds[brand].officialReason } : {}) });
39
41
  return { ...connection };
40
42
  }
43
+ /** Wait only for L's explicit in-progress evidence. No timer, polling, refresh or authorization. */
44
+ async function waitForLogin(api, brand, expected, get, signal) {
45
+ const check = () => {
46
+ signal?.throwIfAborted();
47
+ if (get() !== api)
48
+ throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
49
+ const connection = api.getConnectionState(), state = api.getLoginPromptState();
50
+ requireLoginState(state);
51
+ if (connection.phase !== 'connected' || connection.instanceId !== expected.instanceId || connection.epoch !== expected.epoch)
52
+ return verifiedLogin(api, brand, expected);
53
+ const pending = state.instanceId === connection.instanceId && state.phase !== 'blocked' && state.phase !== 'disposed' && (state.queuedProviders?.includes(brand) || state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand);
54
+ return pending ? undefined : verifiedLogin(api, brand, expected);
55
+ };
56
+ const ready = check();
57
+ if (ready)
58
+ return ready;
59
+ if (typeof api.onLoginPromptState !== 'function' || typeof api.onConnectionState !== 'function')
60
+ throw new NeedError('CONNECT_API_MISMATCH', '我需要登录检查状态订阅能力', 'L onLoginPromptState / onConnectionState');
61
+ return new Promise((resolve, reject) => {
62
+ let ended = false;
63
+ const disposers = [];
64
+ const clear = () => { ended = true; signal?.removeEventListener('abort', abort); for (const off of disposers)
65
+ off(); };
66
+ const changed = () => { if (ended)
67
+ return; try {
68
+ const value = check();
69
+ if (value) {
70
+ clear();
71
+ resolve(value);
72
+ }
73
+ }
74
+ catch (error) {
75
+ clear();
76
+ reject(error);
77
+ } };
78
+ const abort = () => { if (ended)
79
+ return; clear(); reject(new NeedError('ABORTED', 'consumer wait cancelled', 'employee action')); };
80
+ const register = (off) => { if (ended)
81
+ off();
82
+ else
83
+ disposers.push(off); };
84
+ signal?.addEventListener('abort', abort, { once: true });
85
+ try {
86
+ if (signal?.aborted)
87
+ abort();
88
+ if (!ended)
89
+ register(api.onLoginPromptState(changed));
90
+ if (!ended)
91
+ register(api.onConnectionState(changed));
92
+ changed();
93
+ }
94
+ catch (error) {
95
+ clear();
96
+ reject(error);
97
+ }
98
+ });
99
+ }
100
+ async function ensureReady(api, brand, signal) {
101
+ const result = await awaitL(() => api.ensureReady(brand), signal);
102
+ if (!result.ok)
103
+ throw Object.assign(new NeedError(result.error.code, result.error.message, result.error.requires), { technical: loginDiagnostic('ensure-ready', 'L_ENSURE_READY_FAILED', brand, api.getConnectionState(), api.getLoginPromptState(), result.error.officialReason), ...(typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {}) });
104
+ return result.value;
105
+ }
41
106
  export function unwrap(result) {
42
107
  if (!result.ok)
43
- throw new NeedError(result.error.code, result.error.message, result.error.requires);
108
+ throw Object.assign(new NeedError(result.error.code, result.error.message, result.error.requires), typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {});
44
109
  return result.value;
45
110
  }
46
111
  export function validateTarget(t) {
@@ -76,12 +141,12 @@ export async function offerLogin(connect, request, currentApi, agent = CODEX_AGE
76
141
  requireNativeWorkspace(connect);
77
142
  request.signal?.throwIfAborted();
78
143
  const before = connect.getConnectionState();
79
- const target = unwrap(await awaitL(() => connect.ensureReady(agent.loginProvider), request.signal));
144
+ const target = await ensureReady(connect, agent.loginProvider, request.signal);
80
145
  validateTarget(target);
81
146
  request.signal?.throwIfAborted();
82
147
  if (currentApi && currentApi() !== connect)
83
148
  throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
84
- verifiedLogin(connect, agent.loginProvider, before);
149
+ await waitForLogin(connect, agent.loginProvider, before, currentApi ?? (() => connect), request.signal);
85
150
  }
86
151
  export function sameTarget(a, b) {
87
152
  return identity(a) === identity(b) && a.projectRoot === b.projectRoot && a.knownHostsFile === b.knownHostsFile && a.proxyCommand === b.proxyCommand && a.session.command === b.session.command && a.session.root === b.session.root && a.session.settingsPath === b.session.settingsPath;
@@ -99,12 +164,12 @@ export function connectProvider(get, options = {}) {
99
164
  if (request.mode === 'one-shot' && !agent.oneShot)
100
165
  throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
101
166
  const before = api.getConnectionState();
102
- const target = unwrap(await awaitL(() => api.ensureReady(agent.loginProvider), request.signal));
167
+ const target = await ensureReady(api, agent.loginProvider, request.signal);
103
168
  validateTarget(target);
104
169
  request.signal?.throwIfAborted();
105
170
  if (get() !== api)
106
171
  throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
107
- const connection = verifiedLogin(api, agent.loginProvider, before);
172
+ const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
108
173
  if (request.identity && request.identity !== identity(target))
109
174
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
110
175
  admissions.set(request, { api, target, connection });
@@ -115,15 +180,14 @@ export function connectProvider(get, options = {}) {
115
180
  const admitted = admissions.get(request);
116
181
  admissions.delete(request);
117
182
  const { api, target, connection } = admitted;
118
- const stable = () => { request.signal?.throwIfAborted(); if (get() !== api)
119
- throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接'); verifiedLogin(api, agent.loginProvider, connection); };
120
- stable();
183
+ const stable = () => waitForLogin(api, agent.loginProvider, connection, get, request.signal);
184
+ await stable();
121
185
  if (!request.sessionId)
122
186
  throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
123
187
  await options.notice?.reconcile(request, api);
124
- stable();
188
+ await stable();
125
189
  const resolved = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal));
126
- stable();
190
+ await stable();
127
191
  if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
128
192
  throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
129
193
  validateTarget(resolved.target);
@@ -132,22 +196,28 @@ export function connectProvider(get, options = {}) {
132
196
  throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
133
197
  if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
134
198
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
199
+ const details = await options.details?.(request, api, connection, target);
200
+ await stable();
201
+ let workspaceNotice = undefined;
135
202
  if (resolved.notice) {
136
203
  if (!options.notice)
137
204
  throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
138
- await options.notice.deliver(resolved.notice, resolved, request, api);
139
- stable();
205
+ workspaceNotice = await options.notice.deliver(resolved.notice, resolved, request, api);
206
+ await stable();
140
207
  }
141
208
  if (!options.open)
142
209
  throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
143
210
  const runtime = await options.open(resolved.target, project, { ...request, agent });
144
211
  try {
145
- stable();
212
+ await stable();
146
213
  }
147
214
  catch (error) {
148
215
  await runtime.dispose?.();
149
216
  throw error;
150
217
  }
218
+ // The transport can outlive a turn; presentation belongs only to this invocation.
219
+ runtime.workspaceNotice = workspaceNotice || undefined;
220
+ runtime.details = details ?? runtime.details;
151
221
  runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
152
222
  runtime.connection = { ...connection };
153
223
  runtime.loginRequired = () => offerLogin(api, request, get, agent);
package/dist/readiness.js CHANGED
@@ -32,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
32
32
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
33
  done = true;
34
34
  };
35
+ import { loginDiagnostic } from "./login-diagnostic.js";
35
36
  import { randomUUID } from 'node:crypto';
36
37
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
37
38
  import { unwrap, awaitL, requireLoginCoordinator, requireLoginState } from "./provider.js";
@@ -56,18 +57,19 @@ function snapshot(api, agent = CODEX_AGENT) {
56
57
  if (connection.phase !== 'connected')
57
58
  return denied(connection.need?.code ?? (connection.phase === 'disposed' ? 'DISPOSED' : 'CONNECTION_LOST'));
58
59
  const state = api.getLoginPromptState();
60
+ const diagnostic = (reason) => ({ label: '登录检查快照', value: JSON.stringify(loginDiagnostic('readiness', reason, brand, connection, state)) });
59
61
  if (!connection.instanceId || state.instanceId !== connection.instanceId)
60
- return denied('AUTH_STATUS_INVALID');
62
+ return denied('AUTH_STATUS_INVALID', [diagnostic('LOGIN_INSTANCE_MISMATCH')]);
61
63
  requireLoginState(state);
62
64
  const technical = details({ hostContext: state.technicalDetails?.hostContext });
63
65
  if (state.phase === 'blocked' || state.phase === 'disposed')
64
- return denied(state.technicalDetails?.code ?? 'AUTH_STATUS_INVALID', technical);
65
- if (state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand)
66
+ return denied(state.technicalDetails?.code ?? 'AUTH_STATUS_INVALID', [...technical, diagnostic(state.phase === 'disposed' ? 'LOGIN_PHASE_DISPOSED' : 'LOGIN_PHASE_BLOCKED')]);
67
+ if (state.queuedProviders?.includes(brand) || state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand)
66
68
  return { phase: 'checking', loginStatus: 'unknown', title: `正在检查 ${agent.name.zh} 登录状态`, message: '远程主机连接正在检查登录状态。', technical };
67
69
  if (state.providerNeeds[brand])
68
- return denied(state.providerNeeds[brand].code, technical);
70
+ return denied(state.providerNeeds[brand].code, [...technical, diagnostic('LOGIN_PROVIDER_NEED')]);
69
71
  if (typeof state.logins?.[brand] !== 'boolean')
70
- return denied('AUTH_STATUS_INVALID', technical);
72
+ return denied('AUTH_STATUS_INVALID', [...technical, diagnostic('LOGIN_VALUE_INVALID')]);
71
73
  return state.logins[brand] ? { phase: 'ready', loginStatus: 'logged-in', title: `${agent.name.zh}已登录`, message: '本次远程主机连接和主机登录检查已完成。', technical } : { phase: 'login-required', loginStatus: 'not-logged-in', title: `${agent.name.zh} 尚未登录`, message: '需要登录,请到设置 → 远程主机连接 点登录', technical };
72
74
  }
73
75
  export function createReadiness(get, agent = CODEX_AGENT) {
@@ -0,0 +1,39 @@
1
+ import { type DetailObservation } from './conversation-details.ts';
2
+ import type { LoginDiagnostic } from './login-diagnostic.ts';
3
+ import type { ConnectApi as LConnectApi, ConnectionState } from '@ai-workbench/connect/api';
4
+ export type { RemoteConversationDetail as RemoteDetail } from '@ai-workbench/connect/api';
5
+ type Connection = Pick<ConnectionState, 'instanceId' | 'epoch' | 'phase'>;
6
+ export type RemoteDetailsApi = Pick<LConnectApi, 'getConnectionState'> & Partial<Pick<LConnectApi, 'getContractInfo' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'appendLoginDiagnostic' | 'getLoginDiagnosticReceipt' | 'listLoginDiagnostics'>>;
7
+ export interface DetailChannel {
8
+ record(observation: DetailObservation): void;
9
+ beginTurn(ids: {
10
+ turn: string | number | null;
11
+ step: string | number | null;
12
+ }): void;
13
+ endTurn(): void;
14
+ flush(): Promise<void>;
15
+ }
16
+ export interface DetailRequest {
17
+ sessionId: string;
18
+ provider: 'claude' | 'codex';
19
+ identity: string;
20
+ connection: Connection;
21
+ }
22
+ export declare function createRemoteDetails(options: {
23
+ profileDir: string;
24
+ getApi: () => RemoteDetailsApi | undefined;
25
+ }): {
26
+ recordLoginFailure(request: {
27
+ sessionId?: string;
28
+ code: string;
29
+ diagnostic: LoginDiagnostic;
30
+ context?: {
31
+ turn: string | number | null;
32
+ step: string | number | null;
33
+ };
34
+ }): Promise<{
35
+ eventId: string;
36
+ recorded: true;
37
+ }>;
38
+ open(request: DetailRequest): Promise<DetailChannel>;
39
+ };
@@ -0,0 +1,381 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
4
+ import { isAbsolute, join, parse } from 'node:path';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import { sanitizeObservation, isSanitizedRedaction } from "./conversation-details.js";
7
+ import { NeedError } from "./remote.js";
8
+ const failure = (code, message = '技术详情未能持久记录,本轮已停止。') => new NeedError(code, message, '可用的远程主机连接技术详情');
9
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
10
+ const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
11
+ function unwrap(result) {
12
+ if (!object(result))
13
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
14
+ if (result.ok === false && object(result.error) && typeof result.error.code === 'string')
15
+ throw failure(result.error.code);
16
+ if (result.ok !== true)
17
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
18
+ return result.value;
19
+ }
20
+ function identifier(value) { return value === null || typeof value === 'string' && value.trim().length > 0 || typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
21
+ function kind(observation) {
22
+ if (/UNKNOWN|UNSUPPORTED|FOREIGN/.test(observation.code))
23
+ return observation.severity === 'action-required' ? 'action-required' : 'unknown';
24
+ return { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind;
25
+ }
26
+ async function privateDirectory(path) {
27
+ if (!isAbsolute(path))
28
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
29
+ // Existing parent/profile ownership is the host's responsibility; reject symlinks
30
+ // along the full path before creating our own private child directories.
31
+ let part = parse(path).root;
32
+ for (const member of path.slice(part.length).split('/').filter(Boolean)) {
33
+ part = join(part, member);
34
+ try {
35
+ const info = await lstat(part);
36
+ if (!info.isDirectory() || info.isSymbolicLink())
37
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
38
+ }
39
+ catch (error) {
40
+ if (error?.code !== 'ENOENT')
41
+ throw error;
42
+ try {
43
+ await mkdir(part, { mode: 0o700 });
44
+ }
45
+ catch (creation) {
46
+ if (creation?.code !== 'EEXIST')
47
+ throw creation;
48
+ const info = await lstat(part);
49
+ if (!info.isDirectory() || info.isSymbolicLink())
50
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
51
+ }
52
+ }
53
+ }
54
+ if (await realpath(path) !== path)
55
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
56
+ const info = await lstat(path);
57
+ if ((info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
58
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
59
+ }
60
+ async function durableWrite(path, value) {
61
+ const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
62
+ try {
63
+ await file.writeFile(JSON.stringify(value) + '\n');
64
+ await file.sync();
65
+ }
66
+ finally {
67
+ await file.close();
68
+ }
69
+ const parent = await open(join(path, '..'), 'r');
70
+ try {
71
+ await parent.sync();
72
+ }
73
+ finally {
74
+ await parent.close();
75
+ }
76
+ }
77
+ async function readJournal(path) {
78
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
79
+ try {
80
+ const info = await file.stat();
81
+ if (!info.isFile() || (info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
82
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
83
+ return JSON.parse(await file.readFile('utf8'));
84
+ }
85
+ finally {
86
+ await file.close();
87
+ }
88
+ }
89
+ export function createRemoteDetails(options) {
90
+ const root = join(options.profileDir, 'ai-workbench-remote-agent', 'diagnostics');
91
+ const scopes = new Map(), loginScopes = new Map();
92
+ const initialize = async () => { await privateDirectory(join(options.profileDir, 'ai-workbench-remote-agent')); await privateDirectory(root); };
93
+ return {
94
+ async recordLoginFailure(request) {
95
+ await initialize();
96
+ const eventId = randomUUID(), diagnostic = request.diagnostic;
97
+ // The original observed IDs and full approved snapshot are durable before any L call.
98
+ 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' });
99
+ if (!request.sessionId)
100
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
101
+ const sessionId = request.sessionId, provider = diagnostic.provider, api = options.getApi(), connection = diagnostic.connection;
102
+ const stable = () => { const now = api?.getConnectionState(); if (!api || options.getApi() !== api || now?.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
103
+ throw failure('CONNECTION_LOST'); };
104
+ stable();
105
+ if (api.getContractInfo?.().loginDiagnostics !== 1 || !['appendLoginDiagnostic', 'getLoginDiagnosticReceipt', 'listLoginDiagnostics'].every(key => typeof api[key] === 'function'))
106
+ throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
107
+ const parent = join(root, 'login');
108
+ await privateDirectory(parent);
109
+ const key = hash(sessionId + '\0' + provider), dir = join(parent, key);
110
+ await privateDirectory(dir);
111
+ const scope = loginScopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
112
+ loginScopes.set(key, scope);
113
+ const operation = scope.tail.then(async () => {
114
+ stable();
115
+ const generation = connection.instanceId + '\0' + connection.epoch;
116
+ if (!scope.loaded || scope.generation !== generation) {
117
+ const names = await readdir(dir);
118
+ for (const name of names) {
119
+ const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
120
+ if (!match)
121
+ continue;
122
+ const sequence = Number(match[1]);
123
+ if (!Number.isSafeInteger(sequence))
124
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
125
+ scope.maximum = Math.max(scope.maximum, sequence);
126
+ const saved = await readJournal(join(dir, name));
127
+ if (!object(saved) || !object(saved.detail) || saved.detail.sequence !== sequence || saved.detail.eventId !== match[2] || saved.detail.sessionId !== sessionId || saved.detail.provider !== provider)
128
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
129
+ const settled = 'settled-' + match[2] + '.json';
130
+ if (names.includes(settled)) {
131
+ const receipt = await readJournal(join(dir, settled));
132
+ if (!object(receipt) || receipt.eventId !== match[2] || !['recorded', 'rejected'].includes(receipt.state))
133
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
134
+ continue;
135
+ }
136
+ stable();
137
+ const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId: match[2], sessionId, provider }));
138
+ stable();
139
+ if (receipt.eventId !== match[2] || !receipt.recorded)
140
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
141
+ if (!isDeepStrictEqual(receipt.detail, saved.detail))
142
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
143
+ await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
144
+ }
145
+ let cursor, previous = -1, count = 0, expected;
146
+ const seen = new Set();
147
+ do {
148
+ stable();
149
+ const page = unwrap(await api.listLoginDiagnostics({ sessionId, provider, ...(cursor ? { cursor } : {}) }));
150
+ stable();
151
+ if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
152
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
153
+ const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
154
+ if (expected && !isDeepStrictEqual(expected, counts))
155
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
156
+ expected = counts;
157
+ for (const entry of page.items) {
158
+ if (entry.sessionId !== sessionId || entry.provider !== provider || !Number.isSafeInteger(entry.sequence) || entry.sequence < 0 || entry.sequence <= previous)
159
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
160
+ previous = entry.sequence;
161
+ scope.maximum = Math.max(scope.maximum, entry.sequence);
162
+ count++;
163
+ }
164
+ cursor = page.nextCursor;
165
+ if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
166
+ throw failure('REMOTE_DETAIL_CURSOR_INVALID');
167
+ if (cursor)
168
+ seen.add(cursor);
169
+ } while (cursor);
170
+ if (count !== expected?.total)
171
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
172
+ scope.loaded = true;
173
+ scope.generation = generation;
174
+ }
175
+ const mismatch = diagnostic.reason === 'LOGIN_INSTANCE_MISMATCH', unsupported = !!diagnostic.officialReasonRejected;
176
+ const reason = mismatch ? 'LOGIN_INSTANCE_MISMATCH' : unsupported ? 'OFFICIAL_REASON_UNSUPPORTED' : diagnostic.reason;
177
+ // L 0.7.11: observed evidence never changes the current connection scope.
178
+ 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 };
179
+ const payload = { reason, connection: { ...connection }, login, ...(mismatch ? { observedLoginInstanceId: diagnostic.login.instanceId } : {}), ...(diagnostic.officialReason ? { officialReason: diagnostic.officialReason } : {}), ...(unsupported && !mismatch ? { officialReasonRejected: diagnostic.officialReasonRejected } : {}) };
180
+ // The two payload reasons are exclusive in L's contract. Preserve both facts
181
+ // using the approved fixed summary and full digest, without inventing a field.
182
+ const rejected = diagnostic.officialReasonRejected;
183
+ const summary = '登录检查:' + diagnostic.reason + (rejected ? '; OFFICIAL_REASON_UNSUPPORTED; bytes=' + rejected.bytes + '; SHA256=' + rejected.sha256 : '');
184
+ const context = request.context ?? { turn: null, step: null };
185
+ if (!identifier(context.turn) || !identifier(context.step))
186
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
187
+ const sequence = scope.maximum + 1;
188
+ if (!Number.isSafeInteger(sequence))
189
+ throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
190
+ scope.maximum = sequence;
191
+ 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 };
192
+ await durableWrite(join(dir, sequence + '-' + eventId + '.json'), { state: 'pending', detail });
193
+ stable();
194
+ let response;
195
+ try {
196
+ response = await api.appendLoginDiagnostic(structuredClone(detail));
197
+ }
198
+ catch {
199
+ stable();
200
+ const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId, sessionId, provider }));
201
+ stable();
202
+ if (receipt.eventId !== eventId)
203
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
204
+ if (!receipt.recorded)
205
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
206
+ if (!isDeepStrictEqual(receipt.detail, detail))
207
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
208
+ await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
209
+ return;
210
+ }
211
+ stable();
212
+ if (response?.ok === false)
213
+ await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'rejected', code: response.error.code });
214
+ const ack = unwrap(response);
215
+ if (ack.eventId !== eventId || ack.recorded !== true)
216
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
217
+ await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
218
+ });
219
+ scope.tail = operation.then(() => { }, () => { scope.loaded = false; });
220
+ await operation;
221
+ return { eventId, recorded: true };
222
+ },
223
+ async open(request) {
224
+ if (!request.sessionId || !request.identity || !request.connection.instanceId || !Number.isSafeInteger(request.connection.epoch) || request.connection.epoch < 0 || !['claude', 'codex'].includes(request.provider))
225
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID');
226
+ try {
227
+ await initialize();
228
+ }
229
+ catch {
230
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
231
+ }
232
+ const api = options.getApi();
233
+ const stable = () => {
234
+ const now = api?.getConnectionState();
235
+ if (!api || options.getApi() !== api || now?.phase !== 'connected' || now.instanceId !== request.connection.instanceId || now.epoch !== request.connection.epoch)
236
+ throw failure('CONNECTION_LOST');
237
+ };
238
+ stable();
239
+ const capability = api.getContractInfo?.();
240
+ if (!object(capability) || capability.remoteConversationDetails !== 1 || !['appendRemoteConversationDetail', 'getRemoteConversationDetailReceipt', 'listRemoteConversationDetails'].every(key => typeof api[key] === 'function')) {
241
+ await durableWrite(join(root, randomUUID() + '.json'), { version: 1, code: 'TECHNICAL_DETAILS_UNAVAILABLE', summary: '技术详情暂不可用', session: hash(request.sessionId), provider: request.provider });
242
+ throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
243
+ }
244
+ const key = hash(request.identity + '\0' + request.sessionId + '\0' + request.provider), dir = join(root, key);
245
+ await privateDirectory(dir);
246
+ const scope = scopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
247
+ scopes.set(key, scope);
248
+ // Serialize open and appends in this host-user/session/brand scope.
249
+ const load = scope.tail.then(async () => {
250
+ stable();
251
+ const generation = request.connection.instanceId + '\0' + request.connection.epoch;
252
+ if (scope.loaded && scope.generation === generation)
253
+ return;
254
+ const names = await readdir(dir);
255
+ for (const name of names) {
256
+ const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
257
+ if (!match)
258
+ continue;
259
+ const n = Number(match[1]);
260
+ if (!Number.isSafeInteger(n))
261
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
262
+ scope.maximum = Math.max(scope.maximum, n);
263
+ const saved = await readJournal(join(dir, name));
264
+ if (!object(saved) || !object(saved.detail) || saved.detail.eventId !== match[2] || saved.detail.sequence !== n || saved.detail.sessionId !== request.sessionId || saved.detail.provider !== request.provider)
265
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
266
+ const settled = 'settled-' + match[2] + '.json';
267
+ if (names.includes(settled)) {
268
+ const result = await readJournal(join(dir, settled));
269
+ if (!object(result) || result.eventId !== match[2] || !['recorded', 'rejected'].includes(result.state))
270
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
271
+ continue;
272
+ }
273
+ // An interrupted append is reconciled by pure read, with the original
274
+ // event's epoch intact. Missing receipts stay unknown and never trigger append.
275
+ stable();
276
+ const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: match[2], sessionId: request.sessionId, provider: request.provider }));
277
+ stable();
278
+ if (receipt.eventId !== match[2])
279
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
280
+ if (!receipt.recorded)
281
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
282
+ if (!isDeepStrictEqual(receipt.detail, saved.detail))
283
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
284
+ await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
285
+ }
286
+ let cursor, previous = -1;
287
+ const seen = new Set();
288
+ let expected;
289
+ let count = 0;
290
+ do {
291
+ stable();
292
+ const page = unwrap(await api.listRemoteConversationDetails({ sessionId: request.sessionId, provider: request.provider, ...(cursor ? { cursor } : {}) }));
293
+ stable();
294
+ if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
295
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
296
+ const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
297
+ if (expected && !isDeepStrictEqual(expected, counts))
298
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
299
+ expected = counts;
300
+ for (const entry of page.items) {
301
+ if (entry.sessionId !== request.sessionId || entry.provider !== request.provider || !Number.isSafeInteger(entry.sequence) || entry.sequence <= previous)
302
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
303
+ previous = entry.sequence;
304
+ scope.maximum = Math.max(scope.maximum, entry.sequence);
305
+ count++;
306
+ }
307
+ cursor = page.nextCursor;
308
+ if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
309
+ throw failure('REMOTE_DETAIL_CURSOR_INVALID');
310
+ if (cursor)
311
+ seen.add(cursor);
312
+ } while (cursor);
313
+ if (count !== expected?.total)
314
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
315
+ scope.loaded = true;
316
+ scope.generation = generation;
317
+ });
318
+ scope.tail = load.then(() => { }, () => { });
319
+ await load;
320
+ let turn = null, step = null;
321
+ let tail = Promise.resolve(), firstFailure;
322
+ const enqueue = (operation) => {
323
+ const task = scope.tail.then(operation);
324
+ scope.tail = task.then(() => { }, () => { });
325
+ tail = task.catch(error => { scope.loaded = false; firstFailure ??= { error }; });
326
+ };
327
+ return {
328
+ beginTurn(ids) { if (!identifier(ids.turn) || !identifier(ids.step))
329
+ throw failure('REMOTE_DETAIL_SCOPE_INVALID'); turn = ids.turn; step = ids.step; },
330
+ endTurn() { turn = null; step = null; },
331
+ record(observation) {
332
+ const safe = sanitizeObservation(observation, true);
333
+ const wire = (value) => Array.isArray(value) ? value.map(wire) : object(value) ? isSanitizedRedaction(value) ? (typeof value.valueType === 'string' ? { redacted: true, valueType: value.valueType, value: '已脱敏' } : '已脱敏') : Object.fromEntries(Object.entries(value).map(([key, child]) => [key, wire(child)])) : value;
334
+ const sequence = scope.maximum + 1;
335
+ if (!Number.isSafeInteger(sequence))
336
+ throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
337
+ scope.maximum = sequence;
338
+ const detail = Object.freeze({ ...safe, payload: wire(safe.payload), version: 1, eventId: randomUUID(), instanceId: request.connection.instanceId, epoch: request.connection.epoch, sessionId: request.sessionId, provider: request.provider, turn, step, sequence, kind: kind(observation) });
339
+ enqueue(async () => {
340
+ // A private X journal is retained even if L is disconnected or rejects it.
341
+ const file = join(dir, sequence + '-' + detail.eventId + '.json');
342
+ try {
343
+ await durableWrite(file, { state: 'pending', detail });
344
+ }
345
+ catch {
346
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
347
+ }
348
+ stable();
349
+ let response;
350
+ try {
351
+ response = await api.appendRemoteConversationDetail(structuredClone(detail));
352
+ }
353
+ catch {
354
+ stable();
355
+ const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
356
+ stable();
357
+ if (receipt.eventId !== detail.eventId)
358
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
359
+ if (!receipt.recorded)
360
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
361
+ if (!isDeepStrictEqual(receipt.detail, detail))
362
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
363
+ await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
364
+ return;
365
+ }
366
+ stable();
367
+ if (response?.ok === false) {
368
+ await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
369
+ }
370
+ const ack = unwrap(response);
371
+ if (ack.eventId !== detail.eventId || ack.recorded !== true)
372
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
373
+ await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
374
+ });
375
+ },
376
+ async flush() { await tail; if (firstFailure)
377
+ throw firstFailure.error; stable(); },
378
+ };
379
+ }
380
+ };
381
+ }