zob-harness 0.15.1 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/zob-harness/index.ts +1 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-status.ts +92 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +4 -1
- package/.pi/extensions/zob-harness/src/runtime/commands/zlive.ts +10 -5
- package/.pi/extensions/zob-harness/src/runtime/events.ts +8 -10
- package/.pi/extensions/zob-harness/src/runtime/state.ts +5 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +47 -14
- package/.pi/extensions/zob-harness/src/runtime/zpeer-events.ts +53 -0
- package/package.json +1 -1
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -620,7 +623,7 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
620
623
|
deliveryMethod: result.deliveryMethod,
|
|
621
624
|
fallbackDelivery: result.fallback_delivery,
|
|
622
625
|
});
|
|
623
|
-
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 });
|
|
624
627
|
};
|
|
625
628
|
|
|
626
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 });
|
|
@@ -17,6 +17,7 @@ import { loadActiveZagentScopedMode } from "../events.js";
|
|
|
17
17
|
import { resolveRuleProfile } from "../../domains/governance/rules.js";
|
|
18
18
|
import type { HarnessRuntimeState } from "../state.js";
|
|
19
19
|
import { applyMode, renderHarnessWidget } from "../widget.js";
|
|
20
|
+
import { recordZpeerRuntimeEvent } from "../zpeer-events.js";
|
|
20
21
|
|
|
21
22
|
function zpeerCommandProfileId(ctx: ExtensionCommandContext): string {
|
|
22
23
|
const sessionIdentity = ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
|
|
@@ -1140,18 +1141,21 @@ async function executeZteamTmuxActionPlan(_repoRoot: string, plan: ZteamTmuxActi
|
|
|
1140
1141
|
}
|
|
1141
1142
|
|
|
1142
1143
|
export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeState): void {
|
|
1143
|
-
const rememberZpeerEvent = (event: { kind: NonNullable<typeof state.zobLive.lastEvent>["kind"]; roomId?: string; fromAlias?: string; toAlias?: string; status: string; reason?: string; msgId?: string; taskHash?: string; outputHash?: string; priority?: ZpeerInterruptPriority; interruptMode?: ZpeerInterruptMode; interruptStatus?: ZpeerInterruptStatus }):
|
|
1144
|
-
|
|
1144
|
+
const rememberZpeerEvent = (event: { kind: NonNullable<typeof state.zobLive.lastEvent>["kind"]; roomId?: string; fromAlias?: string; toAlias?: string; status: string; reason?: string; msgId?: string; taskHash?: string; outputHash?: string; priority?: ZpeerInterruptPriority; interruptMode?: ZpeerInterruptMode; interruptStatus?: ZpeerInterruptStatus }): NonNullable<typeof state.zobLive.lastEvent> | undefined => {
|
|
1145
|
+
const recorded = recordZpeerRuntimeEvent(state, event);
|
|
1146
|
+
return recorded.accepted ? recorded.event : undefined;
|
|
1145
1147
|
};
|
|
1146
1148
|
|
|
1147
|
-
const emitZpeerEvent = (event: Parameters<typeof rememberZpeerEvent>[0]):
|
|
1148
|
-
rememberZpeerEvent(event);
|
|
1149
|
+
const emitZpeerEvent = (event: Parameters<typeof rememberZpeerEvent>[0]): boolean => {
|
|
1150
|
+
const recorded = rememberZpeerEvent(event);
|
|
1151
|
+
if (!recorded) return false;
|
|
1149
1152
|
void pi.sendMessage({
|
|
1150
1153
|
customType: "zob-zpeer-event",
|
|
1151
1154
|
content: `ZPeer ${event.kind} ${event.fromAlias ? `@${event.fromAlias}` : "?"} → ${event.toAlias ? `@${event.toAlias}` : "?"} ${event.status}`,
|
|
1152
1155
|
display: true,
|
|
1153
|
-
details: { ...
|
|
1156
|
+
details: { ...recorded },
|
|
1154
1157
|
}, { triggerTurn: false });
|
|
1158
|
+
return true;
|
|
1155
1159
|
};
|
|
1156
1160
|
|
|
1157
1161
|
pi.registerCommand("zagent", {
|
|
@@ -1682,6 +1686,7 @@ export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
1682
1686
|
maxReinjects: sendMode.maxReinjects,
|
|
1683
1687
|
onFeedback: (feedback) => {
|
|
1684
1688
|
feedbackEmittedTerminal = feedback.result.status === "waiting" || feedback.result.status === "reply" || feedback.result.status === "completed" || feedback.result.status === "blocked" || feedback.result.status === "error" || feedback.result.status === "timeout" || feedback.result.status === "expired" || feedback.result.status === "required_response_expired";
|
|
1689
|
+
if (feedback.kind === "waiting" && (sendMode.requireResponse === true || sendMode.mode !== "async")) return;
|
|
1685
1690
|
const feedbackRoomId = feedback.result.roomId ?? eventRoomId;
|
|
1686
1691
|
emitZpeerEvent({ kind: feedback.kind, roomId: feedbackRoomId, fromAlias: state.zobLive.peerCard ? peerAliasInRoom(state.zobLive.peerCard, feedbackRoomId) ?? eventFromAlias : eventFromAlias, toAlias: feedback.result.targetAlias ?? targetAlias, status: feedback.result.status, reason: feedback.result.reason, msgId: feedback.result.msgId, taskHash: feedback.result.taskHash, outputHash: feedback.result.outputHash, priority: feedback.result.priority ?? sendMode.priority, interruptMode: feedback.result.interruptMode ?? sendMode.interruptMode, interruptStatus: feedback.result.interruptStatus });
|
|
1687
1692
|
},
|
|
@@ -40,6 +40,7 @@ import { capturePlanArtifact } from "./plan-capture.js";
|
|
|
40
40
|
import { redactPlanTodosBlockForDisplay } from "../domains/plan/plan-todos.js";
|
|
41
41
|
import { applyMode, renderHarnessWidget } from "./widget.js";
|
|
42
42
|
import { createStopRestoreCandidate, markStopRestoreAssistantMessage, markStopRestoreToolVisible } from "./stop-restore.js";
|
|
43
|
+
import { recordZpeerRuntimeEvent } from "./zpeer-events.js";
|
|
43
44
|
|
|
44
45
|
function safelyUpdateZobLivePeer(repoRoot: string, action: "register" | "touch" | "unregister"): void {
|
|
45
46
|
try {
|
|
@@ -51,14 +52,8 @@ function safelyUpdateZobLivePeer(repoRoot: string, action: "register" | "touch"
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function setZpeerLastEvent(state: HarnessRuntimeState, event: Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored"> & { at?: string }):
|
|
55
|
-
state.
|
|
56
|
-
...event,
|
|
57
|
-
at: event.at ?? new Date().toISOString(),
|
|
58
|
-
localOnly: true,
|
|
59
|
-
networkEnabled: false,
|
|
60
|
-
bodyStored: false,
|
|
61
|
-
};
|
|
55
|
+
function setZpeerLastEvent(state: HarnessRuntimeState, event: Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored" | "terminal" | "statusRank" | "superseded" | "supersededByStatus"> & { at?: string }): ZobLiveLastEvent {
|
|
56
|
+
return recordZpeerRuntimeEvent(state, event).event;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
function clearPassivePeerWaitForResponse(state: HarnessRuntimeState, envelope: { msgId?: string; runId?: string; sender?: string; type?: string }): void {
|
|
@@ -1060,7 +1055,10 @@ async function sendInboundZobLiveResponse(pi: ExtensionAPI, state: HarnessRuntim
|
|
|
1060
1055
|
const activeInbound = activeZpeerInboundForResponse(state);
|
|
1061
1056
|
const inbound = activeInbound ?? state.zobLive.inbound;
|
|
1062
1057
|
if (!inbound || inbound.responseSent || !inbound.envelope.replyEndpoint) return;
|
|
1063
|
-
|
|
1058
|
+
// ZOB-COMS-GUARDS: a late reply after expiry is now permitted (the watchdog
|
|
1059
|
+
// already stopped reinjecting; sender-side pendingReplies resolves to no-op).
|
|
1060
|
+
// Dropping this early-out recovers nudge/reply paths (e.g. a late oracle
|
|
1061
|
+
// review) without weakening the real anti-abuse (reinject cap + sender expire).
|
|
1064
1062
|
if (state.zobLive.inboundByMsgId && !activeInbound) return;
|
|
1065
1063
|
const responseText = latestAssistantText(event);
|
|
1066
1064
|
if (!responseText.trim()) return;
|
|
@@ -1118,7 +1116,7 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
|
|
|
1118
1116
|
const taskHash = typeof details.taskHash === "string" ? details.taskHash : undefined;
|
|
1119
1117
|
const outputHash = typeof details.outputHash === "string" ? details.outputHash : undefined;
|
|
1120
1118
|
const route = fromAlias || toAlias ? `${fromAlias ? `@${fromAlias}` : "?"} → ${toAlias ? `@${toAlias}` : "?"}` : "room status";
|
|
1121
|
-
const statusColor = status === "completed" || status === "sent" || status === "prompt_received" || status === "response_sent" ? "success" : status === "blocked" || status === "timeout" || status === "error" || status === "required_response_expired" ? "warning" : "muted";
|
|
1119
|
+
const statusColor = status === "reply" || status === "completed" || status === "sent" || status === "delivered" || status === "prompt_received" || status === "response_sent" ? "success" : status === "blocked" || status === "timeout" || status === "error" || status === "expired" || status === "required_response_expired" ? "warning" : "muted";
|
|
1122
1120
|
const line = [
|
|
1123
1121
|
theme.fg("accent", "◆ ZPeer"),
|
|
1124
1122
|
theme.fg("muted", kind),
|
|
@@ -49,6 +49,10 @@ export interface ZobLiveLastEvent {
|
|
|
49
49
|
priority?: ZpeerInterruptPriority;
|
|
50
50
|
interruptMode?: ZpeerInterruptMode;
|
|
51
51
|
interruptStatus?: ZpeerInterruptStatus;
|
|
52
|
+
terminal?: boolean;
|
|
53
|
+
statusRank?: number;
|
|
54
|
+
superseded?: boolean;
|
|
55
|
+
supersededByStatus?: string;
|
|
52
56
|
at: string;
|
|
53
57
|
localOnly: true;
|
|
54
58
|
networkEnabled: false;
|
|
@@ -98,6 +102,7 @@ export interface ZobLiveRuntimeState {
|
|
|
98
102
|
inboundQueue?: string[];
|
|
99
103
|
activeInboundMsgId?: string;
|
|
100
104
|
lastEvent?: ZobLiveLastEvent;
|
|
105
|
+
latestZpeerEventByMsgId?: Record<string, ZobLiveLastEvent>;
|
|
101
106
|
passivePeerWait?: ZobPassivePeerWaitState;
|
|
102
107
|
leaseOwned?: boolean;
|
|
103
108
|
leaseStatus?: "owned" | "blocked" | "unavailable";
|
|
@@ -34,11 +34,23 @@ import {
|
|
|
34
34
|
} from "../domains/topology/coms.js";
|
|
35
35
|
import { loadTeamDefinition, validateTeamDefinition } from "../domains/topology/teams.js";
|
|
36
36
|
import type { HarnessRuntimeState } from "./state.js";
|
|
37
|
+
import { isCurrentZpeerRuntimeEvent, recordZpeerRuntimeEvent } from "./zpeer-events.js";
|
|
37
38
|
|
|
38
39
|
const SHA256_HEX = /^[a-f0-9]{64}$/i;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 ?? "");
|
|
42
54
|
|
|
43
55
|
type ZpeerFallbackHookFn = ZpeerFallbackDelivery;
|
|
44
56
|
|
|
@@ -141,7 +153,7 @@ function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolPara
|
|
|
141
153
|
if (selfAlias && zpeerAliasesEquivalent(targetAlias, selfAlias)) return "cannot send to self";
|
|
142
154
|
const roomId = safeZpeerRoomId(params.roomId) ?? currentRoomId;
|
|
143
155
|
if (params.roomId && !safeZpeerRoomId(params.roomId)) return "invalid room id";
|
|
144
|
-
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";
|
|
145
157
|
const messageHash = sha256(params.message);
|
|
146
158
|
const now = Date.now();
|
|
147
159
|
const windowMs = 60_000;
|
|
@@ -275,16 +287,29 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
275
287
|
const requestedFromAlias = peerAliasInRoom(self, requestedRoomId) ?? self.zpeerAlias;
|
|
276
288
|
const interrupt = normalizeZpeerInterrupt(params);
|
|
277
289
|
const guardReason = interrupt.error ?? zpeerAskGuardBlock(state, params, requestedFromAlias, requestedRoomId, interrupt.priority);
|
|
278
|
-
const
|
|
290
|
+
const queuedZpeerEvents: Array<{ event: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>; content: string; details: Record<string, unknown> }> = [];
|
|
291
|
+
const flushZpeerAskEvents = async (): Promise<void> => {
|
|
292
|
+
for (const queued of queuedZpeerEvents) {
|
|
293
|
+
if (!isCurrentZpeerRuntimeEvent(state, queued.event)) continue;
|
|
294
|
+
await Promise.resolve(pi.sendMessage({
|
|
295
|
+
customType: "zob-zpeer-event",
|
|
296
|
+
content: queued.content,
|
|
297
|
+
display: true,
|
|
298
|
+
details: queued.details,
|
|
299
|
+
}, { triggerTurn: false }));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const emitZpeerAskEvent = (event: { kind: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>["kind"]; status: string; reason?: string; msgId?: string; roomId?: string; taskHash?: string; outputHash?: string; interruptStatus?: ZpeerInterruptStatus }): boolean => {
|
|
279
303
|
const eventRoomId = event.roomId ?? requestedRoomId;
|
|
280
304
|
const fromAlias = peerAliasInRoom(self, eventRoomId) ?? requestedFromAlias;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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,
|
|
284
309
|
content: `ZPeer agent-request @${fromAlias ?? "?"} → @${targetAlias} ${event.status}`,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
details: { ...recorded.event, source: "agent-request", mode, priority: interrupt.priority, interruptMode: interrupt.interruptMode, bodyStored: false, localOnly: true, networkEnabled: false },
|
|
311
|
+
});
|
|
312
|
+
return true;
|
|
288
313
|
};
|
|
289
314
|
const taskHash = params.message.trim() ? sha256(params.message) : undefined;
|
|
290
315
|
if (guardReason) {
|
|
@@ -293,6 +318,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
293
318
|
const result = { schema: "zob.zpeer-ask-result.v1", status: "blocked", reason: guardReason, targetAlias, taskHash, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus, bodyStored: false };
|
|
294
319
|
emitZpeerAskEvent({ kind: "blocked", status: "blocked", reason: guardReason, taskHash, interruptStatus });
|
|
295
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();
|
|
296
322
|
return { content: [{ type: "text", text: `zpeer_ask blocked: ${guardReason}` }], details: result };
|
|
297
323
|
}
|
|
298
324
|
const timeoutMs = boundedZpeerAskTimeoutMs(mode, params.timeoutMs);
|
|
@@ -323,6 +349,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
323
349
|
? `\n\nTransient ZPeer reply (not stored in .pi/coms):\n${result.transientResponse}`
|
|
324
350
|
: "";
|
|
325
351
|
const interruptSuffix = result.interruptStatus ? ` interrupt=${result.interruptStatus}` : interrupt.priority !== "normal" ? ` priority=${interrupt.priority}` : "";
|
|
352
|
+
await flushZpeerAskEvents();
|
|
326
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 } };
|
|
327
354
|
},
|
|
328
355
|
});
|
|
@@ -338,7 +365,13 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
338
365
|
const responseText = params.message ?? "";
|
|
339
366
|
const outputHash = responseText.trim() ? sha256(responseText) : undefined;
|
|
340
367
|
const inbound = msgId ? state?.zobLive.inboundByMsgId?.[msgId] : undefined;
|
|
341
|
-
const block = !state ? "zpeer runtime state unavailable" : !msgId ? "msgId is required" : !responseText.trim() ? "message is required" : !inbound ? "no active inbound ZPeer message for msgId" : inbound.responseSent || inbound.requiredResponseStatus === "replied" ? "ZPeer msgId already answered" :
|
|
368
|
+
const block = !state ? "zpeer runtime state unavailable" : !msgId ? "msgId is required" : !responseText.trim() ? "message is required" : !inbound ? "no active inbound ZPeer message for msgId" : inbound.responseSent || inbound.requiredResponseStatus === "replied" ? "ZPeer msgId already answered" : // ZOB-COMS-GUARDS: a late reply after the requireResponse watchdog expired is
|
|
369
|
+
// now PERMITTED. The watchdog already stops reinjecting once reinjectCount hits
|
|
370
|
+
// maxReinjects (events.ts) and the sender's pendingReplies entry resolves to a
|
|
371
|
+
// no-op on a late envelope, so a late reply is harmless at the sender and
|
|
372
|
+
// recovers nudge/reply paths (e.g. a late oracle review). The 'replied' /
|
|
373
|
+
// responseSent guard above still prevents double-replies.
|
|
374
|
+
!inbound.envelope.replyEndpoint ? "ZPeer inbound msgId has no reply endpoint" : undefined;
|
|
342
375
|
if (block) {
|
|
343
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() });
|
|
344
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 } };
|
|
@@ -357,8 +390,8 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
|
|
|
357
390
|
state.zobLive.activeInboundMsgId = undefined;
|
|
358
391
|
state.zobLive.inboundQueue = (state.zobLive.inboundQueue ?? []).filter((candidate) => candidate !== inbound.envelope.msgId);
|
|
359
392
|
const roomId = inbound.envelope.runId?.startsWith("zpeer:") ? inbound.envelope.runId.slice("zpeer:".length) : undefined;
|
|
360
|
-
|
|
361
|
-
void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...
|
|
393
|
+
const recorded = recordZpeerRuntimeEvent(state, { kind: "response_sent", roomId, fromAlias: inbound.envelope.receiver, toAlias: inbound.envelope.sender, status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, priority: inbound.priority, interruptMode: inbound.interruptMode });
|
|
394
|
+
if (recorded.accepted) void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...recorded.event } }, { triggerTurn: false });
|
|
362
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() });
|
|
363
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 } };
|
|
364
397
|
} catch (error) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { annotateZpeerStatus, shouldAcceptZpeerStatusUpdate } from "../domains/coms/coms-v2/zpeer-status.js";
|
|
2
|
+
import type { HarnessRuntimeState, ZobLiveLastEvent } from "./state.js";
|
|
3
|
+
|
|
4
|
+
export type ZpeerRuntimeEventInput = Omit<ZobLiveLastEvent, "at" | "localOnly" | "networkEnabled" | "bodyStored" | "terminal" | "statusRank" | "superseded" | "supersededByStatus"> & { at?: string };
|
|
5
|
+
|
|
6
|
+
export interface ZpeerRuntimeEventRecordResult {
|
|
7
|
+
accepted: boolean;
|
|
8
|
+
event: ZobLiveLastEvent;
|
|
9
|
+
current?: ZobLiveLastEvent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function materializeZpeerRuntimeEvent(event: ZpeerRuntimeEventInput): ZobLiveLastEvent {
|
|
13
|
+
return annotateZpeerStatus({
|
|
14
|
+
...event,
|
|
15
|
+
at: event.at ?? new Date().toISOString(),
|
|
16
|
+
localOnly: true as const,
|
|
17
|
+
networkEnabled: false as const,
|
|
18
|
+
bodyStored: false as const,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function recordZpeerRuntimeEvent(state: HarnessRuntimeState, event: ZpeerRuntimeEventInput): ZpeerRuntimeEventRecordResult {
|
|
23
|
+
const next = materializeZpeerRuntimeEvent(event);
|
|
24
|
+
if (!next.msgId) {
|
|
25
|
+
state.zobLive.lastEvent = next;
|
|
26
|
+
return { accepted: true, event: next };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
state.zobLive.latestZpeerEventByMsgId ??= {};
|
|
30
|
+
const current = state.zobLive.latestZpeerEventByMsgId[next.msgId];
|
|
31
|
+
if (!shouldAcceptZpeerStatusUpdate(current, next)) {
|
|
32
|
+
return {
|
|
33
|
+
accepted: false,
|
|
34
|
+
current,
|
|
35
|
+
event: {
|
|
36
|
+
...next,
|
|
37
|
+
superseded: true,
|
|
38
|
+
supersededByStatus: current?.status,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
state.zobLive.latestZpeerEventByMsgId[next.msgId] = next;
|
|
44
|
+
state.zobLive.lastEvent = next;
|
|
45
|
+
return { accepted: true, event: next, current: next };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isCurrentZpeerRuntimeEvent(state: HarnessRuntimeState, event: ZobLiveLastEvent): boolean {
|
|
49
|
+
if (!event.msgId) return true;
|
|
50
|
+
const current = state.zobLive.latestZpeerEventByMsgId?.[event.msgId];
|
|
51
|
+
if (!current) return true;
|
|
52
|
+
return current.at === event.at && current.status === event.status && current.kind === event.kind;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
|
|
6
6
|
"license": "MIT",
|