super-dsh 0.1.6 → 0.1.9
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/client/index.js +741 -256
- package/cordis.patch.yml +16 -0
- package/package.json +7 -2
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +29 -18
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +96 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +15 -1
- package/superd-hub/dist/index.js +16 -0
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +6 -0
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +62 -25
- package/superd-hub/dist/switch-core.js +53 -30
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/lib/client/index.js +741 -256
- package/superd-hub/package.json +11 -2
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A ingest — the post-office's execution side (postoffice R3; ADR 0011,
|
|
3
|
+
* spec D16–D19, plan 2026-09-28-machine-mesh-postoffice-rework 契约要点).
|
|
4
|
+
*
|
|
5
|
+
* WHERE THE WORK LIVES (D16 ruling): when a request envelope (new-conversation
|
|
6
|
+
* / message / stop) arrives — over the wire via `POST /api/superd/a2a`
|
|
7
|
+
* (mesh-rpc) or short-circuited in-process by the initiator's own hub (R4)
|
|
8
|
+
* — the session-plane business logic runs HERE, on the machine that owns the
|
|
9
|
+
* session: resolve the LOCAL target (a2a-host's local leg), create-or-resume
|
|
10
|
+
* the mirror registry row, inject the teaching envelope (renderA2aEnvelope,
|
|
11
|
+
* initiator = envelope.from), and capture the completion on the LOCAL follow
|
|
12
|
+
* stream (subscribeSessionCompletion — same machinery as the initiator-side
|
|
13
|
+
* service; zero cross-machine session-plane calls anywhere).
|
|
14
|
+
*
|
|
15
|
+
* REGISTRY (双侧同形): the mirror row points AT THE INITIATOR —
|
|
16
|
+
* {machine: from.machine, agent: from.agent, sessionId: LOCAL session} — the
|
|
17
|
+
* symmetric dual of the initiator's row. The conversationId is
|
|
18
|
+
* INITIATOR-MINTED, carried on the envelope, and adopted VERBATIM; a
|
|
19
|
+
* collision (the shortId already bound to a different conversation) is
|
|
20
|
+
* rejected fail-loud. The title lands verbatim from the envelope (D17:
|
|
21
|
+
* mirror rows adopt verbatim; titles are immutable on both sides).
|
|
22
|
+
*
|
|
23
|
+
* RETURN ROUTING (D16): a captured completion becomes a reply envelope
|
|
24
|
+
* (aborted-with-no-text → a bounded notice envelope, D3) routed to the
|
|
25
|
+
* initiator: from.machine === self → in-process delivery (a2a-delivery);
|
|
26
|
+
* a machines.json row exists → wire POST (cookie 随行); otherwise the
|
|
27
|
+
* bounded persistent outbox (`<home>/superd/a2a-outbox.json`, ≤32, oldest
|
|
28
|
+
* dropped) until a route appears or the sweep pulls it (R5).
|
|
29
|
+
*
|
|
30
|
+
* Inbound RETURN envelopes (reply/notice POSTed at us, or pulled from a
|
|
31
|
+
* peer's outbox by R5's sweep) deliver into the local initiator session
|
|
32
|
+
* through the same a2a-delivery face; the receipt for those POSTs is the
|
|
33
|
+
* delivery outcome (additive to the contract's new/message/stop receipts).
|
|
34
|
+
*/
|
|
35
|
+
import { randomUUID } from 'node:crypto';
|
|
36
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { join } from 'node:path';
|
|
38
|
+
import { A2A_BUFFER_MAX, A2A_NO_SESSION_CLOSE_REASON, A2A_NO_TURN_ACTIVE_NOTE, addressErrorBody, renderA2aEnvelope, sessionRpc, subscribeSessionCompletion, turnLivenessOf, loadConversations, saveConversations, } from './a2a-service.js';
|
|
39
|
+
import { EnvelopeError } from './envelope.js';
|
|
40
|
+
import { WIRE } from './wire-endpoints.js';
|
|
41
|
+
import { boundNoticeSummary, deliverNotice, deliverReply } from './a2a-delivery.js';
|
|
42
|
+
import { A2aAddressError, isA2aEnvelope, postA2aEnvelope, } from './a2a-wire.js';
|
|
43
|
+
import { ensureSelf, listRemotes } from './machines.js';
|
|
44
|
+
import { cookieOf } from './mesh-link.js';
|
|
45
|
+
function outboxPath(home) {
|
|
46
|
+
return join(home, 'superd', 'a2a-outbox.json');
|
|
47
|
+
}
|
|
48
|
+
function loadOutbox(home) {
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = readFileSync(outboxPath(home), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { envelopes: [] };
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { envelopes: [] };
|
|
62
|
+
}
|
|
63
|
+
const list = parsed.envelopes;
|
|
64
|
+
if (!Array.isArray(list))
|
|
65
|
+
return { envelopes: [] };
|
|
66
|
+
// entries validated on load — corrupt rows drop, never crash the hub
|
|
67
|
+
return { envelopes: list.filter((e) => isA2aEnvelope(e)) };
|
|
68
|
+
}
|
|
69
|
+
function saveOutbox(home, file) {
|
|
70
|
+
const dir = join(home, 'superd');
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
const target = outboxPath(home);
|
|
73
|
+
const tmp = `${target}.tmp`;
|
|
74
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
75
|
+
chmodSync(tmp, 0o600);
|
|
76
|
+
renameSync(tmp, target);
|
|
77
|
+
}
|
|
78
|
+
// ---------------- inbound exactly-once (0.1.6-j double-delivery fix)
|
|
79
|
+
/** Bounded inbound-messageId seen-set: an envelope the sender re-POSTs (parked after a transport glitch, re-pulled by the sweep) is re-ACKed, never re-executed. */
|
|
80
|
+
const SEEN_MESSAGE_IDS_MAX = 128;
|
|
81
|
+
function seenIdsPath(home) {
|
|
82
|
+
return join(home, 'superd', 'a2a-seen.json');
|
|
83
|
+
}
|
|
84
|
+
function seenMessageIds(home) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(readFileSync(seenIdsPath(home), 'utf8'));
|
|
87
|
+
const ids = typeof parsed === 'object' && parsed !== null ? parsed.ids : undefined;
|
|
88
|
+
return new Set(Array.isArray(ids) ? ids.filter((v) => typeof v === 'string') : []);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return new Set();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function rememberMessageId(home, messageId) {
|
|
95
|
+
const ids = [...seenMessageIds(home)];
|
|
96
|
+
if (!ids.includes(messageId))
|
|
97
|
+
ids.push(messageId);
|
|
98
|
+
while (ids.length > SEEN_MESSAGE_IDS_MAX)
|
|
99
|
+
ids.shift();
|
|
100
|
+
mkdirSync(join(home, 'superd'), { recursive: true });
|
|
101
|
+
const target = seenIdsPath(home);
|
|
102
|
+
const tmp = `${target}.tmp`;
|
|
103
|
+
writeFileSync(tmp, JSON.stringify({ ids }, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
104
|
+
chmodSync(tmp, 0o600);
|
|
105
|
+
renameSync(tmp, target);
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------- helpers
|
|
108
|
+
function isRecord(v) {
|
|
109
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
110
|
+
}
|
|
111
|
+
function messageOf(error) {
|
|
112
|
+
return error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|
|
114
|
+
/** Receipt error body of any thrown failure — an EnvelopeError's wire body passes verbatim. */
|
|
115
|
+
function receiptErrorOf(error) {
|
|
116
|
+
if (error instanceof EnvelopeError)
|
|
117
|
+
return { ...error.wire };
|
|
118
|
+
if (error instanceof Error)
|
|
119
|
+
return { code: 'gateway/uncaught', message: error.message };
|
|
120
|
+
return { code: 'gateway/uncaught', message: String(error) };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------- service
|
|
123
|
+
export function createA2aIngest(deps) {
|
|
124
|
+
const now = deps.now ?? Date.now;
|
|
125
|
+
const alog = deps.log;
|
|
126
|
+
const selfId = () => ensureSelf(deps.home, 'local').id;
|
|
127
|
+
// ---------------- registry (mirror rows)
|
|
128
|
+
const mirrorRowOf = (conversationId) => {
|
|
129
|
+
const rows = loadConversations(deps.home).conversations;
|
|
130
|
+
return rows.find((r) => r.shortId === conversationId || r.sessionId === conversationId);
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Adopt the initiator-minted conversationId verbatim (fail-loud on
|
|
134
|
+
* collision); an exact re-delivery of the same conversation is an
|
|
135
|
+
* idempotent adoption (resume). @returns the row.
|
|
136
|
+
*/
|
|
137
|
+
const adoptMirrorRow = (env, sessionId, title) => {
|
|
138
|
+
const file = loadConversations(deps.home);
|
|
139
|
+
const rows = file.conversations;
|
|
140
|
+
const existing = rows.find((r) => r.shortId === env.conversationId || r.sessionId === sessionId);
|
|
141
|
+
if (existing !== undefined) {
|
|
142
|
+
const sameConversation = existing.shortId === env.conversationId &&
|
|
143
|
+
existing.sessionId === sessionId &&
|
|
144
|
+
existing.machine === env.from.machine && existing.agent === env.from.agent &&
|
|
145
|
+
existing.hostAgent === env.to.id;
|
|
146
|
+
if (sameConversation)
|
|
147
|
+
return existing;
|
|
148
|
+
throw new Error(`a2a: conversation-id conflict — "${env.conversationId}" is already bound to ${existing.machine}-${existing.agent}/${existing.sessionId} (claimed by ${env.from.machine}-${env.from.agent}/${sessionId})`);
|
|
149
|
+
}
|
|
150
|
+
const row = {
|
|
151
|
+
shortId: env.conversationId,
|
|
152
|
+
machine: env.from.machine,
|
|
153
|
+
agent: env.from.agent,
|
|
154
|
+
sessionId,
|
|
155
|
+
title,
|
|
156
|
+
// R4 amendment: mirror rows carry the LOCAL a6 hosting the session —
|
|
157
|
+
// presence is the locally-hosted marker continuation routing keys on.
|
|
158
|
+
hostAgent: env.to.id,
|
|
159
|
+
// 0.1.7-i: the OPENER's session persists on mirror rows too — the
|
|
160
|
+
// initiatorSession field's R4 "initiator rows only" contract predates
|
|
161
|
+
// the send-failed notice (a failed write-back must address the original
|
|
162
|
+
// initiator even when this hub has never seen their send path) and the
|
|
163
|
+
// self-route write-back branch already expects it here. Additive; the
|
|
164
|
+
// initiator-row detection keys on hostAgent absence, never this field.
|
|
165
|
+
initiatorSession: env.from.sessionId,
|
|
166
|
+
createdAt: now(),
|
|
167
|
+
lastActivityAt: now(),
|
|
168
|
+
};
|
|
169
|
+
rows.push(row);
|
|
170
|
+
saveConversations(deps.home, { conversations: rows });
|
|
171
|
+
alog?.('info', 'conversation.adopt', { conversation: row.shortId, session: row.sessionId, peer: `${env.from.machine}-${env.from.agent}`, hostAgent: env.to.id });
|
|
172
|
+
return row;
|
|
173
|
+
};
|
|
174
|
+
const touchMirror = (row) => {
|
|
175
|
+
const rows = loadConversations(deps.home).conversations;
|
|
176
|
+
const idx = rows.findIndex((r) => r.sessionId === row.sessionId);
|
|
177
|
+
if (idx === -1)
|
|
178
|
+
return;
|
|
179
|
+
rows[idx] = { ...rows[idx], lastActivityAt: now() };
|
|
180
|
+
saveConversations(deps.home, { conversations: rows });
|
|
181
|
+
};
|
|
182
|
+
/** D30: record the peer's last delivered output on the conversation row (agent_status reads it, ownership-gated). */
|
|
183
|
+
const recordLastOutput = (conversationId, text) => {
|
|
184
|
+
const rows = loadConversations(deps.home).conversations;
|
|
185
|
+
const idx = rows.findIndex((r) => r.shortId === conversationId || r.sessionId === conversationId);
|
|
186
|
+
if (idx === -1)
|
|
187
|
+
return;
|
|
188
|
+
rows[idx] = { ...rows[idx], lastOutput: text, lastOutputAt: now() };
|
|
189
|
+
saveConversations(deps.home, { conversations: rows });
|
|
190
|
+
};
|
|
191
|
+
// ---------------- return routing
|
|
192
|
+
/** Deliver one return envelope into the LOCAL initiator session (self-routed or inbound). */
|
|
193
|
+
const deliverLocal = (envelope, sender, noticeReason = 'peer') => {
|
|
194
|
+
const initiator = { machine: envelope.to.machine, agent: envelope.to.agent, sessionId: envelope.to.sessionId };
|
|
195
|
+
const row = mirrorRowOf(envelope.conversation);
|
|
196
|
+
if (envelope.kind === 'reply') {
|
|
197
|
+
const outcome = deliverReply(deps.delivery, initiator, envelope.text, {
|
|
198
|
+
senderMachine: sender.machine,
|
|
199
|
+
senderAgent: sender.agent,
|
|
200
|
+
conversationId: envelope.conversation,
|
|
201
|
+
messageId: envelope.messageId,
|
|
202
|
+
title: row?.title,
|
|
203
|
+
// D20: the provenance flag rides the wire additively — the delivering
|
|
204
|
+
// side renders the "transcribed, never sent via channel" prefix from
|
|
205
|
+
// it; absent = the peer chose to send this through the channel.
|
|
206
|
+
transcribed: envelope.transcribed === true,
|
|
207
|
+
});
|
|
208
|
+
// D30: the peer's output arrived here — remember it on the row (the
|
|
209
|
+
// initiator can later read it through agent_status; ownership-gated).
|
|
210
|
+
if (envelope.text !== '')
|
|
211
|
+
recordLastOutput(envelope.conversation, envelope.text);
|
|
212
|
+
alog?.('info', 'deliver.local', { conversation: envelope.conversation, kind: envelope.kind, outcome: outcome.delivered, from: `${sender.machine}-${sender.agent}` });
|
|
213
|
+
return outcome;
|
|
214
|
+
}
|
|
215
|
+
const outcome = deliverNotice(deps.delivery, initiator, envelope.summary, {
|
|
216
|
+
senderMachine: sender.machine,
|
|
217
|
+
senderAgent: sender.agent,
|
|
218
|
+
conversationId: envelope.conversation,
|
|
219
|
+
// 0.1.7-h R2: the peer's own reason rides the wire (additive); the
|
|
220
|
+
// fallback covers self-routed notices and pre-0.1.7-h peers.
|
|
221
|
+
reason: envelope.reason ?? noticeReason,
|
|
222
|
+
});
|
|
223
|
+
alog?.('info', 'deliver.local', { conversation: envelope.conversation, kind: envelope.kind, outcome: outcome.delivered, from: `${sender.machine}-${sender.agent}` });
|
|
224
|
+
return outcome;
|
|
225
|
+
};
|
|
226
|
+
const parkInOutbox = (envelope) => {
|
|
227
|
+
const file = loadOutbox(deps.home);
|
|
228
|
+
file.envelopes.push(envelope);
|
|
229
|
+
while (file.envelopes.length > A2A_BUFFER_MAX)
|
|
230
|
+
file.envelopes.shift();
|
|
231
|
+
saveOutbox(deps.home, file);
|
|
232
|
+
};
|
|
233
|
+
const routeReturn = async (envelope, meta) => {
|
|
234
|
+
if (envelope.to.machine === selfId()) {
|
|
235
|
+
deliverLocal(envelope, { machine: selfId(), agent: meta.responderAgent }, meta.noticeReason);
|
|
236
|
+
alog?.('info', 'route.deliver', { conversation: envelope.conversation, kind: envelope.kind });
|
|
237
|
+
return 'delivered';
|
|
238
|
+
}
|
|
239
|
+
const machine = listRemotes(deps.home).find((r) => r.id === envelope.to.machine);
|
|
240
|
+
if (machine !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
await postA2aEnvelope(machine.origin, envelope, {
|
|
243
|
+
cookie: cookieOf(machine.id, deps.home),
|
|
244
|
+
fetchImpl: deps.fetchImpl,
|
|
245
|
+
});
|
|
246
|
+
alog?.('info', 'route.post', { conversation: envelope.conversation, kind: envelope.kind, to: envelope.to.machine });
|
|
247
|
+
return 'posted';
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// peer briefly down — park for the next contact (sweep pull / next send)
|
|
251
|
+
parkInOutbox(envelope);
|
|
252
|
+
alog?.('warn', 'route.park', { conversation: envelope.conversation, kind: envelope.kind, to: envelope.to.machine, reason: 'peer-unreachable' });
|
|
253
|
+
return 'outbox';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
parkInOutbox(envelope);
|
|
257
|
+
alog?.('warn', 'route.park', { conversation: envelope.conversation, kind: envelope.kind, to: envelope.to.machine, reason: 'unknown-machine' });
|
|
258
|
+
return 'outbox';
|
|
259
|
+
};
|
|
260
|
+
/** The completion watcher behind every successful inject (D16 → D20 v3). */
|
|
261
|
+
const watchCompletion = (gateway, row, env) => {
|
|
262
|
+
const to = { machine: env.from.machine, agent: env.from.agent, sessionId: env.from.sessionId };
|
|
263
|
+
// D20: arm-time watermark — a channel send with lastSentAt ABOVE this
|
|
264
|
+
// happened inside this armed turn's window, i.e. the peer's send WAS
|
|
265
|
+
// this turn's artifact and the watch must not add a second one.
|
|
266
|
+
const armedSentAt = row.lastSentAt ?? 0;
|
|
267
|
+
const noticeOf = (summary, reason = 'peer') => ({
|
|
268
|
+
kind: 'notice',
|
|
269
|
+
to,
|
|
270
|
+
conversation: row.shortId,
|
|
271
|
+
summary: boundNoticeSummary(summary),
|
|
272
|
+
messageId: `a2a-notice-${randomUUID()}`,
|
|
273
|
+
sender: { machine: selfId(), agent: env.to.id },
|
|
274
|
+
// 0.1.7-h R2: the cause rides the wire (additive) so the receiving side
|
|
275
|
+
// renders reason= into the model-facing notice line.
|
|
276
|
+
reason,
|
|
277
|
+
});
|
|
278
|
+
subscribeSessionCompletion({ gateway, sessionId: row.sessionId, conversationId: row.shortId }, (completion) => {
|
|
279
|
+
// D20 v3 — the channel consumes exactly two machine facts (turn
|
|
280
|
+
// boundary + last output) and NOTHING from the settlement
|
|
281
|
+
// taxonomy: no abort/complete adjectives, no reason branches. A
|
|
282
|
+
// turn end yields at most ONE artifact:
|
|
283
|
+
// initiator stop stamped inside the window → stopped-by-initiator
|
|
284
|
+
// notice (0.1.7-h R2 — the initiator's own stop must not read
|
|
285
|
+
// as the peer going silent)
|
|
286
|
+
// send inside the window → the send(s) were it (watch stays quiet)
|
|
287
|
+
// latch set (channel used in an earlier turn) → plain notice
|
|
288
|
+
// latch clear + non-empty last output → TRANSCRIBED reply
|
|
289
|
+
// latch clear + empty output → plain notice (reason=peer-silent)
|
|
290
|
+
// Hallucinated/irrelevant content is content-layer business —
|
|
291
|
+
// transcribe verbatim and let the sender judge.
|
|
292
|
+
const fresh = mirrorRowOf(row.shortId);
|
|
293
|
+
// F4 (0.1.8-b): the CHANNEL-SEND-WINDOW check comes FIRST. If the peer
|
|
294
|
+
// already sent through the channel inside this turn, that send IS the
|
|
295
|
+
// turn's artifact — a stop stamped afterwards (the initiator stopping a
|
|
296
|
+
// turn that already answered) must not add a second one. The old order
|
|
297
|
+
// let the stop notice override the quiet rule, yielding two artifacts
|
|
298
|
+
// for one turn.
|
|
299
|
+
if ((fresh?.lastSentAt ?? 0) > armedSentAt) {
|
|
300
|
+
alog?.('info', 'completion.quiet', { conversation: row.shortId, reason: 'channel-send-in-window' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if ((fresh?.stoppedAt ?? 0) > armedSentAt) {
|
|
304
|
+
alog?.('info', 'completion.stopped', { conversation: row.shortId });
|
|
305
|
+
const stopped = noticeOf(`conversation ${row.shortId}: turn stopped by initiator request`, 'stop');
|
|
306
|
+
void routeReturn(stopped, { responderAgent: env.to.id, noticeReason: 'stop' }).catch(() => parkInOutbox(stopped));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const envelope = fresh?.channelUsed === undefined && completion.text !== ''
|
|
310
|
+
? {
|
|
311
|
+
kind: 'reply',
|
|
312
|
+
to,
|
|
313
|
+
conversation: row.shortId,
|
|
314
|
+
text: completion.text,
|
|
315
|
+
messageId: `a2a-relay-${randomUUID()}`,
|
|
316
|
+
sender: { machine: selfId(), agent: env.to.id },
|
|
317
|
+
transcribed: true,
|
|
318
|
+
}
|
|
319
|
+
: noticeOf(`conversation ${row.shortId}: turn ended, no reply`, 'peer');
|
|
320
|
+
alog?.('info', envelope.kind === 'reply' ? 'completion.transcript' : 'completion.notice', {
|
|
321
|
+
conversation: row.shortId,
|
|
322
|
+
...(envelope.kind === 'reply' ? { bytes: completion.text.length } : {}),
|
|
323
|
+
});
|
|
324
|
+
void routeReturn(envelope, { responderAgent: env.to.id }).catch(() => parkInOutbox(envelope));
|
|
325
|
+
}, {
|
|
326
|
+
requestId: env.messageId,
|
|
327
|
+
onError: (error) => {
|
|
328
|
+
alog?.('warn', 'completion.error', { conversation: row.shortId, error: error.message });
|
|
329
|
+
const notice = noticeOf(`A2A completion stream for conversation ${row.shortId} failed: ${error.message}`, 'stream-dead');
|
|
330
|
+
void routeReturn(notice, { responderAgent: env.to.id, noticeReason: 'stream-dead' }).catch(() => parkInOutbox(notice));
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
alog?.('info', 'completion.armed', { conversation: row.shortId, session: row.sessionId, watermark: armedSentAt });
|
|
334
|
+
};
|
|
335
|
+
// ---------------- request kinds
|
|
336
|
+
const ingestRequest = async (env) => {
|
|
337
|
+
const fail = (error, sessionId = '', conversationId) => {
|
|
338
|
+
alog?.('warn', 'ingest.fail', { code: error.code, conversation: conversationId, message: env.messageId });
|
|
339
|
+
return {
|
|
340
|
+
outcome: 'failed',
|
|
341
|
+
messageId: env.messageId,
|
|
342
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
343
|
+
sessionId,
|
|
344
|
+
error,
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
//
|
|
348
|
+
// MESSAGE envelopes resolve the registry row FIRST: a peer write-back
|
|
349
|
+
// (an initiator row, no hostAgent) delivers without ever touching the
|
|
350
|
+
// local session plane — the plane is only resolved when THIS machine
|
|
351
|
+
// hosts the conversation and must inject.
|
|
352
|
+
//
|
|
353
|
+
let row;
|
|
354
|
+
if (env.kind === 'message') {
|
|
355
|
+
row = mirrorRowOf(env.conversationId);
|
|
356
|
+
if (row === undefined) {
|
|
357
|
+
return fail({
|
|
358
|
+
code: 'a2a/conversation-not-found',
|
|
359
|
+
message: `a2a: conversation "${env.conversationId}" is not in the registry — the initiator's conversationId does not resolve here`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
//
|
|
363
|
+
// D20 SELF-ROUTE WRITE-BACK: the single MIRROR row of a local↔local
|
|
364
|
+
// conversation, and the sender is the hosted peer itself (from.agent ===
|
|
365
|
+
// row.hostAgent, from.machine === self) — deliver into the initiator's
|
|
366
|
+
// session with peer attribution, NEVER inject (the row's session is the
|
|
367
|
+
// SENDER's own; injecting would loop the message back to its author).
|
|
368
|
+
//
|
|
369
|
+
// F2 (0.1.8-b): the discriminator is the SENDER'S SESSION, not the
|
|
370
|
+
// address. When the conversation's target IS the caller's own address
|
|
371
|
+
// (self-addressed), `from.agent === row.hostAgent` also holds for the
|
|
372
|
+
// INITIATOR's outbound continuation, so the old address test classified
|
|
373
|
+
// it as the peer's write-back and echoed the initiator's own text back
|
|
374
|
+
// as a "peer reply". The hosted peer's write-back origin carries
|
|
375
|
+
// sessionId === row.sessionId; the initiator's carries its own.
|
|
376
|
+
if (row.hostAgent !== undefined && env.from.machine === selfId() && env.from.sessionId === row.sessionId) {
|
|
377
|
+
if (env.to.machine !== selfId()) {
|
|
378
|
+
return fail({
|
|
379
|
+
code: 'a2a/conversation-machine-mismatch',
|
|
380
|
+
message: `a2a: conversation "${row.shortId}" write-back must target this machine, not ${env.to.machine}`,
|
|
381
|
+
}, row.sessionId, row.shortId);
|
|
382
|
+
}
|
|
383
|
+
if (row.initiatorSession === undefined || row.initiatorSession === '') {
|
|
384
|
+
return fail({
|
|
385
|
+
code: 'a2a/no-initiator-session',
|
|
386
|
+
message: `a2a: conversation "${row.shortId}" has no persisted initiator session — asynchronous write-backs cannot be addressed`,
|
|
387
|
+
}, row.sessionId, row.shortId);
|
|
388
|
+
}
|
|
389
|
+
const writeBackOutcome = deliverReply(deps.delivery, {
|
|
390
|
+
machine: env.to.machine,
|
|
391
|
+
agent: env.to.id,
|
|
392
|
+
sessionId: row.initiatorSession,
|
|
393
|
+
}, env.message, {
|
|
394
|
+
senderMachine: env.from.machine,
|
|
395
|
+
senderAgent: env.from.agent,
|
|
396
|
+
conversationId: row.shortId,
|
|
397
|
+
messageId: env.messageId,
|
|
398
|
+
title: row.title,
|
|
399
|
+
});
|
|
400
|
+
// D30 (0.1.8-b): the peer's channel reply IS its last output — persist it
|
|
401
|
+
// here too (the transcript path records it in deliverLocal; the write-back
|
|
402
|
+
// branches bypassed that, leaving lastOutput dead for channel-using peers).
|
|
403
|
+
if (env.message !== '')
|
|
404
|
+
recordLastOutput(row.shortId, env.message);
|
|
405
|
+
alog?.('info', 'deliver.writeback', { conversation: row.shortId, from: `${env.from.machine}-${env.from.agent}`, via: 'self-route', outcome: writeBackOutcome.delivered });
|
|
406
|
+
// v1 send-receipt shape (exactly-once wrapper contract)
|
|
407
|
+
return { outcome: 'delivered', messageId: env.messageId, conversationId: row.shortId, sessionId: row.initiatorSession };
|
|
408
|
+
}
|
|
409
|
+
if (row.machine !== env.from.machine || row.agent !== env.from.agent) {
|
|
410
|
+
return fail({
|
|
411
|
+
code: 'a2a/conversation-machine-mismatch',
|
|
412
|
+
message: `a2a: conversation "${row.shortId}" belongs to ${row.machine}-${row.agent}, not ${env.from.machine}-${env.from.agent}`,
|
|
413
|
+
}, row.sessionId, row.shortId);
|
|
414
|
+
}
|
|
415
|
+
//
|
|
416
|
+
// R4 amendment — PEER WRITE-BACK: the row is an INITIATOR row (no
|
|
417
|
+
// hostAgent ⇒ the session lives at the PEER). A message envelope from
|
|
418
|
+
// the very peer, addressed to this machine, is the peer using its
|
|
419
|
+
// superd-hub tool with OUR shortId (the teaching envelope's
|
|
420
|
+
// asynchronous-reply clause): deliver into the initiator's session with
|
|
421
|
+
// peer attribution — NEVER a session injection (the session is not
|
|
422
|
+
// ours to drive). deliverReply buffers into the bounded unread buffer
|
|
423
|
+
// when the initiator session is not live (existing machinery, keyed by
|
|
424
|
+
// the initiator's session id).
|
|
425
|
+
//
|
|
426
|
+
if (row.hostAgent === undefined) {
|
|
427
|
+
if (env.to.machine !== selfId()) {
|
|
428
|
+
return fail({
|
|
429
|
+
code: 'a2a/conversation-machine-mismatch',
|
|
430
|
+
message: `a2a: conversation "${row.shortId}" is hosted at ${row.machine}-${row.agent} — a peer write-back must target this machine, not ${env.to.machine}`,
|
|
431
|
+
}, row.sessionId, row.shortId);
|
|
432
|
+
}
|
|
433
|
+
if (row.initiatorSession === undefined || row.initiatorSession === '') {
|
|
434
|
+
return fail({
|
|
435
|
+
code: 'a2a/no-initiator-session',
|
|
436
|
+
message: `a2a: conversation "${row.shortId}" has no persisted initiator session — asynchronous write-backs cannot be addressed`,
|
|
437
|
+
}, row.sessionId, row.shortId);
|
|
438
|
+
}
|
|
439
|
+
const writeBackOutcome = deliverReply(deps.delivery, {
|
|
440
|
+
machine: env.to.machine,
|
|
441
|
+
agent: env.to.id,
|
|
442
|
+
sessionId: row.initiatorSession,
|
|
443
|
+
}, env.message, {
|
|
444
|
+
senderMachine: env.from.machine,
|
|
445
|
+
senderAgent: env.from.agent,
|
|
446
|
+
conversationId: row.shortId,
|
|
447
|
+
messageId: env.messageId,
|
|
448
|
+
title: row.title,
|
|
449
|
+
});
|
|
450
|
+
// D30 (0.1.8-b): persist the peer's channel reply as the conversation's last output
|
|
451
|
+
if (env.message !== '')
|
|
452
|
+
recordLastOutput(row.shortId, env.message);
|
|
453
|
+
// v1 send-receipt shape (double-delivery fix): a {delivered:…} body
|
|
454
|
+
// fails the sender's receipt guard → park → sweep re-POST → the
|
|
455
|
+
// write-back would deliver twice.
|
|
456
|
+
alog?.('info', 'deliver.writeback', { conversation: row.shortId, from: `${env.from.machine}-${env.from.agent}`, via: 'peer', outcome: writeBackOutcome.delivered });
|
|
457
|
+
return { outcome: 'delivered', messageId: env.messageId, conversationId: row.shortId, sessionId: row.initiatorSession };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const local = { machine: env.to.machine, id: env.to.id };
|
|
461
|
+
let gateway;
|
|
462
|
+
try {
|
|
463
|
+
gateway = deps.gatewayFor(local);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
const body = error instanceof A2aAddressError ? addressErrorBody(error) : receiptErrorOf(error);
|
|
467
|
+
return fail(body);
|
|
468
|
+
}
|
|
469
|
+
if (gateway === undefined) {
|
|
470
|
+
return fail({ code: 'a2a/gateway-unavailable', message: A2aAddressError.messageOf('unreachable', local) });
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
if (env.kind === 'new-conversation') {
|
|
474
|
+
const created = await sessionRpc(gateway, WIRE.sessionCreate, {});
|
|
475
|
+
if (!isRecord(created) || typeof created.sessionId !== 'string' || created.sessionId === '') {
|
|
476
|
+
return fail({ code: 'a2a/bad-result', message: 'a2a: session/create returned no sessionId' });
|
|
477
|
+
}
|
|
478
|
+
// D17: mirror rows adopt the title VERBATIM (the '[undefined]' placeholder included)
|
|
479
|
+
row = adoptMirrorRow(env, created.sessionId, env.title);
|
|
480
|
+
}
|
|
481
|
+
// (message envelopes resolved their row above — only mirror rows reach here)
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
// adoptMirrorRow's fail-loud collision + any registry write failure
|
|
485
|
+
return fail(receiptErrorOf(error));
|
|
486
|
+
}
|
|
487
|
+
if (row === undefined) {
|
|
488
|
+
// unreachable by construction (message rows resolved above; new-conversation adopted or failed)
|
|
489
|
+
return fail({ code: 'a2a/conversation-not-found', message: `a2a: conversation "${env.conversationId}" did not resolve to a hosted session` });
|
|
490
|
+
}
|
|
491
|
+
const promptRequest = {
|
|
492
|
+
requestId: env.messageId,
|
|
493
|
+
sessionId: row.sessionId,
|
|
494
|
+
mode: env.delivery ?? 'queue',
|
|
495
|
+
content: [{
|
|
496
|
+
type: 'text',
|
|
497
|
+
text: renderA2aEnvelope({
|
|
498
|
+
machine: env.from.machine,
|
|
499
|
+
agent: env.from.agent,
|
|
500
|
+
title: row.title,
|
|
501
|
+
shortId: row.shortId,
|
|
502
|
+
message: env.message,
|
|
503
|
+
}),
|
|
504
|
+
}],
|
|
505
|
+
};
|
|
506
|
+
try {
|
|
507
|
+
// 'delivered' on every successful submission (end-to-end delivery complete, D28)
|
|
508
|
+
await sessionRpc(gateway, WIRE.promptSubmit, promptRequest);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
return fail(receiptErrorOf(error), row.sessionId, row.shortId);
|
|
512
|
+
}
|
|
513
|
+
touchMirror(row);
|
|
514
|
+
watchCompletion(gateway, row, env);
|
|
515
|
+
return { outcome: 'delivered', messageId: env.messageId, conversationId: row.shortId, sessionId: row.sessionId };
|
|
516
|
+
};
|
|
517
|
+
const ingestStop = async (env) => {
|
|
518
|
+
const stopped = (scope, reason) => {
|
|
519
|
+
alog?.('warn', 'stop.refused', { conversation: env.conversationId, scope, reason });
|
|
520
|
+
return { stopped: false, scope, ...(reason === undefined ? {} : { reason }) };
|
|
521
|
+
};
|
|
522
|
+
const row = mirrorRowOf(env.conversationId);
|
|
523
|
+
if (row === undefined) {
|
|
524
|
+
return stopped(env.scope === 'session' ? 'turn-only' : 'turn', `a2a: conversation "${env.conversationId}" is not in the registry`);
|
|
525
|
+
}
|
|
526
|
+
const local = { machine: env.to.machine, id: env.to.id };
|
|
527
|
+
let gateway;
|
|
528
|
+
try {
|
|
529
|
+
gateway = deps.gatewayFor(local);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
return stopped(env.scope === 'session' ? 'turn-only' : 'turn', messageOf(error));
|
|
533
|
+
}
|
|
534
|
+
if (gateway === undefined) {
|
|
535
|
+
return stopped(env.scope === 'session' ? 'turn-only' : 'turn', A2aAddressError.messageOf('unreachable', local));
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
// D5 honesty (same read as the initiator-side stop): liveness BEFORE cancel
|
|
539
|
+
const live = await turnLivenessOf(gateway, row.sessionId);
|
|
540
|
+
await sessionRpc(gateway, WIRE.turnCancel, { sessionId: row.sessionId });
|
|
541
|
+
const result = live === false
|
|
542
|
+
? (env.scope === 'turn'
|
|
543
|
+
? { scope: 'turn', cancelled: false, note: A2A_NO_TURN_ACTIVE_NOTE }
|
|
544
|
+
: { scope: 'turn-only', cancelled: false, note: A2A_NO_TURN_ACTIVE_NOTE, reason: A2A_NO_SESSION_CLOSE_REASON })
|
|
545
|
+
: (env.scope === 'turn'
|
|
546
|
+
? { scope: 'turn', cancelled: true }
|
|
547
|
+
: { scope: 'turn-only', cancelled: true, reason: A2A_NO_SESSION_CLOSE_REASON });
|
|
548
|
+
const parts = [result.scope === 'turn-only' ? result.reason : undefined, 'note' in result ? result.note : undefined];
|
|
549
|
+
if (result.cancelled === true) {
|
|
550
|
+
// 0.1.7-h R2: stamp the row so this hub's completion watch attributes
|
|
551
|
+
// the turn end it is about to observe to the initiator's stop, not to
|
|
552
|
+
// the peer going silent. Watermark-safe: the watch only honors stamps
|
|
553
|
+
// set after its own arm time.
|
|
554
|
+
const rows = loadConversations(deps.home).conversations;
|
|
555
|
+
const idx = rows.findIndex((r) => r.sessionId === row.sessionId);
|
|
556
|
+
if (idx !== -1) {
|
|
557
|
+
rows[idx] = { ...rows[idx], stoppedAt: Date.now() };
|
|
558
|
+
saveConversations(deps.home, { conversations: rows });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
alog?.('info', 'stop', { conversation: env.conversationId, stopped: result.cancelled, scope: result.scope });
|
|
562
|
+
return {
|
|
563
|
+
stopped: result.cancelled,
|
|
564
|
+
scope: result.scope,
|
|
565
|
+
...(parts.some((p) => p !== undefined) ? { reason: parts.filter((p) => p !== undefined).join('; ') } : {}),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return stopped(env.scope === 'session' ? 'turn-only' : 'turn', messageOf(error));
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const dispatch = async (envelope) => {
|
|
573
|
+
switch (envelope.kind) {
|
|
574
|
+
case 'new-conversation':
|
|
575
|
+
case 'message':
|
|
576
|
+
return ingestRequest(envelope);
|
|
577
|
+
case 'stop':
|
|
578
|
+
return ingestStop(envelope);
|
|
579
|
+
case 'reply':
|
|
580
|
+
case 'notice': {
|
|
581
|
+
// inbound return envelope (peer POSTed / sweep pulled). Sender
|
|
582
|
+
// attribution, in order: the envelope's own `sender` (additive v1 —
|
|
583
|
+
// the producing hub stamps itself), else the registry row (the local
|
|
584
|
+
// row for this conversation points AT the peer), else honest
|
|
585
|
+
// 'unknown'. NEVER selfId() — the receiver is not the sender (the
|
|
586
|
+
// 0.1.6-h cross-machine bug: replies landed attributed bfwu-8ah589).
|
|
587
|
+
const row = mirrorRowOf(envelope.conversation);
|
|
588
|
+
const sender = envelope.sender ??
|
|
589
|
+
(row !== undefined ? { machine: row.machine, agent: row.agent } : { machine: 'unknown', agent: 'unknown' });
|
|
590
|
+
deliverLocal(envelope, sender);
|
|
591
|
+
// v1 send-receipt shape: the sender's postA2aEnvelope guard accepts
|
|
592
|
+
// exactly this. The 0.1.6-j double delivery was a {delivered:…} body
|
|
593
|
+
// failing that guard AFTER the receiver had already delivered.
|
|
594
|
+
return { outcome: 'delivered', messageId: envelope.messageId, conversationId: envelope.conversation, sessionId: envelope.to.sessionId };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
return {
|
|
599
|
+
async ingest(envelope) {
|
|
600
|
+
// exactly-once over an at-least-once transport (2026-09-28 first-person
|
|
601
|
+
// acceptance finding §7.1): an envelope the sender re-POSTs is
|
|
602
|
+
// re-ACKed with its receipt and NEVER re-executed. Stop envelopes
|
|
603
|
+
// carry no messageId and stay naturally idempotent (cancelling a dead
|
|
604
|
+
// turn answers honestly).
|
|
605
|
+
const mid = envelope.messageId;
|
|
606
|
+
if (mid !== undefined && seenMessageIds(deps.home).has(mid)) {
|
|
607
|
+
alog?.('info', 'ingest.park', { message: mid, reason: 'duplicate' });
|
|
608
|
+
return envelope.kind === 'reply' || envelope.kind === 'notice'
|
|
609
|
+
? { outcome: 'delivered', messageId: mid, conversationId: envelope.conversation, sessionId: envelope.to.sessionId }
|
|
610
|
+
: { outcome: 'delivered', messageId: mid, conversationId: envelope.conversationId ?? '', sessionId: '' };
|
|
611
|
+
}
|
|
612
|
+
const out = await dispatch(envelope);
|
|
613
|
+
const outcome = out.outcome;
|
|
614
|
+
const accepted = typeof outcome === 'string' ? outcome === 'delivered' : true;
|
|
615
|
+
if (mid !== undefined && accepted)
|
|
616
|
+
rememberMessageId(deps.home, mid);
|
|
617
|
+
alog?.(accepted ? 'info' : 'warn', 'ingest.done', {
|
|
618
|
+
kind: envelope.kind,
|
|
619
|
+
outcome: accepted ? 'delivered' : typeof outcome === 'string' ? outcome : 'failed',
|
|
620
|
+
message: mid,
|
|
621
|
+
conversation: envelope.conversationId ?? envelope.conversation,
|
|
622
|
+
});
|
|
623
|
+
return out;
|
|
624
|
+
},
|
|
625
|
+
claimOutbox(machine) {
|
|
626
|
+
const file = loadOutbox(deps.home);
|
|
627
|
+
const claimed = file.envelopes.filter((e) => e.to.machine === machine);
|
|
628
|
+
if (claimed.length === 0)
|
|
629
|
+
return [];
|
|
630
|
+
saveOutbox(deps.home, { envelopes: file.envelopes.filter((e) => e.to.machine !== machine) });
|
|
631
|
+
alog?.('info', 'outbox.claim', { machine, count: claimed.length });
|
|
632
|
+
return claimed;
|
|
633
|
+
},
|
|
634
|
+
routeReturn,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type A2aLogLevel = 'info' | 'warn' | 'error';
|
|
2
|
+
/** Field values: scalars only; `undefined` fields are dropped. */
|
|
3
|
+
export type A2aLogFields = Record<string, string | number | boolean | undefined>;
|
|
4
|
+
export type A2aLogFn = (level: A2aLogLevel, event: string, fields?: A2aLogFields) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Render one journal-friendly line: `superd-hub: a2a.<event> k=v k=v`.
|
|
7
|
+
* Values are quoted (JSON) only when they contain whitespace, so the common
|
|
8
|
+
* case stays grep-friendly: `a2a.completion.transcript conversation=abc123 bytes=42`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatA2aLogLine(event: string, fields?: A2aLogFields): string;
|
|
11
|
+
/**
|
|
12
|
+
* The host wiring both rows share: level → process console. NOT ctx.logger —
|
|
13
|
+
* the pack composes no cordis logger-console exporter, so LoggerService
|
|
14
|
+
* records go nowhere (verified live 2026-09-29); console matches the
|
|
15
|
+
* world-join / world-provision precedent and lands in every unit log.
|
|
16
|
+
*/
|
|
17
|
+
export declare function consoleA2aLog(): A2aLogFn;
|