@nelsonlongxiang/dsh-open-a2a-net 0.6.43 → 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.
- package/assets/nexusDist/assets/index-CA_YZTWG.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/cli/a2a.mjs +19 -3
- package/lib/a2a-client.js +10 -2
- package/lib/admitted-bookkeeping.js +7 -0
- package/lib/canvas-discovery.js +11 -1
- package/lib/canvas-member.js +43 -0
- package/lib/canvas-store.js +70 -4
- package/lib/direct-forward.js +9 -7
- package/lib/faces/admin.js +50 -7
- package/lib/faces/files.js +2 -1
- package/lib/faces/wire.js +566 -468
- package/lib/followup-delivery.js +89 -0
- package/lib/idempotency-store.js +8 -0
- package/lib/inbound-verification.js +23 -0
- package/lib/index.js +478 -156
- package/lib/nudge-observability.js +34 -0
- package/lib/origin-claim.js +36 -9
- package/lib/outbound-scope.js +4 -2
- package/lib/peer-directory.js +15 -5
- package/lib/peer-store.js +31 -17
- package/lib/queue-guard.js +4 -3
- package/lib/receipt-ladder.js +1 -1
- package/lib/receipt-retry.js +58 -0
- package/lib/reconciliation-reminders.js +148 -0
- package/lib/reconciliation-state.js +27 -0
- package/lib/session-transcript.js +46 -18
- package/lib/skill/SKILL.md +5 -0
- package/lib/skill/references/cli.md +4 -1
- package/lib/skill/references/routing.md +31 -1
- package/lib/task-auth.js +3 -3
- package/lib/task-delivery.js +23 -0
- package/lib/task-ledger-persistence.js +51 -0
- package/lib/task-ledger-rows.js +5 -0
- package/lib/task-ledger.js +219 -20
- package/lib/tools/info.js +37 -7
- package/lib/types/a2a-client.d.ts +1 -1
- package/lib/types/admitted-bookkeeping.d.ts +5 -0
- package/lib/types/canvas-member.d.ts +10 -0
- package/lib/types/canvas-store.d.ts +8 -0
- package/lib/types/faces/admin.d.ts +19 -0
- package/lib/types/faces/files.d.ts +1 -1
- package/lib/types/faces/wire.d.ts +21 -14
- package/lib/types/followup-delivery.d.ts +25 -0
- package/lib/types/idempotency-store.d.ts +2 -0
- package/lib/types/inbound-verification.d.ts +14 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/nudge-observability.d.ts +17 -0
- package/lib/types/origin-claim.d.ts +7 -1
- package/lib/types/outbound-scope.d.ts +1 -0
- package/lib/types/peer-store.d.ts +4 -0
- package/lib/types/queue-guard.d.ts +9 -2
- package/lib/types/receipt-ladder.d.ts +2 -0
- package/lib/types/receipt-retry.d.ts +22 -0
- package/lib/types/reconciliation-reminders.d.ts +41 -0
- package/lib/types/reconciliation-state.d.ts +32 -0
- package/lib/types/session-transcript.d.ts +6 -1
- package/lib/types/task-auth.d.ts +7 -0
- package/lib/types/task-delivery.d.ts +17 -0
- package/lib/types/task-ledger-persistence.d.ts +3 -0
- package/lib/types/task-ledger-rows.d.ts +19 -2
- package/lib/types/task-ledger.d.ts +48 -1
- package/lib/types/tools/info.d.ts +3 -0
- package/lib/types/types.d.ts +3 -0
- package/nexus-stage/src/federation.ts +1 -0
- package/nexus-stage/src/main.ts +10 -2
- package/nexus-stage/src/member-provenance-picker.ts +13 -0
- package/nexus-stage/src/planning-member-handles.ts +4 -1
- package/nexus-stage/src/planning-view.ts +3 -3
- package/nexus-stage/src/workbench.ts +28 -6
- package/nexus-stage/src/world.ts +1 -0
- package/package.json +3 -2
- package/assets/nexusDist/assets/index-CaA76j0F.js +0 -9
|
@@ -26,7 +26,12 @@ export declare function scanZstdFrames(buffer: Buffer): FrameRange[];
|
|
|
26
26
|
* artifacts are split into frames and decoded frame by frame (node's public
|
|
27
27
|
* decoder is single-frame).
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
type BoundedZstdDecoder = (input: Uint8Array, options: {
|
|
30
|
+
maxOutputLength: number;
|
|
31
|
+
}) => Buffer;
|
|
32
|
+
/** Codec seam for tests; budgets may only tighten the shipped hard caps. */
|
|
33
|
+
export declare function createSessionLogDecoder(decoder?: BoundedZstdDecoder | undefined, outputCap?: number): (buffer: Buffer) => string;
|
|
34
|
+
export declare const decodeSessionLog: (buffer: Buffer) => string;
|
|
30
35
|
/**
|
|
31
36
|
* Extract the last `limit` human-visible turns from a decoded session log.
|
|
32
37
|
* User lines come from user/message data.content[].text; assistant lines from
|
package/lib/types/task-auth.d.ts
CHANGED
|
@@ -43,10 +43,17 @@ export interface AdmissionRecords {
|
|
|
43
43
|
readonly teams: readonly string[];
|
|
44
44
|
}[];
|
|
45
45
|
joined(): readonly string[];
|
|
46
|
+
/** Caller-only proof projection; targets still use the complete local registry. */
|
|
47
|
+
callerJoined?(): readonly string[];
|
|
48
|
+
callerMemberships?(): readonly {
|
|
49
|
+
readonly session: string;
|
|
50
|
+
readonly teams: readonly string[];
|
|
51
|
+
}[];
|
|
46
52
|
canvasNames(): readonly string[];
|
|
47
53
|
/** Canvas names the session sits in. */
|
|
48
54
|
canvasTeamsOf(sessionId: string): readonly string[];
|
|
49
55
|
canvasMembersOf(name: string): readonly string[];
|
|
56
|
+
callerCanvasMembersOf?(name: string): readonly string[];
|
|
50
57
|
canvasRemoteMembersOf(name: string): readonly string[];
|
|
51
58
|
/** The canvasOrg lists of the warm peer-card cache. */
|
|
52
59
|
peerOrgs(): Iterable<readonly PeerOrgEntry[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TaskDelivery {
|
|
2
|
+
version: 1;
|
|
3
|
+
taskId: string;
|
|
4
|
+
messageId: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
fingerprint: string;
|
|
7
|
+
callerKey?: string;
|
|
8
|
+
/** Distinguishes task births from receipt-only or synchronous receiptless occurrences. */
|
|
9
|
+
taskExpected?: true;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
state: 'reserved' | 'enqueued' | 'claimed' | 'cancelled' | 'failed';
|
|
12
|
+
}
|
|
13
|
+
export declare const TASK_DELIVERY_CAP = 512;
|
|
14
|
+
/** Keep completed/cancelled occurrence identities through the server idempotency window. */
|
|
15
|
+
export declare const TASK_DELIVERY_RETENTION_MS: number;
|
|
16
|
+
export declare function deliveryFingerprint(sessionId: string, text: string, sender?: string, localFrom?: string): string;
|
|
17
|
+
export declare function parseTaskDelivery(raw: unknown): TaskDelivery | undefined;
|
|
@@ -105,6 +105,9 @@ export declare function withAuth(raw: unknown, entry: {
|
|
|
105
105
|
export declare function routingOf(row: Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>;
|
|
106
106
|
/** #139: what an outbound row is born with besides its sender. */
|
|
107
107
|
export interface OutboundBirth {
|
|
108
|
+
readonly consumed?: boolean;
|
|
109
|
+
/** Exact receiver selected at local delivery, never inferred from a later canvas roster. */
|
|
110
|
+
readonly receiverSessionId?: string;
|
|
108
111
|
readonly forwarded?: true;
|
|
109
112
|
/** Local-candidate admission, or a forwarded online CLI's verified wire admission. */
|
|
110
113
|
readonly auth?: TaskAuthorization;
|
|
@@ -115,7 +118,7 @@ export interface OutboundBirth {
|
|
|
115
118
|
readonly receiverMembers?: readonly string[];
|
|
116
119
|
}
|
|
117
120
|
/** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
|
|
118
|
-
export declare function birthFacts(peer: string, birth: OutboundBirth | undefined, team?: string, sender?: Sender, now?: number, warn?: (message: string) => void): Pick<TaskRecord, 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'receiverMembers'>;
|
|
121
|
+
export declare function birthFacts(peer: string, birth: OutboundBirth | undefined, team?: string, sender?: Sender, now?: number, warn?: (message: string) => void): Pick<TaskRecord, 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'receiverMembers' | 'receiverSessionId' | 'consumed'>;
|
|
119
122
|
/** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
|
|
120
123
|
export declare const ROUTING_FACT_CAP = 256;
|
|
121
124
|
export declare function parseReceiverMembers(value: unknown, warn?: (message: string) => void): readonly string[] | undefined;
|
|
@@ -125,9 +128,14 @@ export declare function parseReceiverMembers(value: unknown, warn?: (message: st
|
|
|
125
128
|
* a remote outbound row; anything else is dropped (the row survives as one
|
|
126
129
|
* born before the fields, judged by current membership and origin).
|
|
127
130
|
*/
|
|
128
|
-
export declare function withRouting(entry: Record<string, unknown>, warn?: (message: string) => void): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>;
|
|
131
|
+
export declare function withRouting(entry: Record<string, unknown>, warn?: (message: string) => void): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers' | 'receiptDelivery'>;
|
|
129
132
|
/** One tracked outbound task still in the owed book (unsettled). */
|
|
130
133
|
export interface TaskRecord {
|
|
134
|
+
/** Local work answered, but successful receipt delivery is not confirmed; never a settlement. */
|
|
135
|
+
receiptDelivery?: 'receipt-undelivered';
|
|
136
|
+
/** Observation from exact inbox claim or peer response, never an acknowledgement. */
|
|
137
|
+
consumed?: boolean;
|
|
138
|
+
readonly receiverSessionId?: string;
|
|
131
139
|
readonly taskId: string;
|
|
132
140
|
readonly team: string;
|
|
133
141
|
readonly peer: string;
|
|
@@ -184,8 +192,13 @@ export interface TaskRecord {
|
|
|
184
192
|
*/
|
|
185
193
|
reason?: DeadReason;
|
|
186
194
|
}
|
|
195
|
+
export declare const WITHDRAWAL_RESULTS: readonly ["removed", "claimed-steered", "withdraw-failed-steered", "no-agent"];
|
|
196
|
+
export type WithdrawalResult = typeof WITHDRAWAL_RESULTS[number];
|
|
197
|
+
export declare function isWithdrawalResult(value: unknown): value is WithdrawalResult;
|
|
187
198
|
/** One settled task kept for audit, with its receipt-correlated outcome. */
|
|
188
199
|
export interface ArchivedRecord {
|
|
200
|
+
/** Cancellation notification observation; independent of the terminal task outcome. */
|
|
201
|
+
readonly withdrawalResult?: WithdrawalResult;
|
|
189
202
|
readonly taskId: string;
|
|
190
203
|
readonly team: string;
|
|
191
204
|
readonly peer: string;
|
|
@@ -298,6 +311,10 @@ export interface ReceiptResolvedInfo {
|
|
|
298
311
|
}
|
|
299
312
|
/** The persisted ledger document: the owed book plus its settled archive. */
|
|
300
313
|
export interface TaskLedgerSnapshot {
|
|
314
|
+
readonly taskDeliveryRestoreError?: true;
|
|
315
|
+
readonly taskDeliveries?: readonly import('./task-delivery.ts').TaskDelivery[];
|
|
316
|
+
readonly reconciliationSessions?: Record<string, number>;
|
|
317
|
+
readonly reconciliationReminders?: Record<string, import('./reconciliation-state.ts').ReminderState>;
|
|
301
318
|
readonly tasks: readonly TaskRecord[];
|
|
302
319
|
readonly archived?: readonly ArchivedRecord[];
|
|
303
320
|
/** #114 S4: the bounded ring of refused / would-be-refused settlements. */
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* @module @nelsonlongxiang/dsh-open-a2a-net/task-ledger
|
|
20
20
|
*/
|
|
21
21
|
import type { TaskAuthorization } from './task-auth.ts';
|
|
22
|
+
import { type TaskDelivery } from './task-delivery.ts';
|
|
23
|
+
import { type ReminderState } from './reconciliation-state.ts';
|
|
22
24
|
import { type ConservationVerdict } from './ledger-conservation.ts';
|
|
23
25
|
import type { TaskRecord, ArchivedRecord, Sender, OutboundBirth, TaskLedgerOptions, ReceiptResolvedInfo, SettlementRefusalRecord, SettlementJudge, AbandonResult, CancelledResult } from './task-ledger-rows.ts';
|
|
24
26
|
export * from './task-ledger-rows.ts';
|
|
@@ -45,6 +47,49 @@ export declare class TaskLedger {
|
|
|
45
47
|
/** Injectable clock (option `now`). */
|
|
46
48
|
private readonly now;
|
|
47
49
|
private readonly warn;
|
|
50
|
+
/** Last successful commit, for rollback when a mutation cannot persist. */
|
|
51
|
+
private committed;
|
|
52
|
+
private deliveries;
|
|
53
|
+
private deliveryRestoreError;
|
|
54
|
+
markConsumed(taskId: string, consumed: boolean): void;
|
|
55
|
+
deliveryFor(taskId: string): TaskDelivery | undefined;
|
|
56
|
+
deliveryByMessage(sessionId: string, messageId: string): TaskDelivery | undefined;
|
|
57
|
+
reserveDelivery(record: TaskDelivery): TaskDelivery;
|
|
58
|
+
updateDelivery(taskId: string, state: TaskDelivery['state']): void;
|
|
59
|
+
private reminderRecoveryPending;
|
|
60
|
+
private reminders;
|
|
61
|
+
private reminderSessions;
|
|
62
|
+
reminderSessionAt(id: string): number | undefined;
|
|
63
|
+
reminderState(taskId: string): ReminderState | undefined;
|
|
64
|
+
/** Atomic observation update; no task status, answer, or conservation mutation. */
|
|
65
|
+
saveReminders(updates: readonly {
|
|
66
|
+
taskId: string;
|
|
67
|
+
state: ReminderState | undefined;
|
|
68
|
+
}[], session?: {
|
|
69
|
+
id: string;
|
|
70
|
+
at: number | undefined;
|
|
71
|
+
}): void;
|
|
72
|
+
private admittedFailure;
|
|
73
|
+
private admittedFailures;
|
|
74
|
+
noteAdmittedBookkeepingFailure(error: unknown): void;
|
|
75
|
+
orphanDelivery(taskId: string): boolean;
|
|
76
|
+
deliveryBookkeepingState(): {
|
|
77
|
+
orphaned: number;
|
|
78
|
+
failures: number;
|
|
79
|
+
lastError?: {
|
|
80
|
+
at: number;
|
|
81
|
+
code: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
private writeFailure;
|
|
85
|
+
/** Diagnostic-only; never implies that a failed mutation committed. No paths or payloads exposed. */
|
|
86
|
+
persistenceState(): {
|
|
87
|
+
ok: boolean;
|
|
88
|
+
lastError?: {
|
|
89
|
+
at: number;
|
|
90
|
+
code: string;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
48
93
|
/**
|
|
49
94
|
* @param path - persistence file (`<dsh-home>/a2a/tasks.json`); empty = no persistence.
|
|
50
95
|
* @param options - TTL/capacity overrides; defaults come from the exported constants.
|
|
@@ -99,7 +144,7 @@ export declare class TaskLedger {
|
|
|
99
144
|
* @param from - the caller's receipt target (label or callback address).
|
|
100
145
|
* @param contextId - the delivery's conversation id, kept for follow-up routes (empty omits it).
|
|
101
146
|
*/
|
|
102
|
-
trackInbound(taskId: string, team: string, from: string, contextId?: string, receiptExpected?: boolean, brief?: string, auth?: TaskAuthorization, deliveredTo?: string): void;
|
|
147
|
+
trackInbound(taskId: string, team: string, from: string, contextId?: string, receiptExpected?: boolean, brief?: string, auth?: TaskAuthorization, deliveredTo?: string, receiverSessionId?: string, consumed?: boolean): void;
|
|
103
148
|
/** Record an answer only from the local member that actually received this row; never settle or change counters. */
|
|
104
149
|
markAnswered(taskId: string, target: string): boolean;
|
|
105
150
|
/**
|
|
@@ -138,6 +183,7 @@ export declare class TaskLedger {
|
|
|
138
183
|
* the route layer reads team/contextId from here for its best-effort
|
|
139
184
|
* steer, which is the documented boundary until origin-auth lands.
|
|
140
185
|
*/
|
|
186
|
+
recordWithdrawalResult(taskId: string, result: import('./task-ledger-rows.ts').WithdrawalResult): void;
|
|
141
187
|
cancel(taskId: string, reason?: string): CancelledResult;
|
|
142
188
|
/**
|
|
143
189
|
* Correlate one message against the ledger: a receipt (message starting
|
|
@@ -204,4 +250,5 @@ export declare class TaskLedger {
|
|
|
204
250
|
private restore;
|
|
205
251
|
/** Persist the ledger (no-op when no path was configured). */
|
|
206
252
|
private persist;
|
|
253
|
+
private snapshot;
|
|
207
254
|
}
|
|
@@ -14,6 +14,7 @@ import type { TaskLedger } from '../task-ledger.ts';
|
|
|
14
14
|
import type { TeamMembershipStore } from '../team-store.ts';
|
|
15
15
|
/** One directory row shared with listDirectoryTeams (which stays in index.ts). */
|
|
16
16
|
export type DirectoryTeamRow = {
|
|
17
|
+
memberRef?: string;
|
|
17
18
|
team: string;
|
|
18
19
|
session: string;
|
|
19
20
|
name: string;
|
|
@@ -41,9 +42,11 @@ export interface InfoToolsDeps {
|
|
|
41
42
|
readonly ownCardUrl: string;
|
|
42
43
|
readonly peerStore: PeerStore;
|
|
43
44
|
readonly taskLedger: TaskLedger;
|
|
45
|
+
readonly nudgeEscalations?: () => import('../nudge-observability.ts').NudgeEscalations;
|
|
44
46
|
readonly joinedSessions: JoinedSessions;
|
|
45
47
|
readonly teamMemberships: TeamMembershipStore;
|
|
46
48
|
readonly canvasStore: CanvasStore;
|
|
49
|
+
readonly canvasMigrationState: () => Record<string, unknown>;
|
|
47
50
|
readonly exposureStore: ExposureStore;
|
|
48
51
|
readonly idempotencyStore: IdempotencyStore;
|
|
49
52
|
readonly cardCache: Map<string, CardCacheEntry>;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Wire and configuration vocabulary for the A2A client extension.
|
|
3
3
|
* @module @nelsonlongxiang/dsh-open-a2a-net/types
|
|
4
4
|
*/
|
|
5
|
+
import type { RECEIPT_RETRYABLE } from './receipt-retry.ts';
|
|
5
6
|
/** One peer node's agent card — the decentralized discovery record. */
|
|
6
7
|
export interface A2aPeerCard {
|
|
7
8
|
/** Human-facing peer name. */
|
|
@@ -108,6 +109,7 @@ export interface ZoneRecord {
|
|
|
108
109
|
}
|
|
109
110
|
/** Canonical `a2a_route` success value. */
|
|
110
111
|
export interface A2aRouteOk {
|
|
112
|
+
readonly receiverBookkeeping?: 'failed';
|
|
111
113
|
/** Actual member face returned by a forwarded peer delivery. */
|
|
112
114
|
readonly deliveredTo?: string;
|
|
113
115
|
/**
|
|
@@ -150,6 +152,7 @@ export interface A2aRouteOk {
|
|
|
150
152
|
}
|
|
151
153
|
/** Canonical `a2a_route` failure value. */
|
|
152
154
|
export interface A2aRouteError {
|
|
155
|
+
readonly [RECEIPT_RETRYABLE]?: true;
|
|
153
156
|
/** A forwarded receipt never promises another receipt, including uncertain waits. */
|
|
154
157
|
readonly receipt?: 'none';
|
|
155
158
|
readonly ok: false;
|
package/nexus-stage/src/main.ts
CHANGED
|
@@ -299,6 +299,13 @@ const bench = createWorkbench({
|
|
|
299
299
|
return { ...body, ok: res.ok && body.ok === true }
|
|
300
300
|
} catch (error) { return { ok: false, error: String(error) } }
|
|
301
301
|
},
|
|
302
|
+
async confirmMember(name, id, replacement, revision, expectedOwner, peer) {
|
|
303
|
+
try {
|
|
304
|
+
const response=await controlFetch('/__dsh_a2a/canvas',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'confirm-member',name,id,replacement,revision,expectedOwner,...(peer?{peer}:{})})})
|
|
305
|
+
const body=await response.json() as {ok?:boolean;error?:string}
|
|
306
|
+
return {ok:response.ok&&body.ok===true,error:body.error}
|
|
307
|
+
}catch(error){return{ok:false,error:String(error)}}
|
|
308
|
+
},
|
|
302
309
|
async regulateSuspended(action, name, id, revision, expected) {
|
|
303
310
|
try {
|
|
304
311
|
const response = await controlFetch('/__dsh_a2a/canvas', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, name, id, revision, expected }) })
|
|
@@ -306,14 +313,14 @@ const bench = createWorkbench({
|
|
|
306
313
|
return { ok: response.ok && body.ok === true, error: body.error }
|
|
307
314
|
} catch (error) { return { ok: false, error: String(error) } }
|
|
308
315
|
},
|
|
309
|
-
async canvasOp(action, name, id, home) {
|
|
316
|
+
async canvasOp(action, name, id, home, peer) {
|
|
310
317
|
// Same control route the planning board's wire uses; the inspector's
|
|
311
318
|
// member writes are one id at a time, so no serial fan-out needed.
|
|
312
319
|
try {
|
|
313
320
|
const res = await controlFetch('/__dsh_a2a/canvas', {
|
|
314
321
|
method: 'POST',
|
|
315
322
|
headers: { 'content-type': 'application/json' },
|
|
316
|
-
body: JSON.stringify({ action, name, id, ...(home === undefined ? {} : { home }) }),
|
|
323
|
+
body: JSON.stringify({ action, name, id, ...(home === undefined ? {} : { home }), ...(peer === undefined ? {} : { peer }) }),
|
|
317
324
|
})
|
|
318
325
|
const body = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }
|
|
319
326
|
if (res.ok && body.ok !== false) return true
|
|
@@ -486,6 +493,7 @@ async function poll(): Promise<void> {
|
|
|
486
493
|
.filter(r => r !== null && typeof r === 'object' && typeof (r as { team?: unknown }).team === 'string' && typeof (r as { via?: unknown }).via === 'string')
|
|
487
494
|
.map(r => ({
|
|
488
495
|
team: r.team,
|
|
496
|
+
memberRef:r.memberRef,
|
|
489
497
|
name: r.name,
|
|
490
498
|
via: r.via,
|
|
491
499
|
workspace: r.workspace,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Explicit slot provenance confirmation; never clears suspension or infers owner from a label. */
|
|
2
|
+
export interface ProvenanceChoice { replacement: string; label: string; peer?: string }
|
|
3
|
+
export function createMemberProvenancePicker(choices: readonly ProvenanceChoice[], confirm: (choice: ProvenanceChoice) => Promise<{ ok: boolean; error?: string }>): HTMLElement {
|
|
4
|
+
const root=document.createElement('form');root.className='wb-member-provenance'
|
|
5
|
+
const select=document.createElement('select');select.setAttribute('aria-label','确认成员来源')
|
|
6
|
+
select.appendChild(new Option('明确选择来源…',''))
|
|
7
|
+
choices.forEach((choice,index)=>select.appendChild(new Option(choice.label,String(index))))
|
|
8
|
+
const submit=document.createElement('button');submit.type='submit';submit.textContent='确认来源'
|
|
9
|
+
const hint=document.createElement('span');hint.textContent='确认不等于恢复;已暂停槽位仍保持暂停'
|
|
10
|
+
root.append(select,submit,hint)
|
|
11
|
+
root.addEventListener('submit',async event=>{event.preventDefault();if(select.value==='')return;const choice=choices[Number(select.value)];if(!choice)return;submit.disabled=true;const out=await confirm(choice);if(!out.ok){submit.disabled=false;hint.textContent=out.error??'来源无法验证,原槽位保留'}else hint.textContent='来源已确认;等待状态刷新(暂停状态不变)'})
|
|
12
|
+
return root
|
|
13
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { RemoteTeamRow } from './federation'
|
|
3
3
|
import type { SessionLite } from './world'
|
|
4
4
|
|
|
5
|
-
type HandleSession = Pick<SessionLite, 'id' | 'team' | 'joined'>
|
|
5
|
+
type HandleSession = Pick<SessionLite, 'id' | 'team' | 'joined' | 'memberRef'>
|
|
6
6
|
|
|
7
7
|
/** Map only unique joined id8 handles that no remote directory row already owns. */
|
|
8
8
|
export function joinedLocalIdsByHandle(sessions: ReadonlyArray<HandleSession>, publishedHandles: ReadonlySet<string>): Map<string, string> {
|
|
@@ -21,6 +21,9 @@ export function joinedLocalIdsByHandle(sessions: ReadonlyArray<HandleSession>, p
|
|
|
21
21
|
localIds.set(handle, session.id)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
const ownerAliases=new Map<string,string[]>()
|
|
25
|
+
for(const session of sessions)if(session.joined===true && session.memberRef){const ids=ownerAliases.get(session.memberRef)??[];ids.push(session.id);ownerAliases.set(session.memberRef,ids)}
|
|
26
|
+
for(const [ref,ids]of ownerAliases)if(ids.length===1)localIds.set(ref,ids[0]!)
|
|
24
27
|
return localIds
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -1236,7 +1236,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1236
1236
|
const host = peerHostOf(p.url)
|
|
1237
1237
|
// Owner report 2026-11-04: the host's rows now lead with its bare process team (e.g. 'dsh'), which the
|
|
1238
1238
|
// server reads as a local session id and refuses — take the first zone/id8 SESSION handle instead.
|
|
1239
|
-
const
|
|
1239
|
+
const first = lastRemoteTeams.get(host)?.find(row => row.team.includes('/')); const handle = first?.memberRef ?? first?.team
|
|
1240
1240
|
model.upsertNode({
|
|
1241
1241
|
id, x: base.x, y: base.y,
|
|
1242
1242
|
label: p.url, team: '', name: p.url, peerUrl: p.url,
|
|
@@ -1262,7 +1262,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1262
1262
|
let y = hostNode?.y ?? 0
|
|
1263
1263
|
for (const row of rows) {
|
|
1264
1264
|
if (!row.team.includes('/')) continue
|
|
1265
|
-
const id = `member-${row.team.replace(/[^a-z0-9-]/gi, '-')}`
|
|
1265
|
+
const id = `member-${(row.memberRef ?? row.team).replace(/[^a-z0-9-]/gi, '-')}`
|
|
1266
1266
|
if (memberSeen.has(id)) continue
|
|
1267
1267
|
memberSeen.add(id)
|
|
1268
1268
|
const saved = layoutDoc?.nodes[id]
|
|
@@ -1272,7 +1272,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1272
1272
|
model.upsertNode({
|
|
1273
1273
|
id, x: pos.x, y: pos.y,
|
|
1274
1274
|
label: row.team, team: row.team, name: row.name !== undefined && row.name !== '' ? row.name : row.team,
|
|
1275
|
-
remoteHandle: row.team,
|
|
1275
|
+
remoteHandle: row.memberRef ?? row.team,
|
|
1276
1276
|
live: true, remote: true, memberships: [],
|
|
1277
1277
|
})
|
|
1278
1278
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { createChatDrawer, type ChatTarget } from './wb-chat'
|
|
22
22
|
import { createSeatNameEditor } from './seat-name-editor'
|
|
23
23
|
import { createCliHomePicker, type CliHomeOptions } from './cli-home-picker'
|
|
24
|
+
import { createMemberProvenancePicker, type ProvenanceChoice } from './member-provenance-picker'
|
|
24
25
|
import { createRailTabs } from './wb-rail'
|
|
25
26
|
import { DIRECTORY_FILTERS, learnLogTitle, loadLogTitles, renderDirectory, type DirFilter } from './wb-directory'
|
|
26
27
|
import { ledgerFacts, renderLedger as paintLedger } from './wb-ledger'
|
|
@@ -92,6 +93,7 @@ export interface PeerOrgRow {
|
|
|
92
93
|
|
|
93
94
|
/** One session row from /state `sessions` (live or cold joined). */
|
|
94
95
|
export interface SessionRow {
|
|
96
|
+
readonly memberRef?: string
|
|
95
97
|
readonly id: string
|
|
96
98
|
readonly label: string
|
|
97
99
|
readonly team: string
|
|
@@ -112,6 +114,7 @@ export interface SessionRow {
|
|
|
112
114
|
|
|
113
115
|
/** One remote team row from /state `remote`. */
|
|
114
116
|
export interface RemoteRow {
|
|
117
|
+
readonly memberRef?: string
|
|
115
118
|
readonly team: string
|
|
116
119
|
readonly unreachable?: boolean
|
|
117
120
|
readonly name?: string
|
|
@@ -161,7 +164,7 @@ export interface StateBody {
|
|
|
161
164
|
readonly zone?: string
|
|
162
165
|
readonly version?: string
|
|
163
166
|
readonly sessions?: readonly SessionRow[]
|
|
164
|
-
readonly canvas?: { teams?: readonly CanvasTeamRow[]; revision?: number; retainedMembers?: ReadonlyArray<{ team: string; member: string; suspended: boolean; records: readonly unknown[] }> }
|
|
167
|
+
readonly canvas?: { teams?: readonly CanvasTeamRow[]; revision?: number; memberSlots?: ReadonlyArray<{ name: string; member: string; ownerKey: string | null; provenance: string; suspended: boolean; compatibilityInference: boolean }>; retainedMembers?: ReadonlyArray<{ team: string; member: string; suspended: boolean; records: readonly unknown[] }> }
|
|
165
168
|
/** gh#20 W-A/W-C: the peer row carries the link badge source, the host
|
|
166
169
|
* rail's label/version/last-contact facts (absent on older hosts). */
|
|
167
170
|
readonly peers?: ReadonlyArray<{
|
|
@@ -220,8 +223,9 @@ export interface WorkbenchDeps {
|
|
|
220
223
|
abandon(taskId: string, reason: string): Promise<boolean>
|
|
221
224
|
/** Canvas team member change via the control route (0.6.6): `id` is a
|
|
222
225
|
* local session id or a zone-qualified remote handle. True on success. */
|
|
223
|
-
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string, home?: string): Promise<boolean>
|
|
226
|
+
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string, home?: string, peer?: string): Promise<boolean>
|
|
224
227
|
cliHomeOptions?(name: string, id: string): Promise<CliHomeOptions>
|
|
228
|
+
confirmMember?(name: string, id: string, replacement: string, revision: number, expectedOwner: string | null, peer?: string): Promise<{ ok: boolean; error?: string }>
|
|
225
229
|
regulateSuspended?(action: 'restore-suspended' | 'delete-suspended', name: string, id: string, revision: number, expected: readonly unknown[]): Promise<{ ok: boolean; error?: string }>
|
|
226
230
|
/** Quick dispatch (0.6.9): POST /__dsh_a2a/dispatch as a human gesture —
|
|
227
231
|
* one message to a routing team, async delivery, receipt lands in the
|
|
@@ -344,6 +348,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
344
348
|
// by team name — same trick as draftForm so inputs survive the 3s poll).
|
|
345
349
|
let canvasForm: { name: string; form: HTMLFormElement; pick: HTMLSelectElement; manual: HTMLInputElement } | null = null
|
|
346
350
|
const cliHomeForms = new Map<string, HTMLFormElement>()
|
|
351
|
+
const provenanceForms = new Map<string, HTMLElement>()
|
|
347
352
|
// Quick-dispatch form (0.6.9), cached per target routing team the same
|
|
348
353
|
// draftForm way: a textarea's draft must survive the 3s poll remount.
|
|
349
354
|
let dispatchForm: { team: string; form: HTMLFormElement; text: HTMLTextAreaElement; note: HTMLElement } | null = null
|
|
@@ -1033,6 +1038,17 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1033
1038
|
}
|
|
1034
1039
|
card.appendChild(row)
|
|
1035
1040
|
}
|
|
1041
|
+
for (const slot of last?.canvas?.memberSlots ?? []) {
|
|
1042
|
+
if (slot.name !== fresh.name || slot.provenance !== 'unknown' || !deps.confirmMember || last?.canvas?.revision === undefined) continue
|
|
1043
|
+
const badge=el('div','wb-hint');badge.textContent=slot.member+' · 来源未确认'+(slot.compatibilityInference?' · 兼容推断中':'')+(slot.suspended?' · 已暂停':'');card.appendChild(badge)
|
|
1044
|
+
const choices: ProvenanceChoice[]=(last.sessions??[]).filter(s=>s.joined===true).map(s=>({replacement:s.id,label:displayName(s)+' — 本机完整会话'}))
|
|
1045
|
+
for(const remote of last.remote??[])if(remote.team.includes('/') && remote.via)choices.push({replacement:remote.team,label:displayName(remote)+' — '+remote.via,peer:remote.via})
|
|
1046
|
+
const revision=last.canvas.revision
|
|
1047
|
+
const formKey=JSON.stringify([fresh.name,slot.member,revision,slot.ownerKey,choices])
|
|
1048
|
+
let form=provenanceForms.get(formKey)
|
|
1049
|
+
if(!form){form=createMemberProvenancePicker(choices,choice=>deps.confirmMember!(fresh.name,slot.member,choice.replacement,revision,slot.ownerKey,choice.peer));provenanceForms.set(formKey,form)}
|
|
1050
|
+
card.appendChild(form)
|
|
1051
|
+
}
|
|
1036
1052
|
// Add-member form: static DOM re-mounted per poll (draftForm trick) —
|
|
1037
1053
|
// rebuilt only when the inspected team changes.
|
|
1038
1054
|
if (canvasForm?.name !== fresh.name) {
|
|
@@ -1054,10 +1070,11 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1054
1070
|
form.addEventListener('submit', async ev => {
|
|
1055
1071
|
ev.preventDefault()
|
|
1056
1072
|
const handle = manual.value.trim()
|
|
1057
|
-
const
|
|
1073
|
+
const selected=handle!==''?undefined:pick.selectedOptions[0]
|
|
1074
|
+
const id = handle !== '' ? handle : selected?.dataset.member ?? pick.value
|
|
1058
1075
|
if (id === '') return
|
|
1059
1076
|
submit.disabled = true
|
|
1060
|
-
const ok = await deps.canvasOp('add-member', fresh.name, id)
|
|
1077
|
+
const ok = await deps.canvasOp('add-member', fresh.name, id, undefined, selected?.dataset.peer)
|
|
1061
1078
|
submit.disabled = false
|
|
1062
1079
|
if (ok) manual.value = ''
|
|
1063
1080
|
})
|
|
@@ -1089,7 +1106,10 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1089
1106
|
pick.appendChild(new Option('远端会话队(目录卡片)…', '', true, false))
|
|
1090
1107
|
remoteLabeled = true
|
|
1091
1108
|
}
|
|
1092
|
-
|
|
1109
|
+
const option=new Option(`${displayName(remote)} — ${remote.via ?? '来源未确认'}`, JSON.stringify([remote.team,remote.via]))
|
|
1110
|
+
option.dataset.member=remote.team
|
|
1111
|
+
if(remote.via)option.dataset.peer=remote.via
|
|
1112
|
+
pick.appendChild(option)
|
|
1093
1113
|
}
|
|
1094
1114
|
if (!remoteLabeled) {
|
|
1095
1115
|
pick.appendChild(new Option('(暂无可拉入的远端会话队——见下方说明)', '', true, false))
|
|
@@ -1098,7 +1118,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1098
1118
|
card.appendChild(canvasForm.form)
|
|
1099
1119
|
const hint = el('div', 'wb-hint')
|
|
1100
1120
|
hint.textContent =
|
|
1101
|
-
'
|
|
1121
|
+
'成员由人类明确分配。本机写完整会话;远端必须通过已链接主机的有效名片核实并记录完整公钥来源。最小卡、离线或第三方无法验证时保留待确认,不能手输裸句柄绕过来源校验。来源确认不恢复暂停;默认兼容窗口不自动关闭。'
|
|
1102
1122
|
card.appendChild(hint)
|
|
1103
1123
|
// Quick dispatch targets the canvas ROUTING team (0.6.9): local tiers
|
|
1104
1124
|
// first, then the remote members in order — the seam's own priority.
|
|
@@ -1252,6 +1272,8 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1252
1272
|
function update(state: StateBody | null): void {
|
|
1253
1273
|
const unknown = new Set((state?.canvas?.teams ?? []).flatMap(team => team.members.filter(member => member.cliHome === null && member.cliHeld !== true).map(member => JSON.stringify([team.name, member.id]))))
|
|
1254
1274
|
for (const key of cliHomeForms.keys()) if (!unknown.has(key)) cliHomeForms.delete(key)
|
|
1275
|
+
const slots=state?.canvas?.memberSlots??[]
|
|
1276
|
+
for(const key of provenanceForms.keys()){const [name,member,revision]=JSON.parse(key) as [string,string,number];if(revision!==state?.canvas?.revision||!slots.some(slot=>slot.name===name&&slot.member===member&&slot.provenance==='unknown'))provenanceForms.delete(key)}
|
|
1255
1277
|
last = state
|
|
1256
1278
|
// 0.6.12: refresh the readable-log list opportunistically (single flight;
|
|
1257
1279
|
// the sidebar re-renders when rows land, mid-poll remount is idempotent).
|
package/nexus-stage/src/world.ts
CHANGED
|
@@ -154,6 +154,7 @@ export function planSeatClear(sid: string, occupied: ReadonlyArray<{ readonly x:
|
|
|
154
154
|
|
|
155
155
|
/** Minimal session row shape the model needs (topology.SessionRow satisfies it). */
|
|
156
156
|
export interface SessionLite {
|
|
157
|
+
memberRef?: string
|
|
157
158
|
id: string; label: string; team: string; name?: string; description?: string; joined: boolean; live?: boolean; teams?: readonly string[]
|
|
158
159
|
/** Live local sessions only: mid-turn ('running') or idle, and how many receipt-owing tasks wait on it. */
|
|
159
160
|
status?: 'running' | 'idle'; queued?: number
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nelsonlongxiang/dsh-open-a2a-net",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Open A2A network plugin for DeepSeek Harness: signed agent cards, decentralized peer/zone discovery, direct routing model tools, and joinable session nodes with a sidebar control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"verify:docs": "node scripts/doc-drift.mjs",
|
|
89
89
|
"reanchor:arch": "node scripts/reanchor-architecture.mjs",
|
|
90
90
|
"verify:nexus": "node scripts/verify-nexus-ci.mjs && npm --prefix nexus-stage run build && npm --prefix nexus-stage run test && git diff --exit-code -- assets/nexusDist/assets && git diff --ignore-cr-at-eol --exit-code -- assets/nexusDist/index.html",
|
|
91
|
-
"verify": "
|
|
91
|
+
"verify": "node scripts/verify-temp-clean.mjs",
|
|
92
|
+
"verify:core": "pnpm typecheck && pnpm verify:docs && pnpm build && pnpm test && pnpm test:build-layout && pnpm verify:nexus"
|
|
92
93
|
},
|
|
93
94
|
"peerDependencies": {
|
|
94
95
|
"@deepseek-ai/cordis": ">=4.0.1",
|