@nelsonlongxiang/dsh-open-a2a-net 0.6.34 → 0.6.36
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 +2 -2
- package/README_zh.md +2 -2
- package/assets/nexusDist/assets/index-DIQNDBy6.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/cli/a2a.mjs +3 -1
- package/lib/canvas-store.js +23 -3
- package/lib/client.js +63 -63
- package/lib/faces/admin.js +6 -3
- package/lib/faces/wire.js +2 -24
- package/lib/flood-guard.js +7 -1
- package/lib/index.js +13 -2
- package/lib/queue-guard.js +3 -3
- package/lib/response-fields.js +27 -0
- package/lib/skill/references/cli.md +6 -0
- package/lib/skill/references/pitfalls.md +4 -0
- package/lib/skill/references/routing.md +8 -0
- package/lib/task-ledger.js +12 -1
- package/lib/tools/info.js +4 -2
- package/lib/types/canvas-store.d.ts +5 -0
- package/lib/types/flood-guard.d.ts +5 -0
- package/lib/types/queue-guard.d.ts +3 -2
- package/lib/types/response-fields.d.ts +3 -0
- package/lib/types/task-ledger.d.ts +6 -0
- package/nexus-stage/scripts/mockhost.py +14 -6
- package/nexus-stage/src/planning-card.ts +5 -3
- package/nexus-stage/src/planning-view.ts +8 -2
- package/nexus-stage/src/workbench.ts +6 -0
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-D2BGWOeU.js +0 -9
package/lib/client.js
CHANGED
|
@@ -22,8 +22,8 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
return n;
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
-
//#region \0dsh-css:D:\workspace\dsh-open-a2a-net\tmp\release-0.6.
|
|
26
|
-
const css = ".
|
|
25
|
+
//#region \0dsh-css:D:\workspace\dsh-open-a2a-net\tmp\release-0.6.36-51b18cc\src\src\client\A2aControl.module.css.mjs
|
|
26
|
+
const css = ".KfHpVG_wrap{max-width:100%;display:inline-flex;position:relative}.KfHpVG_trigger{align-items:center;gap:4px;display:inline-flex;position:relative}.KfHpVG_unreadBadge{text-align:center;color:#fff;background:var(--dsh-accent,#4c8bf5);border-radius:7px;min-width:14px;height:14px;padding:0 3px;font-size:9px;line-height:14px;animation:2s ease-in-out infinite KfHpVG_badgePulse;position:absolute;top:-6px;right:-8px}@keyframes KfHpVG_badgePulse{0%,to{box-shadow:0 0 #4c8bf573}50%{box-shadow:0 0 0 4px #4c8bf500}}@media (prefers-reduced-motion:reduce){.KfHpVG_unreadBadge{animation:none}}.KfHpVG_popover{z-index:1100;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface-raised,#1c1c22);border-radius:10px;flex-direction:column;gap:8px;width:320px;max-width:calc(100vw - 16px);max-height:60vh;padding:12px;display:flex;position:fixed;overflow-y:auto;box-shadow:0 8px 24px #00000059}.KfHpVG_title{font-size:13px;font-weight:600}.KfHpVG_sectionTitle{border-top:1px solid var(--dsh-border,#2a2a30);text-transform:uppercase;letter-spacing:.04em;opacity:.65;margin-top:4px;padding-top:8px;font-size:11px;font-weight:600}.KfHpVG_stageRow{border-top:1px solid var(--dsh-border,#2a2a30);align-items:center;gap:8px;margin-top:4px;padding-top:8px;display:flex}.KfHpVG_stageTitle{text-transform:uppercase;letter-spacing:.04em;opacity:.65;font-size:11px;font-weight:600}.KfHpVG_stageLinks{gap:6px;display:flex}.KfHpVG_stageLink{border:1px solid var(--dsh-border,#2a2a30);color:inherit;border-radius:999px;align-items:center;padding:3px 8px;font-size:11px;text-decoration:none;display:inline-flex}.KfHpVG_stageLink:hover,.KfHpVG_stageLink:focus-visible{border-color:var(--dsh-accent,#4c8bf5)}.KfHpVG_empty{opacity:.7;font-size:12px}.KfHpVG_row{border:1px solid #0000;border-radius:8px;align-items:center;gap:10px;padding:8px;display:flex}.KfHpVG_joined{border-color:var(--dsh-border,#2a2a30)}.KfHpVG_facts{text-align:left;min-width:0;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-radius:6px;flex-direction:column;flex:1;gap:2px;padding:2px 4px;display:flex}.KfHpVG_facts:hover{background:var(--dsh-surface-hover,#ffffff0f)}.KfHpVG_nameRow{align-items:center;gap:6px;display:flex}.KfHpVG_stateDot{border-radius:50%;flex:none;width:7px;height:7px}.KfHpVG_stateDot.KfHpVG_live{background:var(--dsh-accent,#4c8bf5)}.KfHpVG_stateDot.KfHpVG_live.KfHpVG_joined{background:var(--dsh-success,#3fb96f)}.KfHpVG_stateDot.KfHpVG_cold{background:var(--dsh-warning,#d9a13c)}.KfHpVG_name{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.KfHpVG_description{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.KfHpVG_team{opacity:.55;font-size:11px;font-family:var(--dsh-mono,monospace)}.KfHpVG_note{opacity:.6;font-size:11px}.KfHpVG_searchInput{box-sizing:border-box;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);width:100%;color:inherit;border-radius:6px;padding:5px 8px;font-size:12px}.KfHpVG_searchInput:focus{outline:1px solid var(--dsh-accent,#4c8bf5)}.KfHpVG_sessionGroup{flex-direction:column;gap:2px;display:flex}.KfHpVG_groupHead{color:inherit;font:inherit;opacity:.75;cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;gap:6px;padding:4px 2px;font-size:11px;font-weight:600;display:flex}.KfHpVG_groupChevron{transition:transform .12s;display:inline-block}.KfHpVG_groupChevronClosed{transform:rotate(-90deg)}.KfHpVG_groupTag{text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--dsh-border,#2a2a30);opacity:.8;border-radius:999px;flex:none;max-width:90px;padding:0 6px;font-size:10px;line-height:16px;overflow:hidden}.KfHpVG_groupButton{width:24px;height:24px;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;flex:none;font-size:12px;line-height:1}.KfHpVG_groupButton:hover{border-color:var(--dsh-border,#2a2a30);opacity:1}.KfHpVG_groupPicker{border:1px dashed var(--dsh-border,#2a2a30);border-radius:8px;flex-wrap:wrap;gap:6px;width:100%;padding:8px;display:flex;position:relative}.KfHpVG_groupOption{border:1px solid var(--dsh-border,#2a2a30);color:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11px}.KfHpVG_groupOptionActive{border-color:var(--dsh-accent,#4c8bf5);color:var(--dsh-accent,#4c8bf5)}.KfHpVG_groupNew{align-items:center;gap:6px;width:100%;margin-top:2px;display:flex}.KfHpVG_groupInput{border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);min-width:0;color:inherit;border-radius:6px;flex:1;padding:4px 8px;font-size:12px}.KfHpVG_peerGrid{flex-wrap:wrap;gap:6px;display:flex}.KfHpVG_peerChip{border:1px solid var(--dsh-border,#2a2a30);border-radius:999px;align-items:center;gap:6px;padding:3px 8px;font-size:11px;display:inline-flex}.KfHpVG_peerHost{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.KfHpVG_peerScore{font-family:var(--dsh-mono,monospace);opacity:.6}.KfHpVG_activityList{flex-direction:column;gap:2px;display:flex}.KfHpVG_activityRow{border-radius:4px;align-items:center;gap:8px;padding:2px 4px;font-size:11px;display:flex}.KfHpVG_activityRow[data-ok=false]{opacity:.55}.KfHpVG_activityDir{flex:none;font-weight:600}.KfHpVG_activityDir.KfHpVG_in{color:var(--dsh-success,#3fb96f)}.KfHpVG_activityDir.KfHpVG_out{color:var(--dsh-accent,#4c8bf5)}.KfHpVG_activityTeam{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--dsh-mono,monospace);opacity:.8;flex:1;overflow:hidden}.KfHpVG_activityPeer{text-overflow:ellipsis;white-space:nowrap;opacity:.55;max-width:90px;overflow:hidden}.KfHpVG_activityTime{opacity:.5;font-family:var(--dsh-mono,monospace);flex:none}.KfHpVG_activityRow[data-jump=true]{cursor:pointer}.KfHpVG_activityRow[data-jump=true]:hover{background:var(--dsh-surface-hover,#ffffff0f)}.KfHpVG_fresh{animation:1.2s ease-out 3 KfHpVG_freshFlash}@keyframes KfHpVG_freshFlash{0%{background:#4c8bf538}to{background:0 0}}@media (prefers-reduced-motion:reduce){.KfHpVG_fresh{background:#4c8bf51f;animation:none}}.KfHpVG_inFlightList{flex-direction:column;gap:2px;margin-bottom:4px;display:flex}.KfHpVG_inFlightRow{border:1px dashed var(--dsh-border,#2a2a30);border-radius:4px;align-items:center;gap:8px;padding:3px 4px;font-size:11px;display:flex}.KfHpVG_inFlightPulse{background:var(--dsh-accent,#4c8bf5);border-radius:50%;flex:none;width:7px;height:7px;animation:1.1s ease-in-out infinite KfHpVG_flightPulse}@keyframes KfHpVG_flightPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.55)}}@media (prefers-reduced-motion:reduce){.KfHpVG_inFlightPulse{animation:none}}.KfHpVG_versionTag{color:var(--dsh-color-text-tertiary,#888);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;margin-left:auto;padding:1px 6px;font-size:10px;font-weight:500}.KfHpVG_inFlightStale{opacity:.55}.KfHpVG_canvasCreate{align-items:center;gap:6px;margin-bottom:4px;display:flex}.KfHpVG_canvasCreate .KfHpVG_groupInput{flex:1;min-width:0}.KfHpVG_canvasChip{max-width:100%;color:var(--dsh-color-text-secondary,#aaa);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;align-items:center;gap:4px;margin:2px 4px 2px 0;padding:1px 6px;font-size:11px;display:inline-flex}.KfHpVG_canvasChip>span:not([class]){white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.KfHpVG_canvasChipRemove{cursor:pointer;min-width:24px;min-height:24px;color:var(--dsh-color-text-tertiary,#888);background:0 0;border:none;padding:2px 5px;font-size:11px;line-height:1}.KfHpVG_canvasChipRemove:hover{color:var(--dsh-color-text-primary,#eee)}.KfHpVG_canvasChipRemove{transition:color .14s ease-out}@media (prefers-reduced-motion:reduce){.KfHpVG_canvasChipRemove{transition:none}}.KfHpVG_canvasChipRemove:focus-visible{outline:2px solid var(--dsh-color-primary,#4f8cff);outline-offset:1px;border-radius:4px}.KfHpVG_canvasActions{gap:6px;margin-top:4px;display:flex}.KfHpVG_inFlightStale .KfHpVG_inFlightPulse{opacity:.4;animation:none}.KfHpVG_failMark{color:#f87171;flex-shrink:0;font-size:11px}.KfHpVG_staleNote{color:var(--dsh-text-muted,#94a3b8b3);padding:4px 8px;font-size:11px}";
|
|
27
27
|
const tagId = "@nelsonlongxiang/dsh-open-a2a-net/A2aControl.module.css";
|
|
28
28
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
29
29
|
const tag = document.createElement("style");
|
|
@@ -33,67 +33,67 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
document.head.appendChild(tag);
|
|
34
34
|
}
|
|
35
35
|
var A2aControl_module_css_default = {
|
|
36
|
-
"activityDir": "
|
|
37
|
-
"activityList": "
|
|
38
|
-
"activityPeer": "
|
|
39
|
-
"activityRow": "
|
|
40
|
-
"activityTeam": "
|
|
41
|
-
"activityTime": "
|
|
42
|
-
"badgePulse": "
|
|
43
|
-
"canvasActions": "
|
|
44
|
-
"canvasChip": "
|
|
45
|
-
"canvasChipRemove": "
|
|
46
|
-
"canvasCreate": "
|
|
47
|
-
"cold": "
|
|
48
|
-
"description": "
|
|
49
|
-
"empty": "
|
|
50
|
-
"facts": "
|
|
51
|
-
"failMark": "
|
|
52
|
-
"flightPulse": "
|
|
53
|
-
"fresh": "
|
|
54
|
-
"freshFlash": "
|
|
55
|
-
"groupButton": "
|
|
56
|
-
"groupChevron": "
|
|
57
|
-
"groupChevronClosed": "
|
|
58
|
-
"groupHead": "
|
|
59
|
-
"groupInput": "
|
|
60
|
-
"groupNew": "
|
|
61
|
-
"groupOption": "
|
|
62
|
-
"groupOptionActive": "
|
|
63
|
-
"groupPicker": "
|
|
64
|
-
"groupTag": "
|
|
65
|
-
"in": "
|
|
66
|
-
"inFlightList": "
|
|
67
|
-
"inFlightPulse": "
|
|
68
|
-
"inFlightRow": "
|
|
69
|
-
"inFlightStale": "
|
|
70
|
-
"joined": "
|
|
71
|
-
"live": "
|
|
72
|
-
"name": "
|
|
73
|
-
"nameRow": "
|
|
74
|
-
"note": "
|
|
75
|
-
"out": "
|
|
76
|
-
"peerChip": "
|
|
77
|
-
"peerGrid": "
|
|
78
|
-
"peerHost": "
|
|
79
|
-
"peerScore": "
|
|
80
|
-
"popover": "
|
|
81
|
-
"row": "
|
|
82
|
-
"searchInput": "
|
|
83
|
-
"sectionTitle": "
|
|
84
|
-
"sessionGroup": "
|
|
85
|
-
"stageLink": "
|
|
86
|
-
"stageLinks": "
|
|
87
|
-
"stageRow": "
|
|
88
|
-
"stageTitle": "
|
|
89
|
-
"staleNote": "
|
|
90
|
-
"stateDot": "
|
|
91
|
-
"team": "
|
|
92
|
-
"title": "
|
|
93
|
-
"trigger": "
|
|
94
|
-
"unreadBadge": "
|
|
95
|
-
"versionTag": "
|
|
96
|
-
"wrap": "
|
|
36
|
+
"activityDir": "KfHpVG_activityDir",
|
|
37
|
+
"activityList": "KfHpVG_activityList",
|
|
38
|
+
"activityPeer": "KfHpVG_activityPeer",
|
|
39
|
+
"activityRow": "KfHpVG_activityRow",
|
|
40
|
+
"activityTeam": "KfHpVG_activityTeam",
|
|
41
|
+
"activityTime": "KfHpVG_activityTime",
|
|
42
|
+
"badgePulse": "KfHpVG_badgePulse",
|
|
43
|
+
"canvasActions": "KfHpVG_canvasActions",
|
|
44
|
+
"canvasChip": "KfHpVG_canvasChip",
|
|
45
|
+
"canvasChipRemove": "KfHpVG_canvasChipRemove",
|
|
46
|
+
"canvasCreate": "KfHpVG_canvasCreate",
|
|
47
|
+
"cold": "KfHpVG_cold",
|
|
48
|
+
"description": "KfHpVG_description",
|
|
49
|
+
"empty": "KfHpVG_empty",
|
|
50
|
+
"facts": "KfHpVG_facts",
|
|
51
|
+
"failMark": "KfHpVG_failMark",
|
|
52
|
+
"flightPulse": "KfHpVG_flightPulse",
|
|
53
|
+
"fresh": "KfHpVG_fresh",
|
|
54
|
+
"freshFlash": "KfHpVG_freshFlash",
|
|
55
|
+
"groupButton": "KfHpVG_groupButton",
|
|
56
|
+
"groupChevron": "KfHpVG_groupChevron",
|
|
57
|
+
"groupChevronClosed": "KfHpVG_groupChevronClosed",
|
|
58
|
+
"groupHead": "KfHpVG_groupHead",
|
|
59
|
+
"groupInput": "KfHpVG_groupInput",
|
|
60
|
+
"groupNew": "KfHpVG_groupNew",
|
|
61
|
+
"groupOption": "KfHpVG_groupOption",
|
|
62
|
+
"groupOptionActive": "KfHpVG_groupOptionActive",
|
|
63
|
+
"groupPicker": "KfHpVG_groupPicker",
|
|
64
|
+
"groupTag": "KfHpVG_groupTag",
|
|
65
|
+
"in": "KfHpVG_in",
|
|
66
|
+
"inFlightList": "KfHpVG_inFlightList",
|
|
67
|
+
"inFlightPulse": "KfHpVG_inFlightPulse",
|
|
68
|
+
"inFlightRow": "KfHpVG_inFlightRow",
|
|
69
|
+
"inFlightStale": "KfHpVG_inFlightStale",
|
|
70
|
+
"joined": "KfHpVG_joined",
|
|
71
|
+
"live": "KfHpVG_live",
|
|
72
|
+
"name": "KfHpVG_name",
|
|
73
|
+
"nameRow": "KfHpVG_nameRow",
|
|
74
|
+
"note": "KfHpVG_note",
|
|
75
|
+
"out": "KfHpVG_out",
|
|
76
|
+
"peerChip": "KfHpVG_peerChip",
|
|
77
|
+
"peerGrid": "KfHpVG_peerGrid",
|
|
78
|
+
"peerHost": "KfHpVG_peerHost",
|
|
79
|
+
"peerScore": "KfHpVG_peerScore",
|
|
80
|
+
"popover": "KfHpVG_popover",
|
|
81
|
+
"row": "KfHpVG_row",
|
|
82
|
+
"searchInput": "KfHpVG_searchInput",
|
|
83
|
+
"sectionTitle": "KfHpVG_sectionTitle",
|
|
84
|
+
"sessionGroup": "KfHpVG_sessionGroup",
|
|
85
|
+
"stageLink": "KfHpVG_stageLink",
|
|
86
|
+
"stageLinks": "KfHpVG_stageLinks",
|
|
87
|
+
"stageRow": "KfHpVG_stageRow",
|
|
88
|
+
"stageTitle": "KfHpVG_stageTitle",
|
|
89
|
+
"staleNote": "KfHpVG_staleNote",
|
|
90
|
+
"stateDot": "KfHpVG_stateDot",
|
|
91
|
+
"team": "KfHpVG_team",
|
|
92
|
+
"title": "KfHpVG_title",
|
|
93
|
+
"trigger": "KfHpVG_trigger",
|
|
94
|
+
"unreadBadge": "KfHpVG_unreadBadge",
|
|
95
|
+
"versionTag": "KfHpVG_versionTag",
|
|
96
|
+
"wrap": "KfHpVG_wrap"
|
|
97
97
|
};
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region lib/client/A2aControl.js
|
package/lib/faces/admin.js
CHANGED
|
@@ -152,7 +152,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
152
152
|
rows.push({ id: localId, team: `${config.team}/${id8(localId)}`, joined: sessionNodes.has(localId) || joinedSessions.has(localId), live: sessionNodes.has(localId) });
|
|
153
153
|
continue;
|
|
154
154
|
}
|
|
155
|
-
rows.push({ id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id) } : {}) });
|
|
155
|
+
rows.push({ id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id), ...(canvasStore.wasCliHomeCleared(id) ? { cliHomeCleared: true } : {}) } : {}) });
|
|
156
156
|
continue;
|
|
157
157
|
}
|
|
158
158
|
if (seenLocal.has(id))
|
|
@@ -202,6 +202,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
202
202
|
.map(task => ({
|
|
203
203
|
taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, status: task.status,
|
|
204
204
|
direction: task.direction ?? 'outbound', receiptExpected: task.receiptExpected !== false,
|
|
205
|
+
...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}),
|
|
205
206
|
// The dispatch brief rides every row: a task id and a team
|
|
206
207
|
// say nothing about WHAT is owed — supervision cannot tell
|
|
207
208
|
// a nudge from an abandon without it.
|
|
@@ -220,7 +221,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
220
221
|
tasksStaleCount: taskLedger.list().filter(task => task.status === 'pending' && (task.receiptExpected === false || Date.now() - task.startedAt >= 60 * 60_000)).length,
|
|
221
222
|
tasksDead: taskLedger.list()
|
|
222
223
|
.filter(task => task.status === 'dead')
|
|
223
|
-
.map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.brief !== undefined ? { brief: task.brief } : {}), ...(task.direction !== undefined ? { direction: task.direction } : {}), ...(task.sender !== undefined ? { sender: task.sender } : {}), ...(task.auth !== undefined ? { auth: task.auth } : task.direction === 'inbound' ? { authLegacy: true } : {}) })),
|
|
224
|
+
.map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}), ...(task.brief !== undefined ? { brief: task.brief } : {}), ...(task.direction !== undefined ? { direction: task.direction } : {}), ...(task.sender !== undefined ? { sender: task.sender } : {}), ...(task.auth !== undefined ? { auth: task.auth } : task.direction === 'inbound' ? { authLegacy: true } : {}) })),
|
|
224
225
|
// gh#31: per-task outcomes for rows that LEFT the ledger — the
|
|
225
226
|
// chat drawer used to guess (settled, abandoned or cancelled).
|
|
226
227
|
// Bounded to the archive (≤64), newest first, with the
|
|
@@ -243,6 +244,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
243
244
|
peer: row.peer,
|
|
244
245
|
startedAt: row.startedAt,
|
|
245
246
|
resolvedAt: row.resolvedAt,
|
|
247
|
+
...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
|
|
246
248
|
outcome: row.outcome ?? 'unclassified',
|
|
247
249
|
endedBy: archivedEndedBy(row),
|
|
248
250
|
...(row.direction !== undefined ? { direction: row.direction } : {}),
|
|
@@ -469,8 +471,9 @@ export function registerAdminFaces(webServer, d) {
|
|
|
469
471
|
return '';
|
|
470
472
|
} })();
|
|
471
473
|
const out = name === config.team ? { ok: false, error: `a CLI identity may not be named after this host's zone "${config.team}" — choose another agent name` } : cliDeposits.deposit(card);
|
|
474
|
+
const homeCleared = out.ok && canvasStore.clearCliHomeForLocal(out.handle);
|
|
472
475
|
payload = JSON.stringify(out.ok
|
|
473
|
-
? { ok: true, handle: out.handle, expiresAt: out.expiresAt, cardUrl: `${d.ownCardUrl}/a2a/cli/${encodeURIComponent(out.handle)}` }
|
|
476
|
+
? { ok: true, handle: out.handle, expiresAt: out.expiresAt, ...(homeCleared ? { homeCleared: true } : {}), cardUrl: `${d.ownCardUrl}/a2a/cli/${encodeURIComponent(out.handle)}` }
|
|
474
477
|
: { ok: false, error: out.error });
|
|
475
478
|
}
|
|
476
479
|
else if (action === 'revoke') {
|
package/lib/faces/wire.js
CHANGED
|
@@ -7,6 +7,7 @@ import { briefOf } from "../task-ledger.js";
|
|
|
7
7
|
import { parseReceipt, taskIdRefusal } from "../receipt.js";
|
|
8
8
|
import { buildAuthorization, claimCovers, refusalOf } from "../task-auth.js";
|
|
9
9
|
import { isRootOriginUrl } from "../origin-claim.js";
|
|
10
|
+
import { withResponseFields } from "../response-fields.js";
|
|
10
11
|
import { WIRE_ERROR_PAYLOAD_TOO_LARGE, withinRouteBodyCap } from "../transport-caps.js";
|
|
11
12
|
/**
|
|
12
13
|
* #139 (design §4.4): add `settlement: {status:'refused', reason, task_id}`
|
|
@@ -16,30 +17,7 @@ import { WIRE_ERROR_PAYLOAD_TOO_LARGE, withinRouteBodyCap } from "../transport-c
|
|
|
16
17
|
* body is sent unchanged.
|
|
17
18
|
*/
|
|
18
19
|
export function withSettlementField(res, settlement) {
|
|
19
|
-
|
|
20
|
-
const end = res.end.bind(res);
|
|
21
|
-
let head;
|
|
22
|
-
res.writeHead = (status, headers) => {
|
|
23
|
-
head = { status, headers: { ...(headers ?? {}) } };
|
|
24
|
-
return res;
|
|
25
|
-
};
|
|
26
|
-
res.end = (body) => {
|
|
27
|
-
let out = body;
|
|
28
|
-
if (typeof body === 'string') {
|
|
29
|
-
try {
|
|
30
|
-
const parsed = JSON.parse(body);
|
|
31
|
-
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
32
|
-
out = JSON.stringify({ ...parsed, settlement });
|
|
33
|
-
}
|
|
34
|
-
catch { /* not JSON: unchanged */ }
|
|
35
|
-
}
|
|
36
|
-
if (head !== undefined) {
|
|
37
|
-
if (out !== undefined && 'Content-Length' in head.headers)
|
|
38
|
-
head.headers['Content-Length'] = Buffer.byteLength(out);
|
|
39
|
-
writeHead(head.status, head.headers);
|
|
40
|
-
}
|
|
41
|
-
return end(out);
|
|
42
|
-
};
|
|
20
|
+
withResponseFields(res, { settlement });
|
|
43
21
|
}
|
|
44
22
|
/** The origin of a card URL for the authorization record; the raw text, bounded, when it does not parse. */
|
|
45
23
|
const originOf = (url) => {
|
package/lib/flood-guard.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { withResponseFields } from "./response-fields.js";
|
|
2
|
+
export const FILE_CHANNEL_ADVICE = { reason: 'use-file-channel', message: 'Files must use the file channel, not session messages: a2a_file_send { paths, team }; CLI: a2a-net file send <path…> <team>.' };
|
|
3
|
+
export const fileChannelAdvice = (message) => message.includes('[JPFILE-PART') ? FILE_CHANNEL_ADVICE : undefined;
|
|
1
4
|
export const WIRE_ERROR_RATE_LIMITED = -32008;
|
|
2
5
|
export const MAX_PARTS_PER_TRANSFER = 16;
|
|
3
6
|
// Matches the current L9 threshold; per-sender across ALL targets is stricter.
|
|
@@ -92,10 +95,13 @@ export class FloodGuard {
|
|
|
92
95
|
export function createFloodGuard(outcomes, recordActivity) {
|
|
93
96
|
const guard = new FloodGuard();
|
|
94
97
|
return (caller, message, team, taskId, res) => {
|
|
98
|
+
const advice = fileChannelAdvice(message);
|
|
99
|
+
if (advice !== undefined)
|
|
100
|
+
withResponseFields(res, { advice });
|
|
95
101
|
const reason = guard.check(caller, message);
|
|
96
102
|
if (reason === undefined)
|
|
97
103
|
return false;
|
|
98
|
-
const error = 'flood-guard: ' + reason + ' (gh#91): file data must use an independent channel; no delivery occurred';
|
|
104
|
+
const error = 'flood-guard: ' + reason + ' (gh#91): file data must use an independent channel; no delivery occurred' + (advice === undefined ? '' : '; use-file-channel: ' + advice.message);
|
|
99
105
|
outcomes.recordOutcome(taskId, { status: 'failed', error });
|
|
100
106
|
recordActivity('in', team, caller, false);
|
|
101
107
|
const payload = JSON.stringify({ error, reason, code: WIRE_ERROR_RATE_LIMITED, task_id: taskId, team, delivered: false, task_status: 'TASK_STATE_FAILED' });
|
package/lib/index.js
CHANGED
|
@@ -762,7 +762,10 @@ export function apply(ctx, config) {
|
|
|
762
762
|
// message that still echoes a pending task id means the peer
|
|
763
763
|
// demonstrably responded to it, which settles the debt even when the
|
|
764
764
|
// receipt formality never rode the reply.
|
|
765
|
-
|
|
765
|
+
// #125 (owner data 2026-11-03: two production rows settled by a relay and by another task's receipt):
|
|
766
|
+
// an echo is weak evidence, so under observe it is judged as enforce — only an on-chain settler's echo
|
|
767
|
+
// settles. Formal receipt headers keep observing; settlementAuth 'off' still judges nothing.
|
|
768
|
+
const echoed = taskLedger.resolveEcho(message, judge !== undefined && !judge.enforce ? { ...judge, enforce: true } : judge);
|
|
766
769
|
if (echoed.length > 0) {
|
|
767
770
|
logger.info(`a2a: ${String(echoed.length)} pending task(s) settled by content echo (${echoed.join(', ')})`);
|
|
768
771
|
}
|
|
@@ -2022,6 +2025,7 @@ export function apply(ctx, config) {
|
|
|
2022
2025
|
// placeholder as one on the same-node loop-back.
|
|
2023
2026
|
if (placeholder === true)
|
|
2024
2027
|
return;
|
|
2028
|
+
taskLedger.markAnswered(taskId, sessionTeamOf(target));
|
|
2025
2029
|
// Idempotency: a row THIS ledger tracks must still be pending
|
|
2026
2030
|
// (settled/dead rows never re-send). A foreign task id (an inbound
|
|
2027
2031
|
// route the caller tracks on ITS node) has no local row — the final
|
|
@@ -3314,6 +3318,10 @@ export function apply(ctx, config) {
|
|
|
3314
3318
|
// node team when it has joined, else the node label), so the receiver
|
|
3315
3319
|
// can answer with one a2a_route instead of an unroutable display label.
|
|
3316
3320
|
a2aJoinGateRefusal(agent === undefined ? undefined : { agent });
|
|
3321
|
+
// A target-authored receipt attempt answers its own work even if scope/transport later refuses delivery.
|
|
3322
|
+
const answer = parseReceipt(args.message);
|
|
3323
|
+
if (agent !== undefined && answer !== null)
|
|
3324
|
+
taskLedger.markAnswered(answer.taskId, sessionTeamOf(agent));
|
|
3317
3325
|
// S3 (team-scoped routing, phase-2 default ON per the 2026-09-01 owner
|
|
3318
3326
|
// ruling; set false to restore the open outbound seam): a session
|
|
3319
3327
|
// caller must have declared membership in the target team before it
|
|
@@ -3814,7 +3822,8 @@ export function apply(ctx, config) {
|
|
|
3814
3822
|
async function dispatchAnywhere(team, message, callerSession, contextId, signal, asyncMode, taskId, peerCaller) {
|
|
3815
3823
|
// A local online household is an owner-established fact and receives locally.
|
|
3816
3824
|
// Otherwise only the pinned home may receive; never fall back to directory claims.
|
|
3817
|
-
|
|
3825
|
+
const cliReceipt = CLI_HANDLE_RE.test(team) && parseReceipt(message) !== null;
|
|
3826
|
+
if (cliReceipt && cliDeposits.get(team) === undefined) {
|
|
3818
3827
|
const home = canvasStore.cliHomeOf(team);
|
|
3819
3828
|
if (home === undefined)
|
|
3820
3829
|
throw new Error(`CLI receipt home is not pinned for ${team}`);
|
|
@@ -3832,6 +3841,8 @@ export function apply(ctx, config) {
|
|
|
3832
3841
|
throw new Error(local.error);
|
|
3833
3842
|
return local;
|
|
3834
3843
|
}
|
|
3844
|
+
if (cliReceipt)
|
|
3845
|
+
throw new Error(`local CLI household expired before delivery to ${team}; directory fallback refused`);
|
|
3835
3846
|
const failures = [];
|
|
3836
3847
|
const candidates = await directoryPeerCandidates(memoizedCardFetch(), team, failures);
|
|
3837
3848
|
if (candidates.length === 0) {
|
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) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Add structured fields to any JSON response exit and recompute its Content-Length. */
|
|
2
|
+
export function withResponseFields(res, fields) {
|
|
3
|
+
const writeHead = res.writeHead.bind(res);
|
|
4
|
+
const end = res.end.bind(res);
|
|
5
|
+
let head;
|
|
6
|
+
res.writeHead = (status, headers) => {
|
|
7
|
+
head = { status, headers: { ...(headers ?? {}) } };
|
|
8
|
+
return res;
|
|
9
|
+
};
|
|
10
|
+
res.end = (body) => {
|
|
11
|
+
let out = body;
|
|
12
|
+
if (typeof body === 'string') {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(body);
|
|
15
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
16
|
+
out = JSON.stringify({ ...parsed, ...fields });
|
|
17
|
+
}
|
|
18
|
+
catch { /* not JSON: unchanged */ }
|
|
19
|
+
}
|
|
20
|
+
if (head !== undefined) {
|
|
21
|
+
if (out !== undefined && 'Content-Length' in head.headers)
|
|
22
|
+
head.headers['Content-Length'] = Buffer.byteLength(out);
|
|
23
|
+
writeHead(head.status, head.headers);
|
|
24
|
+
}
|
|
25
|
+
return end(out);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -42,8 +42,14 @@ 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
|
+
## Tasks: ledger and host observability
|
|
46
|
+
|
|
47
|
+
`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.
|
|
48
|
+
|
|
45
49
|
## Files (#91 file channel)
|
|
46
50
|
|
|
51
|
+
Files do not go through session messages: use the file channel below, never JPFILE-PART/base64 chunks in `route`. Legacy part replies carry `advice.reason: use-file-channel` with the send commands; up to 16 compatible parts are still allowed, while over-limit transfers keep `-32008` / `file-parts-exceeded`. The hint alone does not mean rejection.
|
|
52
|
+
|
|
47
53
|
- `a2a-net file send <path…> <team> [--note TEXT]` stages each file (≤64 MiB,
|
|
48
54
|
≤16 per send) on the home host by sha256, granted to `<team>` for 24h, and
|
|
49
55
|
routes ONE message with one `[A2A file] v1 sha256=… size=… name=… from=<home>`
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
- **adopt-style revive only works for previously materialized sessions** —
|
|
10
10
|
it is an idempotent revive, not a creation path.
|
|
11
11
|
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
- **Files do not go through session messages.** Use `a2a_file_send { paths, team }` (up to 16 files, 64 MiB each), then `a2a_file_fetch { ref }` on the received `[A2A file] v1 …` reference. CLI: `a2a-net file send <path…> <team>` and `a2a-net file fetch "<reference line>"`. Do not base64/chunk files into JPFILE-PART messages. During compatibility rollout, accepted legacy parts carry `advice.reason: use-file-channel`; over-limit parts still fail with `-32008` / `file-parts-exceeded` and the same advice. Do not resend rejected parts in a loop.
|
|
15
|
+
|
|
12
16
|
## Routing
|
|
13
17
|
|
|
14
18
|
- **Do not blindly re-dispatch after a sync timeout.** -32005 means the
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Routing deep-dive
|
|
2
2
|
|
|
3
|
+
## Files use the file channel
|
|
4
|
+
|
|
5
|
+
Files do not go through session messages. Send with `a2a_file_send { paths, team }` (up to 16 files, 64 MiB each), not `a2a_route` with file bytes or JPFILE-PART chunks. The receiver uses `a2a_file_fetch { ref }` with the `[A2A file] v1 …` line. CLI: `a2a-net file send <path…> <team>` / `a2a-net file fetch "<reference line>"`. One reference message carries the transfer; ordinary routing and receipt rules below apply to that message. Legacy parts still allowed by the transitional cap receive `advice.reason: use-file-channel`; this hint is not a refusal or a completion receipt.
|
|
6
|
+
|
|
3
7
|
## The receipt contract
|
|
4
8
|
|
|
5
9
|
Every async route (`wait:false`) registers the task on the peer and returns
|
|
@@ -36,6 +40,10 @@ polling `a2a_teams` cannot see another host's ledger.
|
|
|
36
40
|
when known. `archivedCount` and `archivedSplit` (count per outcome) cover
|
|
37
41
|
the whole archive.
|
|
38
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
|
+
|
|
39
47
|
## Idempotency
|
|
40
48
|
|
|
41
49
|
A route carries a fingerprint (caller + message + team + wait-mode). Re-
|
package/lib/task-ledger.js
CHANGED
|
@@ -117,9 +117,10 @@ function withAuth(raw, entry, now) {
|
|
|
117
117
|
// #139: an outbound row carries only its local-candidate admission.
|
|
118
118
|
return auth.ingress === 'local' && entry.peer === 'local' ? { auth } : {};
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
120
|
+
/** Routing and answer facts copied unchanged between a row and its archive entry. */
|
|
121
121
|
function routingOf(row) {
|
|
122
122
|
return {
|
|
123
|
+
...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
|
|
123
124
|
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
124
125
|
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
125
126
|
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
@@ -152,6 +153,7 @@ function withRouting(entry) {
|
|
|
152
153
|
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
153
154
|
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
154
155
|
return {
|
|
156
|
+
...(typeof entry.answeredAt === 'number' && Number.isFinite(entry.answeredAt) && entry.answeredAt >= 0 ? { answeredAt: entry.answeredAt } : {}),
|
|
155
157
|
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
156
158
|
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
157
159
|
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
@@ -369,6 +371,15 @@ export class TaskLedger {
|
|
|
369
371
|
this.trimOwed();
|
|
370
372
|
this.persist();
|
|
371
373
|
}
|
|
374
|
+
/** Record an answer only from the local member that actually received this row; never settle or change counters. */
|
|
375
|
+
markAnswered(taskId, target) {
|
|
376
|
+
const row = this.tasks.find(entry => entry.taskId === taskId);
|
|
377
|
+
if (row === undefined || !(row.direction === 'inbound' || row.peer === 'local') || (row.deliveredTo ?? row.team) !== target || row.answeredAt !== undefined)
|
|
378
|
+
return false;
|
|
379
|
+
row.answeredAt = this.now();
|
|
380
|
+
this.persist();
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
372
383
|
/**
|
|
373
384
|
* v0.5.23 (async-stall): whether a tracked task still owes its receipt.
|
|
374
385
|
* The async nudge consults this — a resolved or dead-lettered task
|
package/lib/tools/info.js
CHANGED
|
@@ -453,7 +453,7 @@ export function createInfoTools(deps) {
|
|
|
453
453
|
description: 'Read-only three-tier ledger of routed A2A tasks: pending rows still owed a receipt, rows '
|
|
454
454
|
+ 'auto-dead-lettered past the stale TTL, and the bounded archive of correlated '
|
|
455
455
|
+ '`[A2A receipt] task <task_id>` outcomes. Use it to reconcile dispatched async work instead of '
|
|
456
|
-
+ 're-routing to ask for progress.',
|
|
456
|
+
+ '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
457
|
parameters: {},
|
|
458
458
|
output: {
|
|
459
459
|
// gh#198: the host's own verdict on whether this result came from outside this host, persisted on tool/result.
|
|
@@ -478,6 +478,7 @@ export function createInfoTools(deps) {
|
|
|
478
478
|
brief: { type: 'string' },
|
|
479
479
|
status: { type: 'string', required: true },
|
|
480
480
|
deadAt: { type: 'number' },
|
|
481
|
+
answeredAt: { type: 'number' },
|
|
481
482
|
direction: { type: 'string', enum: ['inbound', 'outbound'] },
|
|
482
483
|
receiptExpected: { type: 'boolean' },
|
|
483
484
|
forwarded: { type: 'boolean' },
|
|
@@ -571,11 +572,12 @@ export function createInfoTools(deps) {
|
|
|
571
572
|
ok: true,
|
|
572
573
|
// Project the declared fields only: ledger rows also carry internal audit facts (the #114 auth record,
|
|
573
574
|
// #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 }) => ({
|
|
575
|
+
tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, answeredAt, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
|
|
575
576
|
taskId, team, peer, startedAt, status,
|
|
576
577
|
...(contextId !== undefined ? { contextId } : {}),
|
|
577
578
|
...(brief !== undefined ? { brief } : {}),
|
|
578
579
|
...(deadAt !== undefined ? { deadAt } : {}),
|
|
580
|
+
...(answeredAt !== undefined ? { answeredAt } : {}),
|
|
579
581
|
...(direction !== undefined ? { direction } : {}),
|
|
580
582
|
...(receiptExpected !== undefined ? { receiptExpected } : {}),
|
|
581
583
|
...(forwarded !== undefined ? { forwarded } : {}),
|
|
@@ -24,6 +24,8 @@ export interface CanvasSnapshot {
|
|
|
24
24
|
}>;
|
|
25
25
|
/** Pinned CLI handle -> linked home origin (only while some canvas team holds the handle). */
|
|
26
26
|
readonly cliHome?: Readonly<Record<string, string>>;
|
|
27
|
+
/** Local publish cleared an older pin; kept for the owner inspector until membership is removed or re-pinned. */
|
|
28
|
+
readonly cliHomeCleared?: readonly string[];
|
|
27
29
|
}
|
|
28
30
|
/** Upper bounds: a canvas taxonomy, not a database. */
|
|
29
31
|
export declare const CANVAS_TEAM_CAP = 64;
|
|
@@ -53,6 +55,7 @@ export declare class CanvasStore {
|
|
|
53
55
|
/** Team entries in creation order; members in add order (routing priority). */
|
|
54
56
|
private teams;
|
|
55
57
|
private cliHome;
|
|
58
|
+
private cliHomeCleared;
|
|
56
59
|
private readonly path;
|
|
57
60
|
private readonly listeners;
|
|
58
61
|
/**
|
|
@@ -80,6 +83,8 @@ export declare class CanvasStore {
|
|
|
80
83
|
/** Frozen, operator-linked CLI home; undefined means unknown or ambiguous. */
|
|
81
84
|
cliHomeOf(handle: string): string | undefined;
|
|
82
85
|
pinnedCliHomes(): ReadonlyArray<readonly [string, string]>;
|
|
86
|
+
wasCliHomeCleared(handle: string): boolean;
|
|
87
|
+
clearCliHomeForLocal(handle: string): boolean;
|
|
83
88
|
/**
|
|
84
89
|
* The named team's remote member refs (zone-qualified team handles) in
|
|
85
90
|
* routing-priority order — the failover tier after the local live/cold
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { ServerResponse } from 'node:http';
|
|
8
8
|
import type { IdempotencyStore } from './idempotency-store.ts';
|
|
9
|
+
export declare const FILE_CHANNEL_ADVICE: {
|
|
10
|
+
readonly reason: "use-file-channel";
|
|
11
|
+
readonly message: "Files must use the file channel, not session messages: a2a_file_send { paths, team }; CLI: a2a-net file send <path…> <team>.";
|
|
12
|
+
};
|
|
13
|
+
export declare const fileChannelAdvice: (message: string) => typeof FILE_CHANNEL_ADVICE | undefined;
|
|
9
14
|
export declare const WIRE_ERROR_RATE_LIMITED = -32008;
|
|
10
15
|
export declare const MAX_PARTS_PER_TRANSFER = 16;
|
|
11
16
|
export declare const DEFAULT_RATE_PER_MINUTE = 16;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* busy session. This counts what the target already owes and refuses a NEW delivery once the
|
|
4
4
|
* count reaches the cap, visibly and before anything is steered or booked.
|
|
5
5
|
*
|
|
6
|
-
* Counted ("queued for T"): rows this host's ledger still holds pending for T that expect a
|
|
6
|
+
* Counted ("queued for T"): unanswered rows this host's ledger still holds pending for T that expect a
|
|
7
7
|
* receipt — inbound rows born at /a2a/direct (addressed to T, or delivered to T), and same-host
|
|
8
8
|
* routes (peer 'local') to T. Never refused: a receipt for a task this host's ledger holds
|
|
9
9
|
* (terminal; refusing one strands that row — a genuine receipt always returns to the host that
|
|
@@ -22,9 +22,10 @@ export interface QueueRow {
|
|
|
22
22
|
readonly status: string;
|
|
23
23
|
readonly direction?: string;
|
|
24
24
|
readonly receiptExpected?: boolean;
|
|
25
|
+
readonly answeredAt?: number;
|
|
25
26
|
readonly deliveredTo?: string;
|
|
26
27
|
}
|
|
27
|
-
/** How many receipt-owing tasks this host holds pending for `team`. */
|
|
28
|
+
/** How many receipt-owing tasks this host holds pending and unanswered for `team`. */
|
|
28
29
|
export declare function queuedFor(rows: readonly QueueRow[], team: string): number;
|
|
29
30
|
/** The refusal text when `team`'s queue is full, else undefined. `cap <= 0` disables the guard. */
|
|
30
31
|
export declare function queueFullRefusal(rows: readonly QueueRow[], team: string, message: string, cap: number, mailbox: boolean, ownTask: (taskId: string) => boolean): string | undefined;
|
|
@@ -139,6 +139,8 @@ export interface TaskRecord {
|
|
|
139
139
|
readonly peerHost?: string;
|
|
140
140
|
/** #139: the member that actually received the task (canvas / bare-zone deliveries). */
|
|
141
141
|
readonly deliveredTo?: string;
|
|
142
|
+
/** Local target has produced an answer; still owed until receipt delivery/settlement, but no longer queued. */
|
|
143
|
+
answeredAt?: number;
|
|
142
144
|
status: TaskStatus;
|
|
143
145
|
/** When the row was swept to dead-letter (absent while pending). */
|
|
144
146
|
deadAt?: number;
|
|
@@ -194,6 +196,8 @@ export interface ArchivedRecord {
|
|
|
194
196
|
readonly peerKey?: string;
|
|
195
197
|
readonly peerHost?: string;
|
|
196
198
|
readonly deliveredTo?: string;
|
|
199
|
+
/** Local answer time retained as audit metadata, independent of resolution time. */
|
|
200
|
+
readonly answeredAt?: number;
|
|
197
201
|
/** #114 S4: who settled the row (receipt and echo ends), and their chain role. */
|
|
198
202
|
readonly settledBy?: SettledBy;
|
|
199
203
|
}
|
|
@@ -377,6 +381,8 @@ export declare class TaskLedger {
|
|
|
377
381
|
* @param contextId - the delivery's conversation id, kept for follow-up routes (empty omits it).
|
|
378
382
|
*/
|
|
379
383
|
trackInbound(taskId: string, team: string, from: string, contextId?: string, receiptExpected?: boolean, brief?: string, auth?: TaskAuthorization, deliveredTo?: string): void;
|
|
384
|
+
/** Record an answer only from the local member that actually received this row; never settle or change counters. */
|
|
385
|
+
markAnswered(taskId: string, target: string): boolean;
|
|
380
386
|
/**
|
|
381
387
|
* v0.5.23 (async-stall): whether a tracked task still owes its receipt.
|
|
382
388
|
* The async nudge consults this — a resolved or dead-lettered task
|