@nelsonlongxiang/dsh-open-a2a-net 0.6.42 → 0.7.0

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 (79) hide show
  1. package/README.md +1 -0
  2. package/README_zh.md +1 -0
  3. package/assets/nexusDist/assets/index-CA_YZTWG.js +9 -0
  4. package/assets/nexusDist/index.html +1 -1
  5. package/cli/a2a.mjs +43 -7
  6. package/lib/a2a-client.js +10 -2
  7. package/lib/admitted-bookkeeping.js +7 -0
  8. package/lib/canvas-discovery.js +11 -1
  9. package/lib/canvas-member.js +43 -0
  10. package/lib/canvas-store.js +315 -17
  11. package/lib/client.js +62 -72
  12. package/lib/client.js.map +1 -1
  13. package/lib/config.js +1 -0
  14. package/lib/direct-forward.js +9 -7
  15. package/lib/faces/admin.js +150 -67
  16. package/lib/faces/files.js +2 -1
  17. package/lib/faces/wire.js +569 -469
  18. package/lib/followup-delivery.js +89 -0
  19. package/lib/idempotency-store.js +8 -0
  20. package/lib/inbound-verification.js +23 -0
  21. package/lib/index.js +534 -166
  22. package/lib/nudge-observability.js +34 -0
  23. package/lib/origin-claim.js +36 -9
  24. package/lib/outbound-scope.js +4 -2
  25. package/lib/peer-directory.js +15 -5
  26. package/lib/peer-store.js +31 -17
  27. package/lib/queue-guard.js +4 -3
  28. package/lib/receipt-ladder.js +1 -1
  29. package/lib/receipt-retry.js +58 -0
  30. package/lib/reconciliation-reminders.js +148 -0
  31. package/lib/reconciliation-state.js +27 -0
  32. package/lib/session-transcript.js +46 -18
  33. package/lib/skill/SKILL.md +5 -0
  34. package/lib/skill/references/cli.md +4 -1
  35. package/lib/skill/references/routing.md +31 -1
  36. package/lib/task-auth.js +3 -3
  37. package/lib/task-delivery.js +23 -0
  38. package/lib/task-ledger-persistence.js +51 -0
  39. package/lib/task-ledger-rows.js +5 -0
  40. package/lib/task-ledger.js +219 -20
  41. package/lib/tools/info.js +37 -7
  42. package/lib/types/a2a-client.d.ts +1 -1
  43. package/lib/types/admitted-bookkeeping.d.ts +5 -0
  44. package/lib/types/canvas-member.d.ts +10 -0
  45. package/lib/types/canvas-store.d.ts +56 -1
  46. package/lib/types/config.d.ts +2 -0
  47. package/lib/types/faces/admin.d.ts +21 -0
  48. package/lib/types/faces/files.d.ts +1 -1
  49. package/lib/types/faces/wire.d.ts +21 -14
  50. package/lib/types/followup-delivery.d.ts +25 -0
  51. package/lib/types/idempotency-store.d.ts +2 -0
  52. package/lib/types/inbound-verification.d.ts +14 -0
  53. package/lib/types/index.d.ts +5 -0
  54. package/lib/types/nudge-observability.d.ts +17 -0
  55. package/lib/types/origin-claim.d.ts +7 -1
  56. package/lib/types/outbound-scope.d.ts +1 -0
  57. package/lib/types/peer-store.d.ts +4 -0
  58. package/lib/types/queue-guard.d.ts +9 -2
  59. package/lib/types/receipt-ladder.d.ts +2 -0
  60. package/lib/types/receipt-retry.d.ts +22 -0
  61. package/lib/types/reconciliation-reminders.d.ts +41 -0
  62. package/lib/types/reconciliation-state.d.ts +32 -0
  63. package/lib/types/session-transcript.d.ts +6 -1
  64. package/lib/types/task-auth.d.ts +7 -0
  65. package/lib/types/task-delivery.d.ts +17 -0
  66. package/lib/types/task-ledger-persistence.d.ts +3 -0
  67. package/lib/types/task-ledger-rows.d.ts +19 -2
  68. package/lib/types/task-ledger.d.ts +48 -1
  69. package/lib/types/tools/info.d.ts +3 -0
  70. package/lib/types/types.d.ts +3 -0
  71. package/nexus-stage/src/federation.ts +1 -0
  72. package/nexus-stage/src/main.ts +17 -2
  73. package/nexus-stage/src/member-provenance-picker.ts +13 -0
  74. package/nexus-stage/src/planning-member-handles.ts +4 -1
  75. package/nexus-stage/src/planning-view.ts +3 -3
  76. package/nexus-stage/src/workbench.ts +51 -7
  77. package/nexus-stage/src/world.ts +1 -0
  78. package/package.json +3 -2
  79. package/assets/nexusDist/assets/index-NkNSeIx6.js +0 -9
package/lib/tools/info.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * a getter so the deps object can be built before the const is declared.
8
8
  * @module src/tools/info
9
9
  */
10
+ import { reminderView } from "../reconciliation-state.js";
10
11
  import { defineTool } from '@deepseek-ai/dsh-tools';
12
+ import { splitQualifiedMember } from "../canvas-member.js";
11
13
  import { peerDirectoryRows } from "../peer-directory.js";
12
14
  import { canvasMemberships, canvasOwners } from "../canvas-discovery.js";
13
15
  import { peerContentMeta } from "../node-self.js";
@@ -60,7 +62,7 @@ export function createInfoTools(deps) {
60
62
  return id !== undefined && joinedSessions.has(id);
61
63
  });
62
64
  }
63
- return joined.has(row.team) || (row.teams ?? []).some(team => joined.has(team));
65
+ return joined.has(row.team) || [...joined].some(ref => splitQualifiedMember(ref)?.handle === row.team && canvasStore.ownerKeyOf(ref) === card.publicKey) || (row.teams ?? []).some(team => joined.has(team));
64
66
  });
65
67
  /** Human duration for a ledger row: how long a task has been owed or took. */
66
68
  const describeAge = (ms) => {
@@ -163,6 +165,7 @@ export function createInfoTools(deps) {
163
165
  roster: [...teamMemberships.teamsOf(agentId)],
164
166
  canvas: [...canvasTeamsOfSession(agentId)],
165
167
  canvasDetails,
168
+ canvasMigration: (() => { const s = deps.canvasMigrationState(); return Object.fromEntries(['canvasLegacyBareMembers', 'epoch', 'legacyBareMembers', 'legacyInferredMembers', 'legacyInferenceResolutions', 'legacyCollisionWarnings', 'suspendedMembersCount'].map(key => [key, s[key]])); })(),
166
169
  },
167
170
  allowlist: {
168
171
  patterns,
@@ -212,6 +215,7 @@ export function createInfoTools(deps) {
212
215
  additionalProperties: false,
213
216
  properties: {
214
217
  team: { type: 'string', required: true },
218
+ memberRef: { type: 'string', description: 'Owner-qualified session selector, bound to the verified publishing host key; not an independent authority.' },
215
219
  session: { type: 'string', required: true },
216
220
  name: { type: 'string', required: true },
217
221
  seatName: { type: 'string', description: 'Human-owned stable display label, never a route identity.' },
@@ -263,7 +267,8 @@ export function createInfoTools(deps) {
263
267
  const id = resolveLocal(ref);
264
268
  return id !== undefined && joinedSessions.has(id);
265
269
  });
266
- return joined.has(row.team) || (row.teams ?? []).some(name => joined.has(name));
270
+ const card = row.via ? cardCache.get(row.via)?.card : undefined;
271
+ return joined.has(row.team) || [...joined].some(ref => splitQualifiedMember(ref)?.handle === row.team && canvasStore.ownerKeyOf(ref) === card?.publicKey) || (row.teams ?? []).some(name => joined.has(name));
267
272
  });
268
273
  const hiddenRemote = remoteBefore - teams.filter(row => row.local !== true).length;
269
274
  // gh#39 (owner ruling: the listing ADVERTISES targets — an exposed
@@ -371,6 +376,10 @@ export function createInfoTools(deps) {
371
376
  },
372
377
  },
373
378
  },
379
+ deliveryBookkeeping: { type: 'object', additionalProperties: false, required: true, properties: { orphaned: { type: 'number', required: true }, failures: { type: 'number', required: true }, lastError: { type: 'object', additionalProperties: false, properties: { at: { type: 'number', required: true }, code: { type: 'string', required: true } } } } },
380
+ ledgerPersistence: { type: 'object', additionalProperties: false, required: true, properties: { ok: { type: 'boolean', required: true }, lastError: { type: 'object', additionalProperties: false, properties: { at: { type: 'number', required: true }, code: { type: 'string', required: true } } } } },
381
+ nudgeEscalations: { type: 'object', additionalProperties: false, required: true, properties: { queued: { type: 'number', required: true }, unavailable: { type: 'number', required: true }, failed: { type: 'number', required: true }, lastAt: { type: 'number' }, lastError: { type: 'string' } } },
382
+ reconciliationReminders: { type: 'object', additionalProperties: false, required: true, properties: { observed: { type: 'number', required: true }, repeatedlyUnreconciled: { type: 'number', required: true } } },
374
383
  idempotency: {
375
384
  type: 'object',
376
385
  additionalProperties: false,
@@ -390,6 +399,9 @@ export function createInfoTools(deps) {
390
399
  render: (_args, value) => [{
391
400
  type: 'text',
392
401
  text: [
402
+ `Delivery bookkeeping: orphaned=${value.deliveryBookkeeping.orphaned} failures=${value.deliveryBookkeeping.failures}${value.deliveryBookkeeping.lastError ? " code=" + value.deliveryBookkeeping.lastError.code : ""}`,
403
+ `Ledger persistence: ${value.ledgerPersistence.ok ? 'ok' : '!!! FAILED !!!'}${value.ledgerPersistence.lastError ? ' at=' + value.ledgerPersistence.lastError.at + ' code=' + value.ledgerPersistence.lastError.code : ''}`,
404
+ `Nudge escalations: queued=${value.nudgeEscalations.queued} unavailable=${value.nudgeEscalations.unavailable} failed=${value.nudgeEscalations.failed}${value.nudgeEscalations.lastAt === undefined ? '' : ' lastAt=' + value.nudgeEscalations.lastAt}${value.nudgeEscalations.lastError ? ' code=' + value.nudgeEscalations.lastError : ''}`,
393
405
  ...(value.inFlight.length === 0 ? [] : ['In flight:']),
394
406
  ...value.inFlight.map((route) => ` → ${route.team} via ${route.peer === 'local' ? 'this host' : route.peer}`),
395
407
  `Peers (${String(value.peers.length)}):`,
@@ -411,6 +423,10 @@ export function createInfoTools(deps) {
411
423
  inFlight: [...inFlightRoutes.values()].map(route => ({ team: route.team, peer: route.peer, startedAt: route.startedAt })),
412
424
  activity: recentActivity.slice(),
413
425
  idempotency: idempotencyStore.stats(),
426
+ deliveryBookkeeping: taskLedger.deliveryBookkeepingState(),
427
+ ledgerPersistence: taskLedger.persistenceState(),
428
+ nudgeEscalations: deps.nudgeEscalations?.() ?? { queued: 0, unavailable: 0, failed: 0 },
429
+ reconciliationReminders: reminderSummary(),
414
430
  };
415
431
  }
416
432
  // S5 scoped plane: the tracked fleet narrows to peers whose verified
@@ -451,11 +467,19 @@ export function createInfoTools(deps) {
451
467
  peers,
452
468
  inFlight: [...inFlightRoutes.values()].map(route => ({ team: route.team, peer: route.peer, startedAt: route.startedAt })).filter(inScope),
453
469
  activity: recentActivity.filter(inScope),
470
+ deliveryBookkeeping: taskLedger.deliveryBookkeepingState(),
471
+ ledgerPersistence: taskLedger.persistenceState(),
472
+ nudgeEscalations: deps.nudgeEscalations?.() ?? { queued: 0, unavailable: 0, failed: 0 },
454
473
  idempotency: idempotencyStore.stats(),
474
+ reconciliationReminders: reminderSummary(),
455
475
  };
456
476
  },
457
477
  }));
458
478
  }
479
+ function reminderSummary() {
480
+ const seen = taskLedger.list().filter(row => row.status === 'pending').map(row => reminderView(taskLedger.reminderState(row.taskId)));
481
+ return { observed: seen.filter(v => v.reminderAttempts !== undefined).length, repeatedlyUnreconciled: seen.filter(v => v.repeatedlyUnreconciled).length };
482
+ }
459
483
  function registerTasks() {
460
484
  ctx.tools.register(defineTool({
461
485
  name: 'a2a_tasks',
@@ -488,6 +512,9 @@ export function createInfoTools(deps) {
488
512
  status: { type: 'string', required: true },
489
513
  deadAt: { type: 'number' },
490
514
  answeredAt: { type: 'number' },
515
+ receiptDelivery: { type: 'string', enum: ['receipt-undelivered'] },
516
+ consumed: { type: 'boolean' }, deliveryObservation: { type: 'string', enum: ['consumed', 'delivered-unconsumed'] },
517
+ reminderAttempts: { type: 'number' }, reminderDelivered: { type: 'number' }, reminderLastAt: { type: 'number' }, reminderNextAt: { type: 'number' }, reminderResult: { type: 'string' }, reminderDiagnostic: { type: 'string', enum: ['metadata-reset'] }, repeatedlyUnreconciled: { type: 'boolean' },
491
518
  direction: { type: 'string', enum: ['inbound', 'outbound'] },
492
519
  receiptExpected: { type: 'boolean' },
493
520
  forwarded: { type: 'boolean' },
@@ -516,6 +543,7 @@ export function createInfoTools(deps) {
516
543
  team: { type: 'string', required: true },
517
544
  startedAt: { type: 'number', required: true },
518
545
  resolvedAt: { type: 'number', required: true },
546
+ withdrawalResult: { type: 'string', enum: ['removed', 'claimed-steered', 'withdraw-failed-steered', 'no-agent'] },
519
547
  summary: { type: 'string' },
520
548
  brief: { type: 'string' },
521
549
  forwarded: { type: 'boolean' },
@@ -552,7 +580,7 @@ export function createInfoTools(deps) {
552
580
  ...settleable.map((task) => {
553
581
  const context = task.contextId === undefined || task.contextId === '' ? '' : `, follow-up context ${task.contextId}`;
554
582
  const brief = task.brief === undefined || task.brief === '' ? '' : ` — "${task.brief}"`;
555
- return ` - ${task.taskId}${brief} → ${task.team} (via ${task.peer === 'local' ? 'this host' : task.peer}), waiting ${describeAge(Date.now() - task.startedAt)}${context}`;
583
+ return ` - ${task.taskId}${brief} → ${task.team} (via ${task.peer === 'local' ? 'this host' : task.peer}), waiting ${describeAge(Date.now() - task.startedAt)}${context}${task.receiptDelivery ? ' — ' + task.receiptDelivery : ''}${task.reminderAttempts === undefined ? '' : ` — reminders ${task.reminderDelivered ?? 0}/${task.reminderAttempts}, next ${task.reminderNextAt ?? 0}${task.repeatedlyUnreconciled ? ' (多次提醒未对账)' : ''}`}`;
556
584
  }),
557
585
  ...(staleCount > 0 ? [`Delivered conversationally without a formal receipt (${String(staleCount)}) — not debts; they auto-dead-letter at the stale TTL.`] : []),
558
586
  ];
@@ -566,7 +594,7 @@ export function createInfoTools(deps) {
566
594
  ...(value.archivedTotal > 0 ? [`Archived (${String(value.archivedTotal)}), most recent first:`] : []),
567
595
  ...value.archive.map((entry) => {
568
596
  const brief = entry.brief === undefined || entry.brief === '' ? '' : ` — "${entry.brief}"`;
569
- return ` - ${entry.taskId}${brief} → ${entry.team}: after ${describeAge(entry.resolvedAt - entry.startedAt)}, ${entry.summary === undefined || entry.summary === '' ? 'resolved' : entry.summary}`;
597
+ return ` - ${entry.taskId}${brief} → ${entry.team}: after ${describeAge(entry.resolvedAt - entry.startedAt)}, ${entry.summary === undefined || entry.summary === '' ? 'resolved' : entry.summary}${entry.withdrawalResult ? `, withdrawal: ${entry.withdrawalResult}` : ''}`;
570
598
  }),
571
599
  ...(value.archivedTotal > value.archive.length ? [` (+${String(value.archivedTotal - value.archive.length)} older not shown)`] : []),
572
600
  ].join('\n'),
@@ -581,12 +609,14 @@ export function createInfoTools(deps) {
581
609
  ok: true,
582
610
  // Project the declared fields only: ledger rows also carry internal audit facts (the #114 auth record,
583
611
  // #139 peerKey/peerHost) the output schema does not admit — spreading the row failed every call (0.6.32+).
584
- tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, answeredAt, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
585
- taskId, team, peer, startedAt, status,
612
+ tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, answeredAt, receiptDelivery, consumed, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
613
+ taskId, team, peer, startedAt, status, ...reminderView(taskLedger.reminderState(taskId)),
586
614
  ...(contextId !== undefined ? { contextId } : {}),
587
615
  ...(brief !== undefined ? { brief } : {}),
588
616
  ...(deadAt !== undefined ? { deadAt } : {}),
589
617
  ...(answeredAt !== undefined ? { answeredAt } : {}),
618
+ ...(receiptDelivery ? { receiptDelivery } : {}),
619
+ ...(consumed !== undefined ? { consumed, deliveryObservation: consumed ? 'consumed' : 'delivered-unconsumed' } : {}),
590
620
  ...(direction !== undefined ? { direction } : {}),
591
621
  ...(receiptExpected !== undefined ? { receiptExpected } : {}),
592
622
  ...(forwarded !== undefined ? { forwarded } : {}),
@@ -594,7 +624,7 @@ export function createInfoTools(deps) {
594
624
  ...(reason !== undefined ? { reason } : {}),
595
625
  ...(sender !== undefined ? { sender } : {}),
596
626
  })),
597
- archive: archiveAll.slice(0, 5).map(({ taskId, team, startedAt, resolvedAt, summary, brief, sender }) => ({ taskId, team, startedAt, resolvedAt, ...(summary !== undefined && summary !== '' ? { summary } : {}), ...(brief !== undefined ? { brief } : {}), ...(sender !== undefined ? { sender } : {}) })),
627
+ archive: archiveAll.slice(0, 5).map(({ taskId, team, startedAt, resolvedAt, summary, brief, withdrawalResult, sender }) => ({ ...(withdrawalResult ? { withdrawalResult } : {}), taskId, team, startedAt, resolvedAt, ...(summary !== undefined && summary !== '' ? { summary } : {}), ...(brief !== undefined ? { brief } : {}), ...(sender !== undefined ? { sender } : {}) })),
598
628
  archivedTotal: archiveAll.length,
599
629
  };
600
630
  },
@@ -119,7 +119,7 @@ export declare class A2aClient {
119
119
  * @param callerSession - caller label overriding the node's label (the calling session's node label).
120
120
  * @returns the canonical route result (success or explicit failure).
121
121
  */
122
- routeDirect(baseUrl: string, team: string, message: string, contextId?: string, signal?: AbortSignal, callerSession?: string, asyncMode?: boolean, taskIdFromCaller?: string, callbackAddress?: string, guard?: string): Promise<A2aRouteResult>;
122
+ routeDirect(baseUrl: string, team: string, message: string, contextId?: string, signal?: AbortSignal, callerSession?: string, asyncMode?: boolean, taskIdFromCaller?: string, callbackAddress?: string, guard?: string, cancelTaskId?: string): Promise<A2aRouteResult>;
123
123
  /**
124
124
  * Query a peer's `/a2a/query` outcome surface (W7 slice 2): read-only
125
125
  * lookup of one claimed task's settled outcome. The fingerprint is the
@@ -0,0 +1,5 @@
1
+ export declare const ADMITTED_BOOKKEEPING_NOTE = "\u5DF2\u9001\u8FBE\u3001\u63A5\u6536\u65B9\u8BB0\u8D26\u5931\u8D25\uFF0C\u8BF7\u5BF9\u8D26\uFF0C\u4E0D\u8981\u91CD\u6D3E\u3002Delivered, but receiver bookkeeping failed; reconcile the original task, do not redispatch.";
2
+ export declare class AdmittedBookkeepingError extends Error {
3
+ readonly code: string;
4
+ constructor(error: unknown);
5
+ }
@@ -0,0 +1,10 @@
1
+ export interface QualifiedMember {
2
+ handle: string;
3
+ fingerprint: string;
4
+ }
5
+ /** CLI handles keep their existing home/deposit rules and are never parsed as session members. */
6
+ export declare function splitQualifiedMember(ref: string): QualifiedMember | undefined;
7
+ export declare function validMemberOwner(ref: string, key: string): boolean;
8
+ export declare function qualifiedSessionMember(handle: string, key: string): string | undefined;
9
+ export type MemberProvenance = 'local-id' | 'confirmed-local' | 'confirmed-remote' | 'unknown' | 'cli' | 'conflict';
10
+ export declare function memberProvenance(ref: string, ownerKey: string | undefined, ownKey: string): MemberProvenance;
@@ -17,7 +17,21 @@
17
17
  * @module @nelsonlongxiang/dsh-open-a2a-net/canvas-store
18
18
  */
19
19
  /** On-disk shape: ordered team entries, each an ordered member id list. */
20
+ export interface SuspendedCanvasMember {
21
+ readonly team: string;
22
+ readonly member: string;
23
+ readonly localSessionId: string;
24
+ readonly reason: 'leave' | 'archive' | 'remove';
25
+ readonly at: string;
26
+ readonly token: string;
27
+ }
28
+ /** A bare session handle is not a CLI identity or a qualified owner selector. */
29
+ export declare function legacyBareMember(id: string): boolean;
20
30
  export interface CanvasSnapshot {
31
+ readonly formatVersion?: number;
32
+ readonly revision?: number;
33
+ readonly memberOwners?: Readonly<Record<string, string>>;
34
+ readonly suspendedMembers?: readonly SuspendedCanvasMember[];
21
35
  readonly teams: ReadonlyArray<{
22
36
  readonly name: string;
23
37
  readonly members: readonly string[];
@@ -52,16 +66,28 @@ export declare function validRemoteMemberRef(id: string): boolean;
52
66
  * registry knowledge: callers validate join state and resolve members.
53
67
  */
54
68
  export declare class CanvasStore {
69
+ private readonly options;
55
70
  /** Team entries in creation order; members in add order (routing priority). */
56
71
  private teams;
57
72
  private cliHome;
58
73
  private cliHomeCleared;
74
+ private suspended;
75
+ private revision;
76
+ private committed;
77
+ private memberOwners;
78
+ private disk;
79
+ private readOnlyReason;
80
+ private lastWriteError;
81
+ private failedSuspension;
59
82
  private readonly path;
60
83
  private readonly listeners;
61
84
  /**
62
85
  * @param path - the snapshot file; an empty path keeps the store in memory.
63
86
  */
64
- constructor(path: string);
87
+ constructor(path: string, options?: {
88
+ legacyBareMembers?: boolean;
89
+ ownPublicKey?: string;
90
+ });
65
91
  /**
66
92
  * Observe changes: the listener runs after every write that changed the
67
93
  * store (never on restore). A throwing listener cannot break the write.
@@ -73,13 +99,41 @@ export declare class CanvasStore {
73
99
  private restore;
74
100
  /** Persist the current snapshot (no-op without a path), then tell the listeners. */
75
101
  private persist;
102
+ private serialize;
76
103
  private write;
104
+ private replaceFile;
105
+ private lockInfo;
77
106
  /** Every team name, in creation order. */
78
107
  list(): readonly string[];
79
108
  /** Whether the named team exists. */
80
109
  hasTeam(name: string): boolean;
81
110
  /** The named team's member ids in routing-priority order (a copy). */
82
111
  membersOf(name: string): readonly string[];
112
+ /** Inspection only: never use raw rows for routing, authorization or counts. */
113
+ rawMembersOf(name: string): readonly string[];
114
+ isSuspended(name: string, member: string): boolean;
115
+ suspensionOf(name: string, member: string): readonly SuspendedCanvasMember[];
116
+ state(): {
117
+ revision: number;
118
+ suspendedMembers: readonly SuspendedCanvasMember[];
119
+ error?: string;
120
+ lock?: unknown;
121
+ directoryFsync: string;
122
+ };
123
+ ownerKeyOf(member: string): string | undefined;
124
+ private ownerConflict;
125
+ /** Owner control only: CAS the complete slot and preserve suspension plus routing priority. */
126
+ confirmMember(team: string, member: string, replacement: string, ownerKey: string | undefined, revision: unknown, expectedOwner: unknown): {
127
+ ok: boolean;
128
+ error?: string;
129
+ };
130
+ /** Durably revoke canvas consent BEFORE join/agent/roster state is removed. */
131
+ revokeLocal(id: string, ownQualified: string | undefined, inferredBare: string | undefined, reason: SuspendedCanvasMember['reason'], team?: string): boolean;
132
+ /** Owner-only CAS. The caller supplies the exact slot and the complete current suspension records. */
133
+ regulateSuspended(action: 'restore' | 'delete', team: string, member: string, revision: unknown, expected: unknown): {
134
+ ok: boolean;
135
+ error?: string;
136
+ };
83
137
  /** Frozen, operator-linked CLI home; undefined means unknown or ambiguous. */
84
138
  cliHomeOf(handle: string): string | undefined;
85
139
  pinnedCliHomes(): ReadonlyArray<readonly [string, string]>;
@@ -114,6 +168,7 @@ export declare class CanvasStore {
114
168
  */
115
169
  addMember(name: string, id: string, home?: string, options?: {
116
170
  replaceHome?: boolean;
171
+ ownerKey?: string;
117
172
  }): boolean;
118
173
  /**
119
174
  * Remove one member from one team (a no-op when absent).
@@ -77,6 +77,8 @@ export interface Config {
77
77
  * closed, it signs and accepts the audience-bound form only.
78
78
  */
79
79
  readonly originClaimUnboundWindow: boolean;
80
+ /** #335: preserve unsuspended legacy bare member inference until the owner closes the window. */
81
+ readonly canvasLegacyBareMembers: boolean;
80
82
  /**
81
83
  * Extra origins this host answers to as an OriginClaim audience, beyond
82
84
  * its card URL and the address a request arrives on (port-mapped or
@@ -21,6 +21,7 @@ import type { OriginClaimTarget } from '../origin-claim.ts';
21
21
  export type RemoteRowsCache = {
22
22
  at: number;
23
23
  rows: {
24
+ memberRef?: string;
24
25
  team: string;
25
26
  name: string;
26
27
  origin?: string;
@@ -62,6 +63,14 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
62
63
  readonly cliDeposits: CliDepositStore;
63
64
  readonly groupStore: GroupStore;
64
65
  readonly canvasStore: CanvasStore;
66
+ readonly canvasMemberForWrite: (ref: string, peer?: unknown) => Promise<{
67
+ ok: true;
68
+ member: string;
69
+ ownerKey?: string;
70
+ } | {
71
+ ok: false;
72
+ error: string;
73
+ }>;
65
74
  readonly cliHomeForAdd: (handle: string, requested?: unknown) => Promise<CliHomeDecision>;
66
75
  readonly cliHomeOptions: (handle: string) => Promise<{
67
76
  homes: string[];
@@ -70,6 +79,8 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
70
79
  }>;
71
80
  readonly layoutStore: LayoutStore;
72
81
  readonly taskLedger: TaskLedger;
82
+ readonly reminderDiagnostics?: () => unknown;
83
+ readonly nudgeEscalations?: () => import('../nudge-observability.ts').NudgeEscalations;
73
84
  readonly enforceRefused?: {
74
85
  reason: string;
75
86
  blockers: readonly string[];
@@ -126,6 +137,8 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
126
137
  readonly sessionTeamOf: (agent: Agent) => string;
127
138
  /** gh#131: the `<zone>/<id8>` handle a canvas may hold for this local session, if it reads as this session. */
128
139
  readonly ownCanvasHandleOf: (id: string) => string | undefined;
140
+ readonly revokeCanvasMember: (id: string, reason: 'leave' | 'archive' | 'remove', name?: string) => boolean;
141
+ readonly canvasMigrationState: () => Record<string, unknown>;
129
142
  /** gh#131: the local session a remote-shaped member ref reads as, if unambiguous. */
130
143
  readonly localSessionOfHandle: (ref: string) => string | undefined;
131
144
  /** gh#152: the session's canvas teams read through the local lens — a handle-stored membership of this session counts. */
@@ -150,6 +163,14 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
150
163
  count: number;
151
164
  lastAt: number;
152
165
  }>;
166
+ readonly claimRejectsInboundCount: number;
167
+ readonly claimRejectsRecent: ReadonlyArray<{
168
+ caller: string;
169
+ callerUrl: string;
170
+ reason: string;
171
+ count: number;
172
+ lastAt: number;
173
+ }>;
153
174
  readonly ownCardUrl: string;
154
175
  /** #282 S2: this host's canvas owner fingerprint. */
155
176
  readonly ownCanvasFingerprint: string;
@@ -26,7 +26,7 @@ export interface FileFaceDeps {
26
26
  readonly ctx: Context;
27
27
  readonly config: Pick<Config, 'apiKey' | 'team'>;
28
28
  readonly blobStore: BlobStore;
29
- readonly canvasStore: Pick<CanvasStore, 'membersOf'>;
29
+ readonly canvasStore: Pick<CanvasStore, 'membersOf'> & Partial<Pick<CanvasStore, 'ownerKeyOf'>>;
30
30
  readonly verifyOriginClaim: (req: IncomingMessage, options?: {
31
31
  readonly cli?: boolean;
32
32
  }) => Promise<OriginClaimResult>;
@@ -22,6 +22,7 @@ import type { IdempotencyStore } from '../idempotency-store.ts';
22
22
  import type { JoinedSessions } from '../joined-store.ts';
23
23
  import { type TaskLedger } from '../task-ledger.ts';
24
24
  import { type Admission, type RemoteCliProof, type SettlementRefusal, type Settler } from '../task-auth.ts';
25
+ import { type OriginClaimVerdict } from '../origin-claim.ts';
25
26
  import type { Config } from '../index.ts';
26
27
  /**
27
28
  * #139 (design §4.4): add `settlement: {status:'refused', reason, task_id}`
@@ -46,6 +47,7 @@ export type ColdWake = {
46
47
  export type RouteOutcome = {
47
48
  ok: true;
48
49
  reply: string;
50
+ receiverBookkeeping?: 'failed';
49
51
  status?: string;
50
52
  bridge?: 'native-teams';
51
53
  placeholder?: boolean;
@@ -105,18 +107,7 @@ export type NativeRoundOutcome = {
105
107
  readonly aborted?: boolean;
106
108
  };
107
109
  /** What verifyOriginClaim answers (see index.ts; reason widens to string here — the faces only interpolate it). */
108
- export type OriginClaimResult = {
109
- ok: true;
110
- caller: string;
111
- callerUrl: string;
112
- card: A2aPeerCard;
113
- bound: boolean;
114
- cli?: true;
115
- bodyBound?: true;
116
- } | {
117
- ok: false;
118
- reason: string;
119
- };
110
+ export type OriginClaimResult = OriginClaimVerdict;
120
111
  /** The closure captures the wire faces read (grows with each extracted face). */
121
112
  export interface WireDeps {
122
113
  readonly ctx: Context;
@@ -124,6 +115,14 @@ export interface WireDeps {
124
115
  readonly config: Config;
125
116
  readonly cliDeposits: CliDepositStore;
126
117
  readonly canvasStore: CanvasStore;
118
+ readonly canvasMemberForWrite: (ref: string, peer?: unknown, editorCard?: A2aPeerCard) => Promise<{
119
+ ok: true;
120
+ member: string;
121
+ ownerKey?: string;
122
+ } | {
123
+ ok: false;
124
+ error: string;
125
+ }>;
127
126
  readonly canvasNameReserved: (name: string) => boolean;
128
127
  readonly sessionNodes: Map<string, Agent>;
129
128
  readonly joinedSessions: JoinedSessions;
@@ -143,6 +142,9 @@ export interface WireDeps {
143
142
  readonly forwardReceiptBasis: (taskId: string, caller: string, cliHandle: string, claim: Extract<OriginClaimResult, {
144
143
  ok: true;
145
144
  }>) => string | undefined;
145
+ readonly receiptReplayAllowed: (message: string, caller: string, team: string, claim: Extract<OriginClaimResult, {
146
+ ok: true;
147
+ }>) => boolean;
146
148
  readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
147
149
  /** This host's own card URL (read when a request lands; set once the web server settles). */
148
150
  readonly ownCardUrl: string;
@@ -152,7 +154,7 @@ export interface WireDeps {
152
154
  readonly peerKeyAdmits: (req: IncomingMessage, rawBody?: Uint8Array) => Promise<boolean>;
153
155
  readonly a2aCallerRefusal: (caller: string, targetTeam?: string) => string | undefined;
154
156
  /** #114 S2: the same decision with the rule and basis that admitted the caller. */
155
- readonly a2aCallerAdmission: (caller: string, targetTeam?: string, cliProof?: RemoteCliProof) => Admission;
157
+ readonly a2aCallerAdmission: (caller: string, targetTeam?: string, cliProof?: RemoteCliProof, signerKey?: string) => Admission;
156
158
  /** #114 S2: this host's allowlist version right now (design §2.3). */
157
159
  readonly allowlistVersionNow: () => string;
158
160
  /** #114 S6: record the version if it moved and re-judge affected tasks (observe only). */
@@ -192,6 +194,11 @@ export interface WireDeps {
192
194
  readonly sessionTeamOf: (agent: Agent) => string;
193
195
  /** gh#131: the local session one of this host's own `<zone>/<id8>` handles names, if exactly one. */
194
196
  readonly localSessionOfHandle: (ref: string) => string | undefined;
197
+ readonly cancelQueuedTask: (taskId: string, message: string, sender: string, original?: import('../task-ledger.ts').TaskRecord) => Promise<{
198
+ removed: boolean;
199
+ steered: boolean;
200
+ }>;
201
+ readonly followupRelay: (agent: Agent, text: string, sender?: string, localFrom?: string, taskId?: string, callerKey?: string, taskExpected?: boolean) => void;
195
202
  readonly steerRelay: (agent: Agent, text: string, sender?: string) => void;
196
203
  readonly fromClause: (from: string) => string;
197
204
  readonly sanitizeFromClause: (from: string) => string;
@@ -210,7 +217,7 @@ export interface WireDeps {
210
217
  readonly nativeTeamsRound: (prepared: Extract<NativePrepared, {
211
218
  ok: true;
212
219
  }>, team: string, caller: string, message: string, taskId: string | undefined, proofClause: string, external?: AbortSignal, proofHeader?: (team: string, taskId: string, from: string, proofClause: string) => string) => Promise<NativeRoundOutcome>;
213
- readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string, proofClause?: string, directHeader?: (from: string) => string, localFrom?: string, onSteered?: (target: Agent, floor: number) => void) => Promise<RouteOutcome>;
220
+ readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string, proofClause?: string, directHeader?: (from: string) => string, localFrom?: string, onSteered?: (target: Agent, floor: number) => void, callerKey?: string) => Promise<RouteOutcome>;
214
221
  }
215
222
  /**
216
223
  * #187: what a verified claim may vouch for at settlement. The signing key always (the card was
@@ -0,0 +1,25 @@
1
+ /** #356 sourced follow-up admission after the existing A2A authorization/queue gates. */
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import { MessageId } from '@deepseek-ai/dsh-llm';
4
+ import type { TaskLedger } from './task-ledger.ts';
5
+ export interface FollowupDeps {
6
+ ledger: TaskLedger;
7
+ taint: (target: Agent, localFrom?: string) => void;
8
+ warn: (code: string) => void;
9
+ }
10
+ export declare class FollowupDelivery {
11
+ private readonly d;
12
+ constructor(d: FollowupDeps);
13
+ deliver(agent: Agent, text: string, sender?: string, localFrom?: string, taskId?: string, callerKey?: string, taskExpected?: boolean): void;
14
+ /** Only called after the existing cancel authorization has identified the original task. */
15
+ withdraw(taskId: string, controller: {
16
+ updateQueue: (request: {
17
+ sessionId: Agent['id'];
18
+ itemId: MessageId;
19
+ action: {
20
+ kind: 'remove';
21
+ };
22
+ }) => Promise<unknown>;
23
+ } | undefined, resolve: (id: string) => Agent | undefined): Promise<'removed' | 'claimed' | 'legacy'>;
24
+ claimed(agent: Agent, messageId: string): void;
25
+ }
@@ -142,6 +142,8 @@ export declare class IdempotencyStore {
142
142
  * Unknown or already-settled ids answer `false`. Never throws.
143
143
  * @returns whether the outcome was recorded.
144
144
  */
145
+ /** Release only an unchanged, uncompleted receipt attempt whose settlement did not commit. */
146
+ releaseUncompleted(taskId: string, fingerprint: string): void;
145
147
  recordOutcome(taskId: string, outcome: TaskOutcome, settledAt?: number): boolean;
146
148
  /**
147
149
  * Read-only outcome lookup for the `/a2a/query` surface (W7 slice 2).
@@ -0,0 +1,14 @@
1
+ /** Bounded completion gate for inbound request verification. */
2
+ import type { IncomingMessage } from 'node:http';
3
+ import type { Admission } from './task-auth.ts';
4
+ import { type OriginClaimVerdict } from './origin-claim.ts';
5
+ export declare const WIRE_ERROR_INBOUND_VERIFICATION = -32011;
6
+ export declare function hasOriginClaimFields(req: Pick<IncomingMessage, 'headers'>): boolean;
7
+ export declare function completeInboundVerification(work: () => Promise<OriginClaimVerdict>, timeoutMs?: number): Promise<OriginClaimVerdict>;
8
+ export declare function verifiedInboundAdmission(claim: OriginClaimVerdict, present: boolean, requireBound: boolean, admit: (signerKey: string | undefined) => Admission): {
9
+ ok: true;
10
+ admission: Admission;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
@@ -30,6 +30,11 @@ declare module '@deepseek-ai/dsh-llm' {
30
30
  * ui-chat's relay context body renders "来自 X" + the full text from
31
31
  * this field; without it the row falls back to an opaque body. */
32
32
  senderSessionId?: string;
33
+ /** Audited A2A task identity and clean same-host lineage, never a human principal. */
34
+ taskId?: string;
35
+ localSender?: string;
36
+ /** Host-built node reminder only; never copied from an inbound peer source. */
37
+ nodeReminder?: true;
33
38
  };
34
39
  }
35
40
  }
@@ -0,0 +1,17 @@
1
+ /** Observation only; no scheduling, delivery or ledger authority. */
2
+ export interface NudgeEscalations {
3
+ queued: number;
4
+ unavailable: number;
5
+ failed: number;
6
+ lastAt?: number;
7
+ lastError?: string;
8
+ }
9
+ export declare function safeObservationCode(error: unknown, fallback: string): string;
10
+ export declare class NudgeObservability {
11
+ private readonly warn;
12
+ private readonly now;
13
+ private value;
14
+ constructor(warn: (message: string) => void, now?: () => number);
15
+ snapshot(): NudgeEscalations;
16
+ note(result: 'queued' | 'unavailable' | 'failed', team: string, ref: string, error?: unknown): void;
17
+ }
@@ -28,6 +28,8 @@ import type { CardCacheEntry } from './runtime.ts';
28
28
  import type { A2aPeerCard } from './types.ts';
29
29
  /** ts skew tolerance (protocol constant, §7). */
30
30
  export declare const ORIGIN_CLAIM_SKEW_MS: number;
31
+ /** Local verification work budget, including response body reads. */
32
+ export declare const ORIGIN_CLAIM_VERIFY_MS = 5000;
31
33
  /** Domain tag of the bound form: a v1 signature can never verify as v2. */
32
34
  export declare const ORIGIN_CLAIM_BOUND_TAG = "a2a-origin-claim/2";
33
35
  export declare const ORIGIN_CLAIM_BODY_TAG = "a2a-origin-claim/3";
@@ -39,7 +41,7 @@ export interface OriginClaimTarget {
39
41
  readonly method: string;
40
42
  readonly body?: string | Uint8Array;
41
43
  }
42
- export type OriginClaimRefusal = 'absent' | 'bad-caller-shape' | 'bad-ts' | 'no-card' | 'bad-sig' | 'not-derivable' | 'wrong-audience' | 'unbound-closed' | 'body-mismatch';
44
+ export type OriginClaimRefusal = 'absent' | 'bad-caller-shape' | 'bad-ts' | 'no-card' | 'bad-sig' | 'not-derivable' | 'wrong-audience' | 'unbound-closed' | 'body-mismatch' | 'verification-timeout';
43
45
  /** Only the canonical origin root can claim that origin's identity. */
44
46
  export declare function isRootOriginUrl(value: string): boolean;
45
47
  /**
@@ -127,6 +129,10 @@ export declare function verifyOriginClaim(req: IncomingMessage, d: {
127
129
  /** A loopback arrival is an audience of its own (default); off when originClaimUrl declares a tunnel or proxy. */
128
130
  readonly loopbackArrival?: boolean;
129
131
  readonly now: number;
132
+ /** Internal deterministic test seam; production uses ORIGIN_CLAIM_VERIFY_MS. */
133
+ readonly timeoutMs?: number;
134
+ /** Request admission requires a current card read, not a discovery cache assertion. */
135
+ readonly freshCard?: boolean;
130
136
  /** Exact received bytes, supplied by the bounded body reader. Missing bytes cannot verify Sig3. */
131
137
  readonly rawBody?: Uint8Array;
132
138
  /** On the faces that admit CLI identities: the deposit a claim URL names. */
@@ -7,6 +7,7 @@ export interface OutboundScopeRecords {
7
7
  canvasNames(): readonly string[];
8
8
  canvasMembers(name: string): readonly string[];
9
9
  canvasTeam(name: string): string;
10
+ memberHandle?(ref: string): string;
10
11
  peerOrgs(): Iterable<readonly {
11
12
  team?: unknown;
12
13
  members?: unknown;
@@ -165,9 +165,13 @@ export declare class PeerStore {
165
165
  * re-seeded from config on every boot, and scores rebuild on contact.
166
166
  */
167
167
  private dirty;
168
+ private closed;
168
169
  private writeChain;
169
170
  private pendingTimer;
170
171
  private persist;
172
+ private writePending;
173
+ /** Final teardown: drain once, then late peer responses cannot recreate this home. */
174
+ close(): Promise<void>;
171
175
  /**
172
176
  * Await the last scheduled write (tests and teardown). Resolves when the
173
177
  * debounced write chain has settled with the current in-memory state.
@@ -17,6 +17,13 @@ export declare const WIRE_ERROR_QUEUE_FULL = -32009;
17
17
  export declare const DEFAULT_TARGET_QUEUE_CAP = 3;
18
18
  /** The ledger fields the count reads. */
19
19
  export interface QueueRow {
20
+ readonly taskId?: string;
21
+ readonly auth?: {
22
+ readonly caller: string;
23
+ };
24
+ readonly sender?: {
25
+ readonly id: string;
26
+ };
20
27
  readonly team: string;
21
28
  readonly peer: string;
22
29
  readonly status: string;
@@ -30,6 +37,6 @@ export declare function queuedFor(rows: readonly QueueRow[], team: string): numb
30
37
  /** The refusal text when `team`'s queue is full, else undefined. `cap <= 0` disables the guard. */
31
38
  export declare function queueFullRefusal(rows: readonly QueueRow[], team: string, message: string, cap: number, mailbox: boolean, ownTask: (taskId: string) => boolean): string | undefined;
32
39
  /** #282 S1: the one next step for a full target queue. */
33
- export declare const QUEUE_FULL_NEXT = "wait for a receipt from the target (a2a-net inbox / a2a_tasks), then send again with a new task id; do not resend in a loop";
40
+ export declare const QUEUE_FULL_NEXT = "wait for a receipt from the target after it reconciles its outstanding tasks (a2a-net inbox / a2a_tasks), then send again with a new task id; do not resend in a loop";
34
41
  /** Wire-face adapter in the flood guard's shape: a refused fresh key is FAILED, not stranded pending. */
35
- export declare function createQueueGuard(rows: () => readonly QueueRow[], cap: () => number, ownTask: (taskId: string) => boolean, outcomes: Pick<IdempotencyStore, 'recordOutcome'>, recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void): (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
42
+ export declare function createQueueGuard(rows: () => readonly QueueRow[], cap: () => number, ownTask: (taskId: string) => boolean, outcomes: Pick<IdempotencyStore, 'recordOutcome'>, recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void, reminder?: (taskId: string) => Record<string, unknown>): (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
@@ -11,6 +11,8 @@
11
11
  export interface ReceiptLadderDeps {
12
12
  /** Try delivering the receipt text to one routable team. */
13
13
  deliver: (team: string) => Promise<unknown>;
14
+ /** Transient caller failures never enter the receipt-bearing owner tier. */
15
+ transient?: (error: unknown) => boolean;
14
16
  /** This host's process/team mailbox (tier two). */
15
17
  ownerTeam: string;
16
18
  /** Observability hook: `stage` distinguishes `to <team>` / `to owner <team>` / `both lanes`. */