@nelsonlongxiang/dsh-open-a2a-net 0.6.35 → 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 +1 -1
- package/README_zh.md +1 -1
- package/assets/nexusDist/assets/index-DIQNDBy6.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/lib/client.js +63 -63
- package/lib/faces/admin.js +3 -1
- package/lib/index.js +5 -0
- package/lib/queue-guard.js +3 -3
- package/lib/skill/references/routing.md +4 -0
- package/lib/task-ledger.js +12 -1
- package/lib/tools/info.js +4 -2
- package/lib/types/queue-guard.d.ts +3 -2
- 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/package.json +1 -1
- package/assets/nexusDist/assets/index-Du2E6DJL.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
|
@@ -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 } : {}),
|
package/lib/index.js
CHANGED
|
@@ -2025,6 +2025,7 @@ export function apply(ctx, config) {
|
|
|
2025
2025
|
// placeholder as one on the same-node loop-back.
|
|
2026
2026
|
if (placeholder === true)
|
|
2027
2027
|
return;
|
|
2028
|
+
taskLedger.markAnswered(taskId, sessionTeamOf(target));
|
|
2028
2029
|
// Idempotency: a row THIS ledger tracks must still be pending
|
|
2029
2030
|
// (settled/dead rows never re-send). A foreign task id (an inbound
|
|
2030
2031
|
// route the caller tracks on ITS node) has no local row — the final
|
|
@@ -3317,6 +3318,10 @@ export function apply(ctx, config) {
|
|
|
3317
3318
|
// node team when it has joined, else the node label), so the receiver
|
|
3318
3319
|
// can answer with one a2a_route instead of an unroutable display label.
|
|
3319
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));
|
|
3320
3325
|
// S3 (team-scoped routing, phase-2 default ON per the 2026-09-01 owner
|
|
3321
3326
|
// ruling; set false to restore the open outbound seam): a session
|
|
3322
3327
|
// caller must have declared membership in the target team before it
|
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) {
|
|
@@ -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-
|
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 } : {}),
|
|
@@ -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
|
|
@@ -83,11 +83,16 @@ host_prefix = 'dsh'
|
|
|
83
83
|
for _t in _BASE.get('canvas', {}).get('teams', []):
|
|
84
84
|
teams[_t['name']] = [m['id'] for m in _t.get('members', [])]
|
|
85
85
|
for _m in _t.get('members', []):
|
|
86
|
-
member_rows[_m['id']] = _m
|
|
86
|
+
member_rows[_m['id']] = _m # every member row, not only the first
|
|
87
87
|
_p = _m.get('team', '').split('/')[0]
|
|
88
|
-
if _p:
|
|
88
|
+
if _p and '/' not in _m['id']:
|
|
89
89
|
host_prefix = _p
|
|
90
|
-
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def valid_remote_ref(mid):
|
|
93
|
+
"""Mirror of canvas-store.validRemoteMemberRef: a '/'-qualified handle, no whitespace, no empty side."""
|
|
94
|
+
return (isinstance(mid, str) and '/' in mid and mid == mid.strip() and not any(c.isspace() for c in mid)
|
|
95
|
+
and not mid.startswith('/') and not mid.endswith('/') and '//' not in mid)
|
|
91
96
|
|
|
92
97
|
|
|
93
98
|
def valid_name(raw):
|
|
@@ -118,7 +123,9 @@ def build_state(demo: bool = False) -> bytes:
|
|
|
118
123
|
for mid in ids:
|
|
119
124
|
row = member_rows.get(mid)
|
|
120
125
|
if row is None:
|
|
121
|
-
|
|
126
|
+
# A remote ref renders as the real state face does: { id, team: the handle, remote: true }.
|
|
127
|
+
row = ({'id': mid, 'team': mid, 'remote': True} if '/' in mid
|
|
128
|
+
else {'id': mid, 'team': f'{host_prefix}/{mid[8:16] if mid.startswith("session-") else mid[:8]}', 'joined': True, 'live': True})
|
|
122
129
|
member_rows[mid] = row
|
|
123
130
|
members.append(row)
|
|
124
131
|
team_rows.append({'name': name, 'team': f'{host_prefix}/canvas/{name}', 'members': members})
|
|
@@ -208,8 +215,9 @@ class MockHost(SimpleHTTPRequestHandler):
|
|
|
208
215
|
elif action == 'add-member':
|
|
209
216
|
name = valid_name(body.get('name'))
|
|
210
217
|
mid = body.get('id')
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
# Dual member shape, as the real host: a joined local session id, or a remote team handle.
|
|
219
|
+
if name is None or not isinstance(mid, str) or not (mid in joined_ids or valid_remote_ref(mid)):
|
|
220
|
+
self._json(b'{"ok":false,"error":"name and a member (joined session id, or a zone-qualified remote team handle like zone/a1b2c3d4) are required"}')
|
|
213
221
|
return
|
|
214
222
|
roster = teams.setdefault(name, [])
|
|
215
223
|
if mid in roster:
|
|
@@ -43,15 +43,17 @@ export function createCardElement(id: string): HTMLDivElement {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* The right-hand chip. Peer cards show the link badge (gh#20 W-A: linked green / stranger gray, then the
|
|
46
|
-
* verified tick/cross; an absent link field from an older host renders the neutral 'peer')
|
|
47
|
-
*
|
|
46
|
+
* verified tick/cross; an absent link field from an older host renders the neutral 'peer'). A remote MEMBER card
|
|
47
|
+
* (gh#38) is one session on that host, not the host: it reads 成员, colored and ticked by its host's link
|
|
48
|
+
* (it used to fall through to 'peer' and looked like a host card). Session cards show their first membership
|
|
49
|
+
* priority `P<n>`. Writes only on change.
|
|
48
50
|
*/
|
|
49
51
|
export function paintPrio(prioEl: HTMLElement, n: WorldNode, info: { link?: 'linked' | 'stranger'; verified?: boolean } | undefined): void {
|
|
50
52
|
if (n.remote === true) {
|
|
51
53
|
const cls = info?.link === 'linked' ? 'lnk-linked' : info?.link === 'stranger' ? 'lnk-stranger' : ''
|
|
52
54
|
if (prioEl.className !== `prio mono ${cls}`.trim()) prioEl.className = `prio mono ${cls}`.trim()
|
|
53
55
|
const mark = info === undefined ? '' : info.verified === true ? '✓' : '✗'
|
|
54
|
-
const badge = (info?.link ?? 'peer') + mark
|
|
56
|
+
const badge = (n.id.startsWith('member-') ? '成员' : info?.link ?? 'peer') + mark
|
|
55
57
|
if (prioEl.textContent !== badge) prioEl.textContent = badge
|
|
56
58
|
} else {
|
|
57
59
|
if (prioEl.className !== 'prio mono') prioEl.className = 'prio mono'
|
|
@@ -431,7 +431,9 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
431
431
|
if (subEl.textContent !== subText) subEl.textContent = subText
|
|
432
432
|
const tip = unavailable ? `${txt.tip} · ${UNREACHABLE_HINT}` : txt.tip
|
|
433
433
|
if (el.title !== tip) el.title = tip
|
|
434
|
-
|
|
434
|
+
// A member card's chip follows the link of the host that publishes its handle.
|
|
435
|
+
const linkHost = n.id.startsWith('member-') ? memberHostByHandle.get(n.remoteHandle ?? '') : host
|
|
436
|
+
paintPrio(el.querySelector<HTMLElement>('.prio')!, n, n.remote === true && linkHost !== undefined ? peerInfoByHost.get(linkHost) : undefined)
|
|
435
437
|
}
|
|
436
438
|
for (const [id, el] of [...nodeEls]) {
|
|
437
439
|
if (!seen.has(id)) { el.remove(); nodeEls.delete(id) }
|
|
@@ -454,6 +456,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
454
456
|
const peerScoreByText = new Map<string, number | undefined>()
|
|
455
457
|
/** gh#20 W-A: per-host link/verified info for the peer badges. */
|
|
456
458
|
const peerInfoByHost = new Map<string, { link?: 'linked' | 'stranger'; verified?: boolean }>()
|
|
459
|
+
let memberHostByHandle: ReadonlyMap<string, string> = new Map()
|
|
457
460
|
|
|
458
461
|
function renderFederation(): void {
|
|
459
462
|
federation.render(clock())
|
|
@@ -1202,6 +1205,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1202
1205
|
// excluded from bounds, so the column cannot feed back into itself).
|
|
1203
1206
|
const peerSeen = new Set<string>()
|
|
1204
1207
|
lastRemoteTeams = groupRemoteTeams(input.remoteTeams ?? [])
|
|
1208
|
+
memberHostByHandle = new Map([...lastRemoteTeams].flatMap(([h, rows]) => rows.map(row => [row.team, h] as const)))
|
|
1205
1209
|
lastWork = new Map(input.sessions.map(s => [s.id, { status: s.status, queued: s.queued }]))
|
|
1206
1210
|
reachability = unreachableRefs(input)
|
|
1207
1211
|
peerScoreByText.clear()
|
|
@@ -1226,7 +1230,9 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1226
1230
|
// team row's zone/id8 handle (canvas remote refs are exact handles
|
|
1227
1231
|
// per the D1 ruling). Absent until the first remote team row lands.
|
|
1228
1232
|
const host = peerHostOf(p.url)
|
|
1229
|
-
|
|
1233
|
+
// Owner report 2026-11-04: the host's rows now lead with its bare process team (e.g. 'dsh'), which the
|
|
1234
|
+
// server reads as a local session id and refuses — take the first zone/id8 SESSION handle instead.
|
|
1235
|
+
const handle = lastRemoteTeams.get(host)?.find(row => row.team.includes('/'))?.team
|
|
1230
1236
|
model.upsertNode({
|
|
1231
1237
|
id, x: base.x, y: base.y,
|
|
1232
1238
|
label: p.url, team: '', name: p.url, peerUrl: p.url,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nelsonlongxiang/dsh-open-a2a-net",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.36",
|
|
4
4
|
"description": "Open A2A network plugin for DeepSeek Harness: signed agent cards, decentralized peer/zone discovery, direct routing model tools, and joinable session nodes with a sidebar control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|