zob-harness 0.15.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/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.ts +94 -12
- package/.pi/extensions/zob-harness/src/domains/coms/zagents.ts +4 -4
- package/.pi/extensions/zob-harness/src/runtime/events.ts +2 -2
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +48 -5
- package/package.json +1 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +96 -1
- package/scripts/zpeer-static-smoke.mjs +21 -4
|
@@ -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;
|
|
@@ -75,6 +75,19 @@ export interface ZpeerSendFeedback {
|
|
|
75
75
|
result: ZpeerSendResult;
|
|
76
76
|
}
|
|
77
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
|
+
|
|
78
91
|
export interface ZpeerSendOptions {
|
|
79
92
|
mode?: ZpeerSendMode;
|
|
80
93
|
roomId?: string;
|
|
@@ -90,8 +103,8 @@ export interface ZpeerSendOptions {
|
|
|
90
103
|
// the target agent's pane). This keeps coms-v2 IO-free; the app supplies the tmux
|
|
91
104
|
// delivery. A fallback delivery is NOT verified delivery: outputHash stays absent,
|
|
92
105
|
// confirmation_ref stays null, and bodyStored stays false (coms-safety: append-only /
|
|
93
|
-
// best-effort is not delivery success).
|
|
94
|
-
fallbackDelivery?:
|
|
106
|
+
// best-effort is not delivery success). Force/abort is never eligible for fallback.
|
|
107
|
+
fallbackDelivery?: ZpeerFallbackDelivery;
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
interface ZpeerRoomPeer {
|
|
@@ -109,6 +122,37 @@ export function safeZpeerAlias(value: string | undefined): string | undefined {
|
|
|
109
122
|
return trimmed;
|
|
110
123
|
}
|
|
111
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
|
+
|
|
112
156
|
export function safeZpeerRoomId(value: string | undefined): string | undefined {
|
|
113
157
|
const trimmed = value?.trim();
|
|
114
158
|
if (!trimmed || !ROOM_PATTERN.test(trimmed)) return undefined;
|
|
@@ -198,8 +242,12 @@ function withZpeerMembershipState(repoRoot: string, peer: ZobLivePeerCard, membe
|
|
|
198
242
|
});
|
|
199
243
|
}
|
|
200
244
|
|
|
245
|
+
function zpeerStrictSocketEvidenceRequired(): boolean {
|
|
246
|
+
return /^(1|true|yes|on|strict)$/i.test(process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET ?? "");
|
|
247
|
+
}
|
|
248
|
+
|
|
201
249
|
function zpeerReachableStatus(peer: ZobLivePeerCard): ZobLivePeerStatus {
|
|
202
|
-
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer)) return "online";
|
|
250
|
+
if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer, { requireVerified: zpeerStrictSocketEvidenceRequired() })) return "online";
|
|
203
251
|
if (peer.status === "stale") return "stale";
|
|
204
252
|
return "offline";
|
|
205
253
|
}
|
|
@@ -221,9 +269,34 @@ async function peerRespondsToAliasPing(peer: ZobLivePeerCard): Promise<boolean>
|
|
|
221
269
|
}
|
|
222
270
|
}
|
|
223
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
|
+
|
|
224
297
|
async function activeAliasCollision(repoRoot: string, self: ZobLivePeerCard, roomId: string, alias: string): Promise<ZpeerRoomPeer | undefined> {
|
|
225
298
|
for (const entry of peersInRoom(repoRoot, roomId)) {
|
|
226
|
-
if (entry.peer.sessionHash === self.sessionHash || entry.membership.alias
|
|
299
|
+
if (entry.peer.sessionHash === self.sessionHash || !zpeerAliasesEquivalent(entry.membership.alias, alias)) continue;
|
|
227
300
|
if (await peerRespondsToAliasPing(entry.peer)) return entry;
|
|
228
301
|
if (zpeerReachableStatus(entry.peer) === "online") {
|
|
229
302
|
try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort stale alias release */ }
|
|
@@ -290,7 +363,7 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
|
|
|
290
363
|
statusAliases[status].push(entry.membership.alias);
|
|
291
364
|
}
|
|
292
365
|
const onlineAliases = statusAliases.online.sort();
|
|
293
|
-
const duplicateAliases =
|
|
366
|
+
const duplicateAliases = duplicateZpeerAliasLookupKeys(onlineAliases);
|
|
294
367
|
return {
|
|
295
368
|
schema: "zob.zpeer-room-summary.v1",
|
|
296
369
|
projectId,
|
|
@@ -560,8 +633,8 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
560
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 });
|
|
561
634
|
if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
|
|
562
635
|
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
|
|
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 });
|
|
565
638
|
if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
|
|
566
639
|
let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
|
|
567
640
|
if (liveCandidates.length > 1) {
|
|
@@ -575,6 +648,14 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
575
648
|
}
|
|
576
649
|
liveCandidates = responsiveCandidates;
|
|
577
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
|
+
}
|
|
578
659
|
if (liveCandidates.length === 0) {
|
|
579
660
|
const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
|
|
580
661
|
// WS-ZH4: last-resort fallback delivery. When local-socket transport is blocked
|
|
@@ -582,14 +663,15 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
582
663
|
// pane), deliver the prompt best-effort. This is NOT verified delivery:
|
|
583
664
|
// outputHash/confirmation_ref stay absent and bodyStored stays false; the receiver,
|
|
584
665
|
// if it answers at all, answers via its own local_socket later (that ledgered reply
|
|
585
|
-
// is what WS-Q5 counts).
|
|
586
|
-
//
|
|
587
|
-
|
|
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") {
|
|
588
670
|
try {
|
|
589
|
-
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 });
|
|
590
672
|
if (fallback.delivered) {
|
|
591
673
|
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);
|
|
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);
|
|
593
675
|
}
|
|
594
676
|
} catch {
|
|
595
677
|
// 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);
|
|
@@ -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";
|
|
@@ -69,7 +69,7 @@ function clearPassivePeerWaitForResponse(state: HarnessRuntimeState, envelope: {
|
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
const responseRoomId = envelope.runId?.startsWith("zpeer:") ? envelope.runId.slice("zpeer:".length) : undefined;
|
|
72
|
-
if (!wait.msgId && wait.targetAlias && envelope.sender
|
|
72
|
+
if (!wait.msgId && wait.targetAlias && zpeerAliasesEquivalent(envelope.sender, wait.targetAlias) && (!wait.roomId || !responseRoomId || wait.roomId === responseRoomId)) {
|
|
73
73
|
state.zobLive.passivePeerWait = undefined;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -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";
|
|
@@ -37,6 +40,44 @@ const ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50;
|
|
|
37
40
|
const ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10;
|
|
38
41
|
const ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3;
|
|
39
42
|
|
|
43
|
+
type ZpeerFallbackHookFn = ZpeerFallbackDelivery;
|
|
44
|
+
|
|
45
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function asZpeerFallbackHookFn(moduleValue: unknown): ZpeerFallbackHookFn | undefined {
|
|
50
|
+
if (typeof moduleValue === "function") return moduleValue as ZpeerFallbackHookFn;
|
|
51
|
+
if (!isObjectRecord(moduleValue)) return undefined;
|
|
52
|
+
const direct = moduleValue.zpeerFallbackDelivery ?? moduleValue.fallbackDelivery;
|
|
53
|
+
if (typeof direct === "function") return direct as ZpeerFallbackHookFn;
|
|
54
|
+
const defaultExport = moduleValue.default;
|
|
55
|
+
if (typeof defaultExport === "function") return defaultExport as ZpeerFallbackHookFn;
|
|
56
|
+
if (isObjectRecord(defaultExport)) {
|
|
57
|
+
const nested = defaultExport.zpeerFallbackDelivery ?? defaultExport.fallbackDelivery;
|
|
58
|
+
if (typeof nested === "function") return nested as ZpeerFallbackHookFn;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildProjectTransposerFallbackDelivery(repoRoot: string): ZpeerFallbackDelivery | undefined {
|
|
64
|
+
const hookPath = process.env.PROJECT_TRANSPOSER_ZPEER_FALLBACK_HOOK?.trim();
|
|
65
|
+
if (!hookPath) return undefined;
|
|
66
|
+
const resolvedHookPath = isAbsolute(hookPath) ? hookPath : join(repoRoot, hookPath);
|
|
67
|
+
return async (input) => {
|
|
68
|
+
const imported = await import(pathToFileURL(resolvedHookPath).href);
|
|
69
|
+
const fn = asZpeerFallbackHookFn(imported);
|
|
70
|
+
if (!fn) return { delivered: false, reason: "fallback_hook_missing_function" };
|
|
71
|
+
const result = await fn(input);
|
|
72
|
+
if (!isObjectRecord(result)) return { delivered: false, reason: "fallback_hook_invalid_result" };
|
|
73
|
+
return {
|
|
74
|
+
delivered: result.delivered === true,
|
|
75
|
+
target: typeof result.target === "string" ? result.target : undefined,
|
|
76
|
+
reason: typeof result.reason === "string" ? result.reason : undefined,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
function breakGlassApprovalPresent(): boolean {
|
|
41
82
|
return SHA256_HEX.test(process.env.ZOB_COMS_BREAK_GLASS_APPROVAL_HASH ?? "");
|
|
42
83
|
}
|
|
@@ -97,7 +138,7 @@ function normalizeZpeerInterrupt(params: ZpeerAskToolParams): { priority: ZpeerI
|
|
|
97
138
|
function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolParams, selfAlias?: string, currentRoomId = "default", priority: ZpeerInterruptPriority = "normal"): string | undefined {
|
|
98
139
|
const targetAlias = safeZpeerAlias(params.targetAlias);
|
|
99
140
|
if (!targetAlias) return "invalid target alias";
|
|
100
|
-
if (selfAlias && targetAlias
|
|
141
|
+
if (selfAlias && zpeerAliasesEquivalent(targetAlias, selfAlias)) return "cannot send to self";
|
|
101
142
|
const roomId = safeZpeerRoomId(params.roomId) ?? currentRoomId;
|
|
102
143
|
if (params.roomId && !safeZpeerRoomId(params.roomId)) return "invalid room id";
|
|
103
144
|
if (/\b(zpeer_ask|\/zpeer)\b/i.test(params.message)) return "loop guard blocked recursive ZPeer instruction";
|
|
@@ -111,8 +152,9 @@ function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolPara
|
|
|
111
152
|
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
153
|
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
154
|
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
|
-
|
|
155
|
+
const targetAliasLookupKey = zpeerAliasLookupKey(targetAlias) ?? targetAlias;
|
|
156
|
+
if (guard.lastRoomId === roomId && zpeerAliasLookupKey(guard.lastTargetAlias) === targetAliasLookupKey && guard.lastMessageHash === messageHash) return "loop guard blocked duplicate room/target/message in ask window";
|
|
157
|
+
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
158
|
return undefined;
|
|
117
159
|
}
|
|
118
160
|
|
|
@@ -266,6 +308,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
266
308
|
requireResponse,
|
|
267
309
|
responseTimeoutMs: timeoutMs,
|
|
268
310
|
maxReinjects,
|
|
311
|
+
fallbackDelivery: buildProjectTransposerFallbackDelivery(ctx.cwd),
|
|
269
312
|
onFeedback: (feedback) => {
|
|
270
313
|
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
314
|
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,7 +316,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
273
316
|
});
|
|
274
317
|
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
318
|
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() });
|
|
319
|
+
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
320
|
const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
|
|
278
321
|
const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
|
|
279
322
|
const transientReplyText = (result.status === "reply" || result.status === "completed") && result.transientResponse
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
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');
|