@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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- 在 DSH「插件 → 添加插件」填 `@ai-workbench/remote-agent@0.8.0`(由编排发布后可用),或运行 `dsh plugin --profile desktop add @ai-workbench/remote-agent@0.8.0`。
1
+ 在 DSH「插件 → 添加插件」填 `@ai-workbench/remote-agent@0.8.2`(由编排发布后可用),或运行 `dsh plugin --profile desktop add @ai-workbench/remote-agent@0.8.2`。
2
2
 
3
3
  # 远程ACP插件
4
4
 
@@ -52,3 +52,13 @@ Codex单次模式首版仅文本,不转接中途审批;图片或系统/开
52
52
  ## 验证范围
53
53
 
54
54
  当前准备结果与限制记入源码仓`docs/evidence/2026-10-21-X-npm-prep/`(不进npm包)。先前真实emp-a协议诊断及固定三件套GUI是不同轮次,不能代替本次新包安装门。L完整绑定/登录、真实模型、物理断网、HanaMesh及leo E以实际已执行证据为准。主机命令安装来自S,X不修改S仓/安装脚本或登录状态。
55
+
56
+
57
+ D19(0.8.1):相邻正文增量合成连续文本块,工作区首次说明合入真实回复,按官方持久记录核对后才回报“已记入可核对回复”。这不代表界面已经显示。
58
+
59
+ 技术详情由 L 0.7.11 的唯一“远程主机连接”设置节展示,正文只放模型文本。命令目录保留完整描述;未知事件和工具输入输出保留字段、JSON类型及处理结果,未获准原文明确脱敏,数字用量可核对。没有 `remoteConversationDetails:1` 时明确提示“技术详情暂不可用”,本轮不发送模型请求。X 在官方当前 profile 的 `ai-workbench-remote-agent/diagnostics/` 保存私有诊断日志(目录700、文件600、落盘同步);写入/归属/顺序/容量失败中止本轮,不自动重发、换代重投、过期或淘汰。结果未知时只读核对原事件完整回执,未找到仍保持未知。X本轮无新增设置/阈值;L的单事件容量和分页设置在L设置节可见。
60
+
61
+
62
+ 0.8.2 登录检查:需要 L `~0.7.11`,失败来源及固定分支保留到 L“登录前技术详情”(`loginDiagnostics:1`),员工正文保持原文。按 L 明确的本品牌 queued/checking/authorizing 状态订阅等待;不以全局checking猜测在途,不新增刷新、轮询、超时或重试。每轮ensureReady仍是合同规定的显式检查,L的新可信结果可以替代旧检查,较早返回不得覆盖它。
63
+
64
+ L安全规范的officialReason原串完整保留;旧/不支持格式明确记OFFICIAL_REASON_UNSUPPORTED与字节数/SHA,秘密原文不保存。L0.7.11的LOGIN_INSTANCE_MISMATCH记录通过observedLoginInstanceId与原始login.instanceId保存实际观测ID,事件及connection仍绑定当前可信ID/代次;L技术详情和X私有日志均保留真实双ID,不归一、不清空快照。复合未知官方原因以固定summary完整保存OFFICIAL_REASON_UNSUPPORTED及bytes/SHA,不交原文。X私有原快照先同步落盘,再追加L;ACK未知只读核实原事件,跨代不重写/重发。此版本的工程验证不替代leo员工路径复验,真实故障根因尚未证实。
package/dist/acp.d.ts CHANGED
@@ -2,12 +2,14 @@ import * as stable from '@agentclientprotocol/sdk';
2
2
  import * as acp from '@agentclientprotocol/sdk/unstable';
3
3
  import { NeedError, type Need } from './remote.ts';
4
4
  import type { BrokerTransport } from './broker.ts';
5
+ import { type SanitizedObservation } from './conversation-details.ts';
5
6
  export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
6
7
  export interface AcpHooks {
7
8
  onUpdate?: (notification: acp.SessionNotification) => void;
8
9
  onResponse?: (response: acp.PromptResponse) => void;
9
10
  approve?: (request: acp.RequestPermissionRequest, signal?: AbortSignal) => Promise<ApprovalOutcome>;
10
11
  onNeed?: (need: Need) => void;
12
+ onDetail?: (detail: Readonly<SanitizedObservation>) => void;
11
13
  broker?: BrokerTransport;
12
14
  }
13
15
  export interface AcpStartContext {
@@ -29,6 +31,12 @@ export declare class AcpSession {
29
31
  private updating;
30
32
  private cwd?;
31
33
  private systemPrompt;
34
+ private reports;
35
+ private reportingFailure;
36
+ get diagnostics(): readonly Readonly<SanitizedObservation>[];
37
+ private detail;
38
+ private need;
39
+ private checkReporting;
32
40
  constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
33
41
  private observe;
34
42
  get id(): string | undefined;
package/dist/acp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as acp from '@agentclientprotocol/sdk/unstable';
2
2
  import { NeedError } from "./remote.js";
3
+ import { sanitizeObservation } from "./conversation-details.js";
3
4
  /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
4
5
  export class AcpSession {
5
6
  connection;
@@ -15,19 +16,45 @@ export class AcpSession {
15
16
  updating = false;
16
17
  cwd;
17
18
  systemPrompt = '';
19
+ reports = [];
20
+ reportingFailure;
21
+ get diagnostics() { return this.reports.map(entry => sanitizeObservation(entry)); }
22
+ detail(observation) {
23
+ const safe = sanitizeObservation(observation);
24
+ this.reports.push(safe);
25
+ try {
26
+ this.hooks.onDetail?.(safe);
27
+ }
28
+ catch (cause) {
29
+ this.reportingFailure ??= { cause };
30
+ this.connection?.close();
31
+ }
32
+ }
33
+ need(need) {
34
+ this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } });
35
+ this.hooks.onNeed?.(need);
36
+ }
37
+ checkReporting() { if (this.reportingFailure)
38
+ throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
18
39
  constructor(transport, hooks) {
19
40
  this.hooks = hooks;
20
41
  const app = acp.client({ name: '@ai-workbench/remote-agent' })
21
42
  .onNotification(acp.methods.client.session.update, ({ params: notification }) => {
22
- if (this.busy && notification.sessionId === this.sessionId) {
23
- // Session metadata is not evidence that the current prompt was accepted.
43
+ if (this.sessionId && notification.sessionId !== this.sessionId) {
44
+ this.detail({ kind: 'session', severity: 'warning', code: 'ACP_FOREIGN_SESSION_UPDATE', summary: '收到不属于当前远端会话的通知,未作为正文处理', method: 'session/update', payload: notification });
45
+ return;
46
+ }
47
+ const meta = record(notification._meta);
48
+ if (meta && Object.keys(meta).length)
49
+ this.detail({ kind: 'extension', severity: 'info', code: 'ACP_NOTIFICATION_META', summary: '远端通知扩展', method: 'session/update', payload: notification._meta });
50
+ if (this.busy) {
51
+ // Only actual turn output acknowledges prompt acceptance.
24
52
  if (['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update'].includes(notification.update.sessionUpdate))
25
53
  this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
26
- const meta = record(notification._meta);
27
- if (meta && Object.keys(meta).length)
28
- this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: `首版不支持:远端通知扩展字段 ${Object.keys(meta).map(safeLabel).join('、')};扩展值未展开`, requires: '对应的公开扩展映射' });
29
54
  this.hooks.onUpdate?.(notification);
30
55
  }
56
+ else
57
+ this.detail({ kind: notification.update.sessionUpdate === 'available_commands_update' ? 'commands' : 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '远端会话建立、恢复或空闲时的轨迹', method: 'session/update', payload: notification });
31
58
  })
32
59
  .onRequest(acp.methods.client.session.requestPermission, async ({ params: request, signal: rpcSignal, requestId }) => {
33
60
  if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
@@ -35,7 +62,7 @@ export class AcpSession {
35
62
  this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
36
63
  const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
37
64
  if (toolName === 'AskUserQuestion') {
38
- this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
65
+ this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
39
66
  return { outcome: { outcome: 'cancelled' } };
40
67
  }
41
68
  const eligible = request.options.filter(option => {
@@ -43,11 +70,13 @@ export class AcpSession {
43
70
  const sameKind = request.options.filter(candidate => candidate.kind === option.kind);
44
71
  const unique = single && sameKind.length === 1 && request.options.filter(candidate => candidate.optionId === option.optionId).length === 1;
45
72
  if (!unique)
46
- this.hooks.onNeed?.({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: `首版不支持:${option.name} / ${option.optionId} / ${option.kind}`, requires: '唯一对应的允许一次或拒绝一次选项' });
73
+ this.detail({ kind: 'tool', severity: 'info', code: 'APPROVAL_OPTION_NOT_OFFERED', summary: '此远端审批选项未映射到单次审批', method: 'session/request_permission', payload: option });
47
74
  return unique;
48
75
  });
49
- if (!eligible.length)
76
+ if (!eligible.length) {
77
+ this.need({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: '远端没有可唯一映射的单次审批选项,此动作未获授权', requires: '唯一对应的允许一次或拒绝一次选项' });
50
78
  return { outcome: { outcome: 'cancelled' } };
79
+ }
51
80
  const presented = { ...request, options: eligible };
52
81
  const token = this.turnToken;
53
82
  const id = this.sessionId;
@@ -78,7 +107,7 @@ export class AcpSession {
78
107
  }
79
108
  }
80
109
  catch {
81
- this.hooks.onNeed?.({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
110
+ this.need({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
82
111
  }
83
112
  const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
84
113
  const option = kind && eligible.find(item => item.kind === kind);
@@ -87,23 +116,24 @@ export class AcpSession {
87
116
  broker.authorizeResponse(requestId, generation);
88
117
  return { outcome: { outcome: 'selected', optionId: option.optionId } };
89
118
  }
90
- this.hooks.onNeed?.({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
119
+ this.need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
91
120
  message: '此动作未获授权;审批已取消、不可用或没有可映射的单次选项', requires: '本次动作的有效审批' });
92
121
  return { outcome: { outcome: 'cancelled' } };
93
122
  })
94
123
  .onRequest(acp.methods.client.elicitation.create, () => {
95
- this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问或信息收集;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
124
+ this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问或信息收集;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
96
125
  return { action: 'cancel' };
97
126
  })
98
127
  .onNotification(acp.methods.client.elicitation.complete, () => {
99
- this.hooks.onNeed?.({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问完成通知;未提交答案', requires: '可承载此提问的官方交互接口' });
128
+ this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问完成通知;未提交答案', requires: '可承载此提问的官方交互接口' });
100
129
  })
101
- .onRequest('*', () => {
102
- this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: '首版不支持:远端请求的扩展交互;未授权此动作', requires: '受支持的公开 ACP 交互接口' });
130
+ .onRequest('*', ({ method, params }) => {
131
+ this.detail({ kind: 'extension', severity: 'action-required', code: 'ACP_UNKNOWN_REQUEST', summary: '未知远端交互请求未获授权', method, payload: params });
132
+ this.need({ code: 'ACP_EXTENSION_UNSUPPORTED', message: '首版不支持:远端请求的扩展交互;未授权此动作', requires: '受支持的公开 ACP 交互接口' });
103
133
  throw new acp.RequestError(-32601, '不支持此扩展交互');
104
134
  })
105
- .onNotification('*', () => {
106
- this.hooks.onNeed?.({ code: 'ACP_EXTENSION_UNSUPPORTED', message: '首版不支持:收到未知的远端扩展通知', requires: '受支持的公开 ACP 通知接口' });
135
+ .onNotification('*', ({ method, params }) => {
136
+ this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_NOTIFICATION', summary: '收到未知远端扩展通知,已保留技术记录', method, payload: params });
107
137
  });
108
138
  if ('readable' in transport && 'writable' in transport)
109
139
  this.connection = app.connect(this.observe(transport));
@@ -126,8 +156,7 @@ export class AcpSession {
126
156
  const params = record(raw?.params);
127
157
  const update = record(params?.update);
128
158
  if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
129
- if (this.busy && params?.sessionId === this.sessionId)
130
- this.hooks.onNeed?.({ code: 'ACP_UPDATE_UNSUPPORTED', message: `首版不支持:远端事件 ${safeLabel(update.sessionUpdate)};未展开未知事件内容`, requires: '对应的公开 ACP 显示映射' });
159
+ this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_UPDATE', summary: '收到未知远端会话事件,已保留结构', method: 'session/update', payload: params });
131
160
  return;
132
161
  }
133
162
  controller.enqueue(message);
@@ -166,12 +195,13 @@ export class AcpSession {
166
195
  signal?.addEventListener('abort', abort, { once: true });
167
196
  try {
168
197
  const hello = await this.connection.agent.request(acp.methods.agent.initialize, {
169
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.0' },
198
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.1' },
170
199
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} } },
171
200
  }, { cancellationSignal: signal });
172
201
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
173
202
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
174
203
  throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
204
+ this.checkReporting();
175
205
  this.negotiated = hello.agentCapabilities ?? {};
176
206
  const systemPrompt = context?.systemPrompt;
177
207
  if (systemPrompt && !this.negotiated.sessionCapabilities?.systemPrompt) {
@@ -197,6 +227,7 @@ export class AcpSession {
197
227
  this.method = acp.methods.agent.session.new;
198
228
  }
199
229
  this.hooks.broker?.acknowledge(this.method);
230
+ this.checkReporting();
200
231
  this.ready = true;
201
232
  this.cwd = cwd;
202
233
  this.systemPrompt = systemPrompt ?? '';
@@ -205,6 +236,7 @@ export class AcpSession {
205
236
  catch (error) {
206
237
  if (signal?.aborted)
207
238
  throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
239
+ this.checkReporting();
208
240
  throw acpNeed(error);
209
241
  }
210
242
  finally {
@@ -212,6 +244,7 @@ export class AcpSession {
212
244
  }
213
245
  }
214
246
  async updateContext(context, signal) {
247
+ this.checkReporting();
215
248
  if (!this.ready || !this.sessionId || !this.cwd)
216
249
  throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
217
250
  if (this.busy || this.updating)
@@ -233,12 +266,14 @@ export class AcpSession {
233
266
  this.hooks.broker?.acknowledge(method);
234
267
  this.systemPrompt = text;
235
268
  this.ready = true;
236
- this.hooks.onNeed?.({ code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', message: '系统与开发者上下文变更已通过原会话恢复接口传送。Claude 恢复会话沿用首次系统提示,新要求在下次上下文整理后生效;不能据此认定本轮已立即生效。', requires: '远端实际应用新要求;Claude 须完成下一次上下文整理' });
269
+ this.need({ code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', message: '系统与开发者上下文变更已通过原会话恢复接口传送。Claude 恢复会话沿用首次系统提示,新要求在下次上下文整理后生效;不能据此认定本轮已立即生效。', requires: '远端实际应用新要求;Claude 须完成下一次上下文整理' });
270
+ this.checkReporting();
237
271
  return method;
238
272
  }
239
273
  catch (error) {
240
274
  if (signal?.aborted)
241
275
  throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
276
+ this.checkReporting();
242
277
  throw acpNeed(error);
243
278
  }
244
279
  finally {
@@ -254,6 +289,7 @@ export class AcpSession {
254
289
  signal?.throwIfAborted();
255
290
  const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
256
291
  this.validatePrompt(prompt);
292
+ this.checkReporting();
257
293
  this.busy = true;
258
294
  this.turnSignal = signal;
259
295
  this.turnToken = Symbol('turn');
@@ -262,7 +298,7 @@ export class AcpSession {
262
298
  const cancel = () => {
263
299
  rejectAbort?.(new NeedError('ABORTED', '已请求取消本轮,远端状态需确认', '确认主机取消状态'));
264
300
  void this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {
265
- this.hooks.onNeed?.({ code: 'CANCEL_UNCONFIRMED', message: '连接中断,无法确认主机已取消本轮', requires: '恢复连接以确认远端状态' });
301
+ this.need({ code: 'CANCEL_UNCONFIRMED', message: '连接中断,无法确认主机已取消本轮', requires: '恢复连接以确认远端状态' });
266
302
  });
267
303
  };
268
304
  signal?.addEventListener('abort', cancel, { once: true });
@@ -272,12 +308,14 @@ export class AcpSession {
272
308
  this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
273
309
  signal?.throwIfAborted();
274
310
  this.hooks.broker?.completePrompt();
311
+ this.checkReporting();
275
312
  this.hooks.onResponse?.(result);
276
313
  return result.stopReason;
277
314
  }
278
315
  catch (error) {
279
316
  if (signal?.aborted)
280
317
  throw new NeedError('ABORTED', '本轮已取消;已请求主机停止', '确认主机取消状态');
318
+ this.checkReporting();
281
319
  throw acpNeed(error);
282
320
  }
283
321
  finally {
@@ -0,0 +1 @@
1
+ export declare function validOfficialReason(value: unknown): value is string;
@@ -0,0 +1,27 @@
1
+ /** L 0.7.10 officialReason v1 consumer validation; preserve canonical bytes only. */
2
+ const markers = ['AW_REMOTE_COMMAND_KILL_FAILED', 'AUTH_STATUS_JSON_INVALID:SyntaxError', 'Permission denied', 'Host key verification failed', 'Connection refused', 'Connection closed', 'command not found', 'No such file or directory'];
3
+ const names = ['Error', 'TypeError', 'SyntaxError', 'RangeError', 'AbortError', 'DOMException', 'UnknownError'];
4
+ const codes = ['ENOENT', 'EPERM', 'ESRCH', 'EACCES', 'EINVAL', 'EPIPE', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ABORT_ERR', 'UNKNOWN'];
5
+ const stages = ['ssh-status', 'ssh-result', 'remote-frame', 'auth-status', 'connect-check-login', 'integration-pre-login', 'process-spawn', 'process-stdin', 'process-output', 'process-runner'];
6
+ const reasons = ['CANCELLED', 'SSH_EXIT_NONZERO', 'SSH_STDERR_NONEMPTY', 'FRAME_TRUNCATED', 'FRAME_JSON_INVALID', 'FRAME_OBJECT_INVALID', 'FRAME_VERSION_INVALID', 'FRAME_NONCE_INVALID', 'FRAME_AFTER_END', 'FRAME_KEYS_INVALID', 'FRAME_START_MISSING', 'FRAME_BASE64_INVALID', 'REMOTE_COMMAND_SIGNAL', 'FRAME_END_EXIT_INVALID', 'FRAME_SEQUENCE_OR_VALUE_INVALID', 'FRAME_END_MISSING', 'STATUS_ARGUMENT_INVALID', 'STATUS_EXECUTION_FAILED', 'STATUS_JSON_INVALID', 'STATUS_OBJECT_INVALID', 'STATUS_KEYS_INVALID', 'STATUS_VERSION_INVALID', 'STATUS_PROVIDER_INVALID', 'STATUS_EXIT_MISMATCH', 'STATUS_LOGGED_IN_INVALID', 'STATUS_STDERR_NONEMPTY', 'STATUS_EXIT_INVALID', 'STATUS_EXCEPTION', 'STATUS_SUPERSEDED', 'SSH_PORT_MISSING', 'STDIN_FAILED', 'SPAWN_FAILED', 'OUTPUT_LIMIT_EXCEEDED', 'PROCESS_SIGNAL'];
7
+ const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
8
+ const integer = (v) => Number.isSafeInteger(v) && Number(v) >= 0;
9
+ const member = (list, v) => typeof v === 'string' && list.includes(v);
10
+ function summary(v) {
11
+ return object(v) && Object.keys(v).every(k => ['bytes', 'sha256', 'markers', 'cleanupErrors'].includes(k)) && integer(v.bytes) && typeof v.sha256 === 'string' && /^[a-f0-9]{64}$/.test(v.sha256) && Array.isArray(v.markers) && v.markers.every(m => member(markers, m)) && (v.cleanupErrors === undefined || Array.isArray(v.cleanupErrors) && v.cleanupErrors.every(e => object(e) && Object.keys(e).sort().join(',') === 'errorCode,errorName' && member(['Error', 'TypeError', 'RangeError', 'UnknownError'], e.errorName) && member(['EPERM', 'EACCES', 'EINVAL', 'UNKNOWN'], e.errorCode)));
12
+ }
13
+ export function validOfficialReason(value) {
14
+ if (typeof value !== 'string')
15
+ return false;
16
+ let v;
17
+ try {
18
+ v = JSON.parse(value);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ if (!object(v) || JSON.stringify(v) !== value || v.version !== 1 || !member(stages, v.stage) || !member(reasons, v.reason))
24
+ return false;
25
+ const fields = { version: x => x === 1, stage: x => member(stages, x), reason: x => member(reasons, x), errorName: x => member(names, x), errorCode: x => member(codes, x), exitCode: x => x === null || typeof x === 'number' && Number.isFinite(x) && Number.isInteger(x), signal: x => member(['SIGTERM', 'SIGKILL', 'SIGABRT', 'SIGHUP', 'SIGINT', 'SIGSEGV', 'OTHER'], x), index: integer, unexpectedCount: integer, maxOutputBytes: x => integer(x) && Number(x) > 0, observedBytes: integer, stderr: summary, output: summary, missing: x => Array.isArray(x) && x.every(k => member(['nonce', 'protocolVersion', 'type', 'data', 'exitCode', 'signal', 'loggedIn', 'provider'], k)), loggedInType: x => member(['null', 'undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 'bigint'], x), exitCodeType: x => member(['undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 'bigint'], x) };
26
+ return Object.entries(v).every(([key, item]) => Object.hasOwn(fields, key) && fields[key](item));
27
+ }
@@ -1,3 +1,4 @@
1
+ import type { ConnectNeed } from '@ai-workbench/connect/api';
1
2
  import type { ConnectTarget } from './provider.ts';
2
3
  import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
3
4
  /** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
@@ -16,7 +17,7 @@ export type ConnectResult<T> = {
16
17
  code: string;
17
18
  message: string;
18
19
  requires: string;
19
- };
20
+ } & Pick<ConnectNeed, 'officialReason'>;
20
21
  };
21
22
  export interface NativeWorkspaceApi {
22
23
  getContractInfo(): ContractInfo;
@@ -0,0 +1,47 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export interface DetailScope {
5
+ instanceId: string;
6
+ epoch: number;
7
+ sessionId: string;
8
+ provider: 'claude' | 'codex';
9
+ turn: number;
10
+ step: number;
11
+ }
12
+ export interface DetailObservation {
13
+ kind: 'extension' | 'commands' | 'tool' | 'usage' | 'notice' | 'need' | 'session';
14
+ severity: 'info' | 'warning' | 'action-required';
15
+ code: string;
16
+ summary: string;
17
+ method?: string;
18
+ payload?: unknown;
19
+ }
20
+ export type SanitizedObservation = Omit<DetailObservation, 'payload'> & {
21
+ payload: JsonValue;
22
+ };
23
+ export interface DetailRecord extends Omit<DetailObservation, 'payload'>, DetailScope {
24
+ version: 1;
25
+ eventId: string;
26
+ sequence: number;
27
+ payload: JsonValue;
28
+ }
29
+ export type DetailSink = {
30
+ append(record: Readonly<DetailRecord>): Promise<void>;
31
+ };
32
+ /** Only objects created by this module are trusted as redaction markers. */
33
+ export declare function isSanitizedRedaction(value: unknown): boolean;
34
+ /** Sanitize an internal observation without attaching any L wire semantics. */
35
+ export declare function sanitizeObservation(observation: DetailObservation, protectModelMaterial?: boolean): Readonly<SanitizedObservation>;
36
+ /** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
37
+ export declare class ConversationDetails {
38
+ private readonly scope;
39
+ private readonly sink;
40
+ private readonly records;
41
+ private tail;
42
+ private firstFailure;
43
+ constructor(scope: DetailScope, sink: DetailSink);
44
+ record(observation: DetailObservation): void;
45
+ flush(): Promise<void>;
46
+ snapshot(): readonly DetailRecord[];
47
+ }