@nelsonlongxiang/dsh-open-a2a-net 0.6.35 → 0.6.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/README_zh.md +5 -2
- package/assets/nexusDist/assets/index-C7HagaGy.css +1 -0
- package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +1 -1
- package/lib/a2a-client.js +3 -3
- package/lib/client.js +63 -63
- package/lib/faces/admin.js +23 -5
- package/lib/faces/canvas-notify.js +8 -6
- package/lib/faces/wire.js +11 -5
- package/lib/index.js +35 -10
- package/lib/origin-claim.js +30 -7
- package/lib/owner-settings.js +38 -1
- package/lib/peer-link-display.js +15 -0
- package/lib/queue-guard.js +3 -3
- package/lib/skill/references/cli.md +4 -0
- package/lib/skill/references/routing.md +4 -0
- package/lib/skill/references/setup.md +4 -0
- package/lib/task-ledger-rows.js +192 -0
- package/lib/task-ledger.js +11 -189
- package/lib/tools/info.js +7 -3
- package/lib/types/a2a-client.d.ts +1 -0
- package/lib/types/faces/admin.d.ts +1 -0
- package/lib/types/faces/canvas-notify.d.ts +3 -1
- package/lib/types/faces/wire.d.ts +1 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/origin-claim.d.ts +12 -3
- package/lib/types/owner-settings.d.ts +5 -0
- package/lib/types/peer-link-display.d.ts +13 -0
- package/lib/types/queue-guard.d.ts +3 -2
- package/lib/types/task-ledger-rows.d.ts +341 -0
- package/lib/types/task-ledger.d.ts +6 -284
- package/lib/types/tools/info.d.ts +2 -0
- package/lib/types/types.d.ts +2 -1
- package/nexus-stage/scripts/mockhost.py +14 -6
- package/nexus-stage/src/planning-card.ts +5 -3
- package/nexus-stage/src/planning-member-handles.ts +28 -1
- package/nexus-stage/src/planning-menus.ts +12 -5
- package/nexus-stage/src/planning-view.ts +26 -16
- package/nexus-stage/src/seat-name-editor.ts +31 -0
- package/nexus-stage/src/session-drag.ts +64 -0
- package/nexus-stage/src/wb-directory.ts +2 -0
- package/nexus-stage/src/workbench.css +2 -0
- package/nexus-stage/src/workbench.ts +16 -5
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-Du2E6DJL.js +0 -9
- package/assets/nexusDist/assets/index-z33lHtLw.css +0 -1
package/lib/owner-settings.js
CHANGED
|
@@ -8,8 +8,39 @@
|
|
|
8
8
|
* else; a missing entry is appended in the canonical short-id form.
|
|
9
9
|
* @module @nelsonlongxiang/dsh-open-a2a-net/owner-settings
|
|
10
10
|
*/
|
|
11
|
+
/** A label shaped like a routable address (`dsh/66abc2cf`, `zone/canvas/x`, `name/<16 hex>`) would pass for another
|
|
12
|
+
* session in the directory and in `a2a-net teams <query>`; labels are display-only, so that shape is refused. */
|
|
13
|
+
const ADDRESS_SHAPED = /^[A-Za-z0-9._-]+\/\S+$/;
|
|
14
|
+
/** Shared validation for boot config and owner POST; labels are plain text, never handles or authority. */
|
|
15
|
+
export function parseSeatNames(value) {
|
|
16
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length > 1024)
|
|
17
|
+
return undefined;
|
|
18
|
+
const entries = Object.entries(value);
|
|
19
|
+
if (entries.some(([id, name]) => !/^session-[a-zA-Z0-9-]{8,128}$/.test(id) || typeof name !== 'string' || name.trim() === '' || [...name.trim()].length > 64 || /[\p{Cc}\p{Cf}\u2028\u2029\ufffd]/u.test(name) || !/[^\p{Z}\p{C}\p{M}]/u.test(name) || ADDRESS_SHAPED.test(name.trim())))
|
|
20
|
+
return undefined;
|
|
21
|
+
return Object.fromEntries(entries.map(([id, name]) => [id, name.trim()]));
|
|
22
|
+
}
|
|
23
|
+
/** Boot config degrades entry-by-entry; owner POST remains all-or-nothing. Never echo label contents in warnings. */
|
|
24
|
+
export function loadSeatNames(value, warn) {
|
|
25
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
26
|
+
warn('seatNames: ignored invalid map');
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
const kept = {};
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const [id, name] of Object.entries(value)) {
|
|
32
|
+
const entry = parseSeatNames({ [id]: name });
|
|
33
|
+
if (entry === undefined || count >= 1024) {
|
|
34
|
+
warn('seatNames: ignored invalid or over-cap entry');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
Object.assign(kept, entry);
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
return kept;
|
|
41
|
+
}
|
|
11
42
|
/** The exact keys this module may touch, in wire order. */
|
|
12
|
-
const KEYS = ['teamJoinAllowlist', 'teamScopeInfo', 'announce'];
|
|
43
|
+
const KEYS = ['teamJoinAllowlist', 'teamScopeInfo', 'announce', 'seatNames'];
|
|
13
44
|
export function applyOwnerSettings(patchYaml, changes) {
|
|
14
45
|
const lines = patchYaml.split('\n');
|
|
15
46
|
// Find the a2a entry: `- id: a2a` / `- id: 'a2a'` variants.
|
|
@@ -34,6 +65,8 @@ export function applyOwnerSettings(patchYaml, changes) {
|
|
|
34
65
|
writes.push(` teamScopeInfo: ${changes.teamScopeInfo ? 'true' : 'false'}`);
|
|
35
66
|
if (changes.announce !== undefined)
|
|
36
67
|
writes.push(` announce: ${changes.announce ? 'true' : 'false'}`);
|
|
68
|
+
if (changes.seatNames !== undefined)
|
|
69
|
+
writes.push(` seatNames: ${JSON.stringify(changes.seatNames)}`);
|
|
37
70
|
if (writes.length === 0)
|
|
38
71
|
return { yaml: patchYaml, restartNeeded };
|
|
39
72
|
if (entryStart === -1) {
|
|
@@ -81,6 +114,10 @@ export function applyOwnerSettings(patchYaml, changes) {
|
|
|
81
114
|
if (write !== undefined) {
|
|
82
115
|
lines[j] = write;
|
|
83
116
|
found = true;
|
|
117
|
+
// A map may have been authored as block YAML. Replace its nested rows too, not just the heading.
|
|
118
|
+
if (key === 'seatNames')
|
|
119
|
+
while (j + 1 < lines.length && (/^ {5,}\S/.test(lines[j + 1] ?? '') || /^\s*$/.test(lines[j + 1] ?? '')))
|
|
120
|
+
lines.splice(j + 1, 1);
|
|
84
121
|
}
|
|
85
122
|
break;
|
|
86
123
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function peerLinkDisplay(peerUrl, d) {
|
|
2
|
+
const { card } = d;
|
|
3
|
+
if (card === undefined)
|
|
4
|
+
return { link: 'stranger', verified: false };
|
|
5
|
+
const origin = (url) => { try {
|
|
6
|
+
return new URL(url).origin;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return '';
|
|
10
|
+
} };
|
|
11
|
+
const peerOrigin = origin(peerUrl), ownOrigin = origin(d.ownUrl);
|
|
12
|
+
const bilateral = peerOrigin !== '' && ownOrigin !== '' && d.seeds.some(url => origin(url) === peerOrigin) && (card.peers ?? []).some(url => origin(url) === ownOrigin);
|
|
13
|
+
const exactMember = d.refs.some(ref => !d.ownHandles.has(ref) && (card.sessionTeams ?? []).some(row => row.team === ref) && !d.cards.some(other => other.publicKey !== card.publicKey && (other.sessionTeams ?? []).some(row => row.team === ref)));
|
|
14
|
+
return { link: bilateral || exactMember ? 'linked' : 'stranger', verified: true };
|
|
15
|
+
}
|
package/lib/queue-guard.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseReceipt } from "./receipt.js";
|
|
2
2
|
export const WIRE_ERROR_QUEUE_FULL = -32009;
|
|
3
3
|
export const DEFAULT_TARGET_QUEUE_CAP = 3;
|
|
4
|
-
/** How many receipt-owing tasks this host holds pending for `team`. */
|
|
4
|
+
/** How many receipt-owing tasks this host holds pending and unanswered for `team`. */
|
|
5
5
|
export function queuedFor(rows, team) {
|
|
6
|
-
return rows.filter(row => row.status === 'pending' && row.receiptExpected !== false &&
|
|
6
|
+
return rows.filter(row => row.status === 'pending' && row.receiptExpected !== false && row.answeredAt === undefined &&
|
|
7
7
|
(row.direction === 'inbound' || row.peer === 'local') &&
|
|
8
8
|
(row.team === team || row.deliveredTo === team)).length;
|
|
9
9
|
}
|
|
@@ -19,7 +19,7 @@ export function queueFullRefusal(rows, team, message, cap, mailbox, ownTask) {
|
|
|
19
19
|
const queued = queuedFor(rows, team);
|
|
20
20
|
if (queued < limit)
|
|
21
21
|
return undefined;
|
|
22
|
-
return `target-queue-full (gh#224): "${team}" already has ${String(queued)} task(s)
|
|
22
|
+
return `target-queue-full (gh#224): "${team}" already has ${String(queued)} task(s) not yet answered by the target (cap ${String(limit)}) — nothing was delivered. Wait for a receipt from it (a2a inbox / a2a_tasks), or send again later with a NEW task id (this one is recorded as failed); do not resend in a loop. A task that never gets a receipt turns dead after taskStaleTtlMs and stops counting.`;
|
|
23
23
|
}
|
|
24
24
|
/** Wire-face adapter in the flood guard's shape: a refused fresh key is FAILED, not stranded pending. */
|
|
25
25
|
export function createQueueGuard(rows, cap, ownTask, outcomes, recordActivity) {
|
|
@@ -42,6 +42,10 @@ model tools.
|
|
|
42
42
|
405 with an empty body. The fix is a host upgrade, not a retry.
|
|
43
43
|
- `a2a-net revoke` deregisters early.
|
|
44
44
|
|
|
45
|
+
## Team display names
|
|
46
|
+
|
|
47
|
+
`a2a-net teams <query>` searches the owner-set seat label as well as the route handle; `--json` exposes the effective display name. Local rows prefer `seatName`, then the current title (`name`), then the legacy label. Only the human workbench owner panel/config can set `seatNames`; the CLI has no naming command.
|
|
48
|
+
|
|
45
49
|
## Tasks: ledger and host observability
|
|
46
50
|
|
|
47
51
|
`a2a-net tasks --json` returns the existing `pending`, `dead`, and `archived` arrays plus `settlement` and `conservation` copied unchanged from `/__dsh_a2a/state`. These blocks include the lifetime `settlement.refusalsTotal`, refusal ring and mode, and the host conservation verdict/counters/duplicate ids. Older hosts omit a block they do not expose; the CLI does not manufacture zero counters or a passing verdict. Plain `tasks` appends a conservation line (`conserved`, `delta`) and a settlement line (`mode`, `refusalsTotal`, `offChain`) when the respective block exists.
|
|
@@ -40,6 +40,10 @@ polling `a2a_teams` cannot see another host's ledger.
|
|
|
40
40
|
when known. `archivedCount` and `archivedSplit` (count per outcome) cover
|
|
41
41
|
the whole archive.
|
|
42
42
|
|
|
43
|
+
## Queue capacity is not receipt debt
|
|
44
|
+
|
|
45
|
+
A pending row with `answeredAt` records that its actual local target produced a task-attributed final or attempted its own formal receipt. It no longer occupies the `targetQueueCap` limit, even if the caller CLI expired and receipt delivery failed. It is **still pending receipt debt**: do not abandon it just to free capacity, or claim the sender received it. Dead-letter expiry, late receipts and lifetime conservation continue unchanged. State session `queued` and the board both use the same unanswered-row count; unfinished work still counts against the default cap of three. Unrelated agents, unrelated finals and host timeout placeholders cannot mark an answer.
|
|
46
|
+
|
|
43
47
|
## Idempotency
|
|
44
48
|
|
|
45
49
|
A route carries a fingerprint (caller + message + team + wait-mode). Re-
|
|
@@ -21,6 +21,10 @@ presenting the operator's gesture on the operator's behalf is exactly the
|
|
|
21
21
|
| Version | `/__dsh_a2a/state` `version`, or the workbench header badge | the same version on every host you will link (0.6.28+) |
|
|
22
22
|
| Clock | `date` on each host | NTP-synced: card signatures carry `expiresAt`, and the OriginClaim envelope allows ±5 min |
|
|
23
23
|
|
|
24
|
+
## Human seat names
|
|
25
|
+
|
|
26
|
+
The owner can open the workbench header ⚙, select a local session under “会话席位名”, and explicitly save a stable display name (up to 64 characters; a name shaped like a route address such as `dsh/66abc2cf` is refused so it cannot pass for another session). Empty input clears the label. The `seatNames` map is stored in the home patch layer and hot-applies; cards and the board prefer it over the session title, and `a2a_whoami` / `a2a-net teams <name>` expose it. It never changes the route handle, join state or admission. Agents must ask the owner rather than self-name: A2A tool arguments containing `seatName`/`seatNames` are refused; no naming model tool exists. The owner control API keeps its existing apiKey/loopback trust model, not a sandbox against local shell or config-file writers.
|
|
27
|
+
|
|
24
28
|
## 1. One host: sessions talking to each other
|
|
25
29
|
|
|
26
30
|
**Owner:**
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/** Stateless ledger row types, classifiers and restore projections; moved verbatim for #258. */
|
|
2
|
+
import { parseAuthorization } from "./task-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Caller-ended summaries (abandon / cancel) share one isolation law: late
|
|
5
|
+
* receipts never rewrite the verdict — they only stamp lateReceiptAt, and
|
|
6
|
+
* the projected outcome collapses to 'abandoned'.
|
|
7
|
+
*/
|
|
8
|
+
export const CALLER_ENDED_PREFIX = /^caller-(abandoned|cancelled)/;
|
|
9
|
+
/** Hard cap on the owed book (pending + dead-lettered rows, most recent first). */
|
|
10
|
+
export const TASK_CAP = 64;
|
|
11
|
+
/** Hard cap on the settled archive (most recent kept). */
|
|
12
|
+
export const ARCHIVE_CAP = 64;
|
|
13
|
+
/** Longest receipt summary kept on a record (multi-line outcomes collapse). */
|
|
14
|
+
export const SUMMARY_CAP = 200;
|
|
15
|
+
/**
|
|
16
|
+
* Longest dispatch brief kept per row. A ledger row without its payload is
|
|
17
|
+
* unreadable to supervision — "direct-8f3a1c2b → dsh/ec4ce7f8, 3h old" says
|
|
18
|
+
* nothing about whether to nudge, abandon, or read the receipt. The brief is
|
|
19
|
+
* the first non-empty line of the message, capped; the full body never enters
|
|
20
|
+
* the ledger (it can be arbitrarily large and may quote another session's log).
|
|
21
|
+
*/
|
|
22
|
+
export const BRIEF_CAP = 120;
|
|
23
|
+
/** Reduce a dispatch body to one readable line: first non-empty line, collapsed, capped. */
|
|
24
|
+
export function briefOf(message) {
|
|
25
|
+
if (message === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
for (const raw of message.split('\n')) {
|
|
28
|
+
const line = raw.trim().replace(/\s+/g, ' ');
|
|
29
|
+
if (line !== '')
|
|
30
|
+
return line.length <= BRIEF_CAP ? line : `${line.slice(0, BRIEF_CAP - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Age at which an unsettled task auto-dead-letters. A dead row exits the
|
|
36
|
+
* owed view, disarms its async-nudge retry (a cold session cannot consume a
|
|
37
|
+
* re-steer anyway), and still accepts a late receipt from a revived target.
|
|
38
|
+
* Injectable per instance so tests can use short windows; sweeping stays
|
|
39
|
+
* lazy — every read/write entry point sweeps first.
|
|
40
|
+
*/
|
|
41
|
+
export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
42
|
+
export const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled', 'evicted'];
|
|
43
|
+
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
44
|
+
export const ECHO_SUMMARY = '(settled by content echo)';
|
|
45
|
+
const SENDER_KINDS = ['session', 'host', 'bridge', 'cli'];
|
|
46
|
+
/** Longest sender id kept; anything longer is malformed, not truncated. */
|
|
47
|
+
export const SENDER_ID_CAP = 256;
|
|
48
|
+
/**
|
|
49
|
+
* Restore-side allowlist for one persisted sender: a well-formed value comes
|
|
50
|
+
* back as a fresh object holding only kind, id and (when present) face; anything else is dropped whole (the row
|
|
51
|
+
* itself survives — a bad sender never costs the debt record).
|
|
52
|
+
* @param raw - the persisted value.
|
|
53
|
+
* @returns the sender, or undefined when absent or malformed.
|
|
54
|
+
*/
|
|
55
|
+
export function parseSender(raw) {
|
|
56
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
57
|
+
return undefined;
|
|
58
|
+
const { kind, id, face } = raw;
|
|
59
|
+
if (typeof kind !== 'string' || !SENDER_KINDS.includes(kind))
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof id !== 'string' || id === '' || id.length > SENDER_ID_CAP)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (face !== undefined && (typeof face !== 'string' || face === '' || face.length > SENDER_ID_CAP))
|
|
64
|
+
return undefined;
|
|
65
|
+
// `face` belongs to host senders only; on any other kind it is dropped, the sender kept.
|
|
66
|
+
return { kind: kind, id, ...(face !== undefined && kind === 'host' ? { face } : {}) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Spread helper for restore: `{ sender }` when the persisted value parses and
|
|
70
|
+
* the row is not inbound (an inbound row's originator is its caller, never a
|
|
71
|
+
* sender this host recorded), else nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function withSender(raw, direction) {
|
|
74
|
+
if (direction === 'inbound')
|
|
75
|
+
return {};
|
|
76
|
+
const sender = parseSender(raw);
|
|
77
|
+
return sender === undefined ? {} : { sender };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* #114 S2: `{ auth }` when a persisted INBOUND row carries a record that
|
|
81
|
+
* parses whole (parseAuthorization) and belongs to THIS row — its target is
|
|
82
|
+
* the row's team and its receipt target the row's peer — else nothing: the
|
|
83
|
+
* row survives as a pre-S2 row, never with a partially adopted or borrowed
|
|
84
|
+
* record.
|
|
85
|
+
*/
|
|
86
|
+
export function withAuth(raw, entry, now) {
|
|
87
|
+
if (entry.direction !== 'inbound' && entry.direction !== 'outbound')
|
|
88
|
+
return {};
|
|
89
|
+
const auth = parseAuthorization(raw, now);
|
|
90
|
+
if (auth === undefined || auth.target !== entry.team)
|
|
91
|
+
return {};
|
|
92
|
+
if (entry.direction === 'inbound')
|
|
93
|
+
return auth.ingress === 'wire' && `local:${auth.receipt_target}` === entry.peer ? { auth } : {};
|
|
94
|
+
// #139: an outbound row carries only its local-candidate admission.
|
|
95
|
+
return auth.ingress === 'local' && entry.peer === 'local' ? { auth } : {};
|
|
96
|
+
}
|
|
97
|
+
/** Routing and answer facts copied unchanged between a row and its archive entry. */
|
|
98
|
+
export function routingOf(row) {
|
|
99
|
+
return {
|
|
100
|
+
...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
|
|
101
|
+
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
102
|
+
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
103
|
+
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
104
|
+
...(row.deliveredTo !== undefined ? { deliveredTo: row.deliveredTo } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
|
|
108
|
+
export function birthFacts(peer, birth) {
|
|
109
|
+
if (birth === undefined)
|
|
110
|
+
return {};
|
|
111
|
+
const fact = (v) => v !== undefined && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
112
|
+
const remote = peer !== 'local' && !peer.startsWith('local:');
|
|
113
|
+
return {
|
|
114
|
+
...(!remote && birth.auth !== undefined && birth.auth.ingress === 'local' ? { auth: birth.auth } : {}),
|
|
115
|
+
...(remote && birth.forwarded === true ? { forwarded: true } : {}),
|
|
116
|
+
...(remote && fact(birth.peerKey) ? { peerKey: birth.peerKey } : {}),
|
|
117
|
+
...(remote && fact(birth.peerHost) ? { peerHost: birth.peerHost } : {}),
|
|
118
|
+
...(fact(birth.deliveredTo) ? { deliveredTo: birth.deliveredTo } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
|
|
122
|
+
export const ROUTING_FACT_CAP = 256;
|
|
123
|
+
/**
|
|
124
|
+
* #139: restore-side allowlist for the routing facts. Each is kept only in
|
|
125
|
+
* its shape — a non-empty string within the cap — and the peer facts only on
|
|
126
|
+
* a remote outbound row; anything else is dropped (the row survives as one
|
|
127
|
+
* born before the fields, judged by current membership and origin).
|
|
128
|
+
*/
|
|
129
|
+
export function withRouting(entry) {
|
|
130
|
+
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
131
|
+
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
132
|
+
return {
|
|
133
|
+
...(typeof entry.answeredAt === 'number' && Number.isFinite(entry.answeredAt) && entry.answeredAt >= 0 ? { answeredAt: entry.answeredAt } : {}),
|
|
134
|
+
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
135
|
+
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
136
|
+
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
137
|
+
...(fact(entry.deliveredTo) ? { deliveredTo: entry.deliveredTo } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
142
|
+
* fall back to the summary markers this ledger writes itself. The fallback
|
|
143
|
+
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
144
|
+
* only as exact as the legacy data allows.
|
|
145
|
+
* @param row - the archived record.
|
|
146
|
+
* @returns the archive path that ended it.
|
|
147
|
+
*/
|
|
148
|
+
export function archivedEndedBy(row) {
|
|
149
|
+
if (row.endedBy !== undefined)
|
|
150
|
+
return row.endedBy;
|
|
151
|
+
const summary = row.summary ?? '';
|
|
152
|
+
if (summary.startsWith('caller-cancelled'))
|
|
153
|
+
return 'cancelled';
|
|
154
|
+
if (summary.startsWith('caller-abandoned'))
|
|
155
|
+
return 'abandoned';
|
|
156
|
+
if (summary === ECHO_SUMMARY)
|
|
157
|
+
return 'echo';
|
|
158
|
+
return 'receipt';
|
|
159
|
+
}
|
|
160
|
+
/** Bound on ids and callers kept in settlement records; longer values are cut on write and refused on restore. */
|
|
161
|
+
export const SETTLEMENT_ID_CAP = 256;
|
|
162
|
+
/** Bound of the refusal ring (design §4.4). */
|
|
163
|
+
export const SETTLEMENT_REFUSAL_CAP = 64;
|
|
164
|
+
const SETTLEMENT_REASONS = ['settler-off-chain', 'settler-unverified', 'on-behalf-mismatch', 'echo-off-chain'];
|
|
165
|
+
const CHAIN_ROLES = ['target', 'target-member', 'host-face', 'compat', 'legacy', 'off-chain'];
|
|
166
|
+
const PROOF_VALUES = ['origin-claim', 'claim-mismatch', 'legacy-window', 'unbound', 'cli-identity', 'in-process', 'none'];
|
|
167
|
+
/** Restore-side allowlist for settledBy: whole or nothing. */
|
|
168
|
+
export function withSettledBy(raw) {
|
|
169
|
+
if (raw === null || typeof raw !== 'object')
|
|
170
|
+
return {};
|
|
171
|
+
const r = raw;
|
|
172
|
+
if (typeof r.caller !== 'string' || r.caller.length > SETTLEMENT_ID_CAP || typeof r.chain_role !== 'string' || !CHAIN_ROLES.includes(r.chain_role))
|
|
173
|
+
return {};
|
|
174
|
+
return { settledBy: { caller: r.caller, chain_role: r.chain_role } };
|
|
175
|
+
}
|
|
176
|
+
/** Restore-side allowlist for one refusal record: whole or nothing. */
|
|
177
|
+
export function parseRefusal(raw) {
|
|
178
|
+
if (raw === null || typeof raw !== 'object')
|
|
179
|
+
return undefined;
|
|
180
|
+
const r = raw;
|
|
181
|
+
const st = r.settler;
|
|
182
|
+
if (typeof r.taskId !== 'string' || r.taskId === '' || r.taskId.length > SETTLEMENT_ID_CAP || typeof r.at !== 'number' || !Number.isFinite(r.at))
|
|
183
|
+
return undefined;
|
|
184
|
+
if (typeof r.reason !== 'string' || !SETTLEMENT_REASONS.includes(r.reason) || typeof r.head !== 'string' || typeof r.enforced !== 'boolean')
|
|
185
|
+
return undefined;
|
|
186
|
+
if (st === null || typeof st !== 'object' || typeof st.caller !== 'string' || st.caller.length > SETTLEMENT_ID_CAP || typeof st.proof !== 'string' || !PROOF_VALUES.includes(st.proof) || (st.ingress !== 'wire' && st.ingress !== 'local'))
|
|
187
|
+
return undefined;
|
|
188
|
+
const count = r.count === undefined ? 1 : r.count;
|
|
189
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1)
|
|
190
|
+
return undefined;
|
|
191
|
+
return { taskId: r.taskId, at: r.at, settler: { caller: st.caller, proof: st.proof, ingress: st.ingress }, reason: r.reason, head: r.head.slice(0, SUMMARY_CAP), enforced: r.enforced, count };
|
|
192
|
+
}
|
package/lib/task-ledger.js
CHANGED
|
@@ -18,199 +18,12 @@
|
|
|
18
18
|
* illusion: settlements vanished while debts piled up).
|
|
19
19
|
* @module @nelsonlongxiang/dsh-open-a2a-net/task-ledger
|
|
20
20
|
*/
|
|
21
|
-
import { parseAuthorization } from "./task-auth.js";
|
|
22
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
22
|
import { dirname } from 'node:path';
|
|
24
23
|
import { parseReceipt, RECEIPT_OUTCOMES } from "./receipt.js";
|
|
25
24
|
import { classifyConservationBlock, ConservationCounters } from "./ledger-conservation.js";
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* receipts never rewrite the verdict — they only stamp lateReceiptAt, and
|
|
29
|
-
* the projected outcome collapses to 'abandoned'.
|
|
30
|
-
*/
|
|
31
|
-
const CALLER_ENDED_PREFIX = /^caller-(abandoned|cancelled)/;
|
|
32
|
-
/** Hard cap on the owed book (pending + dead-lettered rows, most recent first). */
|
|
33
|
-
export const TASK_CAP = 64;
|
|
34
|
-
/** Hard cap on the settled archive (most recent kept). */
|
|
35
|
-
export const ARCHIVE_CAP = 64;
|
|
36
|
-
/** Longest receipt summary kept on a record (multi-line outcomes collapse). */
|
|
37
|
-
export const SUMMARY_CAP = 200;
|
|
38
|
-
/**
|
|
39
|
-
* Longest dispatch brief kept per row. A ledger row without its payload is
|
|
40
|
-
* unreadable to supervision — "direct-8f3a1c2b → dsh/ec4ce7f8, 3h old" says
|
|
41
|
-
* nothing about whether to nudge, abandon, or read the receipt. The brief is
|
|
42
|
-
* the first non-empty line of the message, capped; the full body never enters
|
|
43
|
-
* the ledger (it can be arbitrarily large and may quote another session's log).
|
|
44
|
-
*/
|
|
45
|
-
export const BRIEF_CAP = 120;
|
|
46
|
-
/** Reduce a dispatch body to one readable line: first non-empty line, collapsed, capped. */
|
|
47
|
-
export function briefOf(message) {
|
|
48
|
-
if (message === undefined)
|
|
49
|
-
return undefined;
|
|
50
|
-
for (const raw of message.split('\n')) {
|
|
51
|
-
const line = raw.trim().replace(/\s+/g, ' ');
|
|
52
|
-
if (line !== '')
|
|
53
|
-
return line.length <= BRIEF_CAP ? line : `${line.slice(0, BRIEF_CAP - 1)}…`;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Age at which an unsettled task auto-dead-letters. A dead row exits the
|
|
59
|
-
* owed view, disarms its async-nudge retry (a cold session cannot consume a
|
|
60
|
-
* re-steer anyway), and still accepts a late receipt from a revived target.
|
|
61
|
-
* Injectable per instance so tests can use short windows; sweeping stays
|
|
62
|
-
* lazy — every read/write entry point sweeps first.
|
|
63
|
-
*/
|
|
64
|
-
export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
65
|
-
const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled', 'evicted'];
|
|
66
|
-
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
67
|
-
const ECHO_SUMMARY = '(settled by content echo)';
|
|
68
|
-
const SENDER_KINDS = ['session', 'host', 'bridge', 'cli'];
|
|
69
|
-
/** Longest sender id kept; anything longer is malformed, not truncated. */
|
|
70
|
-
export const SENDER_ID_CAP = 256;
|
|
71
|
-
/**
|
|
72
|
-
* Restore-side allowlist for one persisted sender: a well-formed value comes
|
|
73
|
-
* back as a fresh object holding only kind, id and (when present) face; anything else is dropped whole (the row
|
|
74
|
-
* itself survives — a bad sender never costs the debt record).
|
|
75
|
-
* @param raw - the persisted value.
|
|
76
|
-
* @returns the sender, or undefined when absent or malformed.
|
|
77
|
-
*/
|
|
78
|
-
export function parseSender(raw) {
|
|
79
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
80
|
-
return undefined;
|
|
81
|
-
const { kind, id, face } = raw;
|
|
82
|
-
if (typeof kind !== 'string' || !SENDER_KINDS.includes(kind))
|
|
83
|
-
return undefined;
|
|
84
|
-
if (typeof id !== 'string' || id === '' || id.length > SENDER_ID_CAP)
|
|
85
|
-
return undefined;
|
|
86
|
-
if (face !== undefined && (typeof face !== 'string' || face === '' || face.length > SENDER_ID_CAP))
|
|
87
|
-
return undefined;
|
|
88
|
-
// `face` belongs to host senders only; on any other kind it is dropped, the sender kept.
|
|
89
|
-
return { kind: kind, id, ...(face !== undefined && kind === 'host' ? { face } : {}) };
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Spread helper for restore: `{ sender }` when the persisted value parses and
|
|
93
|
-
* the row is not inbound (an inbound row's originator is its caller, never a
|
|
94
|
-
* sender this host recorded), else nothing.
|
|
95
|
-
*/
|
|
96
|
-
function withSender(raw, direction) {
|
|
97
|
-
if (direction === 'inbound')
|
|
98
|
-
return {};
|
|
99
|
-
const sender = parseSender(raw);
|
|
100
|
-
return sender === undefined ? {} : { sender };
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* #114 S2: `{ auth }` when a persisted INBOUND row carries a record that
|
|
104
|
-
* parses whole (parseAuthorization) and belongs to THIS row — its target is
|
|
105
|
-
* the row's team and its receipt target the row's peer — else nothing: the
|
|
106
|
-
* row survives as a pre-S2 row, never with a partially adopted or borrowed
|
|
107
|
-
* record.
|
|
108
|
-
*/
|
|
109
|
-
function withAuth(raw, entry, now) {
|
|
110
|
-
if (entry.direction !== 'inbound' && entry.direction !== 'outbound')
|
|
111
|
-
return {};
|
|
112
|
-
const auth = parseAuthorization(raw, now);
|
|
113
|
-
if (auth === undefined || auth.target !== entry.team)
|
|
114
|
-
return {};
|
|
115
|
-
if (entry.direction === 'inbound')
|
|
116
|
-
return auth.ingress === 'wire' && `local:${auth.receipt_target}` === entry.peer ? { auth } : {};
|
|
117
|
-
// #139: an outbound row carries only its local-candidate admission.
|
|
118
|
-
return auth.ingress === 'local' && entry.peer === 'local' ? { auth } : {};
|
|
119
|
-
}
|
|
120
|
-
/** #139: the routing facts settlement reads, copied between a row and its archive entry. */
|
|
121
|
-
function routingOf(row) {
|
|
122
|
-
return {
|
|
123
|
-
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
124
|
-
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
125
|
-
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
126
|
-
...(row.deliveredTo !== undefined ? { deliveredTo: row.deliveredTo } : {}),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
/** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
|
|
130
|
-
function birthFacts(peer, birth) {
|
|
131
|
-
if (birth === undefined)
|
|
132
|
-
return {};
|
|
133
|
-
const fact = (v) => v !== undefined && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
134
|
-
const remote = peer !== 'local' && !peer.startsWith('local:');
|
|
135
|
-
return {
|
|
136
|
-
...(!remote && birth.auth !== undefined && birth.auth.ingress === 'local' ? { auth: birth.auth } : {}),
|
|
137
|
-
...(remote && birth.forwarded === true ? { forwarded: true } : {}),
|
|
138
|
-
...(remote && fact(birth.peerKey) ? { peerKey: birth.peerKey } : {}),
|
|
139
|
-
...(remote && fact(birth.peerHost) ? { peerHost: birth.peerHost } : {}),
|
|
140
|
-
...(fact(birth.deliveredTo) ? { deliveredTo: birth.deliveredTo } : {}),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
/** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
|
|
144
|
-
const ROUTING_FACT_CAP = 256;
|
|
145
|
-
/**
|
|
146
|
-
* #139: restore-side allowlist for the routing facts. Each is kept only in
|
|
147
|
-
* its shape — a non-empty string within the cap — and the peer facts only on
|
|
148
|
-
* a remote outbound row; anything else is dropped (the row survives as one
|
|
149
|
-
* born before the fields, judged by current membership and origin).
|
|
150
|
-
*/
|
|
151
|
-
function withRouting(entry) {
|
|
152
|
-
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
153
|
-
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
154
|
-
return {
|
|
155
|
-
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
156
|
-
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
157
|
-
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
158
|
-
...(fact(entry.deliveredTo) ? { deliveredTo: entry.deliveredTo } : {}),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
163
|
-
* fall back to the summary markers this ledger writes itself. The fallback
|
|
164
|
-
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
165
|
-
* only as exact as the legacy data allows.
|
|
166
|
-
* @param row - the archived record.
|
|
167
|
-
* @returns the archive path that ended it.
|
|
168
|
-
*/
|
|
169
|
-
export function archivedEndedBy(row) {
|
|
170
|
-
if (row.endedBy !== undefined)
|
|
171
|
-
return row.endedBy;
|
|
172
|
-
const summary = row.summary ?? '';
|
|
173
|
-
if (summary.startsWith('caller-cancelled'))
|
|
174
|
-
return 'cancelled';
|
|
175
|
-
if (summary.startsWith('caller-abandoned'))
|
|
176
|
-
return 'abandoned';
|
|
177
|
-
if (summary === ECHO_SUMMARY)
|
|
178
|
-
return 'echo';
|
|
179
|
-
return 'receipt';
|
|
180
|
-
}
|
|
181
|
-
/** Bound on ids and callers kept in settlement records; longer values are cut on write and refused on restore. */
|
|
182
|
-
const SETTLEMENT_ID_CAP = 256;
|
|
183
|
-
/** Bound of the refusal ring (design §4.4). */
|
|
184
|
-
export const SETTLEMENT_REFUSAL_CAP = 64;
|
|
185
|
-
const SETTLEMENT_REASONS = ['settler-off-chain', 'settler-unverified', 'on-behalf-mismatch', 'echo-off-chain'];
|
|
186
|
-
const CHAIN_ROLES = ['target', 'target-member', 'host-face', 'compat', 'legacy', 'off-chain'];
|
|
187
|
-
const PROOF_VALUES = ['origin-claim', 'claim-mismatch', 'legacy-window', 'unbound', 'cli-identity', 'in-process', 'none'];
|
|
188
|
-
/** Restore-side allowlist for settledBy: whole or nothing. */
|
|
189
|
-
function withSettledBy(raw) {
|
|
190
|
-
if (raw === null || typeof raw !== 'object')
|
|
191
|
-
return {};
|
|
192
|
-
const r = raw;
|
|
193
|
-
if (typeof r.caller !== 'string' || r.caller.length > SETTLEMENT_ID_CAP || typeof r.chain_role !== 'string' || !CHAIN_ROLES.includes(r.chain_role))
|
|
194
|
-
return {};
|
|
195
|
-
return { settledBy: { caller: r.caller, chain_role: r.chain_role } };
|
|
196
|
-
}
|
|
197
|
-
/** Restore-side allowlist for one refusal record: whole or nothing. */
|
|
198
|
-
function parseRefusal(raw) {
|
|
199
|
-
if (raw === null || typeof raw !== 'object')
|
|
200
|
-
return undefined;
|
|
201
|
-
const r = raw;
|
|
202
|
-
const st = r.settler;
|
|
203
|
-
if (typeof r.taskId !== 'string' || r.taskId === '' || r.taskId.length > SETTLEMENT_ID_CAP || typeof r.at !== 'number' || !Number.isFinite(r.at))
|
|
204
|
-
return undefined;
|
|
205
|
-
if (typeof r.reason !== 'string' || !SETTLEMENT_REASONS.includes(r.reason) || typeof r.head !== 'string' || typeof r.enforced !== 'boolean')
|
|
206
|
-
return undefined;
|
|
207
|
-
if (st === null || typeof st !== 'object' || typeof st.caller !== 'string' || st.caller.length > SETTLEMENT_ID_CAP || typeof st.proof !== 'string' || !PROOF_VALUES.includes(st.proof) || (st.ingress !== 'wire' && st.ingress !== 'local'))
|
|
208
|
-
return undefined;
|
|
209
|
-
const count = r.count === undefined ? 1 : r.count;
|
|
210
|
-
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1)
|
|
211
|
-
return undefined;
|
|
212
|
-
return { taskId: r.taskId, at: r.at, settler: { caller: st.caller, proof: st.proof, ingress: st.ingress }, reason: r.reason, head: r.head.slice(0, SUMMARY_CAP), enforced: r.enforced, count };
|
|
213
|
-
}
|
|
25
|
+
import { CALLER_ENDED_PREFIX, TASK_CAP, ARCHIVE_CAP, SUMMARY_CAP, TASK_STALE_TTL_MS, ARCHIVE_ENDS, ECHO_SUMMARY, ROUTING_FACT_CAP, SETTLEMENT_ID_CAP, SETTLEMENT_REFUSAL_CAP, withSender, withAuth, routingOf, birthFacts, withRouting, withSettledBy, parseRefusal, archivedEndedBy } from "./task-ledger-rows.js";
|
|
26
|
+
export * from "./task-ledger-rows.js";
|
|
214
27
|
/**
|
|
215
28
|
* Bounded outbound-task ledger with whole-file persistence. Pure of any
|
|
216
29
|
* routing knowledge: callers {@link track} a task when a route leaves it
|
|
@@ -369,6 +182,15 @@ export class TaskLedger {
|
|
|
369
182
|
this.trimOwed();
|
|
370
183
|
this.persist();
|
|
371
184
|
}
|
|
185
|
+
/** Record an answer only from the local member that actually received this row; never settle or change counters. */
|
|
186
|
+
markAnswered(taskId, target) {
|
|
187
|
+
const row = this.tasks.find(entry => entry.taskId === taskId);
|
|
188
|
+
if (row === undefined || !(row.direction === 'inbound' || row.peer === 'local') || (row.deliveredTo ?? row.team) !== target || row.answeredAt !== undefined)
|
|
189
|
+
return false;
|
|
190
|
+
row.answeredAt = this.now();
|
|
191
|
+
this.persist();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
372
194
|
/**
|
|
373
195
|
* v0.5.23 (async-stall): whether a tracked task still owes its receipt.
|
|
374
196
|
* The async nudge consults this — a resolved or dead-lettered task
|
package/lib/tools/info.js
CHANGED
|
@@ -124,7 +124,7 @@ export function createInfoTools(deps) {
|
|
|
124
124
|
type: 'text',
|
|
125
125
|
text: value.face === 'host'
|
|
126
126
|
? `Host face on team ${String(value.node?.team ?? '?')}: routing rides the bare node team (能发即能收)`
|
|
127
|
-
: `Node ${String(value.node?.id ?? '?')}: joined=${String(value.node?.joined)}, live=${String(value.node?.live)}; roster teams: ${(value.teams?.roster ?? []).join(', ') || 'none'}; canvas: ${(value.teams?.canvas ?? []).join(', ') || 'none'}; allowlist effect: ${String(value.allowlist?.effect ?? '?')}`,
|
|
127
|
+
: `Node ${String(value.node?.name ?? '')} ${String(value.node?.id ?? '?')}: joined=${String(value.node?.joined)}, live=${String(value.node?.live)}; roster teams: ${(value.teams?.roster ?? []).join(', ') || 'none'}; canvas: ${(value.teams?.canvas ?? []).join(', ') || 'none'}; allowlist effect: ${String(value.allowlist?.effect ?? '?')}`,
|
|
128
128
|
}],
|
|
129
129
|
},
|
|
130
130
|
presentCall: () => ({ card: 'generic', title: 'A2A whoami', kind: 'other', rawInput: null }),
|
|
@@ -151,6 +151,7 @@ export function createInfoTools(deps) {
|
|
|
151
151
|
face: 'session',
|
|
152
152
|
node: {
|
|
153
153
|
id: agentId,
|
|
154
|
+
...(exec.agent !== undefined ? { name: nodeMetadataOf(exec.agent).name, ...(nodeMetadataOf(exec.agent).seatName !== undefined ? { seatName: nodeMetadataOf(exec.agent).seatName } : {}) } : {}),
|
|
154
155
|
...(joined ? { team: `${config.team}/${id8(agentId)}` } : {}),
|
|
155
156
|
joined,
|
|
156
157
|
live,
|
|
@@ -209,6 +210,7 @@ export function createInfoTools(deps) {
|
|
|
209
210
|
team: { type: 'string', required: true },
|
|
210
211
|
session: { type: 'string', required: true },
|
|
211
212
|
name: { type: 'string', required: true },
|
|
213
|
+
seatName: { type: 'string', description: 'Human-owned stable display label, never a route identity.' },
|
|
212
214
|
description: { type: 'string', required: true },
|
|
213
215
|
local: { type: 'boolean', description: 'true when the team is served by this host (loopback candidate).' },
|
|
214
216
|
origin: { type: 'string', description: 'The publishing host (node session label, LAN IP when known) — the natural grouping for fleet rows.' },
|
|
@@ -453,7 +455,7 @@ export function createInfoTools(deps) {
|
|
|
453
455
|
description: 'Read-only three-tier ledger of routed A2A tasks: pending rows still owed a receipt, rows '
|
|
454
456
|
+ 'auto-dead-lettered past the stale TTL, and the bounded archive of correlated '
|
|
455
457
|
+ '`[A2A receipt] task <task_id>` outcomes. Use it to reconcile dispatched async work instead of '
|
|
456
|
-
+ 're-routing to ask for progress.',
|
|
458
|
+
+ 're-routing to ask for progress. answeredAt means the local target answered, not that the receipt arrived; that row stays owed but no longer occupies target queue capacity.',
|
|
457
459
|
parameters: {},
|
|
458
460
|
output: {
|
|
459
461
|
// gh#198: the host's own verdict on whether this result came from outside this host, persisted on tool/result.
|
|
@@ -478,6 +480,7 @@ export function createInfoTools(deps) {
|
|
|
478
480
|
brief: { type: 'string' },
|
|
479
481
|
status: { type: 'string', required: true },
|
|
480
482
|
deadAt: { type: 'number' },
|
|
483
|
+
answeredAt: { type: 'number' },
|
|
481
484
|
direction: { type: 'string', enum: ['inbound', 'outbound'] },
|
|
482
485
|
receiptExpected: { type: 'boolean' },
|
|
483
486
|
forwarded: { type: 'boolean' },
|
|
@@ -571,11 +574,12 @@ export function createInfoTools(deps) {
|
|
|
571
574
|
ok: true,
|
|
572
575
|
// Project the declared fields only: ledger rows also carry internal audit facts (the #114 auth record,
|
|
573
576
|
// #139 peerKey/peerHost) the output schema does not admit — spreading the row failed every call (0.6.32+).
|
|
574
|
-
tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
|
|
577
|
+
tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, answeredAt, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
|
|
575
578
|
taskId, team, peer, startedAt, status,
|
|
576
579
|
...(contextId !== undefined ? { contextId } : {}),
|
|
577
580
|
...(brief !== undefined ? { brief } : {}),
|
|
578
581
|
...(deadAt !== undefined ? { deadAt } : {}),
|
|
582
|
+
...(answeredAt !== undefined ? { answeredAt } : {}),
|
|
579
583
|
...(direction !== undefined ? { direction } : {}),
|
|
580
584
|
...(receiptExpected !== undefined ? { receiptExpected } : {}),
|
|
581
585
|
...(forwarded !== undefined ? { forwarded } : {}),
|
|
@@ -45,6 +45,7 @@ export interface A2aClientOptions {
|
|
|
45
45
|
readonly originClaim?: (target: {
|
|
46
46
|
readonly url: string;
|
|
47
47
|
readonly method: string;
|
|
48
|
+
readonly body?: string;
|
|
48
49
|
}) => Record<string, string>;
|
|
49
50
|
/** Delayed-callback seam (HTTP timeouts). */
|
|
50
51
|
readonly schedule: A2aSchedule;
|
|
@@ -141,6 +141,7 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
|
|
|
141
141
|
readonly unboundInboundCount: number;
|
|
142
142
|
readonly boundMissInboundCount: number;
|
|
143
143
|
readonly ownCardUrl: string;
|
|
144
|
+
ownerSeatNames: Record<string, string>;
|
|
144
145
|
ownerAllowlist: string[];
|
|
145
146
|
ownerTeamScopeInfo: boolean;
|
|
146
147
|
ownerAnnounceRestartPending: boolean;
|
|
@@ -44,7 +44,9 @@ export interface CanvasNotifyDeps {
|
|
|
44
44
|
readonly canvasStore: CanvasStore;
|
|
45
45
|
readonly cardCache: Map<string, CardCacheEntry>;
|
|
46
46
|
readonly originClaimHeaders: (target: OriginClaimTarget) => Record<string, string>;
|
|
47
|
-
readonly verifyOriginClaim: (req: IncomingMessage
|
|
47
|
+
readonly verifyOriginClaim: (req: IncomingMessage, options?: {
|
|
48
|
+
readonly rawBody?: Uint8Array;
|
|
49
|
+
}) => Promise<{
|
|
48
50
|
ok: true;
|
|
49
51
|
caller: string;
|
|
50
52
|
callerUrl: string;
|
|
@@ -128,6 +128,7 @@ export interface WireDeps {
|
|
|
128
128
|
readonly idempotencyStore: IdempotencyStore;
|
|
129
129
|
readonly verifyOriginClaim: (req: IncomingMessage, options?: {
|
|
130
130
|
readonly cli?: boolean;
|
|
131
|
+
readonly rawBody?: Uint8Array;
|
|
131
132
|
}) => Promise<OriginClaimResult>;
|
|
132
133
|
readonly forwardDirect: (request: ForwardRequest) => Promise<A2aRouteResult>;
|
|
133
134
|
readonly remoteCliProof: (handle: string, claim: Extract<OriginClaimResult, {
|