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.
@@ -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?: (input: { targetAlias: string; roomId: string; taskHash?: string; prompt: string }) => Promise<{ delivered: boolean; target?: string; reason?: string }>;
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 !== alias) continue;
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 = onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
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 === targetAlias && entry.peer.sessionHash !== self.sessionHash);
564
- if (targetAlias === senderAlias) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
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). If no fallback is supplied or it fails/declines, fall
586
- // through to the standard blocked result unchanged.
587
- if (options.fallbackDelivery && targetAlias && priority === "normal") {
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.includes(alias)) errors.push(`${label}.allowedAliases must be a subset of the base policy: ${alias}`);
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.includes(alias)) return false;
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 === 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 }): void => {
1144
- state.zobLive.lastEvent = { ...event, at: new Date().toISOString(), localOnly: true, networkEnabled: false, bodyStored: false };
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]): void => {
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: { ...state.zobLive.lastEvent },
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 }): void {
55
- state.zobLive.lastEvent = {
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 === wait.targetAlias && (!wait.roomId || !responseRoomId || wait.roomId === responseRoomId)) {
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
- if (activeInbound?.requiredResponseStatus === "expired") return;
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
- const ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50;
37
- const ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10;
38
- const ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3;
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 === selfAlias) return "cannot send to self";
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
- if (guard.lastRoomId === roomId && guard.lastTargetAlias === targetAlias && guard.lastMessageHash === messageHash) return "loop guard blocked duplicate room/target/message in ask window";
115
- 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: targetAlias, lastMessageHash: messageHash };
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 emitZpeerAskEvent = (event: { kind: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>["kind"]; status: string; reason?: string; msgId?: string; roomId?: string; taskHash?: string; outputHash?: string; interruptStatus?: ZpeerInterruptStatus }): void => {
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
- state.zobLive.lastEvent = { 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, at: new Date().toISOString(), localOnly: true, networkEnabled: false, bodyStored: false };
240
- void pi.sendMessage({
241
- customType: "zob-zpeer-event",
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
- display: true,
244
- details: { ...state.zobLive.lastEvent, source: "agent-request", mode, priority: interrupt.priority, interruptMode: interrupt.interruptMode, bodyStored: false, localOnly: true, networkEnabled: false },
245
- }, { triggerTurn: false });
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" : inbound.requiredResponseStatus === "expired" ? "ZPeer msgId required response already expired" : !inbound.envelope.replyEndpoint ? "ZPeer inbound msgId has no reply endpoint" : undefined;
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
- state.zobLive.lastEvent = { 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, at: new Date().toISOString(), localOnly: true, networkEnabled: false, bodyStored: false };
318
- void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...state.zobLive.lastEvent } }, { triggerTurn: false });
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.0",
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
- alpha = zpeer.refreshZpeerSelf(repoRoot, alpha);
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
- 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']) {
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 ['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 === "normal"']) {
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 === wait.targetAlias']) {
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');