zob-harness 0.14.0 → 0.15.1
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/.pi/capabilities/zob-public-runtime-capabilities.json +22 -2
- package/.pi/extensions/zob-harness/src/core/constants.ts +3 -3
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/envelope.ts +23 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/pending-replies.ts +33 -5
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +4 -1
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/response-capture.ts +2 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts +2 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +144 -22
- package/.pi/extensions/zob-harness/src/domains/coms/zagents.ts +4 -4
- package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +76 -7
- package/.pi/extensions/zob-harness/src/runtime/events.ts +98 -7
- package/.pi/extensions/zob-harness/src/runtime/schemas.ts +9 -1
- package/.pi/extensions/zob-harness/src/runtime/state.ts +8 -1
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +108 -9
- package/.pi/prompts/implement.md +1 -1
- package/.pi/prompts/orchestrator.md +1 -1
- package/.pi/skills/zob-coms-safety/SKILL.md +1 -1
- package/.pi/skills/zob-coms-v2-live/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +173 -3
- package/scripts/zpeer-static-smoke.mjs +49 -11
|
@@ -719,7 +719,27 @@
|
|
|
719
719
|
"docRefs": [
|
|
720
720
|
".pi/extensions/zob-harness/src/AGENTS.md"
|
|
721
721
|
],
|
|
722
|
-
"noShipNotes": "Agent-initiated visible ZPeer ask; local_socket only, room-scoped with optional explicit roomId, mode defaults async, normal/urgent/force are rate/loop guarded, force requires a hashed reason and role/topology guards, raw bodies transient and durable records hash-only."
|
|
722
|
+
"noShipNotes": "Agent-initiated visible ZPeer ask; local_socket only, room-scoped with optional explicit roomId, mode defaults async, opt-in requireResponse tracks msgId-correlated replies with bounded timeout/reinjection metadata, normal/urgent/force are rate/loop guarded, force requires a hashed reason and role/topology guards, raw bodies transient and durable records hash-only."
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"name": "zpeer_reply",
|
|
726
|
+
"family": "coms-v2-live",
|
|
727
|
+
"modes": [
|
|
728
|
+
"explore",
|
|
729
|
+
"plan",
|
|
730
|
+
"implement",
|
|
731
|
+
"oracle",
|
|
732
|
+
"factory",
|
|
733
|
+
"orchestrator"
|
|
734
|
+
],
|
|
735
|
+
"skillRefs": [
|
|
736
|
+
".pi/skills/zob-coms-v2-live/SKILL.md",
|
|
737
|
+
".pi/skills/zob-coms-safety/SKILL.md"
|
|
738
|
+
],
|
|
739
|
+
"docRefs": [
|
|
740
|
+
".pi/extensions/zob-harness/src/AGENTS.md"
|
|
741
|
+
],
|
|
742
|
+
"noShipNotes": "Explicit msgId-bound ZPeer reply path for active inbound messages; local_socket only, requires exact active msgId, wrong/expired/already-answered msgIds are blocked, raw reply body is transient and durable metadata stores outputHash/status only."
|
|
723
743
|
},
|
|
724
744
|
{
|
|
725
745
|
"name": "zob_goal_room_send",
|
|
@@ -1671,7 +1691,7 @@
|
|
|
1671
1691
|
"AGENTS.md",
|
|
1672
1692
|
".pi/extensions/zob-harness/src/AGENTS.md"
|
|
1673
1693
|
],
|
|
1674
|
-
"noShipNotes": "Local-only multi-room-scoped peer UX with active-room compatibility; transient prompt/response over local_socket, persisted command/mission metadata is hash/body-free."
|
|
1694
|
+
"noShipNotes": "Local-only multi-room-scoped peer UX with active-room compatibility; supports /zpeer reply <msgId> <response> and --require-response; transient prompt/response over local_socket, persisted command/mission metadata is hash/body-free."
|
|
1675
1695
|
},
|
|
1676
1696
|
{
|
|
1677
1697
|
"name": "zagent",
|
|
@@ -46,7 +46,7 @@ export const SUPERVISED_READONLY_CHILD_TOOLS = ["read", "grep", "find", "ls"] as
|
|
|
46
46
|
export const READ_ONLY_CHAIN_TOOLS = ["read", "grep", "find", "ls"] as const;
|
|
47
47
|
export const BLOCKED_CHAIN_TOOLS = ["bash", "edit", "write", "delegate_agent", "delegate_task", "orchestrate_run", "factory_run", "factory_quarantine_review", "factory_quarantine_activate", "factory_quarantine_verify_activation", "chain_run"] as const;
|
|
48
48
|
|
|
49
|
-
export const ZOB_COMS_TOOLS = ["zob_coms_send", "zob_coms_ack", "zob_coms_status", "zob_coms_reply", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask"] as const;
|
|
49
|
+
export const ZOB_COMS_TOOLS = ["zob_coms_send", "zob_coms_ack", "zob_coms_status", "zob_coms_reply", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zpeer_reply"] as const;
|
|
50
50
|
export const ZOB_GOAL_ROOM_TOOLS = ["zob_goal_room_send", "zob_goal_room_list"] as const;
|
|
51
51
|
export const ZOB_GOVERNED_REQUEST_TOOLS = ["zob_governed_request_extract"] as const;
|
|
52
52
|
export const ZOB_WORKSPACE_CLAIM_TOOLS = ["zob_workspace_claim", "zob_workspace_release", "zob_workspace_claims_list"] as const;
|
|
@@ -70,10 +70,10 @@ export const ZOB_AUTONOMOUS_READ_TOOLS = ["zob_autonomous_validate_run", "zob_au
|
|
|
70
70
|
export const ZOB_AUTONOMOUS_FACTORY_TOOLS = ["zob_autonomous_dry_run", "zob_autonomous_readonly_smoke"] as const;
|
|
71
71
|
|
|
72
72
|
export const MODE_TOOLS: Record<ModeName, string[]> = {
|
|
73
|
-
explore: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
73
|
+
explore: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zpeer_reply", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
74
74
|
plan: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_PLAN_LAUNCH_TOOLS, ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_WORKLIST_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
75
75
|
implement: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", ...ZOB_PLAN_LAUNCH_TOOLS, ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_WORKLIST_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
76
|
-
oracle: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
76
|
+
oracle: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zpeer_reply", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
77
77
|
orchestrator: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_PLAN_LAUNCH_TOOLS, ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_WORKLIST_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS],
|
|
78
78
|
factory: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "factory_run", "factory_quarantine_review", "factory_quarantine_activate", "factory_quarantine_verify_activation", "chain_run", ...ZOB_PLAN_LAUNCH_TOOLS, ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_AUTONOMOUS_FACTORY_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_WORKLIST_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
79
79
|
// Vanilla is handled specially by applyMode: all currently available Pi tools are enabled.
|
|
@@ -28,6 +28,13 @@ export interface ZobLiveEnvelope {
|
|
|
28
28
|
artifactHashes?: string[];
|
|
29
29
|
replyEndpoint?: string;
|
|
30
30
|
replyEndpointHash?: string;
|
|
31
|
+
requireResponse?: boolean;
|
|
32
|
+
responseTimeoutMs?: number;
|
|
33
|
+
responseRequiredBy?: string;
|
|
34
|
+
maxReinjects?: number;
|
|
35
|
+
reinjectCount?: number;
|
|
36
|
+
replyToMsgId?: string;
|
|
37
|
+
responseHash?: string;
|
|
31
38
|
transientPrompt?: string;
|
|
32
39
|
transientResponse?: string;
|
|
33
40
|
priority?: ZpeerInterruptPriority;
|
|
@@ -76,6 +83,10 @@ export function buildZobLiveAckEnvelope(request: ZobLiveEnvelope, interruptStatu
|
|
|
76
83
|
interruptMode: request.interruptMode,
|
|
77
84
|
interruptReasonHash: request.interruptReasonHash,
|
|
78
85
|
interruptStatus: interruptStatus ?? request.interruptStatus,
|
|
86
|
+
requireResponse: request.requireResponse,
|
|
87
|
+
responseTimeoutMs: request.responseTimeoutMs,
|
|
88
|
+
responseRequiredBy: request.responseRequiredBy,
|
|
89
|
+
maxReinjects: request.maxReinjects,
|
|
79
90
|
});
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -122,6 +133,18 @@ export function validateZobLiveEnvelope(value: unknown): string[] {
|
|
|
122
133
|
if (value.interruptMode !== undefined && (typeof value.interruptMode !== "string" || !ZPEER_INTERRUPT_MODES.has(value.interruptMode))) errors.push("ZOB live envelope interruptMode is invalid");
|
|
123
134
|
if (value.interruptReasonHash !== undefined && (typeof value.interruptReasonHash !== "string" || !/^[a-f0-9]{64}$/i.test(value.interruptReasonHash))) errors.push("ZOB live envelope interruptReasonHash must be sha256 hex when provided");
|
|
124
135
|
if (value.interruptStatus !== undefined && (typeof value.interruptStatus !== "string" || !ZPEER_INTERRUPT_STATUSES.has(value.interruptStatus))) errors.push("ZOB live envelope interruptStatus is invalid");
|
|
136
|
+
if (value.requireResponse !== undefined && typeof value.requireResponse !== "boolean") errors.push("ZOB live envelope requireResponse must be boolean when provided");
|
|
137
|
+
if (value.responseTimeoutMs !== undefined && (typeof value.responseTimeoutMs !== "number" || !Number.isFinite(value.responseTimeoutMs) || value.responseTimeoutMs < 25 || value.responseTimeoutMs > 30 * 60 * 1000)) errors.push("ZOB live envelope responseTimeoutMs must be 25..1800000 when provided");
|
|
138
|
+
if (value.responseRequiredBy !== undefined && (typeof value.responseRequiredBy !== "string" || !Number.isFinite(Date.parse(value.responseRequiredBy)))) errors.push("ZOB live envelope responseRequiredBy must be an ISO timestamp when provided");
|
|
139
|
+
if (value.maxReinjects !== undefined && (typeof value.maxReinjects !== "number" || !Number.isInteger(value.maxReinjects) || value.maxReinjects < 0 || value.maxReinjects > 3)) errors.push("ZOB live envelope maxReinjects must be 0..3 when provided");
|
|
140
|
+
if (value.reinjectCount !== undefined && (typeof value.reinjectCount !== "number" || !Number.isInteger(value.reinjectCount) || value.reinjectCount < 0 || value.reinjectCount > 3)) errors.push("ZOB live envelope reinjectCount must be 0..3 when provided");
|
|
141
|
+
if (value.replyToMsgId !== undefined && (typeof value.replyToMsgId !== "string" || value.replyToMsgId.trim().length === 0)) errors.push("ZOB live envelope replyToMsgId must be non-empty when provided");
|
|
142
|
+
if (value.responseHash !== undefined && (typeof value.responseHash !== "string" || !/^[a-f0-9]{64}$/i.test(value.responseHash))) errors.push("ZOB live envelope responseHash must be sha256 hex when provided");
|
|
143
|
+
if (value.type === "prompt" && value.requireResponse === true) {
|
|
144
|
+
if (typeof value.replyEndpoint !== "string" || value.replyEndpoint.length === 0) errors.push("ZOB live required-response prompt requires replyEndpoint");
|
|
145
|
+
if (typeof value.responseRequiredBy !== "string") errors.push("ZOB live required-response prompt requires responseRequiredBy");
|
|
146
|
+
}
|
|
147
|
+
if (value.type === "response" && value.replyToMsgId !== value.msgId) errors.push("ZOB live response replyToMsgId must match msgId");
|
|
125
148
|
if (value.type === "prompt") {
|
|
126
149
|
if (typeof value.sender !== "string" || typeof value.receiver !== "string") errors.push("ZOB live prompt envelope requires sender and receiver");
|
|
127
150
|
if (typeof value.taskHash !== "string") errors.push("ZOB live prompt envelope requires taskHash");
|
|
@@ -2,7 +2,7 @@ import type { ZobLiveEnvelope } from "./envelope.js";
|
|
|
2
2
|
|
|
3
3
|
export interface ZobPendingReplyResult {
|
|
4
4
|
msgId: string;
|
|
5
|
-
status: "completed" | "error" | "timeout";
|
|
5
|
+
status: "completed" | "error" | "timeout" | "required_response_expired";
|
|
6
6
|
envelope?: ZobLiveEnvelope;
|
|
7
7
|
errorHash?: string;
|
|
8
8
|
}
|
|
@@ -10,6 +10,7 @@ export interface ZobPendingReplyResult {
|
|
|
10
10
|
interface PendingReply {
|
|
11
11
|
msgId: string;
|
|
12
12
|
createdAt: number;
|
|
13
|
+
requireResponse: boolean;
|
|
13
14
|
resolve: (result: ZobPendingReplyResult) => void;
|
|
14
15
|
timer: ReturnType<typeof setTimeout>;
|
|
15
16
|
}
|
|
@@ -17,11 +18,16 @@ interface PendingReply {
|
|
|
17
18
|
export class ZobPendingReplies {
|
|
18
19
|
private readonly pending = new Map<string, PendingReply>();
|
|
19
20
|
private readonly completed = new Map<string, ZobPendingReplyResult>();
|
|
21
|
+
private readonly expired = new Set<string>();
|
|
20
22
|
|
|
21
|
-
wait(msgId: string, timeoutMs: number): Promise<ZobPendingReplyResult> {
|
|
23
|
+
wait(msgId: string, timeoutMs: number, options: { requireResponse?: boolean } = {}): Promise<ZobPendingReplyResult> {
|
|
22
24
|
const completed = this.completed.get(msgId);
|
|
23
25
|
if (completed) {
|
|
24
26
|
this.completed.delete(msgId);
|
|
27
|
+
if (options.requireResponse === true && completed.status === "completed" && completed.envelope?.replyToMsgId !== msgId) {
|
|
28
|
+
this.expired.add(msgId);
|
|
29
|
+
return Promise.resolve({ msgId, status: "required_response_expired" });
|
|
30
|
+
}
|
|
25
31
|
return Promise.resolve(completed);
|
|
26
32
|
}
|
|
27
33
|
const boundedTimeout = Math.max(25, Math.min(30 * 60 * 1000, Math.floor(timeoutMs)));
|
|
@@ -29,22 +35,27 @@ export class ZobPendingReplies {
|
|
|
29
35
|
return new Promise<ZobPendingReplyResult>((resolve) => {
|
|
30
36
|
const timer = setTimeout(() => {
|
|
31
37
|
this.pending.delete(msgId);
|
|
32
|
-
|
|
38
|
+
if (options.requireResponse === true) this.expired.add(msgId);
|
|
39
|
+
resolve({ msgId, status: options.requireResponse === true ? "required_response_expired" : "timeout" });
|
|
33
40
|
}, boundedTimeout);
|
|
34
41
|
timer.unref?.();
|
|
35
|
-
this.pending.set(msgId, { msgId, createdAt: Date.now(), resolve, timer });
|
|
42
|
+
this.pending.set(msgId, { msgId, createdAt: Date.now(), requireResponse: options.requireResponse === true, resolve, timer });
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
complete(msgId: string, envelope: ZobLiveEnvelope): boolean {
|
|
40
|
-
|
|
47
|
+
if (this.expired.has(msgId)) return false;
|
|
41
48
|
const item = this.pending.get(msgId);
|
|
49
|
+
if (item?.requireResponse && envelope.replyToMsgId !== msgId) return false;
|
|
50
|
+
if (envelope.replyToMsgId !== undefined && envelope.replyToMsgId !== msgId) return false;
|
|
51
|
+
const result: ZobPendingReplyResult = { msgId, status: "completed", envelope };
|
|
42
52
|
if (!item) {
|
|
43
53
|
this.completed.set(msgId, result);
|
|
44
54
|
return false;
|
|
45
55
|
}
|
|
46
56
|
clearTimeout(item.timer);
|
|
47
57
|
this.pending.delete(msgId);
|
|
58
|
+
this.expired.delete(msgId);
|
|
48
59
|
item.resolve(result);
|
|
49
60
|
return true;
|
|
50
61
|
}
|
|
@@ -58,11 +69,27 @@ export class ZobPendingReplies {
|
|
|
58
69
|
}
|
|
59
70
|
clearTimeout(item.timer);
|
|
60
71
|
this.pending.delete(msgId);
|
|
72
|
+
this.expired.delete(msgId);
|
|
73
|
+
item.resolve(result);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expire(msgId: string, errorHash?: string): boolean {
|
|
78
|
+
const result: ZobPendingReplyResult = { msgId, status: "required_response_expired", errorHash };
|
|
79
|
+
this.expired.add(msgId);
|
|
80
|
+
const item = this.pending.get(msgId);
|
|
81
|
+
if (!item) {
|
|
82
|
+
this.completed.set(msgId, result);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
clearTimeout(item.timer);
|
|
86
|
+
this.pending.delete(msgId);
|
|
61
87
|
item.resolve(result);
|
|
62
88
|
return true;
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
cancel(msgId: string): boolean {
|
|
92
|
+
this.expired.delete(msgId);
|
|
66
93
|
const item = this.pending.get(msgId);
|
|
67
94
|
if (!item) return false;
|
|
68
95
|
clearTimeout(item.timer);
|
|
@@ -75,6 +102,7 @@ export class ZobPendingReplies {
|
|
|
75
102
|
return [
|
|
76
103
|
...[...this.pending.values()].map((item) => ({ msgId: item.msgId, ageMs: now - item.createdAt, status: "pending", bodyStored: false })),
|
|
77
104
|
...[...this.completed.values()].map((item) => ({ msgId: item.msgId, status: item.status, bodyStored: false })),
|
|
105
|
+
...[...this.expired.values()].map((msgId) => ({ msgId, status: "required_response_expired", bodyStored: false })),
|
|
78
106
|
];
|
|
79
107
|
}
|
|
80
108
|
}
|
|
@@ -246,6 +246,7 @@ function buildTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { n
|
|
|
246
246
|
contextUsedPct: peer.contextUsedPct,
|
|
247
247
|
queueDepth: peer.queueDepth,
|
|
248
248
|
status: peer.status === "offline" ? "offline" : "online",
|
|
249
|
+
socketVerifiedAt: peer.socketVerifiedAt,
|
|
249
250
|
zpeerRoomId: peer.zpeerRoomId,
|
|
250
251
|
zpeerAlias: peer.zpeerAlias,
|
|
251
252
|
zpeerActiveRoomId: peer.zpeerActiveRoomId,
|
|
@@ -291,6 +292,7 @@ function leaseToPeerCard(lease: ZobLiveTeamAgentLease, nowMs: number): ZobLivePe
|
|
|
291
292
|
contextUsedPct: lease.contextUsedPct,
|
|
292
293
|
queueDepth: lease.queueDepth,
|
|
293
294
|
status: deriveLeaseStatus(lease, nowMs),
|
|
295
|
+
socketVerifiedAt: lease.socketVerifiedAt,
|
|
294
296
|
zpeerRoomId: lease.zpeerRoomId,
|
|
295
297
|
zpeerAlias: lease.zpeerAlias,
|
|
296
298
|
zpeerActiveRoomId: lease.zpeerActiveRoomId,
|
|
@@ -578,8 +580,9 @@ export async function sweepZobLivePeerHealth(repoRoot: string, input: { teamName
|
|
|
578
580
|
}
|
|
579
581
|
}
|
|
580
582
|
if (responds) {
|
|
583
|
+
const livePeer = { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard;
|
|
584
|
+
applySweepPeerUpdate(repoRoot, livePeer, nowMs);
|
|
581
585
|
if (peer.status !== "online") {
|
|
582
|
-
applySweepPeerUpdate(repoRoot, { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard, nowMs);
|
|
583
586
|
revived += 1;
|
|
584
587
|
} else {
|
|
585
588
|
retainedLive += 1;
|
|
@@ -45,6 +45,8 @@ export function buildZobLiveResponseEnvelope(request: ZobLiveEnvelope, transient
|
|
|
45
45
|
outputHash: capture.outputHash,
|
|
46
46
|
artifactRefs: capture.artifactRefs,
|
|
47
47
|
artifactHashes: capture.artifactHashes,
|
|
48
|
+
replyToMsgId: request.msgId,
|
|
49
|
+
responseHash: capture.outputHash,
|
|
48
50
|
transientResponse,
|
|
49
51
|
});
|
|
50
52
|
}
|
|
@@ -104,6 +104,7 @@ export interface ZobLivePeerCard {
|
|
|
104
104
|
contextUsedPct: number;
|
|
105
105
|
queueDepth: number;
|
|
106
106
|
status: ZobLivePeerStatus;
|
|
107
|
+
socketVerifiedAt?: string;
|
|
107
108
|
zpeerRoomId?: string;
|
|
108
109
|
zpeerAlias?: string;
|
|
109
110
|
zpeerActiveRoomId?: string;
|
|
@@ -141,6 +142,7 @@ export interface ZobLiveTeamAgentLease {
|
|
|
141
142
|
contextUsedPct: number;
|
|
142
143
|
queueDepth: number;
|
|
143
144
|
status: ZobLivePeerStatus;
|
|
145
|
+
socketVerifiedAt?: string;
|
|
144
146
|
zpeerRoomId?: string;
|
|
145
147
|
zpeerAlias?: string;
|
|
146
148
|
zpeerActiveRoomId?: string;
|
|
@@ -43,7 +43,7 @@ export interface ZpeerPeerRoomSummary extends ZpeerRoomSummary {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export type ZpeerSendMode = "await" | "async" | "long";
|
|
46
|
-
export type ZpeerSendStatus = "delivered" | "waiting" | "reply" | "completed" | "blocked" | "error" | "timeout" | "expired";
|
|
46
|
+
export type ZpeerSendStatus = "delivered" | "waiting" | "reply" | "completed" | "blocked" | "error" | "timeout" | "expired" | "required_response_expired";
|
|
47
47
|
|
|
48
48
|
export interface ZpeerSendResult {
|
|
49
49
|
status: ZpeerSendStatus;
|
|
@@ -58,6 +58,12 @@ export interface ZpeerSendResult {
|
|
|
58
58
|
interruptMode?: ZpeerInterruptMode;
|
|
59
59
|
interruptStatus?: ZpeerInterruptStatus;
|
|
60
60
|
interruptReasonHash?: string;
|
|
61
|
+
requireResponse?: boolean;
|
|
62
|
+
responseRequiredBy?: string;
|
|
63
|
+
responseTimeoutMs?: number;
|
|
64
|
+
maxReinjects?: number;
|
|
65
|
+
responseReceived?: boolean;
|
|
66
|
+
deliveryStatus?: "delivered" | "blocked" | "stale" | "offline" | "transport_error";
|
|
61
67
|
deliveryMethod?: "local_socket" | "tmux_sendkeys";
|
|
62
68
|
fallback_delivery?: boolean;
|
|
63
69
|
best_effort?: boolean;
|
|
@@ -69,20 +75,36 @@ export interface ZpeerSendFeedback {
|
|
|
69
75
|
result: ZpeerSendResult;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
export type ZpeerFallbackDelivery = (input: {
|
|
79
|
+
targetAlias: string;
|
|
80
|
+
senderAlias: string;
|
|
81
|
+
roomId: string;
|
|
82
|
+
taskHash?: string;
|
|
83
|
+
prompt: string;
|
|
84
|
+
priority: ZpeerInterruptPriority;
|
|
85
|
+
interruptMode: ZpeerInterruptMode;
|
|
86
|
+
requireResponse: boolean;
|
|
87
|
+
responseTimeoutMs?: number;
|
|
88
|
+
maxReinjects?: number;
|
|
89
|
+
}) => Promise<{ delivered: boolean; target?: string; reason?: string }>;
|
|
90
|
+
|
|
72
91
|
export interface ZpeerSendOptions {
|
|
73
92
|
mode?: ZpeerSendMode;
|
|
74
93
|
roomId?: string;
|
|
75
94
|
priority?: ZpeerInterruptPriority;
|
|
76
95
|
interruptMode?: ZpeerInterruptMode;
|
|
77
96
|
interruptReasonHash?: string;
|
|
97
|
+
requireResponse?: boolean;
|
|
98
|
+
responseTimeoutMs?: number;
|
|
99
|
+
maxReinjects?: number;
|
|
78
100
|
onFeedback?: (feedback: ZpeerSendFeedback) => void;
|
|
79
101
|
// WS-ZH4: optional best-effort fallback delivery seam. When local-socket delivery is
|
|
80
102
|
// blocked (0 live candidates), the app may inject a fallback (e.g. tmux send-keys to
|
|
81
103
|
// the target agent's pane). This keeps coms-v2 IO-free; the app supplies the tmux
|
|
82
104
|
// delivery. A fallback delivery is NOT verified delivery: outputHash stays absent,
|
|
83
105
|
// confirmation_ref stays null, and bodyStored stays false (coms-safety: append-only /
|
|
84
|
-
// best-effort is not delivery success).
|
|
85
|
-
fallbackDelivery?:
|
|
106
|
+
// best-effort is not delivery success). Force/abort is never eligible for fallback.
|
|
107
|
+
fallbackDelivery?: ZpeerFallbackDelivery;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
interface ZpeerRoomPeer {
|
|
@@ -100,6 +122,37 @@ export function safeZpeerAlias(value: string | undefined): string | undefined {
|
|
|
100
122
|
return trimmed;
|
|
101
123
|
}
|
|
102
124
|
|
|
125
|
+
export function zpeerAliasLookupKey(value: string | undefined): string | undefined {
|
|
126
|
+
const alias = safeZpeerAlias(value);
|
|
127
|
+
return alias ? alias.replaceAll("-", "_") : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function zpeerAliasesEquivalent(left: string | undefined, right: string | undefined): boolean {
|
|
131
|
+
const leftKey = zpeerAliasLookupKey(left);
|
|
132
|
+
const rightKey = zpeerAliasLookupKey(right);
|
|
133
|
+
return Boolean(leftKey && rightKey && leftKey === rightKey);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function zpeerAliasIncluded(aliases: readonly string[] | undefined, alias: string | undefined): boolean {
|
|
137
|
+
const key = zpeerAliasLookupKey(alias);
|
|
138
|
+
return Boolean(key && aliases?.some((candidate) => zpeerAliasLookupKey(candidate) === key));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function duplicateZpeerAliasLookupKeys(aliases: readonly string[]): string[] {
|
|
142
|
+
const seen = new Set<string>();
|
|
143
|
+
const duplicates = new Set<string>();
|
|
144
|
+
for (const alias of aliases) {
|
|
145
|
+
const key = zpeerAliasLookupKey(alias);
|
|
146
|
+
if (!key) continue;
|
|
147
|
+
if (seen.has(key)) duplicates.add(alias);
|
|
148
|
+
else seen.add(key);
|
|
149
|
+
}
|
|
150
|
+
return aliases.filter((alias) => {
|
|
151
|
+
const key = zpeerAliasLookupKey(alias);
|
|
152
|
+
return Boolean(key && duplicates.has(alias)) || Boolean(key && aliases.filter((candidate) => zpeerAliasLookupKey(candidate) === key).length > 1);
|
|
153
|
+
}).filter((alias, index, all) => all.indexOf(alias) === index).sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
export function safeZpeerRoomId(value: string | undefined): string | undefined {
|
|
104
157
|
const trimmed = value?.trim();
|
|
105
158
|
if (!trimmed || !ROOM_PATTERN.test(trimmed)) return undefined;
|
|
@@ -189,8 +242,12 @@ function withZpeerMembershipState(repoRoot: string, peer: ZobLivePeerCard, membe
|
|
|
189
242
|
});
|
|
190
243
|
}
|
|
191
244
|
|
|
245
|
+
function zpeerStrictSocketEvidenceRequired(): boolean {
|
|
246
|
+
return /^(1|true|yes|on|strict)$/i.test(process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET ?? "");
|
|
247
|
+
}
|
|
248
|
+
|
|
192
249
|
function zpeerReachableStatus(peer: ZobLivePeerCard): ZobLivePeerStatus {
|
|
193
|
-
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer)) return "online";
|
|
250
|
+
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer, { requireVerified: zpeerStrictSocketEvidenceRequired() })) return "online";
|
|
194
251
|
if (peer.status === "stale") return "stale";
|
|
195
252
|
return "offline";
|
|
196
253
|
}
|
|
@@ -212,9 +269,34 @@ async function peerRespondsToAliasPing(peer: ZobLivePeerCard): Promise<boolean>
|
|
|
212
269
|
}
|
|
213
270
|
}
|
|
214
271
|
|
|
272
|
+
async function probeAndReviveZpeerCandidate(repoRoot: string, entry: ZpeerRoomPeer): Promise<ZpeerRoomPeer | undefined> {
|
|
273
|
+
if (!hasLocalSocketEndpointEvidence(entry.peer)) return undefined;
|
|
274
|
+
const nowIso = new Date().toISOString();
|
|
275
|
+
try {
|
|
276
|
+
const response = await sendZobLocalEnvelope(entry.peer.endpoint, {
|
|
277
|
+
schema: "zob.live-envelope.v1",
|
|
278
|
+
type: "ping",
|
|
279
|
+
msgId: `zpeer-candidate-probe:${entry.peer.roleId}:${Date.now()}`,
|
|
280
|
+
hops: 0,
|
|
281
|
+
timestamp: nowIso,
|
|
282
|
+
bodyStored: false,
|
|
283
|
+
}, { timeoutMs: 1_000 });
|
|
284
|
+
if (response.type !== "pong" && response.type !== "ack") return undefined;
|
|
285
|
+
const revived = { ...entry.peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard;
|
|
286
|
+
if (revived.zpeerAdhoc === true || revived.projectId !== buildZobComsProjectId(repoRoot)) {
|
|
287
|
+
writeZobLivePeerCardToProjectId(revived);
|
|
288
|
+
} else {
|
|
289
|
+
writeZobLiveTeamAgentLease(repoRoot, revived, { reason: "zpeer_candidate_probe" });
|
|
290
|
+
}
|
|
291
|
+
return { ...entry, peer: revived };
|
|
292
|
+
} catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
215
297
|
async function activeAliasCollision(repoRoot: string, self: ZobLivePeerCard, roomId: string, alias: string): Promise<ZpeerRoomPeer | undefined> {
|
|
216
298
|
for (const entry of peersInRoom(repoRoot, roomId)) {
|
|
217
|
-
if (entry.peer.sessionHash === self.sessionHash || entry.membership.alias
|
|
299
|
+
if (entry.peer.sessionHash === self.sessionHash || !zpeerAliasesEquivalent(entry.membership.alias, alias)) continue;
|
|
218
300
|
if (await peerRespondsToAliasPing(entry.peer)) return entry;
|
|
219
301
|
if (zpeerReachableStatus(entry.peer) === "online") {
|
|
220
302
|
try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort stale alias release */ }
|
|
@@ -281,7 +363,7 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
|
|
|
281
363
|
statusAliases[status].push(entry.membership.alias);
|
|
282
364
|
}
|
|
283
365
|
const onlineAliases = statusAliases.online.sort();
|
|
284
|
-
const duplicateAliases =
|
|
366
|
+
const duplicateAliases = duplicateZpeerAliasLookupKeys(onlineAliases);
|
|
285
367
|
return {
|
|
286
368
|
schema: "zob.zpeer-room-summary.v1",
|
|
287
369
|
projectId,
|
|
@@ -425,6 +507,12 @@ function appendZpeerPeerRecords(repoRoot: string, record: {
|
|
|
425
507
|
interruptMode?: ZpeerInterruptMode;
|
|
426
508
|
interruptStatus?: ZpeerInterruptStatus;
|
|
427
509
|
interruptReasonHash?: string;
|
|
510
|
+
requireResponse?: boolean;
|
|
511
|
+
responseRequiredBy?: string;
|
|
512
|
+
responseTimeoutMs?: number;
|
|
513
|
+
maxReinjects?: number;
|
|
514
|
+
responseReceived?: boolean;
|
|
515
|
+
deliveryStatus?: "delivered" | "blocked" | "stale" | "offline" | "transport_error";
|
|
428
516
|
deliveryMethod?: "local_socket" | "tmux_sendkeys";
|
|
429
517
|
fallbackDelivery?: boolean;
|
|
430
518
|
}): void {
|
|
@@ -444,6 +532,12 @@ function appendZpeerPeerRecords(repoRoot: string, record: {
|
|
|
444
532
|
interruptMode: record.interruptMode,
|
|
445
533
|
interruptStatus: record.interruptStatus,
|
|
446
534
|
interruptReasonHash: record.interruptReasonHash,
|
|
535
|
+
requireResponse: record.requireResponse,
|
|
536
|
+
responseRequiredBy: record.responseRequiredBy,
|
|
537
|
+
responseTimeoutMs: record.responseTimeoutMs,
|
|
538
|
+
maxReinjects: record.maxReinjects,
|
|
539
|
+
responseReceived: record.responseReceived,
|
|
540
|
+
deliveryStatus: record.deliveryStatus,
|
|
447
541
|
deliveryMethod: record.deliveryMethod,
|
|
448
542
|
fallbackDelivery: record.fallbackDelivery,
|
|
449
543
|
localOnly: true,
|
|
@@ -495,6 +589,9 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
495
589
|
const interruptMode = options.interruptMode ?? (priority === "force" ? "abort" : priority === "urgent" ? "steer" : "none");
|
|
496
590
|
const interruptReasonHash = options.interruptReasonHash;
|
|
497
591
|
const interruptRequested = priority !== "normal" || interruptMode !== "none";
|
|
592
|
+
const requireResponse = options.requireResponse === true;
|
|
593
|
+
const responseTimeoutMs = Math.max(1_000, Math.min(30 * 60 * 1000, Math.floor(options.responseTimeoutMs ?? (mode === "long" ? 30 * 60 * 1000 : 10 * 60 * 1000))));
|
|
594
|
+
const maxReinjects = Math.max(0, Math.min(3, Math.floor(options.maxReinjects ?? 1)));
|
|
498
595
|
const emitFeedback = (kind: ZpeerSendFeedback["kind"], result: ZpeerSendResult): void => {
|
|
499
596
|
options.onFeedback?.({ kind, result });
|
|
500
597
|
};
|
|
@@ -514,10 +611,16 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
514
611
|
interruptMode: result.interruptMode ?? interruptMode,
|
|
515
612
|
interruptStatus: result.interruptStatus,
|
|
516
613
|
interruptReasonHash: result.interruptReasonHash ?? interruptReasonHash,
|
|
614
|
+
requireResponse: result.requireResponse,
|
|
615
|
+
responseRequiredBy: result.responseRequiredBy,
|
|
616
|
+
responseTimeoutMs: result.responseTimeoutMs,
|
|
617
|
+
maxReinjects: result.maxReinjects,
|
|
618
|
+
responseReceived: result.responseReceived,
|
|
619
|
+
deliveryStatus: result.deliveryStatus,
|
|
517
620
|
deliveryMethod: result.deliveryMethod,
|
|
518
621
|
fallbackDelivery: result.fallback_delivery,
|
|
519
622
|
});
|
|
520
|
-
return { roomId, priority, interruptMode, interruptReasonHash, ...result };
|
|
623
|
+
return { roomId, priority, interruptMode, interruptReasonHash, requireResponse: requireResponse || undefined, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, ...result };
|
|
521
624
|
};
|
|
522
625
|
|
|
523
626
|
if (priority === "force" && !interruptReasonHash) return finish("attempt", { status: "blocked", reason: "force interrupt requires reason hash", targetAlias: targetAlias ?? undefined, taskHash, interruptStatus: "force_blocked", bodyStored: false });
|
|
@@ -530,8 +633,8 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
530
633
|
if (priority === "force" && !ZPEER_FORCE_ALLOWED_SENDER_ROLE_TYPES.has(self.roleType)) return finish("attempt", { status: "blocked", reason: `force interrupt not allowed from role type ${self.roleType}`, targetAlias: targetAlias ?? undefined, taskHash, interruptStatus: "force_blocked", bodyStored: false });
|
|
531
634
|
if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
|
|
532
635
|
if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
|
|
533
|
-
const candidates = peersInRoom(repoRoot, roomId).filter((entry) => entry.membership.alias
|
|
534
|
-
if (targetAlias
|
|
636
|
+
const candidates = peersInRoom(repoRoot, roomId).filter((entry) => zpeerAliasesEquivalent(entry.membership.alias, targetAlias) && entry.peer.sessionHash !== self.sessionHash);
|
|
637
|
+
if (zpeerAliasesEquivalent(targetAlias, senderAlias)) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
|
|
535
638
|
if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
|
|
536
639
|
let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
|
|
537
640
|
if (liveCandidates.length > 1) {
|
|
@@ -545,6 +648,14 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
545
648
|
}
|
|
546
649
|
liveCandidates = responsiveCandidates;
|
|
547
650
|
}
|
|
651
|
+
if (liveCandidates.length === 0) {
|
|
652
|
+
const probedCandidates: ZpeerRoomPeer[] = [];
|
|
653
|
+
for (const entry of candidates) {
|
|
654
|
+
const revived = await probeAndReviveZpeerCandidate(repoRoot, entry);
|
|
655
|
+
if (revived) probedCandidates.push(revived);
|
|
656
|
+
}
|
|
657
|
+
liveCandidates = probedCandidates;
|
|
658
|
+
}
|
|
548
659
|
if (liveCandidates.length === 0) {
|
|
549
660
|
const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
|
|
550
661
|
// WS-ZH4: last-resort fallback delivery. When local-socket transport is blocked
|
|
@@ -552,14 +663,15 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
552
663
|
// pane), deliver the prompt best-effort. This is NOT verified delivery:
|
|
553
664
|
// outputHash/confirmation_ref stay absent and bodyStored stays false; the receiver,
|
|
554
665
|
// if it answers at all, answers via its own local_socket later (that ledgered reply
|
|
555
|
-
// is what WS-Q5 counts).
|
|
556
|
-
//
|
|
557
|
-
|
|
666
|
+
// is what WS-Q5 counts). Urgent/steer is eligible when the caller explicitly wires
|
|
667
|
+
// a safe fallback hook; force/abort is never eligible. If no fallback is supplied or
|
|
668
|
+
// it fails/declines, fall through to the standard blocked result unchanged.
|
|
669
|
+
if (options.fallbackDelivery && targetAlias && priority !== "force") {
|
|
558
670
|
try {
|
|
559
|
-
const fallback = await options.fallbackDelivery({ targetAlias, roomId, taskHash, prompt: transientPrompt });
|
|
671
|
+
const fallback = await options.fallbackDelivery({ targetAlias, senderAlias, roomId, taskHash, prompt: transientPrompt, priority, interruptMode, requireResponse, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined });
|
|
560
672
|
if (fallback.delivered) {
|
|
561
673
|
const fallbackMsgId = `zpeer-fallback:${self.sessionHash.slice(0, 8)}:${Date.now()}`;
|
|
562
|
-
return finish("attempt", { status: "delivered", reason: `tmux_sendkeys_fallback: peer @${targetAlias} socket ${statuses}; best-effort delivery to ${fallback.target ?? targetAlias} (not verified)`, msgId: fallbackMsgId, targetAlias, taskHash, deliveryMethod: "tmux_sendkeys", fallback_delivery: true, best_effort: true, bodyStored: false }, candidates.length);
|
|
674
|
+
return finish("attempt", { status: "delivered", reason: `tmux_sendkeys_fallback: peer @${targetAlias} socket ${statuses}; best-effort delivery to ${fallback.target ?? targetAlias} (not verified)`, msgId: fallbackMsgId, targetAlias, taskHash, deliveryStatus: "blocked", deliveryMethod: "tmux_sendkeys", fallback_delivery: true, best_effort: true, responseReceived: requireResponse ? false : undefined, bodyStored: false }, candidates.length);
|
|
563
675
|
}
|
|
564
676
|
} catch {
|
|
565
677
|
// best-effort fallback failed; fall through to the standard blocked result.
|
|
@@ -578,7 +690,8 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
578
690
|
if (self.transport !== "local_socket" || !self.endpoint || self.endpoint.startsWith("pending-") || self.endpoint === "observe-only") return finish("attempt", { status: "blocked", reason: "current session has no local_socket reply endpoint", targetAlias, taskHash, bodyStored: false }, 1);
|
|
579
691
|
|
|
580
692
|
const msgId = `zpeer:${self.sessionHash.slice(0, 8)}:${target.peer.sessionHash.slice(0, 8)}:${Date.now()}`;
|
|
581
|
-
|
|
693
|
+
const responseRequiredBy = requireResponse ? new Date(Date.now() + responseTimeoutMs).toISOString() : undefined;
|
|
694
|
+
finish("attempt", { status: "delivered", msgId, targetAlias, taskHash, requireResponse: requireResponse || undefined, responseRequiredBy, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, deliveryStatus: "delivered", responseReceived: requireResponse ? false : undefined, bodyStored: false }, 1);
|
|
582
695
|
const liveEnvelope = buildZobLiveEnvelope({
|
|
583
696
|
type: "prompt",
|
|
584
697
|
msgId,
|
|
@@ -594,6 +707,10 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
594
707
|
interruptRequested,
|
|
595
708
|
interruptMode,
|
|
596
709
|
interruptReasonHash,
|
|
710
|
+
requireResponse: requireResponse || undefined,
|
|
711
|
+
responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined,
|
|
712
|
+
responseRequiredBy,
|
|
713
|
+
maxReinjects: requireResponse ? maxReinjects : undefined,
|
|
597
714
|
});
|
|
598
715
|
try {
|
|
599
716
|
const ack = await sendZobLocalEnvelope(target.peer.endpoint, liveEnvelope, { timeoutMs: 5_000 });
|
|
@@ -601,27 +718,32 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
601
718
|
const ackInterruptStatus = ack.interruptStatus;
|
|
602
719
|
appendZpeerPeerRecords(repoRoot, { event: "ack", status: "delivered", roomId, msgId, senderAlias, targetAlias, taskHash, priority, interruptMode, interruptStatus: ackInterruptStatus, interruptReasonHash, peerCount: 1 });
|
|
603
720
|
if (ackInterruptStatus === "force_blocked") return finish("terminal", { status: "blocked", reason: "force interrupt blocked by receiver policy", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, bodyStored: false }, 1);
|
|
604
|
-
if (mode === "async") {
|
|
605
|
-
const waiting = finish("terminal", { status: "waiting", reason: "delivered locally; awaiting async reply", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, bodyStored: false }, 1);
|
|
721
|
+
if (mode === "async" && !requireResponse) {
|
|
722
|
+
const waiting = finish("terminal", { status: "waiting", reason: "delivered locally; awaiting async reply", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, deliveryStatus: "delivered", bodyStored: false }, 1);
|
|
606
723
|
emitFeedback("waiting", waiting);
|
|
607
724
|
return waiting;
|
|
608
725
|
}
|
|
609
|
-
emitFeedback("delivered", { status: "delivered", roomId, msgId, targetAlias, taskHash, priority, interruptMode, interruptStatus: ackInterruptStatus, interruptReasonHash, bodyStored: false });
|
|
610
|
-
emitFeedback("waiting", { status: "waiting", roomId, reason: mode === "long" ? "waiting for long peer reply" : "waiting for peer reply", msgId, targetAlias, taskHash, priority, interruptMode, interruptStatus: ackInterruptStatus, interruptReasonHash, bodyStored: false });
|
|
726
|
+
emitFeedback("delivered", { status: "delivered", roomId, msgId, targetAlias, taskHash, priority, interruptMode, interruptStatus: ackInterruptStatus, interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, deliveryStatus: "delivered", responseReceived: requireResponse ? false : undefined, bodyStored: false });
|
|
727
|
+
emitFeedback("waiting", { status: "waiting", roomId, reason: requireResponse ? "waiting for required peer response" : mode === "long" ? "waiting for long peer reply" : "waiting for peer reply", msgId, targetAlias, taskHash, priority, interruptMode, interruptStatus: ackInterruptStatus, interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, deliveryStatus: "delivered", responseReceived: requireResponse ? false : undefined, bodyStored: false });
|
|
611
728
|
const reply = await awaitReply(msgId);
|
|
612
729
|
const replyEnvelope = reply["envelope"];
|
|
613
730
|
if (reply.status === "completed") {
|
|
614
731
|
const transientResponse = replyEnvelope?.transientResponse;
|
|
615
|
-
const result = finish("terminal", { status: "reply", msgId, targetAlias, taskHash, outputHash: replyEnvelope?.outputHash ?? (transientResponse ? sha256(transientResponse) : undefined), transientResponse, interruptStatus: ackInterruptStatus, bodyStored: false }, 1);
|
|
732
|
+
const result = finish("terminal", { status: "reply", msgId, targetAlias, taskHash, outputHash: replyEnvelope?.outputHash ?? (transientResponse ? sha256(transientResponse) : undefined), transientResponse, interruptStatus: ackInterruptStatus, requireResponse: requireResponse || undefined, responseRequiredBy, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, deliveryStatus: "delivered", responseReceived: requireResponse ? true : undefined, bodyStored: false }, 1);
|
|
616
733
|
emitFeedback("reply", result);
|
|
617
734
|
return result;
|
|
618
735
|
}
|
|
736
|
+
if (reply.status === "required_response_expired") {
|
|
737
|
+
const result = finish("terminal", { status: "required_response_expired", reason: "required peer response expired", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, requireResponse: true, responseRequiredBy, responseTimeoutMs, maxReinjects, deliveryStatus: "delivered", responseReceived: false, bodyStored: false }, 1);
|
|
738
|
+
emitFeedback("expired", result);
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
619
741
|
if (reply.status === "timeout") {
|
|
620
|
-
const result = finish("terminal", { status: "timeout", reason: "await response timed out", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, bodyStored: false }, 1);
|
|
742
|
+
const result = finish("terminal", { status: "timeout", reason: "await response timed out", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, deliveryStatus: "delivered", bodyStored: false }, 1);
|
|
621
743
|
emitFeedback(mode === "long" ? "expired" : "timeout", result);
|
|
622
744
|
return result;
|
|
623
745
|
}
|
|
624
|
-
const result = finish("terminal", { status: "error", reason: replyEnvelope?.errorHash ?? "peer response error", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, bodyStored: false }, 1);
|
|
746
|
+
const result = finish("terminal", { status: "error", reason: replyEnvelope?.errorHash ?? "peer response error", msgId, targetAlias, taskHash, interruptStatus: ackInterruptStatus, requireResponse: requireResponse || undefined, responseRequiredBy, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, deliveryStatus: "delivered", responseReceived: requireResponse ? false : undefined, bodyStored: false }, 1);
|
|
625
747
|
emitFeedback("error", result);
|
|
626
748
|
return result;
|
|
627
749
|
} catch (error) {
|
|
@@ -3,7 +3,7 @@ import { basename, join, relative, resolve } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import type { ModeName } from "../../core/types/core.js";
|
|
5
5
|
import { MODE_TOOLS } from "../../core/constants.js";
|
|
6
|
-
import { safeZpeerAlias, safeZpeerRoomId } from "./coms-v2/zpeer.js";
|
|
6
|
+
import { safeZpeerAlias, safeZpeerRoomId, zpeerAliasIncluded, zpeerAliasesEquivalent } from "./coms-v2/zpeer.js";
|
|
7
7
|
import { parseJsonFile } from "../../core/utils/json.js";
|
|
8
8
|
import { isSafeArtifactName } from "../../core/utils/paths.js";
|
|
9
9
|
import { isRecord } from "../../core/utils/records.js";
|
|
@@ -438,7 +438,7 @@ function communicationPolicyNarrowingErrors(base: ZAgentCommunicationPolicy | un
|
|
|
438
438
|
for (const room of overlay.allowedRooms ?? []) if (!base.allowedRooms.includes(room)) errors.push(`${label}.allowedRooms must be a subset of the base policy: ${room}`);
|
|
439
439
|
}
|
|
440
440
|
if (base?.allowedAliases) {
|
|
441
|
-
for (const alias of overlay.allowedAliases ?? []) if (!base.allowedAliases
|
|
441
|
+
for (const alias of overlay.allowedAliases ?? []) if (!zpeerAliasIncluded(base.allowedAliases, alias)) errors.push(`${label}.allowedAliases must be a subset of the base policy: ${alias}`);
|
|
442
442
|
}
|
|
443
443
|
return errors;
|
|
444
444
|
}
|
|
@@ -865,7 +865,7 @@ function policyAllowsZpeerContact(policy: ZAgentCommunicationPolicy | undefined,
|
|
|
865
865
|
if (!policy) return true;
|
|
866
866
|
if (policy.zpeerContact === false || policy.allowZpeerContact === false) return false;
|
|
867
867
|
if (roomId && policy.allowedRooms && !policy.allowedRooms.includes(roomId)) return false;
|
|
868
|
-
if (alias && policy.allowedAliases && !policy.allowedAliases
|
|
868
|
+
if (alias && policy.allowedAliases && !zpeerAliasIncluded(policy.allowedAliases, alias)) return false;
|
|
869
869
|
if (policy.requireActiveRoom && !roomId) return false;
|
|
870
870
|
return true;
|
|
871
871
|
}
|
|
@@ -876,7 +876,7 @@ export function zteamAllowsZpeerContact(team: ZTeamManifest, zagentId: string, r
|
|
|
876
876
|
...(team.members ?? []),
|
|
877
877
|
...(team.agents ?? []),
|
|
878
878
|
];
|
|
879
|
-
const member = members.find((candidate) => zteamMemberAgentId(candidate) === zagentId || candidate.alias
|
|
879
|
+
const member = members.find((candidate) => zteamMemberAgentId(candidate) === zagentId || zpeerAliasesEquivalent(candidate.alias, alias));
|
|
880
880
|
if (!member) return false;
|
|
881
881
|
if (!policyAllowsZpeerContact(member.communicationPolicy, roomId, alias ?? member.alias)) return false;
|
|
882
882
|
const rooms = zteamMemberRooms(member, team.defaultRoom);
|