@ai-workbench/remote-agent 0.8.15 → 0.8.17

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.
@@ -1,3 +1,4 @@
1
+ import { closeCode, withFailureEvidence } from "./close-diagnostics.js";
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { constants } from 'node:fs';
3
4
  import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
@@ -362,40 +363,67 @@ export function createRemoteDetails(options) {
362
363
  scope.maximum = sequence;
363
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) });
364
365
  enqueue(async () => {
365
- // A private X journal is retained even if L is disconnected or rejects it.
366
+ // Pending is not a delivery verdict. Keep the exact failed stage separately.
366
367
  const file = join(dir, sequence + '-' + detail.eventId + '.json');
368
+ let phase = 'journal', appendAttempted = false, outcomeKnown = false;
367
369
  try {
368
- await durableWrite(file, { state: 'pending', detail });
369
- }
370
- catch {
371
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
372
- }
373
- stable();
374
- let response;
375
- try {
376
- response = await (localScope === undefined ? api.appendRemoteConversationDetail(structuredClone(detail)) : api.appendRemoteConversationDetail(structuredClone(detail), localScope));
377
- }
378
- catch {
370
+ try {
371
+ await durableWrite(file, { state: 'pending', detail });
372
+ }
373
+ catch {
374
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
375
+ }
376
+ phase = 'append-scope';
379
377
  stable();
380
- const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
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';
381
403
  stable();
382
- if (receipt.eventId !== detail.eventId)
383
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
384
- if (!receipt.recorded)
385
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
386
- if (!isDeepStrictEqual(receipt.detail, detail))
387
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
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';
388
413
  await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
389
- return;
414
+ outcomeKnown = true;
390
415
  }
391
- stable();
392
- if (response?.ok === false) {
393
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
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;
394
426
  }
395
- const ack = unwrap(response);
396
- if (ack.eventId !== detail.eventId || ack.recorded !== true)
397
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
398
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
399
427
  });
400
428
  },
401
429
  async flush() { await tail; if (firstFailure)
package/dist/remote.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ConnectNeed } from '@ai-workbench/connect/api';
1
2
  import type { LoginDiagnostic } from './login-diagnostic.ts';
2
3
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
3
4
  /** Development-only direct SSH. Product transports must consume L's exported sshArgs (v0.2). */
@@ -19,6 +20,7 @@ export declare class NeedError extends Error {
19
20
  requires: string;
20
21
  technical?: LoginDiagnostic;
21
22
  officialReason?: string;
23
+ connectNeed?: Readonly<ConnectNeed>;
22
24
  constructor(code: string, message: string, requires: string);
23
25
  }
24
26
  export declare function shellQuote(value: string): string;
package/dist/remote.js CHANGED
@@ -4,6 +4,7 @@ export class NeedError extends Error {
4
4
  requires;
5
5
  technical;
6
6
  officialReason;
7
+ connectNeed;
7
8
  constructor(code, message, requires) {
8
9
  super(message);
9
10
  this.name = 'NeedError';
@@ -1,9 +1,11 @@
1
+ import { withFailureEvidence } from "./failure-evidence.js";
2
+ import { connectNeedError } from "./connect-need.js";
1
3
  import { isDeepStrictEqual } from 'node:util';
2
4
  import { sanitizeDisplayLabel } from "./conversation-details.js";
3
5
  import { NeedError } from "./remote.js";
4
6
  const fail = (code) => new NeedError(code, '工作区来源尚未确认,本轮未执行;你的原话仍保留。', '当前工作区来源及原始员工输入');
5
7
  const codeOf = (value) => value instanceof NeedError ? value.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
6
- const diagnosticFailure = (error, diagnostic) => Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '工作区来源诊断未能保存,本轮已停止;原话保留。', '原错误代码 ' + codeOf(error) + ';诊断错误代码 ' + codeOf(diagnostic)), { cause: new AggregateError([error, diagnostic], 'WORKSPACE_SOURCE_AND_DIAGNOSTIC_FAILED') });
8
+ const diagnosticFailure = (error, diagnostic) => withFailureEvidence(error, [diagnostic], 'diagnostic', undefined, codeOf(error));
7
9
  /** Explicit human source transition only. No source selection is inferred from a folder path. */
8
10
  export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
9
11
  let initialContext = { workspaceId: null, bindingId: null, source: null, status: null };
@@ -81,7 +83,7 @@ export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
81
83
  const selected = await api.selectWorkspaceLocation({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: frozen.bindingId, source: 'local' });
82
84
  request.signal?.throwIfAborted();
83
85
  if (!selected.ok)
84
- throw new NeedError(selected.error.code, selected.error.message, selected.error.requires);
86
+ throw connectNeedError(selected.error);
85
87
  const current = await stable(false), value = selected.value, returned = value.items.filter(item => item.workspaceId === workspace.id);
86
88
  if (value.phase !== 'ready' || value.instanceId !== connection.instanceId || value.epoch !== connection.epoch || returned.length !== 1 || returned[0].source !== 'local' || returned[0].status !== 'selected' || !returned[0].bindingId || returned[0].bindingId === frozen.bindingId || returned[0].localPath !== workspace.path || current.source !== 'local' || current.status !== 'selected' || current.bindingId !== returned[0].bindingId)
87
89
  throw fail('WORKSPACE_SOURCE_SELECTION_UNCONFIRMED');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "zod": "4.6.7"
40
40
  },
41
41
  "devDependencies": {
42
- "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.19.tgz",
42
+ "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.20.tgz",
43
43
  "@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.4.tgz",
44
44
  "@deepseek-ai/cordis": "4.0.4",
45
45
  "@deepseek-ai/cordis-plugin-group": "1.0.4",
@@ -68,7 +68,7 @@
68
68
  "zustand": "4.4.7"
69
69
  },
70
70
  "peerDependencies": {
71
- "@ai-workbench/connect": "~0.7.19",
71
+ "@ai-workbench/connect": "~0.7.20",
72
72
  "@ai-workbench/network": "~0.4.4",
73
73
  "@deepseek-ai/cordis": "4.0.4",
74
74
  "@deepseek-ai/dsh-agent": "0.2.0-rc.2",