zob-harness 0.15.2 → 0.15.3
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/runtime/commands/zlive.ts +1 -1
- package/.pi/extensions/zob-harness/src/runtime/events.ts +6 -6
- package/.pi/extensions/zob-harness/src/runtime/schemas.ts +2 -2
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +8 -4
- package/package.json +1 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +7 -1
- package/scripts/zpeer-static-smoke.mjs +7 -2
|
@@ -1154,7 +1154,7 @@ export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
1154
1154
|
content: `ZPeer ${event.kind} ${event.fromAlias ? `@${event.fromAlias}` : "?"} → ${event.toAlias ? `@${event.toAlias}` : "?"} ${event.status}`,
|
|
1155
1155
|
display: true,
|
|
1156
1156
|
details: { ...recorded },
|
|
1157
|
-
}, { triggerTurn: false });
|
|
1157
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
1158
1158
|
return true;
|
|
1159
1159
|
};
|
|
1160
1160
|
|
|
@@ -239,7 +239,7 @@ function handleInboundZpeerPrompt(pi: ExtensionAPI, state: HarnessRuntimeState,
|
|
|
239
239
|
content: priority === "normal" ? "ZPeer inbound prompt received (transient body delivered only to agent turn)" : `ZPeer ${priority} inbound prompt received (transient body delivered only to agent turn)`,
|
|
240
240
|
display: true,
|
|
241
241
|
details: { kind: "inbound", roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: interruptStatus ?? "prompt_received", msgId: envelope.msgId, taskHash: envelope.taskHash, priority, interruptMode, interruptStatus, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
242
|
-
}, { triggerTurn: false });
|
|
242
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
243
243
|
void pi.sendMessage({
|
|
244
244
|
customType: "zob-coms-inbound",
|
|
245
245
|
content: envelope.transientPrompt ?? "",
|
|
@@ -615,7 +615,7 @@ function formatZpeerLastEvent(event: ZobLiveLastEvent | undefined): string {
|
|
|
615
615
|
|
|
616
616
|
function buildZpeerAwarenessPrompt(state: HarnessRuntimeState, repoRoot: string): string {
|
|
617
617
|
if (!state.zobLive.peerCard) {
|
|
618
|
-
return "\n\nZPEER AWARENESS\n- local peer endpoint: unavailable this turn\n- Use zpeer_ask with mode=\"async\" or /zpeer only when useful or user-requested for peer coordination; avoid spam/loops and do not invent hidden worker-to-worker chat.";
|
|
618
|
+
return "\n\nZPEER AWARENESS\n- local peer endpoint: unavailable this turn\n- Use zpeer_ask with mode=\"async\" or /zpeer only when useful or user-requested for peer coordination; avoid spam/loops and do not invent hidden worker-to-worker chat.\n- If the user explicitly asks for a reply, status, or confirmation, use requireResponse=true with mode=\"await\" or mode=\"long\"; status=waiting is delivery-only, not evidence that the peer is working or done.";
|
|
619
619
|
}
|
|
620
620
|
const summaries = buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard);
|
|
621
621
|
const activeSummary = summaries.find((summary) => summary.active) ?? summaries[0];
|
|
@@ -632,7 +632,7 @@ function buildZpeerAwarenessPrompt(state: HarnessRuntimeState, repoRoot: string)
|
|
|
632
632
|
const activePeerAliases = (activeSummary?.onlineAliases ?? []).filter((alias) => alias !== activeSelfAlias).slice(0, 8).map((alias) => `@${alias}`);
|
|
633
633
|
const activeUnavailable = (activeSummary?.stale ?? 0) + (activeSummary?.offline ?? 0);
|
|
634
634
|
const activeDuplicateLine = activeSummary && activeSummary.duplicateAliases.length > 0 ? `\n- duplicate live aliases: ${activeSummary.duplicateAliases.map((alias) => `@${alias}`).join(", ")}` : "";
|
|
635
|
-
return `\n\nZPEER AWARENESS (transient, rebuilt each turn)\n- active room: ${activeSummary?.roomId ?? "default"}\n- memberships: ${memberships}\n- self: @${activeSelfAlias}\n- online peers: ${activePeerAliases.join(", ") || "none"}\n- unavailable peers: ${activeUnavailable} (stale=${activeSummary?.stale ?? 0}, offline=${activeSummary?.offline ?? 0})${activeDuplicateLine}\n- rooms:\n${roomLines.join("\n") || " - none"}\n- Use zpeer_ask with explicit roomId when targeting a non-active room.\n- posture: local_socket-only, room-scoped, hash-only durable ledgers, bodyStored=false, networkEnabled=false\n- For non-trivial review/debug/planning peer coordination, agents may use zpeer_ask with mode=\"async\" so the request is visible, governed, and non-blocking; /zpeer remains the interactive command path.\n- Passive wait rule: if the only remaining action is waiting for ZPeer/coms replies, stop the turn and remain idle; do not poll, call tools, or continue just to wait.\n- Use ZPeer only when useful or user-requested; avoid spam, duplicate asks, and reply loops; do not use it for hidden free chat or to bypass topology/safety gates.\n- Raw ZPeer bodies are transient; durable records must remain hash-only/bodyStored=false.\n- last ZPeer event: ${formatZpeerLastEvent(state.zobLive.lastEvent)}`;
|
|
635
|
+
return `\n\nZPEER AWARENESS (transient, rebuilt each turn)\n- active room: ${activeSummary?.roomId ?? "default"}\n- memberships: ${memberships}\n- self: @${activeSelfAlias}\n- online peers: ${activePeerAliases.join(", ") || "none"}\n- unavailable peers: ${activeUnavailable} (stale=${activeSummary?.stale ?? 0}, offline=${activeSummary?.offline ?? 0})${activeDuplicateLine}\n- rooms:\n${roomLines.join("\n") || " - none"}\n- Use zpeer_ask with explicit roomId when targeting a non-active room.\n- posture: local_socket-only, room-scoped, hash-only durable ledgers, bodyStored=false, networkEnabled=false\n- For non-trivial review/debug/planning peer coordination, agents may use zpeer_ask with mode=\"async\" so the request is visible, governed, and non-blocking; /zpeer remains the interactive command path.\n- If the user explicitly asks for a reply, status, or confirmation, use requireResponse=true with mode=\"await\" or mode=\"long\"; status=waiting is delivery-only, not evidence that the peer is working or done.\n- Passive wait rule: if the only remaining action is waiting for ZPeer/coms replies, stop the turn and remain idle; do not poll, call tools, or continue just to wait.\n- Use ZPeer only when useful or user-requested; avoid spam, duplicate asks, and reply loops; do not use it for hidden free chat or to bypass topology/safety gates.\n- Raw ZPeer bodies are transient; durable records must remain hash-only/bodyStored=false.\n- last ZPeer event: ${formatZpeerLastEvent(state.zobLive.lastEvent)}`;
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
const SAME_AGENT_MODE_INTENT_PROMPT = [
|
|
@@ -780,7 +780,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
|
|
|
780
780
|
content: envelope.errorCode === "zpeer_required_response_expired" ? "ZPeer required response expired" : "ZPeer error received",
|
|
781
781
|
display: true,
|
|
782
782
|
details: { kind: state.zobLive.lastEvent?.kind, roomId: state.zobLive.lastEvent?.roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: state.zobLive.lastEvent?.status, msgId: envelope.msgId, taskHash: envelope.taskHash, errorHash: envelope.errorHash, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
783
|
-
}, { triggerTurn: false });
|
|
783
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
784
784
|
return buildZobLiveAckEnvelope(envelope);
|
|
785
785
|
}
|
|
786
786
|
if (envelope.type === "response") {
|
|
@@ -802,7 +802,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
|
|
|
802
802
|
content: "ZPeer reply received (transient response available; durable records remain hash-only)",
|
|
803
803
|
display: true,
|
|
804
804
|
details: { kind: "reply", roomId: state.zobLive.lastEvent?.roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: "reply", msgId: envelope.msgId, taskHash: envelope.taskHash, outputHash: envelope.outputHash, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
805
|
-
}, { triggerTurn: false });
|
|
805
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
806
806
|
if (!completedActiveWait && envelope.transientResponse) {
|
|
807
807
|
void pi.sendMessage({
|
|
808
808
|
customType: "zob-zpeer-response",
|
|
@@ -1101,7 +1101,7 @@ async function sendInboundZobLiveResponse(pi: ExtensionAPI, state: HarnessRuntim
|
|
|
1101
1101
|
content: "ZPeer response sent (transient response delivered over local socket; durable records remain hash-only)",
|
|
1102
1102
|
display: true,
|
|
1103
1103
|
details: { kind: "response_sent", roomId: state.zobLive.lastEvent?.roomId, fromAlias: inbound.envelope.receiver, toAlias: inbound.envelope.sender, status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash: responseEnvelope.outputHash, priority: activeInbound?.priority, interruptMode: activeInbound?.interruptMode, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
1104
|
-
}, { triggerTurn: false });
|
|
1104
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
1107
|
export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeState): void {
|
|
@@ -354,13 +354,13 @@ const ZpeerAskParams = Type.Object({
|
|
|
354
354
|
targetAlias: Type.String({ description: "ZPeer target alias in the current local room. May include or omit the leading @." }),
|
|
355
355
|
message: Type.String({ description: "Transient peer request body. Used only for local live delivery; never persisted in durable ledgers or reports." }),
|
|
356
356
|
roomId: Type.Optional(Type.String({ description: "Optional ZPeer room id. Defaults to the current active local room." })),
|
|
357
|
-
mode: Type.Optional(StringEnum(["async", "await", "long"] as const, { description: "Send mode. Default async for
|
|
357
|
+
mode: Type.Optional(StringEnum(["async", "await", "long"] as const, { description: "Send mode. Default async for non-blocking coordination; when an actual reply/status is required, use await or long with requireResponse=true.", default: "async" })),
|
|
358
358
|
reason: Type.Optional(Type.String({ description: "Optional transient coordination/interrupt reason. Required for force; hashed only in visible metadata; raw value is not persisted." })),
|
|
359
359
|
urgency: Type.Optional(StringEnum(["normal", "urgent", "force"] as const, { description: "ZPeer delivery priority. normal=follow-up, urgent=steer, force=controlled abort+steer when policy allows." })),
|
|
360
360
|
force: Type.Optional(Type.Boolean({ description: "Alias for urgency=force. Requires reason and remains local/hash-only." })),
|
|
361
361
|
interruptMode: Type.Optional(StringEnum(["none", "steer", "abort"] as const, { description: "Requested interrupt mode. Derived from urgency when omitted; invalid broadening is blocked by runtime." })),
|
|
362
362
|
timeoutMs: Type.Optional(Type.Number({ description: "Bounded reply wait timeout for await/long modes, and required-response expiration when requireResponse=true; capped by runtime." })),
|
|
363
|
-
requireResponse: Type.Optional(Type.Boolean({ description: "Opt in to a real msgId-correlated required response.
|
|
363
|
+
requireResponse: Type.Optional(Type.Boolean({ description: "Opt in to a real msgId-correlated required response. Set true whenever the user asks for a reply, status, or confirmation; the sender waits only for the exact msgId reply and expires explicitly." })),
|
|
364
364
|
maxReinjects: Type.Optional(Type.Number({ description: "Bounded receiver reminder budget for requireResponse. Default 1, capped 0..3." })),
|
|
365
365
|
});
|
|
366
366
|
|
|
@@ -275,7 +275,11 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
275
275
|
name: "zpeer_ask",
|
|
276
276
|
label: "ZPeer Ask",
|
|
277
277
|
description: "Ask a visible local ZPeer via the governed room-scoped local_socket path. Defaults to mode=async; raw message/reply bodies are transient and durable metadata is hash-only.",
|
|
278
|
-
promptSnippet: "Use zpeer_ask with mode=\"async\" for
|
|
278
|
+
promptSnippet: "Use zpeer_ask with mode=\"async\" for non-blocking coordination; when an actual reply/status is required, set requireResponse=true and use mode=\"await\" or mode=\"long\".",
|
|
279
|
+
promptGuidelines: [
|
|
280
|
+
"When the user asks for an actual reply, current status, confirmation, or a response requirement, call zpeer_ask with requireResponse=true and mode=\"await\" or mode=\"long\".",
|
|
281
|
+
"Treat zpeer_ask status=waiting as delivery-only: it does not prove the peer read the request, is working, or finished. If nothing else is actionable, stop/idle instead of polling.",
|
|
282
|
+
],
|
|
279
283
|
parameters: ZpeerAskParams,
|
|
280
284
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
281
285
|
if (!state?.zobLive.peerCard) return { content: [{ type: "text", text: "zpeer_ask blocked: current session has not registered a local peer endpoint" }], details: { schema: "zob.zpeer-ask-result.v1", status: "blocked", reason: "local_peer_unavailable", bodyStored: false } };
|
|
@@ -296,7 +300,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
296
300
|
content: queued.content,
|
|
297
301
|
display: true,
|
|
298
302
|
details: queued.details,
|
|
299
|
-
}, { triggerTurn: false }));
|
|
303
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" }));
|
|
300
304
|
}
|
|
301
305
|
};
|
|
302
306
|
const emitZpeerAskEvent = (event: { kind: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>["kind"]; status: string; reason?: string; msgId?: string; roomId?: string; taskHash?: string; outputHash?: string; interruptStatus?: ZpeerInterruptStatus }): boolean => {
|
|
@@ -344,7 +348,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
344
348
|
updatePassivePeerWaitState(state, result, { roomId: requestedRoomId, targetAlias });
|
|
345
349
|
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() });
|
|
346
350
|
const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
|
|
347
|
-
const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
|
|
351
|
+
const passiveWaitSuffix = result.status === "waiting" ? " · delivery-only: no reply received; not evidence the target is working or done · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
|
|
348
352
|
const transientReplyText = (result.status === "reply" || result.status === "completed") && result.transientResponse
|
|
349
353
|
? `\n\nTransient ZPeer reply (not stored in .pi/coms):\n${result.transientResponse}`
|
|
350
354
|
: "";
|
|
@@ -391,7 +395,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
391
395
|
state.zobLive.inboundQueue = (state.zobLive.inboundQueue ?? []).filter((candidate) => candidate !== inbound.envelope.msgId);
|
|
392
396
|
const roomId = inbound.envelope.runId?.startsWith("zpeer:") ? inbound.envelope.runId.slice("zpeer:".length) : undefined;
|
|
393
397
|
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 });
|
|
398
|
+
if (recorded.accepted) void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...recorded.event } }, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
395
399
|
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() });
|
|
396
400
|
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 } };
|
|
397
401
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
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",
|
|
@@ -471,10 +471,11 @@ async function main() {
|
|
|
471
471
|
const registeredTools = new Map();
|
|
472
472
|
const appendEntries = [];
|
|
473
473
|
const feedMessages = [];
|
|
474
|
+
const feedDeliveries = [];
|
|
474
475
|
const mockPi = {
|
|
475
476
|
registerTool: (tool) => registeredTools.set(tool.name, tool),
|
|
476
477
|
appendEntry: (customType, data) => appendEntries.push({ customType, data }),
|
|
477
|
-
sendMessage: (message) => { feedMessages.push(message); return Promise.resolve(); },
|
|
478
|
+
sendMessage: (message, options) => { feedMessages.push(message); feedDeliveries.push({ message, options }); return Promise.resolve(); },
|
|
478
479
|
};
|
|
479
480
|
const toolState = { zobLive: { peerCard: alpha, pendingReplies: { wait: waitForReply } } };
|
|
480
481
|
toolsComs.registerComsTools(mockPi, toolState);
|
|
@@ -488,6 +489,9 @@ async function main() {
|
|
|
488
489
|
assert(JSON.stringify(toolResult?.content ?? '').includes('idle/passive wait: no follow-up turn queued'), 'zpeer_ask async waiting result must tell the agent to idle instead of polling/continuing just to wait');
|
|
489
490
|
const asyncWaitingFeed = feedMessages.filter((item) => item.customType === 'zob-zpeer-event' && item.details?.source === 'agent-request' && item.details?.status === 'waiting' && item.details?.msgId === toolResult?.details?.msgId);
|
|
490
491
|
assert(asyncWaitingFeed.length === 1, `zpeer_ask async must emit exactly one compact waiting feed event, got ${asyncWaitingFeed.length}`);
|
|
492
|
+
const asyncWaitingDelivery = feedDeliveries.find((item) => item.message.details?.source === 'agent-request' && item.message.details?.msgId === toolResult?.details?.msgId);
|
|
493
|
+
assert(asyncWaitingDelivery?.options?.triggerTurn === false && asyncWaitingDelivery?.options?.deliverAs === 'nextTurn', 'zpeer_ask waiting feed must use nextTurn delivery and never create an extra steering/follow-up turn');
|
|
494
|
+
assert(JSON.stringify(toolResult?.content ?? '').includes('delivery-only: no reply received; not evidence the target is working or done'), 'zpeer_ask waiting result must not imply peer progress from delivery-only status');
|
|
491
495
|
assert(!feedMessages.some((item) => item.customType === 'zob-zpeer-event' && item.details?.source === 'agent-request' && item.details?.kind === 'attempt'), 'zpeer_ask async must not emit a pre-ACK attempt feed event');
|
|
492
496
|
assert(!containsRawBody(toolResult), 'zpeer_ask async tool result must not echo the full prompt/response body');
|
|
493
497
|
assert(appendEntries.some((item) => item.customType === 'zob-zpeer' && item.data?.schema === 'zob.zpeer-ask.v1' && item.data?.action === 'agent_request' && item.data?.mode === 'async' && item.data?.bodyStored === false), 'zpeer_ask must append hash-only visible command metadata');
|
|
@@ -538,6 +542,8 @@ async function main() {
|
|
|
538
542
|
const explicitReplyCountBefore = receivedResponses.length;
|
|
539
543
|
const explicitReplyResult = await zpeerReply.execute('tool-call-zpeer-reply', { msgId: explicitReplyMsgId, message: rawResponse }, undefined, undefined, { cwd: repoRoot });
|
|
540
544
|
assert(explicitReplyResult?.details?.status === 'response_sent' && explicitReplyResult?.details?.msgId === explicitReplyMsgId && explicitReplyResult?.details?.outputHash === hashing.sha256(rawResponse), 'zpeer_reply must send a msgId-bound response with outputHash metadata');
|
|
545
|
+
const explicitReplyFeedDelivery = feedDeliveries.find((item) => item.message.details?.kind === 'response_sent' && item.message.details?.msgId === explicitReplyMsgId);
|
|
546
|
+
assert(explicitReplyFeedDelivery?.options?.triggerTurn === false && explicitReplyFeedDelivery?.options?.deliverAs === 'nextTurn', 'zpeer_reply feed must use nextTurn delivery and never create an extra steering/follow-up turn');
|
|
541
547
|
assert(!toolState.zobLive.inboundByMsgId?.[explicitReplyMsgId] && toolState.zobLive.activeInboundMsgId === undefined && toolState.zobLive.inboundQueue.length === 0, 'zpeer_reply must clear the answered inbound msgId from receiver state');
|
|
542
548
|
assert(receivedResponses.length > explicitReplyCountBefore && receivedResponses.at(-1)?.replyToMsgId === explicitReplyMsgId && receivedResponses.at(-1)?.responseHash === hashing.sha256(rawResponse), 'zpeer_reply must deliver a local response envelope bound to replyToMsgId');
|
|
543
549
|
const wrongExplicitReply = await zpeerReply.execute('tool-call-zpeer-reply-wrong', { msgId: 'missing-msgid', message: rawResponse }, undefined, undefined, { cwd: repoRoot });
|
|
@@ -61,10 +61,11 @@ const zpeerAskMatches = toolsComs.match(/name: "zpeer_ask"/g) ?? [];
|
|
|
61
61
|
if (zpeerAskMatches.length !== 1) failures.push(`expected exactly one zpeer_ask tool, found ${zpeerAskMatches.length}`);
|
|
62
62
|
const zpeerReplyMatches = toolsComs.match(/name: "zpeer_reply"/g) ?? [];
|
|
63
63
|
if (zpeerReplyMatches.length !== 1) failures.push(`expected exactly one zpeer_reply tool, found ${zpeerReplyMatches.length}`);
|
|
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', '
|
|
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', 'clampRate(process.env.ZOB_ZPEER_ASK_RATE_LIMIT_PER_MINUTE, 50)', 'clampRate(process.env.ZOB_ZPEER_URGENT_RATE_LIMIT_PER_MINUTE, 10)', 'clampRate(process.env.ZOB_ZPEER_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', 'delivery-only: no reply received; not evidence the target is working or done', 'promptGuidelines:', 'requireResponse=true and mode=\\"await\\" or mode=\\"long\\"', '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']) {
|
|
65
65
|
if (!toolsComs.includes(needle)) failures.push(`zpeer_ask tool missing ${needle}`);
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
if (!toolsComs.includes('}, { triggerTurn: false, deliverAs: "nextTurn" }')) failures.push('zpeer tool feed cards must use nextTurn delivery so metadata-only UI cannot create steering turns');
|
|
68
|
+
for (const needle of ['parameters: ZpeerReplyParams', 'buildZobLiveResponseEnvelope', 'sendZobLocalEnvelope(replyEndpoint', 'replyToMsgId: inbound.envelope.msgId', 'action: "reply"', 'action: "reply_blocked"', 'status: "response_sent"', 'late reply after the requireResponse watchdog expired']) {
|
|
68
69
|
if (!toolsComs.includes(needle)) failures.push(`zpeer_reply tool missing ${needle}`);
|
|
69
70
|
}
|
|
70
71
|
if (toolsComs.includes('emitZpeerAskEvent({ kind: "attempt", status: "agent-request"')) failures.push('zpeer_ask async must not emit pre-ACK attempt feed noise');
|
|
@@ -178,6 +179,7 @@ for (const needle of ['customType: "zob-zpeer-event"', 'if (sendMode.mode !== "a
|
|
|
178
179
|
if (!command.includes(needle)) failures.push(`zpeer command missing UX event/status support ${needle}`);
|
|
179
180
|
}
|
|
180
181
|
if (!command.includes('const eventFromAlias = peerAliasInRoom(state.zobLive.peerCard, eventRoomId)') || !command.includes('peerAliasInRoom(state.zobLive.peerCard, resultRoomId)')) failures.push('/zpeer in <room> events must use room-scoped sender alias');
|
|
182
|
+
if (!command.includes('}, { triggerTurn: false, deliverAs: "nextTurn" });')) failures.push('/zpeer feed cards must use nextTurn delivery so metadata-only UI cannot create steering turns');
|
|
181
183
|
for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'output:', 'content:', 'message:', 'text:', 'diff:', 'patch:']) {
|
|
182
184
|
const appendBlocks = [...command.matchAll(/appendEntry\("zob-zpeer",\s*\{[\s\S]*?\}\);/g)].map((m) => m[0]);
|
|
183
185
|
if (appendBlocks.some((block) => block.includes(forbidden))) failures.push(`zpeer appendEntry contains forbidden key ${forbidden}`);
|
|
@@ -215,6 +217,9 @@ for (const needle of ['readZpeerNewCarryoverProfile(repoRoot)', 'const carryover
|
|
|
215
217
|
}
|
|
216
218
|
if (!events.includes('event.source === "extension" && !event.text.trim()') || !events.includes('action: "handled" as const')) failures.push('runtime must handle empty extension follow-ups without continuing the agent');
|
|
217
219
|
if (!events.includes('ZPeer async reply received from @') || !events.includes('{ triggerTurn: true, deliverAs: "followUp" }')) failures.push('runtime must resume an idle agent with a follow-up when an async ZPeer reply arrives');
|
|
220
|
+
const eventZpeerFeedFragments = events.split('customType: "zob-zpeer-event"').slice(1);
|
|
221
|
+
if (eventZpeerFeedFragments.length === 0 || eventZpeerFeedFragments.some((fragment) => !fragment.slice(0, 800).includes('}, { triggerTurn: false, deliverAs: "nextTurn" });'))) failures.push('every runtime ZPeer feed card must use nextTurn delivery so it cannot race ahead of actionable prompt/response bodies');
|
|
222
|
+
if (!events.includes('status=waiting is delivery-only, not evidence that the peer is working or done')) failures.push('runtime awareness must prevent interpreting waiting as peer progress/completion evidence');
|
|
218
223
|
for (const needle of ['forceAbortAllowedForCurrentState', 'interruptStatus = "force_blocked"', 'interruptStatus = "force_downgraded"', 'ctx.abort()', 'const deliverAs = priority === "normal" ? "followUp" : "steer"', 'inboundByMsgId', 'activeInboundMsgId', 'state.zobLive.inboundByMsgId && !activeInbound', 'scheduleZpeerRequiredResponseWatchdog', 'required_response_reinject', 'required_response_expired', 'Original transient message:', 'zpeer_required_response_expired', 'pendingReplies.expire', 'forceAbortRepeated: false', 'replyToMsgId !== envelope.msgId', 'wrong or missing replyToMsgId']) {
|
|
219
224
|
if (!events.includes(needle)) failures.push(`runtime missing urgent/force/msgId-safe inbound support ${needle}`);
|
|
220
225
|
}
|