zob-harness 0.15.0 → 0.15.2
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/extensions/zob-harness/index.ts +1 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +4 -1
- 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-status.ts +92 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +98 -13
- package/.pi/extensions/zob-harness/src/domains/coms/zagents.ts +4 -4
- package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +10 -5
- package/.pi/extensions/zob-harness/src/runtime/events.ts +10 -12
- package/.pi/extensions/zob-harness/src/runtime/state.ts +5 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +95 -19
- package/.pi/extensions/zob-harness/src/runtime/zpeer-events.ts +53 -0
- package/package.json +1 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +96 -1
- package/scripts/zpeer-static-smoke.mjs +21 -4
|
@@ -617,6 +617,7 @@ export { ZobPendingReplies } from "./src/domains/coms/coms-v2/pending-replies.js
|
|
|
617
617
|
export { buildZobLiveResponseCapture, buildZobLiveResponseEnvelope } from "./src/domains/coms/coms-v2/response-capture.js";
|
|
618
618
|
export { appendLiveSendRequestedRef, appendLiveDeliveredStatus, appendLiveRunningStatus, appendLiveCompletedRef, appendLiveErrorStatus, appendPeerStaleStatus } from "./src/domains/coms/coms-v2/ledger-bridge.js";
|
|
619
619
|
export { redactZobComsText, writeZobComsRedactedCapture } from "./src/domains/coms/coms-v2/transcript-capture.js";
|
|
620
|
+
export { annotateZpeerStatus, isZpeerTerminalStatus, shouldAcceptZpeerStatusUpdate, zpeerStatusRank } from "./src/domains/coms/coms-v2/zpeer-status.js";
|
|
620
621
|
export type { ZobLiveEnvelope, ZobLiveEnvelopeType } from "./src/domains/coms/coms-v2/envelope.js";
|
|
621
622
|
export type { ZobComsTranscriptCapturePolicy, ZobComsTranscriptMode, ZobComsTranscriptRetentionClass, ZobComsTransportMode, ZobComsV2Policy, ZobLivePeerCard, ZobLivePeerStatus, ZobLivePresenceSummary, ZobLiveRegistrySnapshot } from "./src/domains/coms/coms-v2/types.js";
|
|
622
623
|
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface ZpeerStatusComparable {
|
|
2
|
+
status?: string;
|
|
3
|
+
kind?: string;
|
|
4
|
+
at?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const ZPEER_TERMINAL_STATUSES = new Set([
|
|
8
|
+
"reply",
|
|
9
|
+
"completed",
|
|
10
|
+
"response_sent",
|
|
11
|
+
"blocked",
|
|
12
|
+
"error",
|
|
13
|
+
"timeout",
|
|
14
|
+
"expired",
|
|
15
|
+
"required_response_expired",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const ZPEER_STATUS_RANKS: Record<string, number> = {
|
|
19
|
+
status: 0,
|
|
20
|
+
attempt: 5,
|
|
21
|
+
heartbeat: 5,
|
|
22
|
+
sent: 10,
|
|
23
|
+
delivered: 20,
|
|
24
|
+
urgent_delivered: 25,
|
|
25
|
+
force_accepted: 25,
|
|
26
|
+
force_downgraded: 25,
|
|
27
|
+
required_response_reinject: 30,
|
|
28
|
+
inbound: 35,
|
|
29
|
+
prompt_received: 35,
|
|
30
|
+
waiting: 40,
|
|
31
|
+
response_sent: 90,
|
|
32
|
+
reply: 100,
|
|
33
|
+
completed: 100,
|
|
34
|
+
blocked: 100,
|
|
35
|
+
force_blocked: 100,
|
|
36
|
+
error: 100,
|
|
37
|
+
timeout: 100,
|
|
38
|
+
expired: 100,
|
|
39
|
+
required_response_expired: 100,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function zpeerStatusKey(input: ZpeerStatusComparable): string | undefined {
|
|
43
|
+
return input.status ?? input.kind;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parsedTime(value: string | undefined): number | undefined {
|
|
47
|
+
if (!value) return undefined;
|
|
48
|
+
const parsed = Date.parse(value);
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isZpeerTerminalStatus(status: string | undefined, kind?: string): boolean {
|
|
53
|
+
const primary = status ?? kind;
|
|
54
|
+
return Boolean((primary && ZPEER_TERMINAL_STATUSES.has(primary)) || (kind && ZPEER_TERMINAL_STATUSES.has(kind)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function zpeerStatusRank(status: string | undefined, kind?: string): number {
|
|
58
|
+
const primary = status ?? kind;
|
|
59
|
+
if (primary && ZPEER_STATUS_RANKS[primary] !== undefined) return ZPEER_STATUS_RANKS[primary];
|
|
60
|
+
if (kind && ZPEER_STATUS_RANKS[kind] !== undefined) return ZPEER_STATUS_RANKS[kind];
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function shouldAcceptZpeerStatusUpdate(current: ZpeerStatusComparable | undefined, incoming: ZpeerStatusComparable): boolean {
|
|
65
|
+
if (!current) return true;
|
|
66
|
+
|
|
67
|
+
const currentTerminal = isZpeerTerminalStatus(current.status, current.kind);
|
|
68
|
+
const incomingTerminal = isZpeerTerminalStatus(incoming.status, incoming.kind);
|
|
69
|
+
if (currentTerminal && !incomingTerminal) return false;
|
|
70
|
+
if (!currentTerminal && incomingTerminal) return true;
|
|
71
|
+
|
|
72
|
+
const currentRank = zpeerStatusRank(current.status, current.kind);
|
|
73
|
+
const incomingRank = zpeerStatusRank(incoming.status, incoming.kind);
|
|
74
|
+
if (incomingRank > currentRank) return true;
|
|
75
|
+
if (incomingRank < currentRank) return false;
|
|
76
|
+
|
|
77
|
+
const currentAt = parsedTime(current.at);
|
|
78
|
+
const incomingAt = parsedTime(incoming.at);
|
|
79
|
+
if (currentAt !== undefined && incomingAt !== undefined) return incomingAt >= currentAt;
|
|
80
|
+
|
|
81
|
+
const currentKey = zpeerStatusKey(current);
|
|
82
|
+
const incomingKey = zpeerStatusKey(incoming);
|
|
83
|
+
return currentKey === incomingKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function annotateZpeerStatus<T extends ZpeerStatusComparable>(event: T): T & { terminal: boolean; statusRank: number } {
|
|
87
|
+
return {
|
|
88
|
+
...event,
|
|
89
|
+
terminal: isZpeerTerminalStatus(event.status, event.kind),
|
|
90
|
+
statusRank: zpeerStatusRank(event.status, event.kind),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -10,6 +10,7 @@ import { validateZobComsEdge } from "../../topology/coms.js";
|
|
|
10
10
|
import { loadTeamDefinition, validateTeamDefinition } from "../../topology/teams.js";
|
|
11
11
|
import { loadZteamManifest, zteamAllowsZpeerContact } from "../zagents.js";
|
|
12
12
|
import { sha256 } from "../../../core/utils/hashing.js";
|
|
13
|
+
import { annotateZpeerStatus } from "./zpeer-status.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_ROOM_ID = "default";
|
|
15
16
|
const ALIAS_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{1,31}$/;
|
|
@@ -67,6 +68,8 @@ export interface ZpeerSendResult {
|
|
|
67
68
|
deliveryMethod?: "local_socket" | "tmux_sendkeys";
|
|
68
69
|
fallback_delivery?: boolean;
|
|
69
70
|
best_effort?: boolean;
|
|
71
|
+
terminal?: boolean;
|
|
72
|
+
statusRank?: number;
|
|
70
73
|
bodyStored: false;
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -75,6 +78,19 @@ export interface ZpeerSendFeedback {
|
|
|
75
78
|
result: ZpeerSendResult;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
export type ZpeerFallbackDelivery = (input: {
|
|
82
|
+
targetAlias: string;
|
|
83
|
+
senderAlias: string;
|
|
84
|
+
roomId: string;
|
|
85
|
+
taskHash?: string;
|
|
86
|
+
prompt: string;
|
|
87
|
+
priority: ZpeerInterruptPriority;
|
|
88
|
+
interruptMode: ZpeerInterruptMode;
|
|
89
|
+
requireResponse: boolean;
|
|
90
|
+
responseTimeoutMs?: number;
|
|
91
|
+
maxReinjects?: number;
|
|
92
|
+
}) => Promise<{ delivered: boolean; target?: string; reason?: string }>;
|
|
93
|
+
|
|
78
94
|
export interface ZpeerSendOptions {
|
|
79
95
|
mode?: ZpeerSendMode;
|
|
80
96
|
roomId?: string;
|
|
@@ -90,8 +106,8 @@ export interface ZpeerSendOptions {
|
|
|
90
106
|
// the target agent's pane). This keeps coms-v2 IO-free; the app supplies the tmux
|
|
91
107
|
// delivery. A fallback delivery is NOT verified delivery: outputHash stays absent,
|
|
92
108
|
// confirmation_ref stays null, and bodyStored stays false (coms-safety: append-only /
|
|
93
|
-
// best-effort is not delivery success).
|
|
94
|
-
fallbackDelivery?:
|
|
109
|
+
// best-effort is not delivery success). Force/abort is never eligible for fallback.
|
|
110
|
+
fallbackDelivery?: ZpeerFallbackDelivery;
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
interface ZpeerRoomPeer {
|
|
@@ -109,6 +125,37 @@ export function safeZpeerAlias(value: string | undefined): string | undefined {
|
|
|
109
125
|
return trimmed;
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
export function zpeerAliasLookupKey(value: string | undefined): string | undefined {
|
|
129
|
+
const alias = safeZpeerAlias(value);
|
|
130
|
+
return alias ? alias.replaceAll("-", "_") : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function zpeerAliasesEquivalent(left: string | undefined, right: string | undefined): boolean {
|
|
134
|
+
const leftKey = zpeerAliasLookupKey(left);
|
|
135
|
+
const rightKey = zpeerAliasLookupKey(right);
|
|
136
|
+
return Boolean(leftKey && rightKey && leftKey === rightKey);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function zpeerAliasIncluded(aliases: readonly string[] | undefined, alias: string | undefined): boolean {
|
|
140
|
+
const key = zpeerAliasLookupKey(alias);
|
|
141
|
+
return Boolean(key && aliases?.some((candidate) => zpeerAliasLookupKey(candidate) === key));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function duplicateZpeerAliasLookupKeys(aliases: readonly string[]): string[] {
|
|
145
|
+
const seen = new Set<string>();
|
|
146
|
+
const duplicates = new Set<string>();
|
|
147
|
+
for (const alias of aliases) {
|
|
148
|
+
const key = zpeerAliasLookupKey(alias);
|
|
149
|
+
if (!key) continue;
|
|
150
|
+
if (seen.has(key)) duplicates.add(alias);
|
|
151
|
+
else seen.add(key);
|
|
152
|
+
}
|
|
153
|
+
return aliases.filter((alias) => {
|
|
154
|
+
const key = zpeerAliasLookupKey(alias);
|
|
155
|
+
return Boolean(key && duplicates.has(alias)) || Boolean(key && aliases.filter((candidate) => zpeerAliasLookupKey(candidate) === key).length > 1);
|
|
156
|
+
}).filter((alias, index, all) => all.indexOf(alias) === index).sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
112
159
|
export function safeZpeerRoomId(value: string | undefined): string | undefined {
|
|
113
160
|
const trimmed = value?.trim();
|
|
114
161
|
if (!trimmed || !ROOM_PATTERN.test(trimmed)) return undefined;
|
|
@@ -198,8 +245,12 @@ function withZpeerMembershipState(repoRoot: string, peer: ZobLivePeerCard, membe
|
|
|
198
245
|
});
|
|
199
246
|
}
|
|
200
247
|
|
|
248
|
+
function zpeerStrictSocketEvidenceRequired(): boolean {
|
|
249
|
+
return /^(1|true|yes|on|strict)$/i.test(process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET ?? "");
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
function zpeerReachableStatus(peer: ZobLivePeerCard): ZobLivePeerStatus {
|
|
202
|
-
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer)) return "online";
|
|
253
|
+
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer, { requireVerified: zpeerStrictSocketEvidenceRequired() })) return "online";
|
|
203
254
|
if (peer.status === "stale") return "stale";
|
|
204
255
|
return "offline";
|
|
205
256
|
}
|
|
@@ -221,9 +272,34 @@ async function peerRespondsToAliasPing(peer: ZobLivePeerCard): Promise<boolean>
|
|
|
221
272
|
}
|
|
222
273
|
}
|
|
223
274
|
|
|
275
|
+
async function probeAndReviveZpeerCandidate(repoRoot: string, entry: ZpeerRoomPeer): Promise<ZpeerRoomPeer | undefined> {
|
|
276
|
+
if (!hasLocalSocketEndpointEvidence(entry.peer)) return undefined;
|
|
277
|
+
const nowIso = new Date().toISOString();
|
|
278
|
+
try {
|
|
279
|
+
const response = await sendZobLocalEnvelope(entry.peer.endpoint, {
|
|
280
|
+
schema: "zob.live-envelope.v1",
|
|
281
|
+
type: "ping",
|
|
282
|
+
msgId: `zpeer-candidate-probe:${entry.peer.roleId}:${Date.now()}`,
|
|
283
|
+
hops: 0,
|
|
284
|
+
timestamp: nowIso,
|
|
285
|
+
bodyStored: false,
|
|
286
|
+
}, { timeoutMs: 1_000 });
|
|
287
|
+
if (response.type !== "pong" && response.type !== "ack") return undefined;
|
|
288
|
+
const revived = { ...entry.peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard;
|
|
289
|
+
if (revived.zpeerAdhoc === true || revived.projectId !== buildZobComsProjectId(repoRoot)) {
|
|
290
|
+
writeZobLivePeerCardToProjectId(revived);
|
|
291
|
+
} else {
|
|
292
|
+
writeZobLiveTeamAgentLease(repoRoot, revived, { reason: "zpeer_candidate_probe" });
|
|
293
|
+
}
|
|
294
|
+
return { ...entry, peer: revived };
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
224
300
|
async function activeAliasCollision(repoRoot: string, self: ZobLivePeerCard, roomId: string, alias: string): Promise<ZpeerRoomPeer | undefined> {
|
|
225
301
|
for (const entry of peersInRoom(repoRoot, roomId)) {
|
|
226
|
-
if (entry.peer.sessionHash === self.sessionHash || entry.membership.alias
|
|
302
|
+
if (entry.peer.sessionHash === self.sessionHash || !zpeerAliasesEquivalent(entry.membership.alias, alias)) continue;
|
|
227
303
|
if (await peerRespondsToAliasPing(entry.peer)) return entry;
|
|
228
304
|
if (zpeerReachableStatus(entry.peer) === "online") {
|
|
229
305
|
try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort stale alias release */ }
|
|
@@ -290,7 +366,7 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
|
|
|
290
366
|
statusAliases[status].push(entry.membership.alias);
|
|
291
367
|
}
|
|
292
368
|
const onlineAliases = statusAliases.online.sort();
|
|
293
|
-
const duplicateAliases =
|
|
369
|
+
const duplicateAliases = duplicateZpeerAliasLookupKeys(onlineAliases);
|
|
294
370
|
return {
|
|
295
371
|
schema: "zob.zpeer-room-summary.v1",
|
|
296
372
|
projectId,
|
|
@@ -547,7 +623,7 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
547
623
|
deliveryMethod: result.deliveryMethod,
|
|
548
624
|
fallbackDelivery: result.fallback_delivery,
|
|
549
625
|
});
|
|
550
|
-
return { roomId, priority, interruptMode, interruptReasonHash, requireResponse: requireResponse || undefined, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, ...result };
|
|
626
|
+
return annotateZpeerStatus({ roomId, priority, interruptMode, interruptReasonHash, requireResponse: requireResponse || undefined, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined, ...result });
|
|
551
627
|
};
|
|
552
628
|
|
|
553
629
|
if (priority === "force" && !interruptReasonHash) return finish("attempt", { status: "blocked", reason: "force interrupt requires reason hash", targetAlias: targetAlias ?? undefined, taskHash, interruptStatus: "force_blocked", bodyStored: false });
|
|
@@ -560,8 +636,8 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
560
636
|
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 });
|
|
561
637
|
if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
|
|
562
638
|
if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
|
|
563
|
-
const candidates = peersInRoom(repoRoot, roomId).filter((entry) => entry.membership.alias
|
|
564
|
-
if (targetAlias
|
|
639
|
+
const candidates = peersInRoom(repoRoot, roomId).filter((entry) => zpeerAliasesEquivalent(entry.membership.alias, targetAlias) && entry.peer.sessionHash !== self.sessionHash);
|
|
640
|
+
if (zpeerAliasesEquivalent(targetAlias, senderAlias)) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
|
|
565
641
|
if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
|
|
566
642
|
let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
|
|
567
643
|
if (liveCandidates.length > 1) {
|
|
@@ -575,6 +651,14 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
575
651
|
}
|
|
576
652
|
liveCandidates = responsiveCandidates;
|
|
577
653
|
}
|
|
654
|
+
if (liveCandidates.length === 0) {
|
|
655
|
+
const probedCandidates: ZpeerRoomPeer[] = [];
|
|
656
|
+
for (const entry of candidates) {
|
|
657
|
+
const revived = await probeAndReviveZpeerCandidate(repoRoot, entry);
|
|
658
|
+
if (revived) probedCandidates.push(revived);
|
|
659
|
+
}
|
|
660
|
+
liveCandidates = probedCandidates;
|
|
661
|
+
}
|
|
578
662
|
if (liveCandidates.length === 0) {
|
|
579
663
|
const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
|
|
580
664
|
// WS-ZH4: last-resort fallback delivery. When local-socket transport is blocked
|
|
@@ -582,14 +666,15 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
582
666
|
// pane), deliver the prompt best-effort. This is NOT verified delivery:
|
|
583
667
|
// outputHash/confirmation_ref stay absent and bodyStored stays false; the receiver,
|
|
584
668
|
// if it answers at all, answers via its own local_socket later (that ledgered reply
|
|
585
|
-
// is what WS-Q5 counts).
|
|
586
|
-
//
|
|
587
|
-
|
|
669
|
+
// is what WS-Q5 counts). Urgent/steer is eligible when the caller explicitly wires
|
|
670
|
+
// a safe fallback hook; force/abort is never eligible. If no fallback is supplied or
|
|
671
|
+
// it fails/declines, fall through to the standard blocked result unchanged.
|
|
672
|
+
if (options.fallbackDelivery && targetAlias && priority !== "force") {
|
|
588
673
|
try {
|
|
589
|
-
const fallback = await options.fallbackDelivery({ targetAlias, roomId, taskHash, prompt: transientPrompt });
|
|
674
|
+
const fallback = await options.fallbackDelivery({ targetAlias, senderAlias, roomId, taskHash, prompt: transientPrompt, priority, interruptMode, requireResponse, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined });
|
|
590
675
|
if (fallback.delivered) {
|
|
591
676
|
const fallbackMsgId = `zpeer-fallback:${self.sessionHash.slice(0, 8)}:${Date.now()}`;
|
|
592
|
-
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);
|
|
677
|
+
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);
|
|
593
678
|
}
|
|
594
679
|
} catch {
|
|
595
680
|
// best-effort fallback failed; fall through to the standard blocked result.
|
|
@@ -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);
|
|
@@ -17,6 +17,7 @@ import { loadActiveZagentScopedMode } from "../events.js";
|
|
|
17
17
|
import { resolveRuleProfile } from "../../domains/governance/rules.js";
|
|
18
18
|
import type { HarnessRuntimeState } from "../state.js";
|
|
19
19
|
import { applyMode, renderHarnessWidget } from "../widget.js";
|
|
20
|
+
import { recordZpeerRuntimeEvent } from "../zpeer-events.js";
|
|
20
21
|
|
|
21
22
|
function zpeerCommandProfileId(ctx: ExtensionCommandContext): string {
|
|
22
23
|
const sessionIdentity = ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
|
|
@@ -1140,18 +1141,21 @@ async function executeZteamTmuxActionPlan(_repoRoot: string, plan: ZteamTmuxActi
|
|
|
1140
1141
|
}
|
|
1141
1142
|
|
|
1142
1143
|
export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeState): void {
|
|
1143
|
-
const rememberZpeerEvent = (event: { kind: NonNullable<typeof state.zobLive.lastEvent>["kind"]; roomId?: string; fromAlias?: string; toAlias?: string; status: string; reason?: string; msgId?: string; taskHash?: string; outputHash?: string; priority?: ZpeerInterruptPriority; interruptMode?: ZpeerInterruptMode; interruptStatus?: ZpeerInterruptStatus }):
|
|
1144
|
-
|
|
1144
|
+
const rememberZpeerEvent = (event: { kind: NonNullable<typeof state.zobLive.lastEvent>["kind"]; roomId?: string; fromAlias?: string; toAlias?: string; status: string; reason?: string; msgId?: string; taskHash?: string; outputHash?: string; priority?: ZpeerInterruptPriority; interruptMode?: ZpeerInterruptMode; interruptStatus?: ZpeerInterruptStatus }): NonNullable<typeof state.zobLive.lastEvent> | undefined => {
|
|
1145
|
+
const recorded = recordZpeerRuntimeEvent(state, event);
|
|
1146
|
+
return recorded.accepted ? recorded.event : undefined;
|
|
1145
1147
|
};
|
|
1146
1148
|
|
|
1147
|
-
const emitZpeerEvent = (event: Parameters<typeof rememberZpeerEvent>[0]):
|
|
1148
|
-
rememberZpeerEvent(event);
|
|
1149
|
+
const emitZpeerEvent = (event: Parameters<typeof rememberZpeerEvent>[0]): boolean => {
|
|
1150
|
+
const recorded = rememberZpeerEvent(event);
|
|
1151
|
+
if (!recorded) return false;
|
|
1149
1152
|
void pi.sendMessage({
|
|
1150
1153
|
customType: "zob-zpeer-event",
|
|
1151
1154
|
content: `ZPeer ${event.kind} ${event.fromAlias ? `@${event.fromAlias}` : "?"} → ${event.toAlias ? `@${event.toAlias}` : "?"} ${event.status}`,
|
|
1152
1155
|
display: true,
|
|
1153
|
-
details: { ...
|
|
1156
|
+
details: { ...recorded },
|
|
1154
1157
|
}, { triggerTurn: false });
|
|
1158
|
+
return true;
|
|
1155
1159
|
};
|
|
1156
1160
|
|
|
1157
1161
|
pi.registerCommand("zagent", {
|
|
@@ -1682,6 +1686,7 @@ export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
1682
1686
|
maxReinjects: sendMode.maxReinjects,
|
|
1683
1687
|
onFeedback: (feedback) => {
|
|
1684
1688
|
feedbackEmittedTerminal = feedback.result.status === "waiting" || feedback.result.status === "reply" || feedback.result.status === "completed" || feedback.result.status === "blocked" || feedback.result.status === "error" || feedback.result.status === "timeout" || feedback.result.status === "expired" || feedback.result.status === "required_response_expired";
|
|
1689
|
+
if (feedback.kind === "waiting" && (sendMode.requireResponse === true || sendMode.mode !== "async")) return;
|
|
1685
1690
|
const feedbackRoomId = feedback.result.roomId ?? eventRoomId;
|
|
1686
1691
|
emitZpeerEvent({ kind: feedback.kind, roomId: feedbackRoomId, fromAlias: state.zobLive.peerCard ? peerAliasInRoom(state.zobLive.peerCard, feedbackRoomId) ?? eventFromAlias : eventFromAlias, toAlias: feedback.result.targetAlias ?? targetAlias, status: feedback.result.status, reason: feedback.result.reason, msgId: feedback.result.msgId, taskHash: feedback.result.taskHash, outputHash: feedback.result.outputHash, priority: feedback.result.priority ?? sendMode.priority, interruptMode: feedback.result.interruptMode ?? sendMode.interruptMode, interruptStatus: feedback.result.interruptStatus });
|
|
1687
1692
|
},
|
|
@@ -11,7 +11,7 @@ import { bindZobLocalEndpoint, makeZobLocalEndpoint, sendZobLocalEnvelope } from
|
|
|
11
11
|
import { readZobComsV2Policy } from "../domains/coms/coms-v2/policy.js";
|
|
12
12
|
import { claimZobLiveTeamAgentLease, pruneExpiredZobLivePeers, registerCurrentZobLivePeer, releaseZobLiveTeamAgentLease, touchCurrentZobLivePeer, unregisterCurrentZobLivePeer, writeZobLivePeerCard } from "../domains/coms/coms-v2/registry.js";
|
|
13
13
|
import { clearZpeerNewCarryoverProfile, readZpeerLocalProfile, readZpeerNewCarryoverProfile, writeZpeerLocalProfileFromPeer, writeZpeerNewCarryoverProfile, zpeerProfileIdIsSharedFallback } from "../domains/coms/coms-v2/zpeer-profile.js";
|
|
14
|
-
import { buildZpeerPeerRoomSummaries, ensureZpeerFields, refreshZpeerSelf } from "../domains/coms/coms-v2/zpeer.js";
|
|
14
|
+
import { buildZpeerPeerRoomSummaries, ensureZpeerFields, refreshZpeerSelf, zpeerAliasesEquivalent } from "../domains/coms/coms-v2/zpeer.js";
|
|
15
15
|
import type { ZpeerRoomMembership } from "../domains/coms/coms-v2/types.js";
|
|
16
16
|
import { buildZobLiveResponseEnvelope } from "../domains/coms/coms-v2/response-capture.js";
|
|
17
17
|
import { writeZobComsRedactedCapture } from "../domains/coms/coms-v2/transcript-capture.js";
|
|
@@ -40,6 +40,7 @@ import { capturePlanArtifact } from "./plan-capture.js";
|
|
|
40
40
|
import { redactPlanTodosBlockForDisplay } from "../domains/plan/plan-todos.js";
|
|
41
41
|
import { applyMode, renderHarnessWidget } from "./widget.js";
|
|
42
42
|
import { createStopRestoreCandidate, markStopRestoreAssistantMessage, markStopRestoreToolVisible } from "./stop-restore.js";
|
|
43
|
+
import { recordZpeerRuntimeEvent } from "./zpeer-events.js";
|
|
43
44
|
|
|
44
45
|
function safelyUpdateZobLivePeer(repoRoot: string, action: "register" | "touch" | "unregister"): void {
|
|
45
46
|
try {
|
|
@@ -51,14 +52,8 @@ function safelyUpdateZobLivePeer(repoRoot: string, action: "register" | "touch"
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function setZpeerLastEvent(state: HarnessRuntimeState, event: Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored"> & { at?: string }):
|
|
55
|
-
state.
|
|
56
|
-
...event,
|
|
57
|
-
at: event.at ?? new Date().toISOString(),
|
|
58
|
-
localOnly: true,
|
|
59
|
-
networkEnabled: false,
|
|
60
|
-
bodyStored: false,
|
|
61
|
-
};
|
|
55
|
+
function setZpeerLastEvent(state: HarnessRuntimeState, event: Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored" | "terminal" | "statusRank" | "superseded" | "supersededByStatus"> & { at?: string }): ZobLiveLastEvent {
|
|
56
|
+
return recordZpeerRuntimeEvent(state, event).event;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
function clearPassivePeerWaitForResponse(state: HarnessRuntimeState, envelope: { msgId?: string; runId?: string; sender?: string; type?: string }): void {
|
|
@@ -69,7 +64,7 @@ function clearPassivePeerWaitForResponse(state: HarnessRuntimeState, envelope: {
|
|
|
69
64
|
return;
|
|
70
65
|
}
|
|
71
66
|
const responseRoomId = envelope.runId?.startsWith("zpeer:") ? envelope.runId.slice("zpeer:".length) : undefined;
|
|
72
|
-
if (!wait.msgId && wait.targetAlias && envelope.sender
|
|
67
|
+
if (!wait.msgId && wait.targetAlias && zpeerAliasesEquivalent(envelope.sender, wait.targetAlias) && (!wait.roomId || !responseRoomId || wait.roomId === responseRoomId)) {
|
|
73
68
|
state.zobLive.passivePeerWait = undefined;
|
|
74
69
|
}
|
|
75
70
|
}
|
|
@@ -1060,7 +1055,10 @@ async function sendInboundZobLiveResponse(pi: ExtensionAPI, state: HarnessRuntim
|
|
|
1060
1055
|
const activeInbound = activeZpeerInboundForResponse(state);
|
|
1061
1056
|
const inbound = activeInbound ?? state.zobLive.inbound;
|
|
1062
1057
|
if (!inbound || inbound.responseSent || !inbound.envelope.replyEndpoint) return;
|
|
1063
|
-
|
|
1058
|
+
// ZOB-COMS-GUARDS: a late reply after expiry is now permitted (the watchdog
|
|
1059
|
+
// already stopped reinjecting; sender-side pendingReplies resolves to no-op).
|
|
1060
|
+
// Dropping this early-out recovers nudge/reply paths (e.g. a late oracle
|
|
1061
|
+
// review) without weakening the real anti-abuse (reinject cap + sender expire).
|
|
1064
1062
|
if (state.zobLive.inboundByMsgId && !activeInbound) return;
|
|
1065
1063
|
const responseText = latestAssistantText(event);
|
|
1066
1064
|
if (!responseText.trim()) return;
|
|
@@ -1118,7 +1116,7 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
1118
1116
|
const taskHash = typeof details.taskHash === "string" ? details.taskHash : undefined;
|
|
1119
1117
|
const outputHash = typeof details.outputHash === "string" ? details.outputHash : undefined;
|
|
1120
1118
|
const route = fromAlias || toAlias ? `${fromAlias ? `@${fromAlias}` : "?"} → ${toAlias ? `@${toAlias}` : "?"}` : "room status";
|
|
1121
|
-
const statusColor = status === "completed" || status === "sent" || status === "prompt_received" || status === "response_sent" ? "success" : status === "blocked" || status === "timeout" || status === "error" || status === "required_response_expired" ? "warning" : "muted";
|
|
1119
|
+
const statusColor = status === "reply" || status === "completed" || status === "sent" || status === "delivered" || status === "prompt_received" || status === "response_sent" ? "success" : status === "blocked" || status === "timeout" || status === "error" || status === "expired" || status === "required_response_expired" ? "warning" : "muted";
|
|
1122
1120
|
const line = [
|
|
1123
1121
|
theme.fg("accent", "◆ ZPeer"),
|
|
1124
1122
|
theme.fg("muted", kind),
|
|
@@ -49,6 +49,10 @@ export interface ZobLiveLastEvent {
|
|
|
49
49
|
priority?: ZpeerInterruptPriority;
|
|
50
50
|
interruptMode?: ZpeerInterruptMode;
|
|
51
51
|
interruptStatus?: ZpeerInterruptStatus;
|
|
52
|
+
terminal?: boolean;
|
|
53
|
+
statusRank?: number;
|
|
54
|
+
superseded?: boolean;
|
|
55
|
+
supersededByStatus?: string;
|
|
52
56
|
at: string;
|
|
53
57
|
localOnly: true;
|
|
54
58
|
networkEnabled: false;
|
|
@@ -98,6 +102,7 @@ export interface ZobLiveRuntimeState {
|
|
|
98
102
|
inboundQueue?: string[];
|
|
99
103
|
activeInboundMsgId?: string;
|
|
100
104
|
lastEvent?: ZobLiveLastEvent;
|
|
105
|
+
latestZpeerEventByMsgId?: Record<string, ZobLiveLastEvent>;
|
|
101
106
|
passivePeerWait?: ZobPassivePeerWaitState;
|
|
102
107
|
leaseOwned?: boolean;
|
|
103
108
|
leaseStatus?: "owned" | "blocked" | "unavailable";
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
1
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import { readZobComsV2Policy } from "../domains/coms/coms-v2/policy.js";
|
|
3
6
|
import { readZobLiveRegistrySnapshot } from "../domains/coms/coms-v2/registry.js";
|
|
4
|
-
import { peerAliasInRoom, refreshZpeerSelf, safeZpeerAlias, safeZpeerRoomId, sendZpeerPrompt, type ZpeerSendMode, type ZpeerSendResult } from "../domains/coms/coms-v2/zpeer.js";
|
|
7
|
+
import { peerAliasInRoom, refreshZpeerSelf, safeZpeerAlias, safeZpeerRoomId, sendZpeerPrompt, zpeerAliasesEquivalent, zpeerAliasLookupKey, type ZpeerFallbackDelivery, type ZpeerSendMode, type ZpeerSendResult } from "../domains/coms/coms-v2/zpeer.js";
|
|
5
8
|
import { buildZobLiveEnvelope, type ZpeerInterruptMode, type ZpeerInterruptPriority, type ZpeerInterruptStatus } from "../domains/coms/coms-v2/envelope.js";
|
|
6
9
|
import { sendZobLocalEnvelope } from "../domains/coms/coms-v2/local-transport.js";
|
|
7
10
|
import { buildZobLiveResponseEnvelope } from "../domains/coms/coms-v2/response-capture.js";
|
|
@@ -31,11 +34,61 @@ import {
|
|
|
31
34
|
} from "../domains/topology/coms.js";
|
|
32
35
|
import { loadTeamDefinition, validateTeamDefinition } from "../domains/topology/teams.js";
|
|
33
36
|
import type { HarnessRuntimeState } from "./state.js";
|
|
37
|
+
import { isCurrentZpeerRuntimeEvent, recordZpeerRuntimeEvent } from "./zpeer-events.js";
|
|
34
38
|
|
|
35
39
|
const SHA256_HEX = /^[a-f0-9]{64}$/i;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
// ZOB-COMS-GUARDS: per-agent zpeer rate caps are env-configurable with a finite
|
|
41
|
+
// ceiling (never Infinity) so trusted closed topologies (e.g. project-transposer's
|
|
42
|
+
// 64-agent fan-out) don't trip the default 50/10/3 while real floods are still
|
|
43
|
+
// bounded. Defaults unchanged for open/hostile topologies.
|
|
44
|
+
const clampRate = (v: string | undefined, fallback: number): number => Math.max(1, Math.min(1000, Number.parseInt(v ?? "") || fallback));
|
|
45
|
+
const ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = clampRate(process.env.ZOB_ZPEER_ASK_RATE_LIMIT_PER_MINUTE, 50);
|
|
46
|
+
const ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = clampRate(process.env.ZOB_ZPEER_URGENT_RATE_LIMIT_PER_MINUTE, 10);
|
|
47
|
+
const ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = clampRate(process.env.ZOB_ZPEER_FORCE_RATE_LIMIT_PER_MINUTE, 3);
|
|
48
|
+
// ZOB-COMS-GUARDS: the loop-keyword block (reject any zpeer_ask whose message
|
|
49
|
+
// text contains 'zpeer_ask'/'/zpeer') over-fires on trusted topologies whose
|
|
50
|
+
// prompts legitimately quote these tokens. Env-bypassed for trusted topologies;
|
|
51
|
+
// default (unset) keeps current behavior for open/hostile use. Genuine anti-loop
|
|
52
|
+
// (duplicate guard below + reinject cap + sender-side expire) is preserved.
|
|
53
|
+
const ZPEER_RECURSION_KEYWORD_GUARD_DISABLED = /^(1|true|yes|on)$/i.test(process.env.ZOB_ZPEER_DISABLE_RECURSION_KEYWORD_GUARD ?? "");
|
|
54
|
+
|
|
55
|
+
type ZpeerFallbackHookFn = ZpeerFallbackDelivery;
|
|
56
|
+
|
|
57
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
58
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asZpeerFallbackHookFn(moduleValue: unknown): ZpeerFallbackHookFn | undefined {
|
|
62
|
+
if (typeof moduleValue === "function") return moduleValue as ZpeerFallbackHookFn;
|
|
63
|
+
if (!isObjectRecord(moduleValue)) return undefined;
|
|
64
|
+
const direct = moduleValue.zpeerFallbackDelivery ?? moduleValue.fallbackDelivery;
|
|
65
|
+
if (typeof direct === "function") return direct as ZpeerFallbackHookFn;
|
|
66
|
+
const defaultExport = moduleValue.default;
|
|
67
|
+
if (typeof defaultExport === "function") return defaultExport as ZpeerFallbackHookFn;
|
|
68
|
+
if (isObjectRecord(defaultExport)) {
|
|
69
|
+
const nested = defaultExport.zpeerFallbackDelivery ?? defaultExport.fallbackDelivery;
|
|
70
|
+
if (typeof nested === "function") return nested as ZpeerFallbackHookFn;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildProjectTransposerFallbackDelivery(repoRoot: string): ZpeerFallbackDelivery | undefined {
|
|
76
|
+
const hookPath = process.env.PROJECT_TRANSPOSER_ZPEER_FALLBACK_HOOK?.trim();
|
|
77
|
+
if (!hookPath) return undefined;
|
|
78
|
+
const resolvedHookPath = isAbsolute(hookPath) ? hookPath : join(repoRoot, hookPath);
|
|
79
|
+
return async (input) => {
|
|
80
|
+
const imported = await import(pathToFileURL(resolvedHookPath).href);
|
|
81
|
+
const fn = asZpeerFallbackHookFn(imported);
|
|
82
|
+
if (!fn) return { delivered: false, reason: "fallback_hook_missing_function" };
|
|
83
|
+
const result = await fn(input);
|
|
84
|
+
if (!isObjectRecord(result)) return { delivered: false, reason: "fallback_hook_invalid_result" };
|
|
85
|
+
return {
|
|
86
|
+
delivered: result.delivered === true,
|
|
87
|
+
target: typeof result.target === "string" ? result.target : undefined,
|
|
88
|
+
reason: typeof result.reason === "string" ? result.reason : undefined,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
}
|
|
39
92
|
|
|
40
93
|
function breakGlassApprovalPresent(): boolean {
|
|
41
94
|
return SHA256_HEX.test(process.env.ZOB_COMS_BREAK_GLASS_APPROVAL_HASH ?? "");
|
|
@@ -97,10 +150,10 @@ function normalizeZpeerInterrupt(params: ZpeerAskToolParams): { priority: ZpeerI
|
|
|
97
150
|
function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolParams, selfAlias?: string, currentRoomId = "default", priority: ZpeerInterruptPriority = "normal"): string | undefined {
|
|
98
151
|
const targetAlias = safeZpeerAlias(params.targetAlias);
|
|
99
152
|
if (!targetAlias) return "invalid target alias";
|
|
100
|
-
if (selfAlias && targetAlias
|
|
153
|
+
if (selfAlias && zpeerAliasesEquivalent(targetAlias, selfAlias)) return "cannot send to self";
|
|
101
154
|
const roomId = safeZpeerRoomId(params.roomId) ?? currentRoomId;
|
|
102
155
|
if (params.roomId && !safeZpeerRoomId(params.roomId)) return "invalid room id";
|
|
103
|
-
if (/\b(zpeer_ask|\/zpeer)\b/i.test(params.message)) return "loop guard blocked recursive ZPeer instruction";
|
|
156
|
+
if (!ZPEER_RECURSION_KEYWORD_GUARD_DISABLED && /\b(zpeer_ask|\/zpeer)\b/i.test(params.message)) return "loop guard blocked recursive ZPeer instruction";
|
|
104
157
|
const messageHash = sha256(params.message);
|
|
105
158
|
const now = Date.now();
|
|
106
159
|
const windowMs = 60_000;
|
|
@@ -111,8 +164,9 @@ function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolPara
|
|
|
111
164
|
if (guard.count >= ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window`;
|
|
112
165
|
if (priority === "urgent" && urgentCount >= ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE} urgent ZPeer asks per 60s window`;
|
|
113
166
|
if (priority === "force" && forceCount >= ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE} force ZPeer asks per 60s window`;
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
const targetAliasLookupKey = zpeerAliasLookupKey(targetAlias) ?? targetAlias;
|
|
168
|
+
if (guard.lastRoomId === roomId && zpeerAliasLookupKey(guard.lastTargetAlias) === targetAliasLookupKey && guard.lastMessageHash === messageHash) return "loop guard blocked duplicate room/target/message in ask window";
|
|
169
|
+
state.zobLive.zpeerAskGuard = { windowStartedMs: guard.windowStartedMs, count: guard.count + 1, urgentCount: priority === "urgent" ? urgentCount + 1 : urgentCount, forceCount: priority === "force" ? forceCount + 1 : forceCount, lastRoomId: roomId, lastTargetAlias: targetAliasLookupKey, lastMessageHash: messageHash };
|
|
116
170
|
return undefined;
|
|
117
171
|
}
|
|
118
172
|
|
|
@@ -233,16 +287,29 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
233
287
|
const requestedFromAlias = peerAliasInRoom(self, requestedRoomId) ?? self.zpeerAlias;
|
|
234
288
|
const interrupt = normalizeZpeerInterrupt(params);
|
|
235
289
|
const guardReason = interrupt.error ?? zpeerAskGuardBlock(state, params, requestedFromAlias, requestedRoomId, interrupt.priority);
|
|
236
|
-
const
|
|
290
|
+
const queuedZpeerEvents: Array<{ event: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>; content: string; details: Record<string, unknown> }> = [];
|
|
291
|
+
const flushZpeerAskEvents = async (): Promise<void> => {
|
|
292
|
+
for (const queued of queuedZpeerEvents) {
|
|
293
|
+
if (!isCurrentZpeerRuntimeEvent(state, queued.event)) continue;
|
|
294
|
+
await Promise.resolve(pi.sendMessage({
|
|
295
|
+
customType: "zob-zpeer-event",
|
|
296
|
+
content: queued.content,
|
|
297
|
+
display: true,
|
|
298
|
+
details: queued.details,
|
|
299
|
+
}, { triggerTurn: false }));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const emitZpeerAskEvent = (event: { kind: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>["kind"]; status: string; reason?: string; msgId?: string; roomId?: string; taskHash?: string; outputHash?: string; interruptStatus?: ZpeerInterruptStatus }): boolean => {
|
|
237
303
|
const eventRoomId = event.roomId ?? requestedRoomId;
|
|
238
304
|
const fromAlias = peerAliasInRoom(self, eventRoomId) ?? requestedFromAlias;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
305
|
+
const recorded = recordZpeerRuntimeEvent(state, { kind: event.kind, roomId: eventRoomId, fromAlias, toAlias: targetAlias, status: event.status, reason: event.reason, msgId: event.msgId, taskHash: event.taskHash, outputHash: event.outputHash, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus: event.interruptStatus });
|
|
306
|
+
if (!recorded.accepted) return false;
|
|
307
|
+
queuedZpeerEvents.push({
|
|
308
|
+
event: recorded.event,
|
|
242
309
|
content: `ZPeer agent-request @${fromAlias ?? "?"} → @${targetAlias} ${event.status}`,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
310
|
+
details: { ...recorded.event, source: "agent-request", mode, priority: interrupt.priority, interruptMode: interrupt.interruptMode, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
311
|
+
});
|
|
312
|
+
return true;
|
|
246
313
|
};
|
|
247
314
|
const taskHash = params.message.trim() ? sha256(params.message) : undefined;
|
|
248
315
|
if (guardReason) {
|
|
@@ -251,6 +318,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
251
318
|
const result = { schema: "zob.zpeer-ask-result.v1", status: "blocked", reason: guardReason, targetAlias, taskHash, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus, bodyStored: false };
|
|
252
319
|
emitZpeerAskEvent({ kind: "blocked", status: "blocked", reason: guardReason, taskHash, interruptStatus });
|
|
253
320
|
pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-ask.v1", action: "agent_request_blocked", mode, status: "blocked", priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus, reasonHash: sha256(guardReason), targetAliasHash: sha256(targetAlias), roomIdHash: sha256(requestedRoomId), taskHash, reasonInputHash: params.reason ? sha256(params.reason) : undefined, interruptReasonHash: interrupt.interruptReasonHash, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
|
|
321
|
+
await flushZpeerAskEvents();
|
|
254
322
|
return { content: [{ type: "text", text: `zpeer_ask blocked: ${guardReason}` }], details: result };
|
|
255
323
|
}
|
|
256
324
|
const timeoutMs = boundedZpeerAskTimeoutMs(mode, params.timeoutMs);
|
|
@@ -266,6 +334,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
266
334
|
requireResponse,
|
|
267
335
|
responseTimeoutMs: timeoutMs,
|
|
268
336
|
maxReinjects,
|
|
337
|
+
fallbackDelivery: buildProjectTransposerFallbackDelivery(ctx.cwd),
|
|
269
338
|
onFeedback: (feedback) => {
|
|
270
339
|
feedbackEmittedTerminal = feedback.result.status === "waiting" || feedback.result.status === "reply" || feedback.result.status === "completed" || feedback.result.status === "blocked" || feedback.result.status === "error" || feedback.result.status === "timeout" || feedback.result.status === "expired" || feedback.result.status === "required_response_expired";
|
|
271
340
|
emitZpeerAskEvent({ kind: feedback.kind, roomId: feedback.result.roomId, status: feedback.result.status, reason: feedback.result.reason, msgId: feedback.result.msgId, taskHash: feedback.result.taskHash, outputHash: feedback.result.outputHash, interruptStatus: feedback.result.interruptStatus });
|
|
@@ -273,13 +342,14 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
273
342
|
});
|
|
274
343
|
if (!feedbackEmittedTerminal) emitZpeerAskEvent({ kind: zpeerTerminalKind(result.status), roomId: result.roomId, status: result.status, reason: result.reason, msgId: result.msgId, taskHash: result.taskHash, outputHash: result.outputHash, interruptStatus: result.interruptStatus });
|
|
275
344
|
updatePassivePeerWaitState(state, result, { roomId: requestedRoomId, targetAlias });
|
|
276
|
-
pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-ask.v1", action: "agent_request", mode, status: result.status, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus: result.interruptStatus, reasonHash: result.reason ? sha256(result.reason) : undefined, msgId: result.msgId, targetAliasHash: result.targetAlias ? sha256(result.targetAlias) : sha256(targetAlias), roomIdHash: sha256(result.roomId ?? requestedRoomId), taskHash: result.taskHash, outputHash: result.outputHash, reasonInputHash: params.reason ? sha256(params.reason) : undefined, interruptReasonHash: interrupt.interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy: result.responseRequiredBy, responseTimeoutMs: result.responseTimeoutMs, maxReinjects: result.maxReinjects, responseReceived: result.responseReceived, deliveryStatus: result.deliveryStatus, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
|
|
345
|
+
pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-ask.v1", action: "agent_request", mode, status: result.status, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus: result.interruptStatus, reasonHash: result.reason ? sha256(result.reason) : undefined, msgId: result.msgId, targetAliasHash: result.targetAlias ? sha256(result.targetAlias) : sha256(targetAlias), roomIdHash: sha256(result.roomId ?? requestedRoomId), taskHash: result.taskHash, outputHash: result.outputHash, reasonInputHash: params.reason ? sha256(params.reason) : undefined, interruptReasonHash: interrupt.interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy: result.responseRequiredBy, responseTimeoutMs: result.responseTimeoutMs, maxReinjects: result.maxReinjects, responseReceived: result.responseReceived, deliveryStatus: result.deliveryStatus, deliveryMethod: result.deliveryMethod, fallbackDelivery: result.fallback_delivery, bestEffort: result.best_effort, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
|
|
277
346
|
const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
|
|
278
347
|
const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
|
|
279
348
|
const transientReplyText = (result.status === "reply" || result.status === "completed") && result.transientResponse
|
|
280
349
|
? `\n\nTransient ZPeer reply (not stored in .pi/coms):\n${result.transientResponse}`
|
|
281
350
|
: "";
|
|
282
351
|
const interruptSuffix = result.interruptStatus ? ` interrupt=${result.interruptStatus}` : interrupt.priority !== "normal" ? ` priority=${interrupt.priority}` : "";
|
|
352
|
+
await flushZpeerAskEvents();
|
|
283
353
|
return { content: [{ type: "text", text: ok ? `zpeer_ask ${result.status}: @${result.targetAlias ?? targetAlias}${interruptSuffix}${result.outputHash ? ` outputHash=${result.outputHash}` : ""}${passiveWaitSuffix}${transientReplyText}` : `zpeer_ask ${result.status}: ${result.reason ?? "see metadata"}${interruptSuffix}` }], details: { schema: "zob.zpeer-ask-result.v1", mode, ...result } };
|
|
284
354
|
},
|
|
285
355
|
});
|
|
@@ -295,7 +365,13 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
295
365
|
const responseText = params.message ?? "";
|
|
296
366
|
const outputHash = responseText.trim() ? sha256(responseText) : undefined;
|
|
297
367
|
const inbound = msgId ? state?.zobLive.inboundByMsgId?.[msgId] : undefined;
|
|
298
|
-
const block = !state ? "zpeer runtime state unavailable" : !msgId ? "msgId is required" : !responseText.trim() ? "message is required" : !inbound ? "no active inbound ZPeer message for msgId" : inbound.responseSent || inbound.requiredResponseStatus === "replied" ? "ZPeer msgId already answered" :
|
|
368
|
+
const block = !state ? "zpeer runtime state unavailable" : !msgId ? "msgId is required" : !responseText.trim() ? "message is required" : !inbound ? "no active inbound ZPeer message for msgId" : inbound.responseSent || inbound.requiredResponseStatus === "replied" ? "ZPeer msgId already answered" : // ZOB-COMS-GUARDS: a late reply after the requireResponse watchdog expired is
|
|
369
|
+
// now PERMITTED. The watchdog already stops reinjecting once reinjectCount hits
|
|
370
|
+
// maxReinjects (events.ts) and the sender's pendingReplies entry resolves to a
|
|
371
|
+
// no-op on a late envelope, so a late reply is harmless at the sender and
|
|
372
|
+
// recovers nudge/reply paths (e.g. a late oracle review). The 'replied' /
|
|
373
|
+
// responseSent guard above still prevents double-replies.
|
|
374
|
+
!inbound.envelope.replyEndpoint ? "ZPeer inbound msgId has no reply endpoint" : undefined;
|
|
299
375
|
if (block) {
|
|
300
376
|
pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-reply.v1", action: "reply_blocked", status: "blocked", reasonHash: sha256(block), msgId, outputHash, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
|
|
301
377
|
return { content: [{ type: "text", text: `zpeer_reply blocked: ${block}` }], details: { schema: "zob.zpeer-reply-result.v1", status: "blocked", reason: block, msgId, outputHash, bodyStored: false, localOnly: true, networkEnabled: false } };
|
|
@@ -314,8 +390,8 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
314
390
|
state.zobLive.activeInboundMsgId = undefined;
|
|
315
391
|
state.zobLive.inboundQueue = (state.zobLive.inboundQueue ?? []).filter((candidate) => candidate !== inbound.envelope.msgId);
|
|
316
392
|
const roomId = inbound.envelope.runId?.startsWith("zpeer:") ? inbound.envelope.runId.slice("zpeer:".length) : undefined;
|
|
317
|
-
|
|
318
|
-
void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...
|
|
393
|
+
const recorded = recordZpeerRuntimeEvent(state, { kind: "response_sent", roomId, fromAlias: inbound.envelope.receiver, toAlias: inbound.envelope.sender, status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, priority: inbound.priority, interruptMode: inbound.interruptMode });
|
|
394
|
+
if (recorded.accepted) void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...recorded.event } }, { triggerTurn: false });
|
|
319
395
|
pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-reply.v1", action: "reply", status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, priority: inbound.priority, interruptMode: inbound.interruptMode, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
|
|
320
396
|
return { content: [{ type: "text", text: `zpeer_reply sent: msgId=${inbound.envelope.msgId} outputHash=${outputHash}` }], details: { schema: "zob.zpeer-reply-result.v1", status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, bodyStored: false, localOnly: true, networkEnabled: false } };
|
|
321
397
|
} catch (error) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { annotateZpeerStatus, shouldAcceptZpeerStatusUpdate } from "../domains/coms/coms-v2/zpeer-status.js";
|
|
2
|
+
import type { HarnessRuntimeState, ZobLiveLastEvent } from "./state.js";
|
|
3
|
+
|
|
4
|
+
export type ZpeerRuntimeEventInput = Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored" | "terminal" | "statusRank" | "superseded" | "supersededByStatus"> & { at?: string };
|
|
5
|
+
|
|
6
|
+
export interface ZpeerRuntimeEventRecordResult {
|
|
7
|
+
accepted: boolean;
|
|
8
|
+
event: ZobLiveLastEvent;
|
|
9
|
+
current?: ZobLiveLastEvent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function materializeZpeerRuntimeEvent(event: ZpeerRuntimeEventInput): ZobLiveLastEvent {
|
|
13
|
+
return annotateZpeerStatus({
|
|
14
|
+
...event,
|
|
15
|
+
at: event.at ?? new Date().toISOString(),
|
|
16
|
+
localOnly: true as const,
|
|
17
|
+
networkEnabled: false as const,
|
|
18
|
+
bodyStored: false as const,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function recordZpeerRuntimeEvent(state: HarnessRuntimeState, event: ZpeerRuntimeEventInput): ZpeerRuntimeEventRecordResult {
|
|
23
|
+
const next = materializeZpeerRuntimeEvent(event);
|
|
24
|
+
if (!next.msgId) {
|
|
25
|
+
state.zobLive.lastEvent = next;
|
|
26
|
+
return { accepted: true, event: next };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
state.zobLive.latestZpeerEventByMsgId ??= {};
|
|
30
|
+
const current = state.zobLive.latestZpeerEventByMsgId[next.msgId];
|
|
31
|
+
if (!shouldAcceptZpeerStatusUpdate(current, next)) {
|
|
32
|
+
return {
|
|
33
|
+
accepted: false,
|
|
34
|
+
current,
|
|
35
|
+
event: {
|
|
36
|
+
...next,
|
|
37
|
+
superseded: true,
|
|
38
|
+
supersededByStatus: current?.status,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
state.zobLive.latestZpeerEventByMsgId[next.msgId] = next;
|
|
44
|
+
state.zobLive.lastEvent = next;
|
|
45
|
+
return { accepted: true, event: next, current: next };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isCurrentZpeerRuntimeEvent(state: HarnessRuntimeState, event: ZobLiveLastEvent): boolean {
|
|
49
|
+
if (!event.msgId) return true;
|
|
50
|
+
const current = state.zobLive.latestZpeerEventByMsgId?.[event.msgId];
|
|
51
|
+
if (!current) return true;
|
|
52
|
+
return current.at === event.at && current.status === event.status && current.kind === event.kind;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@ const previousZpeerProfile = process.env.ZPEER_PROFILE;
|
|
|
19
19
|
const previousComsSessionId = process.env.ZOB_COMS_SESSION_ID;
|
|
20
20
|
const previousTmuxPane = process.env.TMUX_PANE;
|
|
21
21
|
const previousZobComsRoleId = process.env.ZOB_COMS_ROLE_ID;
|
|
22
|
+
const previousZpeerStrict = process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
|
|
22
23
|
const servers = [];
|
|
23
24
|
|
|
24
25
|
function fail(message) {
|
|
@@ -101,6 +102,7 @@ async function main() {
|
|
|
101
102
|
const zpeerProfile = await import(`${compiledComsV2}/zpeer-profile.js`);
|
|
102
103
|
const toolsComs = await import(`${compiledSrc}/runtime/tools-coms.js`);
|
|
103
104
|
const localTransport = await import(`${compiledComsV2}/local-transport.js`);
|
|
105
|
+
const liveRegistry = await import(`${compiledComsV2}/registry.js`);
|
|
104
106
|
const envelope = await import(`${compiledComsV2}/envelope.js`);
|
|
105
107
|
const pendingModule = await import(`${compiledComsV2}/pending-replies.js`);
|
|
106
108
|
const hashing = await import(`${compiledSrc}/core/utils/hashing.js`);
|
|
@@ -203,6 +205,10 @@ async function main() {
|
|
|
203
205
|
const workerTwoEndpoint = join(root, 'worker-two.sock');
|
|
204
206
|
const adhocOneEndpoint = join(root, 'adhoc-one.sock');
|
|
205
207
|
const adhocTwoEndpoint = join(root, 'adhoc-two.sock');
|
|
208
|
+
const planningLeadEndpoint = join(root, 'planning-lead.sock');
|
|
209
|
+
const equivalenceSenderEndpoint = join(root, 'equivalence-sender.sock');
|
|
210
|
+
const hyphenWorkerEndpoint = join(root, 'hyphen-worker.sock');
|
|
211
|
+
const underscoreWorkerEndpoint = join(root, 'underscore-worker.sock');
|
|
206
212
|
const pendingReplies = new Map();
|
|
207
213
|
const receivedPrompts = [];
|
|
208
214
|
const receivedResponses = [];
|
|
@@ -258,6 +264,13 @@ async function main() {
|
|
|
258
264
|
receivedPrompts.push(incoming);
|
|
259
265
|
return envelope.buildZobLiveAckEnvelope(incoming);
|
|
260
266
|
}));
|
|
267
|
+
servers.push(await localTransport.bindZobLocalEndpoint(planningLeadEndpoint, async (incoming) => {
|
|
268
|
+
receivedPrompts.push(incoming);
|
|
269
|
+
return envelope.buildZobLiveAckEnvelope(incoming);
|
|
270
|
+
}));
|
|
271
|
+
servers.push(await localTransport.bindZobLocalEndpoint(equivalenceSenderEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
|
|
272
|
+
servers.push(await localTransport.bindZobLocalEndpoint(hyphenWorkerEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
|
|
273
|
+
servers.push(await localTransport.bindZobLocalEndpoint(underscoreWorkerEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
|
|
261
274
|
|
|
262
275
|
const oldHeartbeatAt = new Date(Date.now() - 180_000).toISOString();
|
|
263
276
|
let alpha = zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'alpha', roomId: 'room-one', endpoint: alphaEndpoint, endpointHash: hashing.sha256(alphaEndpoint), sha256: hashing.sha256, heartbeatAt: oldHeartbeatAt }), 'room-one', 'alpha');
|
|
@@ -273,15 +286,26 @@ async function main() {
|
|
|
273
286
|
assert(staleSummary.online === 0, `stale room-one summary expected 0 online peers before refresh, got ${staleSummary.online}`);
|
|
274
287
|
assert(staleSummary.aliases.includes('alpha') && staleSummary.aliases.includes('beta') && !staleSummary.aliases.includes('gamma'), 'stale room-one aliases must include alpha/beta only');
|
|
275
288
|
|
|
276
|
-
|
|
289
|
+
const alphaSocketVerifiedAtMs = Date.now();
|
|
290
|
+
alpha = zpeer.refreshZpeerSelf(repoRoot, alpha, undefined, undefined, undefined, { socketVerifiedAtMs: alphaSocketVerifiedAtMs });
|
|
277
291
|
beta = zpeer.refreshZpeerSelf(peerRepoRoot, beta);
|
|
278
292
|
workerOne = zpeer.refreshZpeerSelf(repoRoot, workerOne);
|
|
279
293
|
workerTwo = zpeer.refreshZpeerSelf(repoRoot, workerTwo);
|
|
294
|
+
const leaseBackedAlpha = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === alpha.sessionHash);
|
|
295
|
+
assert(typeof leaseBackedAlpha?.socketVerifiedAt === 'string' && Date.parse(leaseBackedAlpha.socketVerifiedAt) >= alphaSocketVerifiedAtMs, 'stable lease-backed registry snapshot must preserve peer socketVerifiedAt');
|
|
280
296
|
const initialSummary = zpeer.buildZpeerRoomSummary(repoRoot, alpha);
|
|
281
297
|
assert(initialSummary.peerCount === 2, `room-one summary expected 2 peers, got ${initialSummary.peerCount}`);
|
|
282
298
|
assert(initialSummary.online === 2, `room-one summary expected 2 online peers after refresh, got ${initialSummary.online}`);
|
|
283
299
|
assert(initialSummary.aliases.includes('alpha') && initialSummary.aliases.includes('beta') && !initialSummary.aliases.includes('gamma'), 'room-one aliases must include alpha/beta only');
|
|
284
300
|
|
|
301
|
+
const sweepRetainedPeer = zpeer.refreshZpeerSelf(repoRoot, zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'sweepretained', roomId: 'sweep-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'sweep-lead', roleType: 'lead' }), 'sweep-room', 'sweepretained'));
|
|
302
|
+
const sweepBefore = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === sweepRetainedPeer.sessionHash);
|
|
303
|
+
assert(sweepBefore?.status === 'online' && sweepBefore.socketVerifiedAt === undefined, 'retained-live sweep fixture must start online without socketVerifiedAt');
|
|
304
|
+
const sweepResult = await liveRegistry.sweepZobLivePeerHealth(repoRoot, { teamName: 'zob-core', sample: 99 });
|
|
305
|
+
const sweepAfter = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === sweepRetainedPeer.sessionHash);
|
|
306
|
+
assert(sweepResult.retainedLive >= 1, `health sweep must count responsive already-online peers as retainedLive, got ${sweepResult.retainedLive}`);
|
|
307
|
+
assert(sweepAfter?.status === 'online' && typeof sweepAfter.socketVerifiedAt === 'string', 'health sweep must stamp heartbeat/status/socketVerifiedAt for responsive retained-live peers');
|
|
308
|
+
|
|
285
309
|
const waitForReply = (msgId) => new Promise((resolve) => {
|
|
286
310
|
const timer = setTimeout(() => resolve({ status: 'timeout' }), 5_000);
|
|
287
311
|
pendingReplies.set(msgId, {
|
|
@@ -304,6 +328,22 @@ async function main() {
|
|
|
304
328
|
assert(adhocAsync.status === 'waiting', `ad-hoc same-room send expected waiting after ACK, got ${adhocAsync.status}${adhocAsync.reason ? `: ${adhocAsync.reason}` : ''}`);
|
|
305
329
|
assert(receivedPrompts.length === adhocPromptCountBefore + 1 && receivedPrompts.at(-1).receiver === 'adhoctwo', 'ad-hoc same-room send must deliver to the target without a stable team-agent lease');
|
|
306
330
|
|
|
331
|
+
const planningLead = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'planning_lead', roomId: 'equiv-room', endpoint: planningLeadEndpoint, endpointHash: hashing.sha256(planningLeadEndpoint), sha256: hashing.sha256, roleId: 'planning-lead', roleType: 'lead' }), 'equiv-room', 'planning_lead'), zpeerAdhoc: true });
|
|
332
|
+
const equivalenceSender = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'equivsender', roomId: 'equiv-room', endpoint: equivalenceSenderEndpoint, endpointHash: hashing.sha256(equivalenceSenderEndpoint), sha256: hashing.sha256, roleId: 'equivalence-sender', roleType: 'lead' }), 'equiv-room', 'equivsender'), zpeerAdhoc: true });
|
|
333
|
+
const planningPromptCountBefore = receivedPrompts.length;
|
|
334
|
+
const planningResult = await zpeer.sendZpeerPrompt(repoRoot, equivalenceSender, 'planning-lead', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
|
|
335
|
+
assert(planningResult.status === 'waiting', `hyphen target lookup must reach underscore live alias, got ${planningResult.status}${planningResult.reason ? `: ${planningResult.reason}` : ''}`);
|
|
336
|
+
assert(receivedPrompts.length === planningPromptCountBefore + 1 && receivedPrompts.at(-1).receiver === 'planning_lead', 'hyphen target lookup must deliver envelope to actual stored room membership alias planning_lead');
|
|
337
|
+
assert(planningLead.zpeerAlias === 'planning_lead' && zpeer.peerAliasInRoom(planningLead, 'equiv-room') === 'planning_lead', 'hyphen lookup must not migrate stored/display alias planning_lead');
|
|
338
|
+
const planningSelfBlocked = await zpeer.sendZpeerPrompt(repoRoot, planningLead, 'planning-lead', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
|
|
339
|
+
assert(planningSelfBlocked.status === 'blocked' && String(planningSelfBlocked.reason).includes('self'), 'self-send guard must treat planning-lead and planning_lead as equivalent');
|
|
340
|
+
zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'worker-one', roomId: 'equiv-room', endpoint: hyphenWorkerEndpoint, endpointHash: hashing.sha256(hyphenWorkerEndpoint), sha256: hashing.sha256, roleId: 'hyphen-worker', roleType: 'lead' }), 'equiv-room', 'worker-one'), zpeerAdhoc: true });
|
|
341
|
+
zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'worker_one', roomId: 'equiv-room', endpoint: underscoreWorkerEndpoint, endpointHash: hashing.sha256(underscoreWorkerEndpoint), sha256: hashing.sha256, roleId: 'underscore-worker', roleType: 'lead' }), 'equiv-room', 'worker_one'), zpeerAdhoc: true });
|
|
342
|
+
const ambiguousResult = await zpeer.sendZpeerPrompt(repoRoot, equivalenceSender, 'worker-one', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
|
|
343
|
+
assert(ambiguousResult.status === 'blocked' && String(ambiguousResult.reason).includes('duplicate live alias'), 'worker-one vs worker_one live aliases must block as ambiguous under lookup equivalence');
|
|
344
|
+
const equivalenceSummary = zpeer.buildZpeerRoomSummary(repoRoot, equivalenceSender, 'equiv-room');
|
|
345
|
+
assert(equivalenceSummary.duplicateAliases.includes('worker-one') && equivalenceSummary.duplicateAliases.includes('worker_one'), 'room summary must report live lookup-equivalent duplicate aliases');
|
|
346
|
+
|
|
307
347
|
const joinedAlpha = await zpeer.joinZpeerRoom(repoRoot, alpha, 'shared-room', 'sharedalpha', 'bridge');
|
|
308
348
|
assert(joinedAlpha.ok === true, `alpha multi-room join expected ok, got ${joinedAlpha.reason ?? 'not ok'}`);
|
|
309
349
|
alpha = joinedAlpha.peer;
|
|
@@ -534,6 +574,58 @@ async function main() {
|
|
|
534
574
|
assert(String(workerDirectResult.reason).includes('zpeer topology blocked'), 'same-room worker-to-worker send must fall through to legacy topology block');
|
|
535
575
|
assert(receivedPrompts.length === promptCountBeforeWorkerDirect, 'same-room worker-to-worker send must not deliver a prompt to workertwo');
|
|
536
576
|
|
|
577
|
+
let strictPeer = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'strictpeer', roomId: 'strict-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'strict-lead', roleType: 'lead' }), 'strict-room', 'strictpeer'), zpeerAdhoc: true });
|
|
578
|
+
const normalStrictSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
|
|
579
|
+
assert(normalStrictSummary.online === 1, 'without strict mode an online peer with an existing socket may remain reachable by legacy existsSync floor');
|
|
580
|
+
process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = '1';
|
|
581
|
+
const strictUnverifiedSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
|
|
582
|
+
assert(strictUnverifiedSummary.online === 0 && strictUnverifiedSummary.offline === 1, 'strict socket mode must require recent socketVerifiedAt evidence');
|
|
583
|
+
strictPeer = zpeer.refreshZpeerSelf(repoRoot, strictPeer, undefined, undefined, undefined, { socketVerifiedAtMs: Date.now() });
|
|
584
|
+
const strictVerifiedSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
|
|
585
|
+
assert(strictVerifiedSummary.online === 1, 'strict socket mode must revive when socketVerifiedAt is freshly stamped');
|
|
586
|
+
|
|
587
|
+
const joinedProbeRoom = await zpeer.joinZpeerRoom(repoRoot, alpha, 'strict-probe-room', 'probealpha');
|
|
588
|
+
assert(joinedProbeRoom.ok === true, `strict probe room join expected ok, got ${joinedProbeRoom.reason ?? 'not ok'}`);
|
|
589
|
+
alpha = joinedProbeRoom.peer;
|
|
590
|
+
const strictProbePeer = zpeer.refreshZpeerSelf(repoRoot, zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'strictprobe', roomId: 'strict-probe-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'strict-probe-lead', roleType: 'lead' }), 'strict-probe-room', 'strictprobe'));
|
|
591
|
+
const strictProbeSummary = zpeer.buildZpeerRoomSummary(repoRoot, alpha, 'strict-probe-room');
|
|
592
|
+
assert(strictProbeSummary.offline >= 1 && strictProbeSummary.online < strictProbeSummary.peerCount, 'strict mode must classify the unverified local-socket probe candidate offline before send');
|
|
593
|
+
const revivedFallbackInputs = [];
|
|
594
|
+
const revivedViaProbe = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'strictprobe', rawPrompt, waitForReply, {
|
|
595
|
+
roomId: 'strict-probe-room',
|
|
596
|
+
mode: 'async',
|
|
597
|
+
priority: 'urgent',
|
|
598
|
+
interruptMode: 'steer',
|
|
599
|
+
fallbackDelivery: async (input) => { revivedFallbackInputs.push(input); return { delivered: true, target: 'strictprobe-pane' }; },
|
|
600
|
+
});
|
|
601
|
+
const strictProbeStamped = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === strictProbePeer.sessionHash);
|
|
602
|
+
assert(revivedViaProbe.status === 'waiting' && revivedViaProbe.deliveryStatus === 'delivered' && revivedViaProbe.fallback_delivery !== true, `responsive strict-offline candidate must use local_socket, got ${revivedViaProbe.status}/${revivedViaProbe.deliveryStatus}/${revivedViaProbe.deliveryMethod}`);
|
|
603
|
+
assert(revivedFallbackInputs.length === 0, 'responsive strict-offline candidate must be probed/revived before tmux fallback hook is called');
|
|
604
|
+
assert(strictProbeStamped?.status === 'online' && typeof strictProbeStamped.socketVerifiedAt === 'string', 'responsive strict-offline probe must stamp socketVerifiedAt/online into the stable lease-backed registry');
|
|
605
|
+
if (previousZpeerStrict === undefined) delete process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
|
|
606
|
+
else process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = previousZpeerStrict;
|
|
607
|
+
|
|
608
|
+
const fallbackPeer = {
|
|
609
|
+
...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'fallbackbeta', roomId: 'room-one', endpoint: join(root, 'dead-fallbackbeta.sock'), endpointHash: hashing.sha256(join(root, 'dead-fallbackbeta.sock')), sha256: hashing.sha256, roleId: 'fallback-lead', roleType: 'lead' }), 'room-one', 'fallbackbeta'),
|
|
610
|
+
heartbeatAt: new Date().toISOString(),
|
|
611
|
+
status: 'offline',
|
|
612
|
+
socketVerifiedAt: undefined,
|
|
613
|
+
};
|
|
614
|
+
liveRegistry.writeZobLiveTeamAgentLease(repoRoot, fallbackPeer, { reason: 'smoke_demote_fallback_peer' });
|
|
615
|
+
const fallbackInputs = [];
|
|
616
|
+
const fallbackResult = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'fallbackbeta', rawPrompt, waitForReply, {
|
|
617
|
+
mode: 'async',
|
|
618
|
+
priority: 'urgent',
|
|
619
|
+
interruptMode: 'steer',
|
|
620
|
+
fallbackDelivery: async (input) => { fallbackInputs.push(input); return { delivered: true, target: 'fallbackbeta-pane' }; },
|
|
621
|
+
});
|
|
622
|
+
assert(fallbackResult.status === 'delivered' && fallbackResult.fallback_delivery === true && fallbackResult.best_effort === true && fallbackResult.deliveryMethod === 'tmux_sendkeys', `urgent fallback expected best-effort delivered/tmux_sendkeys, got ${fallbackResult.status}/${fallbackResult.deliveryMethod}`);
|
|
623
|
+
assert(fallbackResult.outputHash === undefined && fallbackResult.deliveryStatus === 'blocked', 'fallback must not carry outputHash and must mark local_socket deliveryStatus blocked/not verified');
|
|
624
|
+
assert(fallbackInputs.length === 1 && fallbackInputs[0].targetAlias === 'fallbackbeta' && fallbackInputs[0].senderAlias === 'alpha' && fallbackInputs[0].priority === 'urgent', 'fallback hook must receive room-scoped sender/target/priority metadata');
|
|
625
|
+
let forceFallbackCalled = false;
|
|
626
|
+
const forceFallbackBlocked = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'fallbackbeta', rawPrompt, waitForReply, { mode: 'async', priority: 'force', interruptMode: 'abort', interruptReasonHash: forceReasonHash, fallbackDelivery: async () => { forceFallbackCalled = true; return { delivered: true }; } });
|
|
627
|
+
assert(forceFallbackBlocked.status === 'blocked' && forceFallbackCalled === false, 'force fallback must be blocked and must not call the fallbackDelivery hook');
|
|
628
|
+
|
|
537
629
|
const messagesPath = join(repoRoot, '.pi', 'coms', 'peer-messages.jsonl');
|
|
538
630
|
const statusesPath = join(repoRoot, '.pi', 'coms', 'peer-status.jsonl');
|
|
539
631
|
const messages = readJsonl(messagesPath);
|
|
@@ -559,6 +651,7 @@ async function main() {
|
|
|
559
651
|
assert(messages.some((record) => record.event === 'ack' && record.priority === 'force' && record.interruptMode === 'abort' && record.interruptStatus === 'force_accepted' && record.interruptReasonHash === forceReasonHash), 'peer ledger must include force accepted interrupt metadata with reason hash only');
|
|
560
652
|
assert(messages.some((record) => record.event === 'attempt' && record.status === 'blocked' && record.priority === 'force' && record.interruptStatus === 'force_blocked' && record.interruptReasonHash === forceReasonHash && record.targetAliasHash === hashing.sha256('workertwo')), 'peer ledger must include force role-policy blocked metadata');
|
|
561
653
|
assert(messages.some((record) => record.event === 'attempt' && record.status === 'blocked' && record.targetAliasHash === hashing.sha256('workertwo') && record.reasonHash), 'peer ledger must include hash-only blocked record for same-room worker-to-worker send');
|
|
654
|
+
assert(messages.some((record) => record.event === 'attempt' && record.status === 'delivered' && record.deliveryMethod === 'tmux_sendkeys' && record.fallbackDelivery === true && record.outputHash === undefined), 'peer ledger must include best-effort tmux fallback metadata without outputHash');
|
|
562
655
|
|
|
563
656
|
const realRepoComs = join(process.cwd(), '.pi', 'coms');
|
|
564
657
|
assert(messagesPath !== join(realRepoComs, 'peer-messages.jsonl'), 'smoke must not target real .pi/coms peer-messages ledger');
|
|
@@ -588,5 +681,7 @@ try {
|
|
|
588
681
|
else process.env.TMUX_PANE = previousTmuxPane;
|
|
589
682
|
if (previousZobComsRoleId === undefined) delete process.env.ZOB_COMS_ROLE_ID;
|
|
590
683
|
else process.env.ZOB_COMS_ROLE_ID = previousZobComsRoleId;
|
|
684
|
+
if (previousZpeerStrict === undefined) delete process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
|
|
685
|
+
else process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = previousZpeerStrict;
|
|
591
686
|
rmSync(root, { recursive: true, force: true });
|
|
592
687
|
}
|
|
@@ -35,8 +35,10 @@ const files = [
|
|
|
35
35
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/pending-replies.ts',
|
|
36
36
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/response-capture.ts',
|
|
37
37
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts',
|
|
38
|
+
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts',
|
|
38
39
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts',
|
|
39
40
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/transcript-capture.ts',
|
|
41
|
+
'.pi/extensions/zob-harness/src/domains/coms/zagents.ts',
|
|
40
42
|
'.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts',
|
|
41
43
|
'.pi/extensions/zob-harness/src/runtime/commands.ts',
|
|
42
44
|
'.pi/extensions/zob-harness/src/runtime/tools-coms.ts',
|
|
@@ -59,7 +61,7 @@ const zpeerAskMatches = toolsComs.match(/name: "zpeer_ask"/g) ?? [];
|
|
|
59
61
|
if (zpeerAskMatches.length !== 1) failures.push(`expected exactly one zpeer_ask tool, found ${zpeerAskMatches.length}`);
|
|
60
62
|
const zpeerReplyMatches = toolsComs.match(/name: "zpeer_reply"/g) ?? [];
|
|
61
63
|
if (zpeerReplyMatches.length !== 1) failures.push(`expected exactly one zpeer_reply tool, found ${zpeerReplyMatches.length}`);
|
|
62
|
-
for (const needle of ['parameters: ZpeerAskParams', 'sendZpeerPrompt(ctx.cwd', 'mode = params.mode ?? "async"', 'requireResponse = params.requireResponse === true', 'pendingReplies.wait(msgId, timeoutMs, { requireResponse })', 'maxReinjects', 'zpeerAskGuardBlock', 'ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50', 'ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10', 'ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3', 'normalizeZpeerInterrupt', 'force interrupt requires reason', 'max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window', 'idle/passive wait: no follow-up turn queued', 'customType: "zob-zpeer-event"', 'source: "agent-request"', 'action: "agent_request"', 'feedbackEmittedTerminal', 'reasonInputHash', 'interruptReasonHash', 'bodyStored: false', 'promptBodiesStored: false', 'outputBodiesStored: false']) {
|
|
64
|
+
for (const needle of ['parameters: ZpeerAskParams', 'sendZpeerPrompt(ctx.cwd', 'mode = params.mode ?? "async"', 'requireResponse = params.requireResponse === true', 'pendingReplies.wait(msgId, timeoutMs, { requireResponse })', 'maxReinjects', 'zpeerAskGuardBlock', 'ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50', 'ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10', 'ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3', 'normalizeZpeerInterrupt', 'force interrupt requires reason', 'max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window', 'idle/passive wait: no follow-up turn queued', 'customType: "zob-zpeer-event"', 'source: "agent-request"', 'action: "agent_request"', 'feedbackEmittedTerminal', 'reasonInputHash', 'interruptReasonHash', 'buildProjectTransposerFallbackDelivery(ctx.cwd)', 'PROJECT_TRANSPOSER_ZPEER_FALLBACK_HOOK', 'fallbackDelivery: result.fallback_delivery', 'bestEffort: result.best_effort', 'bodyStored: false', 'promptBodiesStored: false', 'outputBodiesStored: false']) {
|
|
63
65
|
if (!toolsComs.includes(needle)) failures.push(`zpeer_ask tool missing ${needle}`);
|
|
64
66
|
}
|
|
65
67
|
for (const needle of ['parameters: ZpeerReplyParams', 'buildZobLiveResponseEnvelope', 'sendZobLocalEnvelope(replyEndpoint', 'replyToMsgId: inbound.envelope.msgId', 'action: "reply"', 'action: "reply_blocked"', 'status: "response_sent"', 'ZPeer msgId required response already expired']) {
|
|
@@ -94,8 +96,10 @@ const envelope = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/e
|
|
|
94
96
|
const pendingReplies = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/pending-replies.ts'];
|
|
95
97
|
const responseCapture = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/response-capture.ts'];
|
|
96
98
|
const liveRegistry = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts'];
|
|
99
|
+
const liveTypes = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts'];
|
|
97
100
|
const zpeerProfile = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts'];
|
|
98
|
-
|
|
101
|
+
const zagents = contents['.pi/extensions/zob-harness/src/domains/coms/zagents.ts'];
|
|
102
|
+
for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'required_response_expired', 'requireResponse', 'responseRequiredBy', 'responseReceived', 'deliveryStatus', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping', 'zpeerAdhoc', 'zpeerStrictSocketEvidenceRequired', 'ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET', 'ZpeerFallbackDelivery', 'probeAndReviveZpeerCandidate', 'zpeer-candidate-probe', 'reason: "zpeer_candidate_probe"']) {
|
|
99
103
|
if (!zpeer.includes(needle)) failures.push(`zpeer missing ${needle}`);
|
|
100
104
|
}
|
|
101
105
|
for (const needle of ['peer-messages.jsonl', 'peer-status.jsonl', 'appendZpeerPeerRecords', 'reasonHash', 'priority', 'interruptMode', 'interruptStatus', 'interruptReasonHash', 'bodyStored: false']) {
|
|
@@ -117,11 +121,22 @@ if (roomFirstTopologyIndex === -1) failures.push('zpeer topology must allow same
|
|
|
117
121
|
if (!zpeer.includes('const bothPeersAreWorkers = self.roleType === "worker" && target.roleType === "worker";')) failures.push('zpeer topology must preserve worker-to-worker same-room legacy topology block');
|
|
118
122
|
if (hardTeamMismatchIndex !== -1 && (roomFirstTopologyIndex === -1 || hardTeamMismatchIndex < roomFirstTopologyIndex)) failures.push('zpeer topology must not hard-block cross-team peers before same-room allowance');
|
|
119
123
|
if (!zpeer.includes('const candidates = peersInRoom(repoRoot, roomId).filter') || !zpeer.includes('if (!selfMembership)') || !zpeer.includes('current peer is observer-only in room')) failures.push('zpeer same-room allowance must preserve room candidate, self membership, and observer guards');
|
|
120
|
-
for (const needle of ['
|
|
124
|
+
for (const needle of ['export function zpeerAliasLookupKey', 'export function zpeerAliasesEquivalent', 'export function zpeerAliasIncluded', 'duplicateZpeerAliasLookupKeys(onlineAliases)', 'zpeerAliasesEquivalent(entry.membership.alias, alias)', 'zpeerAliasesEquivalent(entry.membership.alias, targetAlias)', 'zpeerAliasesEquivalent(targetAlias, senderAlias)', 'receiver: target.membership.alias']) {
|
|
125
|
+
if (!zpeer.includes(needle)) failures.push(`zpeer alias hyphen/underscore equivalence missing ${needle}`);
|
|
126
|
+
}
|
|
127
|
+
if (zpeer.includes('entry.membership.alias === targetAlias') || zpeer.includes('targetAlias === senderAlias') || zpeer.includes('entry.membership.alias !== alias')) failures.push('zpeer must not use literal-only alias matching in send/collision/self guards');
|
|
128
|
+
for (const needle of ['zpeerAliasIncluded(policy.allowedAliases, alias)', 'zpeerAliasesEquivalent(candidate.alias, alias)', 'zpeerAliasIncluded(base.allowedAliases, alias)', 'safeZpeerAlias(alias) !== alias']) {
|
|
129
|
+
if (!zagents.includes(needle)) failures.push(`zagent alias policy equivalence/validation missing ${needle}`);
|
|
130
|
+
}
|
|
131
|
+
for (const needle of ['ZPEER_FORCE_ALLOWED_SENDER_ROLE_TYPES', 'ZPEER_FORCE_ALLOWED_RECEIVER_ROLE_TYPES', 'force interrupt not allowed from role type', 'force interrupt not allowed to role type', 'priority !== "force"']) {
|
|
121
132
|
if (!zpeer.includes(needle)) failures.push(`zpeer force sender policy/fallback guard missing ${needle}`);
|
|
122
133
|
}
|
|
123
134
|
if (!liveRegistry.includes('readZobLiveRegistryAllProjectsSnapshot') || !liveRegistry.includes('join(projectsDir, entry.name, "agents")')) failures.push('live registry must expose all-project agents room discovery helper');
|
|
124
135
|
if (!liveRegistry.includes('peer.zpeerAdhoc !== true') || !liveRegistry.includes('mergeLeaseBackedAndAdhocPeers(leaseDirs.flatMap')) failures.push('live registry must merge explicit ad-hoc room peer cards into lease-backed summaries');
|
|
136
|
+
if ((liveTypes.match(/socketVerifiedAt\?: string;/g) ?? []).length < 2) failures.push('live peer card and stable team-agent lease types must expose optional socketVerifiedAt');
|
|
137
|
+
for (const needle of ['socketVerifiedAt: peer.socketVerifiedAt', 'socketVerifiedAt: lease.socketVerifiedAt', 'const livePeer = { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso }', 'applySweepPeerUpdate(repoRoot, livePeer, nowMs)', 'retainedLive += 1']) {
|
|
138
|
+
if (!liveRegistry.includes(needle)) failures.push(`live registry socket verification propagation/sweep stamping missing ${needle}`);
|
|
139
|
+
}
|
|
125
140
|
for (const needle of ['ZobLiveTeamAgentLease', 'zob.live-team-agent-lease.v1', 'stableLease: true', 'exclusiveBy: "teamId+agentId"', 'claimZobLiveTeamAgentLease', 'leaseRespondsToPing', 'pingZobLocalEndpoint', 'releaseZobLiveTeamAgentLease', 'ownsZobLiveTeamAgentLease', 'sameLeaseOwner', 'retireInactiveZobLiveTeamAgentLeases', 'readLeasesFromDir', 'hasLeaseDomain', 'mergeLeaseBackedAndAdhocPeers']) {
|
|
126
141
|
if (!liveRegistry.includes(needle)) failures.push(`live registry stable lease support missing ${needle}`);
|
|
127
142
|
}
|
|
@@ -189,6 +204,8 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'r
|
|
|
189
204
|
if (znewPreDispatchBlock.includes(forbidden)) failures.push(`/new pre-dispatch hook contains forbidden raw/body-like key ${forbidden}`);
|
|
190
205
|
if (znewShutdownBlock.includes(forbidden)) failures.push(`/new session_shutdown hook contains forbidden raw/body-like key ${forbidden}`);
|
|
191
206
|
}
|
|
207
|
+
if (!toolsComs.includes('zpeerAliasesEquivalent(targetAlias, selfAlias)') || !toolsComs.includes('zpeerAliasLookupKey(guard.lastTargetAlias)')) failures.push('zpeer_ask guard must apply alias lookup equivalence to self and duplicate loop checks');
|
|
208
|
+
if (!events.includes('zpeerAliasesEquivalent(envelope.sender, wait.targetAlias)')) failures.push('passive ZPeer wait clearing must apply alias lookup equivalence');
|
|
192
209
|
if (!events.includes('readZpeerLocalProfile(repoRoot, profileId)')) failures.push('runtime must load session-scoped zpeer profile before ensuring/registering fields');
|
|
193
210
|
if (!events.includes('sharedZpeerProfile ? undefined : zpeerProfile?.alias') || !events.includes('sharedZpeerProfile ? undefined : zpeerProfile?.memberships')) failures.push('runtime must not restore alias/memberships from shared role fallback zpeer profiles');
|
|
194
211
|
if ((events.match(/writeZpeerLocalProfileFromPeer\(repoRoot, state\.zobLive\.peerCard, profileId\)/g) ?? []).length < 3 || !events.includes('zpeerRuntimeProfileId(ctx)')) failures.push('runtime must persist zpeer self profile under the current Pi session during initial registration, refresh, and shutdown');
|
|
@@ -253,7 +270,7 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'message:', '
|
|
|
253
270
|
const passiveWaitBlocks = [...toolsComs.matchAll(/state\.zobLive\.passivePeerWait = \{[\s\S]*?\n \};/g)].map((m) => m[0]);
|
|
254
271
|
if (passiveWaitBlocks.some((block) => block.includes(forbidden))) failures.push(`passivePeerWait block contains forbidden body-like key ${forbidden}`);
|
|
255
272
|
}
|
|
256
|
-
for (const needle of ['clearPassivePeerWaitForResponse', 'envelope.type !== "response"', 'envelope.msgId === wait.msgId', 'state.zobLive.passivePeerWait = undefined', 'envelope.sender
|
|
273
|
+
for (const needle of ['clearPassivePeerWaitForResponse', 'envelope.type !== "response"', 'envelope.msgId === wait.msgId', 'state.zobLive.passivePeerWait = undefined', 'zpeerAliasesEquivalent(envelope.sender, wait.targetAlias)']) {
|
|
257
274
|
if (!events.includes(needle)) failures.push(`runtime events missing passive wait response clear ${needle}`);
|
|
258
275
|
}
|
|
259
276
|
if (!contents['.pi/extensions/zob-harness/src/domains/coms/mission-control.ts'].includes('zpeerRooms')) failures.push('Mission Control missing zpeerRooms summary');
|