@ai-workbench/remote-agent 0.8.19 → 0.8.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +27 -134
  2. package/dist/acp.d.ts +23 -16
  3. package/dist/acp.js +209 -226
  4. package/dist/agents.d.ts +0 -1
  5. package/dist/agents.js +2 -2
  6. package/dist/broker-runtime.d.ts +3 -5
  7. package/dist/broker-runtime.js +45 -342
  8. package/dist/broker.d.ts +6 -31
  9. package/dist/broker.js +42 -113
  10. package/dist/catalog-ssh.d.ts +2 -31
  11. package/dist/catalog-ssh.js +9 -39
  12. package/dist/claude-local.d.ts +1 -1
  13. package/dist/claude-local.js +41 -105
  14. package/dist/client.js +34 -126
  15. package/dist/close-diagnostics.d.ts +6 -20
  16. package/dist/close-diagnostics.js +13 -57
  17. package/dist/connect-contract.d.ts +4 -8
  18. package/dist/conversation-details.d.ts +0 -29
  19. package/dist/conversation-details.js +8 -99
  20. package/dist/employee-endpoint.d.ts +1 -1
  21. package/dist/employee-endpoint.js +21 -19
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +7 -1
  25. package/dist/employee-info.js +24 -3
  26. package/dist/employee-location.js +3 -4
  27. package/dist/employee-mcp.js +44 -69
  28. package/dist/employee-native.d.ts +1 -3
  29. package/dist/employee-native.js +6 -27
  30. package/dist/employee-presentation.d.ts +0 -5
  31. package/dist/employee-presentation.js +0 -15
  32. package/dist/employee-provider.d.ts +3 -11
  33. package/dist/employee-provider.js +69 -205
  34. package/dist/employee-reasons.js +1 -5
  35. package/dist/employee-shell.d.ts +2 -1
  36. package/dist/employee-shell.js +10 -13
  37. package/dist/employee-ssh.d.ts +1 -3
  38. package/dist/employee-ssh.js +13 -16
  39. package/dist/employee-tools.d.ts +5 -5
  40. package/dist/failure-evidence.d.ts +2 -10
  41. package/dist/failure-evidence.js +12 -61
  42. package/dist/host-workspace.d.ts +4 -2
  43. package/dist/host-workspace.js +6 -2
  44. package/dist/index.d.ts +13 -43
  45. package/dist/index.js +192 -484
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +5 -3
  49. package/dist/mapping.js +31 -36
  50. package/dist/model-catalog.d.ts +2 -41
  51. package/dist/model-catalog.js +24 -282
  52. package/dist/model-configuration.d.ts +0 -3
  53. package/dist/model-configuration.js +8 -47
  54. package/dist/model-presentation.d.ts +1 -0
  55. package/dist/model-presentation.js +2 -2
  56. package/dist/permissions.d.ts +15 -0
  57. package/dist/permissions.js +30 -0
  58. package/dist/preflight-diagnostic.js +11 -22
  59. package/dist/preflight-payload.d.ts +1 -1
  60. package/dist/preflight-payload.js +2 -2
  61. package/dist/presentation.js +21 -41
  62. package/dist/provider.d.ts +12 -34
  63. package/dist/provider.js +17 -92
  64. package/dist/readiness.js +1 -7
  65. package/dist/remote-details.d.ts +41 -17
  66. package/dist/remote-details.js +84 -381
  67. package/dist/workspace-source.d.ts +1 -25
  68. package/dist/workspace-source.js +1 -138
  69. package/package.json +11 -12
  70. package/dist/catalog-diagnostics.d.ts +0 -16
  71. package/dist/catalog-diagnostics.js +0 -108
  72. package/dist/host-layout.d.ts +0 -8
  73. package/dist/host-layout.js +0 -9
  74. package/dist/job.d.ts +0 -17
  75. package/dist/job.js +0 -57
  76. package/dist/model-defaults.d.ts +0 -29
  77. package/dist/model-defaults.js +0 -62
  78. package/dist/oneshot.d.ts +0 -13
  79. package/dist/oneshot.js +0 -78
  80. package/dist/protocol-boundary.d.ts +0 -13
  81. package/dist/protocol-boundary.js +0 -49
  82. package/dist/provider-locale.d.ts +0 -19
  83. package/dist/provider-locale.js +0 -73
  84. package/dist/recovery-checkpoint.d.ts +0 -6
  85. package/dist/recovery-checkpoint.js +0 -71
  86. package/dist/recovery.d.ts +0 -51
  87. package/dist/recovery.js +0 -61
  88. package/dist/sdk-ssh-wrapper.d.ts +0 -2
  89. package/dist/sdk-ssh-wrapper.js +0 -34
  90. package/dist/stage-timing.d.ts +0 -31
  91. package/dist/stage-timing.js +0 -61
  92. package/dist/workspace-notice.d.ts +0 -47
  93. package/dist/workspace-notice.js +0 -273
  94. package/dist/workspace-source-confirmation.d.ts +0 -4
  95. package/dist/workspace-source-confirmation.js +0 -12
@@ -1,30 +1,12 @@
1
- import { closeCode, withFailureEvidence } from "./close-diagnostics.js";
2
- import { createHash, randomUUID } from 'node:crypto';
1
+ import { closeCode } from "./close-diagnostics.js";
2
+ import { randomUUID, createHash } from 'node:crypto';
3
3
  import { constants } from 'node:fs';
4
- import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
4
+ import { mkdir, open, lstat, realpath, appendFile } from 'node:fs/promises';
5
5
  import { isAbsolute, join, parse } from 'node:path';
6
- import { isDeepStrictEqual } from 'node:util';
7
6
  import { sanitizeObservation, isSanitizedRedaction } from "./conversation-details.js";
8
- import { validPreflightPayload } from "./preflight-payload.js";
9
7
  import { NeedError } from "./remote.js";
10
- const failure = (code, message = '技术详情未能持久记录,本轮已停止。') => new NeedError(code, message, '可用的远程主机连接技术详情');
11
- const hash = (value) => createHash('sha256').update(value).digest('hex');
8
+ const failure = (code) => new NeedError(code, '技术详情未能保存', '诊断存储');
12
9
  const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
13
- function unwrap(result) {
14
- if (!object(result))
15
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
16
- if (result.ok === false && object(result.error) && typeof result.error.code === 'string')
17
- throw failure(result.error.code);
18
- if (result.ok !== true)
19
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
20
- return result.value;
21
- }
22
- function identifier(value) { return value === null || typeof value === 'string' && value.trim().length > 0 || typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
23
- function kind(observation) {
24
- if (/UNKNOWN|UNSUPPORTED|FOREIGN/.test(observation.code))
25
- return observation.severity === 'action-required' ? 'action-required' : 'unknown';
26
- return { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind;
27
- }
28
10
  export async function privateDirectory(path) {
29
11
  if (!isAbsolute(path))
30
12
  throw failure('REMOTE_DETAIL_STORAGE_FAILED');
@@ -59,376 +41,97 @@ export async function privateDirectory(path) {
59
41
  if ((info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
60
42
  throw failure('REMOTE_DETAIL_STORAGE_FAILED');
61
43
  }
62
- export async function durableWrite(path, value) {
63
- const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
64
- try {
65
- await file.writeFile(JSON.stringify(value) + '\n');
66
- await file.sync();
67
- }
68
- finally {
69
- await file.close();
70
- }
71
- const parent = await open(join(path, '..'), 'r');
72
- try {
73
- await parent.sync();
74
- }
75
- finally {
76
- await parent.close();
77
- }
78
- }
79
- export async function readJournal(path) {
80
- const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
81
- try {
82
- const info = await file.stat();
83
- if (!info.isFile() || (info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
84
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
85
- return JSON.parse(await file.readFile('utf8'));
86
- }
87
- finally {
88
- await file.close();
89
- }
90
- }
44
+ /** Diagnostics are best effort. No delivery journal, receipt lookup, replay or turn gate. */
91
45
  export function createRemoteDetails(options) {
92
- const root = join(options.profileDir, 'ai-workbench-remote-agent', 'diagnostics');
93
- const scopes = new Map(), loginScopes = new Map();
94
- const initialize = async () => { await privateDirectory(join(options.profileDir, 'ai-workbench-remote-agent')); await privateDirectory(root); };
95
- async function recordConnectionDiagnostic(request) {
96
- request = { ...request, connection: request.connection && structuredClone(request.connection), payload: structuredClone(request.payload), snapshot: structuredClone(request.snapshot), context: request.context && { ...request.context } };
97
- await initialize();
46
+ let sequence = 0;
47
+ let writes = Promise.resolve();
48
+ const memory = new Map();
49
+ const filename = (sessionId, provider) => createHash('sha256').update(sessionId + '\0' + provider).digest('hex') + '.jsonl';
50
+ const root = options.profileDir ? join(options.profileDir, 'ai-workbench-remote-agent', 'logs') : undefined;
51
+ const warn = (error) => { try {
52
+ (options.warn ?? console.warn)('[remote-agent] 技术详情未保存:' + closeCode(error, 'DIAGNOSTIC_WRITE_FAILED'));
53
+ }
54
+ catch { /* Logging cannot become a turn gate. */ } };
55
+ 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;
56
+ function send(request, observation, context, diagnosticKind, owner) {
98
57
  const eventId = randomUUID();
99
- 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' });
100
- if (!request.sessionId)
101
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
102
- const sessionId = request.sessionId, provider = request.provider, api = request.binding ? request.binding.api : options.getApi(), connection = request.connection;
103
- if (!connection)
104
- throw failure('CONNECTION_LOST');
105
- 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)
106
- throw failure('CONNECTION_LOST'); };
107
- stable();
108
- const info = api.getContractInfo?.();
109
- if ((request.kind === 'login-check' ? info?.loginDiagnostics : info?.preflightDiagnostics) !== 1 || !['appendLoginDiagnostic', 'getLoginDiagnosticReceipt', 'listLoginDiagnostics'].every(key => typeof api[key] === 'function'))
110
- throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
111
- const parent = join(root, 'login');
112
- await privateDirectory(parent);
113
- const key = hash(sessionId + '\0' + provider), dir = join(parent, key);
114
- await privateDirectory(dir);
115
- const scope = loginScopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
116
- loginScopes.set(key, scope);
117
- const operation = scope.tail.then(async () => {
118
- stable();
119
- const generation = connection.instanceId + '\0' + connection.epoch;
120
- if (!scope.loaded || scope.generation !== generation) {
121
- const names = await readdir(dir);
122
- for (const name of names) {
123
- const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
124
- if (!match)
125
- continue;
126
- const sequence = Number(match[1]);
127
- if (!Number.isSafeInteger(sequence))
128
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
129
- scope.maximum = Math.max(scope.maximum, sequence);
130
- const saved = await readJournal(join(dir, name));
131
- if (!object(saved) || !object(saved.detail) || saved.detail.sequence !== sequence || saved.detail.eventId !== match[2] || saved.detail.sessionId !== sessionId || saved.detail.provider !== provider)
132
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
133
- const settled = 'settled-' + match[2] + '.json';
134
- if (names.includes(settled)) {
135
- const receipt = await readJournal(join(dir, settled));
136
- if (!object(receipt) || receipt.eventId !== match[2] || !['recorded', 'rejected'].includes(receipt.state))
137
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
138
- continue;
139
- }
140
- stable();
141
- const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId: match[2], sessionId, provider }));
142
- stable();
143
- if (receipt.eventId !== match[2] || !receipt.recorded)
144
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
145
- if (!isDeepStrictEqual(receipt.detail, saved.detail))
146
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
147
- await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
58
+ try {
59
+ const connection = request.connection;
60
+ if (!request.sessionId)
61
+ throw failure('DIAGNOSTIC_CONTEXT_UNAVAILABLE');
62
+ const safe = sanitizeObservation(observation, true);
63
+ sequence = Math.max(sequence + 1, Date.now());
64
+ const detail = { ...safe, payload: wire(safe.payload), version: 1, eventId, instanceId: connection?.instanceId ?? null, epoch: connection?.epoch ?? null, sessionId: request.sessionId, provider: request.provider, ...context, sequence, kind: diagnosticKind ?? { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind };
65
+ const name = filename(request.sessionId, request.provider), records = memory.get(name);
66
+ if (records && (!owner || owner === records))
67
+ records.push(detail);
68
+ writes = writes.then(async () => { if (!root)
69
+ throw failure('DIAGNOSTIC_CONTEXT_UNAVAILABLE'); await privateDirectory(join(root, '..')); await privateDirectory(root); await appendFile(join(root, name), JSON.stringify(detail) + '\n', { mode: 0o600, flag: constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW }); }).catch(warn);
70
+ }
71
+ catch (error) {
72
+ warn(error);
73
+ }
74
+ return { eventId };
75
+ }
76
+ return {
77
+ /** Await queued best-effort writes only when releasing the profile, never before dispatch. */
78
+ async dispose() { await writes; memory.clear(); },
79
+ releaseSession(sessionId) { for (const provider of ['claude', 'codex'])
80
+ memory.delete(filename(sessionId, provider)); },
81
+ async read(query) {
82
+ await writes;
83
+ const name = filename(query.sessionId, query.provider), records = new Map();
84
+ try {
85
+ if (!root)
86
+ throw failure('DIAGNOSTIC_CONTEXT_UNAVAILABLE');
87
+ await privateDirectory(join(root, '..'));
88
+ await privateDirectory(root);
89
+ const file = await open(join(root, name), constants.O_RDONLY | constants.O_NOFOLLOW);
90
+ let value;
91
+ try {
92
+ const info = await file.stat();
93
+ if (!info.isFile() || (info.mode & 0o077) !== 0 || process.getuid && info.uid !== process.getuid())
94
+ throw failure('DIAGNOSTIC_LOG_INVALID');
95
+ value = await file.readFile('utf8');
148
96
  }
149
- let cursor, previous = -1, count = 0, expected;
150
- const seen = new Set();
151
- do {
152
- stable();
153
- const page = unwrap(await api.listLoginDiagnostics({ sessionId, provider, ...(cursor ? { cursor } : {}) }));
154
- stable();
155
- if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
156
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
157
- const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
158
- if (expected && !isDeepStrictEqual(expected, counts))
159
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
160
- expected = counts;
161
- for (const entry of page.items) {
162
- if (entry.sessionId !== sessionId || entry.provider !== provider || !Number.isSafeInteger(entry.sequence) || entry.sequence < 0 || entry.sequence <= previous)
163
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
164
- previous = entry.sequence;
165
- scope.maximum = Math.max(scope.maximum, entry.sequence);
166
- count++;
97
+ finally {
98
+ await file.close();
99
+ }
100
+ for (const line of value.split('\n'))
101
+ if (line) {
102
+ try {
103
+ const record = JSON.parse(line);
104
+ if (record.sessionId === query.sessionId && record.provider === query.provider)
105
+ records.set(record.eventId, record);
106
+ }
107
+ catch {
108
+ warn(failure('DIAGNOSTIC_LOG_INVALID'));
109
+ }
167
110
  }
168
- cursor = page.nextCursor;
169
- if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
170
- throw failure('REMOTE_DETAIL_CURSOR_INVALID');
171
- if (cursor)
172
- seen.add(cursor);
173
- } while (cursor);
174
- if (count !== expected?.total)
175
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
176
- scope.loaded = true;
177
- scope.generation = generation;
178
111
  }
179
- const context = request.context ?? { turn: null, step: null };
180
- if (!identifier(context.turn) || !identifier(context.step))
181
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
182
- const sequence = scope.maximum + 1;
183
- if (!Number.isSafeInteger(sequence))
184
- throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
185
- scope.maximum = sequence;
186
- 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 };
187
- await durableWrite(join(dir, sequence + '-' + eventId + '.json'), { state: 'pending', detail });
188
- stable();
189
- let response;
190
- try {
191
- response = await api.appendLoginDiagnostic(structuredClone(detail));
112
+ catch (error) {
113
+ if (error.code !== 'ENOENT')
114
+ warn(error);
192
115
  }
193
- catch {
194
- stable();
195
- const receipt = unwrap(await api.getLoginDiagnosticReceipt({ eventId, sessionId, provider }));
196
- stable();
197
- if (receipt.eventId !== eventId)
198
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
199
- if (!receipt.recorded)
200
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
201
- if (!isDeepStrictEqual(receipt.detail, detail))
202
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
203
- await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
204
- return;
205
- }
206
- stable();
207
- if (response?.ok === false)
208
- await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'rejected', code: response.error.code });
209
- const ack = unwrap(response);
210
- if (ack.eventId !== eventId || ack.recorded !== true)
211
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
212
- await durableWrite(join(dir, 'settled-' + eventId + '.json'), { eventId, state: 'recorded' });
213
- });
214
- scope.tail = operation.then(() => { }, () => { scope.loaded = false; });
215
- await operation;
216
- return { eventId, recorded: true };
217
- }
218
- return {
219
- async recordLocalFailure(request) {
220
- await initialize();
221
- if (!/^[A-Z][A-Z0-9_]*$/.test(request.code))
222
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
223
- 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) } : {} });
116
+ for (const record of memory.get(name) ?? [])
117
+ records.set(record.eventId, record);
118
+ return structuredClone([...records.values()]);
224
119
  },
120
+ async recordLocalFailure(request) { send(request, request.observation ?? { kind: 'need', severity: 'warning', code: request.code, summary: '本轮未完成', payload: {} }, { turn: null, step: null }); },
225
121
  async recordLoginFailure(request) {
226
- const diagnostic = request.diagnostic, connection = diagnostic.connection;
227
- const mismatch = diagnostic.reason === 'LOGIN_INSTANCE_MISMATCH', unsupported = !!diagnostic.officialReasonRejected;
228
- const reason = mismatch ? 'LOGIN_INSTANCE_MISMATCH' : unsupported ? 'OFFICIAL_REASON_UNSUPPORTED' : diagnostic.reason;
229
- // L 0.7.11: observed evidence never changes the current connection scope.
230
- 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 };
231
- const payload = { reason, connection: { ...connection }, login, ...(mismatch ? { observedLoginInstanceId: diagnostic.login.instanceId } : {}), ...(diagnostic.officialReason ? { officialReason: diagnostic.officialReason } : {}), ...(unsupported && !mismatch ? { officialReasonRejected: diagnostic.officialReasonRejected } : {}) };
232
- // The two payload reasons are exclusive in L's contract. Preserve both facts
233
- // using the approved fixed summary and full digest, without inventing a field.
234
- const rejected = diagnostic.officialReasonRejected;
235
- const summary = '登录检查:' + diagnostic.reason + (rejected ? '; OFFICIAL_REASON_UNSUPPORTED; bytes=' + rejected.bytes + '; SHA256=' + rejected.sha256 : '');
236
- return recordConnectionDiagnostic({ ...request, provider: diagnostic.provider, connection: diagnostic.connection, kind: 'login-check', severity: 'action-required', summary, method: diagnostic.source, payload, snapshot: { diagnostic } });
122
+ const d = request.diagnostic, mismatch = d.reason === 'LOGIN_INSTANCE_MISMATCH', unsupported = !!d.officialReasonRejected;
123
+ const login = { phase: d.login.phase, instanceId: d.login.instanceId, logins: Object.fromEntries(Object.entries(d.login.logins).filter(([, v]) => typeof v === 'boolean' || v === 'absent')), queuedProviders: d.login.queuedProviders, checkingProviders: d.login.checkingProviders };
124
+ const payload = { reason: mismatch ? 'LOGIN_INSTANCE_MISMATCH' : unsupported ? 'OFFICIAL_REASON_UNSUPPORTED' : d.reason, connection: { ...d.connection }, login, ...(mismatch ? { observedLoginInstanceId: d.login.instanceId } : {}), ...(d.officialReason ? { officialReason: d.officialReason } : {}), ...(unsupported && !mismatch ? { officialReasonRejected: d.officialReasonRejected } : {}) };
125
+ return send({ sessionId: request.sessionId, provider: d.provider, connection: d.connection }, { kind: 'need', severity: 'warning', code: request.code, summary: '登录检查:' + d.reason, method: d.source, payload: payload }, request.context ?? { turn: null, step: null }, 'login-check');
237
126
  },
238
127
  async recordPreflightCheck(request) {
239
- const observation = sanitizeObservation(request.observation, true);
240
- if (!/^[A-Z][A-Z0-9_]*$/.test(observation.code) || !validPreflightPayload(observation.payload))
241
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
242
- const api = request.binding ? request.binding.api : options.getApi(), connection = request.binding ? request.binding.connection : api?.getConnectionState();
243
- 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 } });
128
+ return send({ sessionId: request.sessionId, provider: request.provider, connection: request.binding?.connection }, request.observation, request.context ?? { turn: null, step: null }, 'preflight-check');
244
129
  },
245
130
  async open(request) {
246
- request = structuredClone(request);
247
- if (request.localAcceptanceId !== undefined && (typeof request.localAcceptanceId !== 'string' || !request.localAcceptanceId.trim()))
248
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
249
- const localScope = request.localAcceptanceId === undefined ? undefined : Object.freeze({ localAcceptanceId: request.localAcceptanceId });
250
- if (!request.sessionId || !request.identity || !request.connection.instanceId || !Number.isSafeInteger(request.connection.epoch) || request.connection.epoch < 0 || !['claude', 'codex'].includes(request.provider))
251
- throw failure('REMOTE_DETAIL_SCOPE_INVALID');
252
- try {
253
- await initialize();
254
- }
255
- catch {
256
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
257
- }
258
- const api = options.getApi();
259
- const stable = () => {
260
- const now = api?.getConnectionState();
261
- if (!api || options.getApi() !== api || now?.phase !== 'connected' || now.instanceId !== request.connection.instanceId || now.epoch !== request.connection.epoch)
262
- throw failure('CONNECTION_LOST');
263
- };
264
- stable();
265
- const capability = api.getContractInfo?.();
266
- if (!object(capability) || capability.remoteConversationDetails !== 1 || !['appendRemoteConversationDetail', 'getRemoteConversationDetailReceipt', 'listRemoteConversationDetails'].every(key => typeof api[key] === 'function')) {
267
- await durableWrite(join(root, randomUUID() + '.json'), { version: 1, code: 'TECHNICAL_DETAILS_UNAVAILABLE', summary: '技术详情暂不可用', session: hash(request.sessionId), provider: request.provider });
268
- throw failure('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。');
269
- }
270
- const key = hash(request.identity + '\0' + request.sessionId + '\0' + request.provider), dir = join(root, key);
271
- await privateDirectory(dir);
272
- const scope = scopes.get(key) ?? { tail: Promise.resolve(), maximum: 0, loaded: false };
273
- scopes.set(key, scope);
274
- // Serialize open and appends in this host-user/session/brand scope.
275
- const load = scope.tail.then(async () => {
276
- stable();
277
- const generation = request.connection.instanceId + '\0' + request.connection.epoch;
278
- if (scope.loaded && scope.generation === generation)
279
- return;
280
- const names = await readdir(dir);
281
- for (const name of names) {
282
- const match = /^(\d+)-([0-9a-f-]+)\.json$/.exec(name);
283
- if (!match)
284
- continue;
285
- const n = Number(match[1]);
286
- if (!Number.isSafeInteger(n))
287
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
288
- scope.maximum = Math.max(scope.maximum, n);
289
- const saved = await readJournal(join(dir, name));
290
- 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)
291
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
292
- const settled = 'settled-' + match[2] + '.json';
293
- if (names.includes(settled)) {
294
- const result = await readJournal(join(dir, settled));
295
- if (!object(result) || result.eventId !== match[2] || !['recorded', 'rejected'].includes(result.state))
296
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
297
- continue;
298
- }
299
- // An interrupted append is reconciled by pure read, with the original
300
- // event's epoch intact. Missing receipts stay unknown and never trigger append.
301
- stable();
302
- const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: match[2], sessionId: request.sessionId, provider: request.provider }));
303
- stable();
304
- if (receipt.eventId !== match[2])
305
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
306
- if (!receipt.recorded)
307
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
308
- if (!isDeepStrictEqual(receipt.detail, saved.detail))
309
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
310
- await durableWrite(join(dir, settled), { eventId: match[2], state: 'recorded' });
311
- }
312
- let cursor, previous = -1;
313
- const seen = new Set();
314
- let expected;
315
- let count = 0;
316
- do {
317
- stable();
318
- const page = unwrap(await api.listRemoteConversationDetails({ sessionId: request.sessionId, provider: request.provider, ...(cursor ? { cursor } : {}) }));
319
- stable();
320
- if (!object(page) || !Array.isArray(page.items) || ![page.total, page.unknownCount, page.actionRequiredCount].every(n => Number.isSafeInteger(n) && n >= 0))
321
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
322
- const counts = { total: page.total, unknownCount: page.unknownCount, actionRequiredCount: page.actionRequiredCount };
323
- if (expected && !isDeepStrictEqual(expected, counts))
324
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
325
- expected = counts;
326
- for (const entry of page.items) {
327
- if (entry.sessionId !== request.sessionId || entry.provider !== request.provider || !Number.isSafeInteger(entry.sequence) || entry.sequence <= previous)
328
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
329
- previous = entry.sequence;
330
- scope.maximum = Math.max(scope.maximum, entry.sequence);
331
- count++;
332
- }
333
- cursor = page.nextCursor;
334
- if (cursor !== undefined && (typeof cursor !== 'string' || !cursor || seen.has(cursor)))
335
- throw failure('REMOTE_DETAIL_CURSOR_INVALID');
336
- if (cursor)
337
- seen.add(cursor);
338
- } while (cursor);
339
- if (count !== expected?.total)
340
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
341
- scope.loaded = true;
342
- scope.generation = generation;
343
- });
344
- scope.tail = load.then(() => { }, () => { });
345
- await load;
346
- let turn = null, step = null;
347
- let tail = Promise.resolve(), firstFailure;
348
- const enqueue = (operation) => {
349
- const task = scope.tail.then(operation);
350
- scope.tail = task.then(() => { }, () => { });
351
- tail = task.catch(error => { scope.loaded = false; firstFailure ??= { error }; });
352
- };
353
- return {
354
- beginTurn(ids) { if (!identifier(ids.turn) || !identifier(ids.step))
355
- throw failure('REMOTE_DETAIL_SCOPE_INVALID'); turn = ids.turn; step = ids.step; },
356
- endTurn() { turn = null; step = null; },
357
- record(observation) {
358
- const safe = sanitizeObservation(observation, true);
359
- 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;
360
- const sequence = scope.maximum + 1;
361
- if (!Number.isSafeInteger(sequence))
362
- throw failure('REMOTE_DETAIL_SEQUENCE_INVALID');
363
- scope.maximum = sequence;
364
- 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) });
365
- enqueue(async () => {
366
- // Pending is not a delivery verdict. Keep the exact failed stage separately.
367
- const file = join(dir, sequence + '-' + detail.eventId + '.json');
368
- let phase = 'journal', appendAttempted = false, outcomeKnown = false;
369
- try {
370
- try {
371
- await durableWrite(file, { state: 'pending', detail });
372
- }
373
- catch {
374
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
375
- }
376
- phase = 'append-scope';
377
- stable();
378
- let response;
379
- try {
380
- phase = 'append';
381
- appendAttempted = true;
382
- response = await (localScope === undefined ? api.appendRemoteConversationDetail(structuredClone(detail)) : api.appendRemoteConversationDetail(structuredClone(detail), localScope));
383
- }
384
- catch {
385
- phase = 'receipt-scope';
386
- stable();
387
- phase = 'receipt';
388
- const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
389
- phase = 'receipt-scope';
390
- stable();
391
- if (receipt.eventId !== detail.eventId)
392
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
393
- if (!receipt.recorded)
394
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
395
- if (!isDeepStrictEqual(receipt.detail, detail))
396
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
397
- phase = 'settlement';
398
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
399
- outcomeKnown = true;
400
- return;
401
- }
402
- phase = 'ack-scope';
403
- stable();
404
- if (response?.ok === false) {
405
- phase = 'settlement';
406
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
407
- outcomeKnown = true;
408
- }
409
- const ack = unwrap(response);
410
- if (ack.eventId !== detail.eventId || ack.recorded !== true)
411
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
412
- phase = 'settlement';
413
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
414
- outcomeKnown = true;
415
- }
416
- catch (error) {
417
- if (!outcomeKnown) {
418
- try {
419
- await durableWrite(join(dir, 'unconfirmed-' + detail.eventId + '.json'), { version: 1, eventId: detail.eventId, state: 'unconfirmed', phase, appendAttempted, code: closeCode(error, 'REMOTE_DETAIL_STORAGE_FAILED') });
420
- }
421
- catch {
422
- throw withFailureEvidence(error, [failure('REMOTE_DETAIL_STORAGE_FAILED')], 'diagnostic');
423
- }
424
- }
425
- throw error;
426
- }
427
- });
428
- },
429
- async flush() { await tail; if (firstFailure)
430
- throw firstFailure.error; stable(); },
431
- };
432
- }
131
+ const name = filename(request.sessionId, request.provider), owner = memory.get(name) ?? [];
132
+ memory.set(name, owner);
133
+ let context = { turn: null, step: null };
134
+ return { beginTurn(ids) { context = { ...ids }; }, endTurn() { context = { turn: null, step: null }; }, record(observation) { send(request, observation, context, undefined, owner); } };
135
+ },
433
136
  };
434
137
  }
@@ -1,25 +1 @@
1
- import type { ConnectApi, RuntimeRequest } from './provider.ts';
2
- import type { DetailObservation } from './conversation-details.ts';
3
- export type SourceDecision = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
4
- export type SourceAdmissionStage = 'source-context' | 'source-confirmation' | 'input-acceptance' | 'login-readiness' | 'local-admission' | 'input-prepare' | 'model-catalog' | 'runtime-resolve';
5
- export interface WorkspaceSourceAdmission {
6
- assertCurrent(): void;
7
- recordFailure(error: unknown, stage: SourceAdmissionStage): Promise<void>;
8
- }
9
- export interface WorkspaceSourceOptions {
10
- workspace(request: RuntimeRequest): Promise<{
11
- id: string;
12
- path: string;
13
- }>;
14
- currentUserTurn(request: RuntimeRequest): {
15
- turn: number;
16
- userMessageId: string;
17
- };
18
- confirm(question: {
19
- question: string;
20
- action: string;
21
- }, request: RuntimeRequest): Promise<SourceDecision>;
22
- record(request: RuntimeRequest, observation: DetailObservation): Promise<void>;
23
- }
24
- /** Explicit human source transition only. No source selection is inferred from a folder path. */
25
- export declare function confirmLegacyWorkspaceSource(get: () => ConnectApi | undefined, api: ConnectApi, request: RuntimeRequest, hooks: WorkspaceSourceOptions): Promise<WorkspaceSourceAdmission | undefined>;
1
+ export type SourceAdmissionStage = 'source-context' | 'input-acceptance' | 'login-readiness' | 'local-admission' | 'input-prepare' | 'model-catalog' | 'runtime-resolve';