zob-harness 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +24 -3
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts +1 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +5 -3
- package/.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts +28 -3
- package/.pi/extensions/zob-harness/src/runtime/delegation-activity.ts +289 -0
- package/.pi/extensions/zob-harness/src/runtime/delegation-feed.ts +27 -0
- package/.pi/extensions/zob-harness/src/runtime/delegation-monitor.ts +8 -0
- package/.pi/extensions/zob-harness/src/runtime/events.ts +67 -33
- package/.pi/extensions/zob-harness/src/runtime/state.ts +2 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/helpers.ts +31 -1
- package/.pi/extensions/zob-harness/src/runtime/tools-delegation/register.ts +3 -0
- package/.pi/extensions/zob-harness/src/runtime/widget.ts +14 -4
- package/package.json +1 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +18 -0
- package/scripts/zpeer-static-smoke.mjs +10 -3
|
@@ -97,6 +97,21 @@ function readLeasesFromDir(dir: string, nowMs: number, teamName?: string): ZobLi
|
|
|
97
97
|
.map((lease) => leaseToPeerCard(lease, nowMs));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function peerRegistryKey(peer: Pick<ZobLivePeerCard, "projectId" | "roleId" | "sessionHash">): string {
|
|
101
|
+
return `${peer.projectId}:${peer.roleId}:${peer.sessionHash}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mergeLeaseBackedAndAdhocPeers(leasePeers: ZobLivePeerCard[], agentPeers: ZobLivePeerCard[]): ZobLivePeerCard[] {
|
|
105
|
+
const byKey = new Map<string, ZobLivePeerCard>();
|
|
106
|
+
for (const peer of leasePeers) byKey.set(peerRegistryKey(peer), peer);
|
|
107
|
+
for (const peer of agentPeers) {
|
|
108
|
+
if (peer.zpeerAdhoc !== true) continue;
|
|
109
|
+
const key = peerRegistryKey(peer);
|
|
110
|
+
if (!byKey.has(key)) byKey.set(key, peer);
|
|
111
|
+
}
|
|
112
|
+
return [...byKey.values()];
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
function boundedOfflinePeerRetentionMs(value: number | undefined): number {
|
|
101
116
|
const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
|
|
102
117
|
const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
|
|
@@ -514,8 +529,13 @@ export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-c
|
|
|
514
529
|
export function readZobLiveRegistrySnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
|
|
515
530
|
const { dir, projectId, kind } = projectLeasesDir(repoRoot);
|
|
516
531
|
const nowMs = Date.now();
|
|
517
|
-
if (existsSync(dir)) return buildSnapshot(projectId, kind, readLeasesFromDir(dir, nowMs, teamName), teamName);
|
|
518
532
|
const agents = projectAgentsDir(repoRoot);
|
|
533
|
+
if (existsSync(dir)) {
|
|
534
|
+
return buildSnapshot(projectId, kind, mergeLeaseBackedAndAdhocPeers(
|
|
535
|
+
readLeasesFromDir(dir, nowMs, teamName),
|
|
536
|
+
readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName),
|
|
537
|
+
), teamName);
|
|
538
|
+
}
|
|
519
539
|
return buildSnapshot(agents.projectId, agents.kind, readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName), teamName);
|
|
520
540
|
}
|
|
521
541
|
|
|
@@ -523,9 +543,10 @@ export function readZobLiveRegistryAllProjectsSnapshot(repoRoot: string, teamNam
|
|
|
523
543
|
const { projectId, kind } = projectAgentsDir(repoRoot);
|
|
524
544
|
const nowMs = Date.now();
|
|
525
545
|
const leaseDirs = allProjectLeasesDirs();
|
|
546
|
+
const agentPeers = allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
|
|
526
547
|
const hasLeaseDomain = leaseDirs.some((dir) => existsSync(dir));
|
|
527
548
|
const peers = hasLeaseDomain
|
|
528
|
-
? leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName))
|
|
529
|
-
:
|
|
549
|
+
? mergeLeaseBackedAndAdhocPeers(leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName)), agentPeers)
|
|
550
|
+
: agentPeers;
|
|
530
551
|
return buildSnapshot(projectId, kind, peers, teamName);
|
|
531
552
|
}
|
|
@@ -234,7 +234,7 @@ export function refreshZpeerSelf(repoRoot: string, peer: ZobLivePeerCard, roomId
|
|
|
234
234
|
const ensured = ensureZpeerFields(repoRoot, peer, roomId, alias, restoredMemberships);
|
|
235
235
|
if (!hasLocalSocketEndpointEvidence(ensured)) return ensured;
|
|
236
236
|
const refreshed = writeZobLivePeerCard(repoRoot, { ...ensured, heartbeatAt: new Date().toISOString(), status: "online" });
|
|
237
|
-
writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
|
|
237
|
+
if (refreshed.zpeerAdhoc !== true) writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
|
|
238
238
|
return refreshed;
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -479,8 +479,10 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
479
479
|
};
|
|
480
480
|
|
|
481
481
|
if (!selfMembership) return finish("attempt", { status: "blocked", reason: `current peer is not a member of room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
if (self.zpeerAdhoc !== true) {
|
|
483
|
+
const leaseOwnership = ownsZobLiveTeamAgentLease(repoRoot, self);
|
|
484
|
+
if (!leaseOwnership.owned) return finish("attempt", { status: "blocked", reason: `current peer does not own stable team-agent lease (${leaseOwnership.reason})`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
|
|
485
|
+
}
|
|
484
486
|
if (selfMembership.role === "observer") return finish("attempt", { status: "blocked", reason: `current peer is observer-only in room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
|
|
485
487
|
if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
|
|
486
488
|
if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
|
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import type
|
|
6
|
+
import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
import { discoverAgents } from "./agents.js";
|
|
9
9
|
import { SUPERVISED_READONLY_CHILD_TOOLS } from "../../core/constants.js";
|
|
@@ -24,6 +24,28 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
|
24
24
|
return { command: "pi", args };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function childModelPattern(ctx: ExtensionContext, agent: HarnessAgent, modelOverride: string | undefined): string | undefined {
|
|
28
|
+
if (modelOverride?.trim()) return modelOverride.trim();
|
|
29
|
+
if (agent.model?.trim()) return agent.model.trim();
|
|
30
|
+
const model = ctx.model;
|
|
31
|
+
if (!model?.provider || !model.id) return undefined;
|
|
32
|
+
return `${model.provider}/${model.id}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function providerFromModelPattern(model: string | undefined): string | undefined {
|
|
36
|
+
if (!model) return undefined;
|
|
37
|
+
const [provider] = model.split("/");
|
|
38
|
+
return provider && provider !== model ? provider : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveCodexFastModeExtension(ctx: ExtensionContext, model: string | undefined): string | undefined {
|
|
42
|
+
const provider = ctx.model?.provider ?? providerFromModelPattern(model);
|
|
43
|
+
const usesCodexProvider = provider === "openai-codex" || provider === "codex-auto" || provider?.startsWith("codex-") === true;
|
|
44
|
+
if (!usesCodexProvider) return undefined;
|
|
45
|
+
const extensionPath = join(getAgentDir(), "extensions", "codex-fast-mode.ts");
|
|
46
|
+
return existsSync(extensionPath) ? extensionPath : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
const CHILD_THINKING_LEVELS = new Set<string>(["low", "medium", "high", "xhigh"]);
|
|
28
50
|
|
|
29
51
|
function validateChildThinkingOverride(thinking: string | undefined, fieldName = "thinking"): string[] {
|
|
@@ -90,13 +112,14 @@ async function runChildAgent(
|
|
|
90
112
|
pathPolicy?: { allowedPaths?: string[]; forbiddenPaths?: string[]; sandboxRoot?: string },
|
|
91
113
|
thinkingOverride?: ChildThinkingLevel,
|
|
92
114
|
): Promise<ChildResult> {
|
|
115
|
+
const resolvedModel = childModelPattern(ctx, agent, modelOverride);
|
|
93
116
|
const result: ChildResult = {
|
|
94
117
|
agent: agent.name,
|
|
95
118
|
task,
|
|
96
119
|
exitCode: 0,
|
|
97
120
|
output: "",
|
|
98
121
|
stderr: "",
|
|
99
|
-
model:
|
|
122
|
+
model: resolvedModel,
|
|
100
123
|
usage: usageEmpty(),
|
|
101
124
|
};
|
|
102
125
|
|
|
@@ -123,10 +146,12 @@ async function runChildAgent(
|
|
|
123
146
|
|
|
124
147
|
try {
|
|
125
148
|
const childSafetyExtension = join(ctx.cwd, ".pi", "extensions", "zob-child-safety", "index.ts");
|
|
149
|
+
const childCodexFastModeExtension = resolveCodexFastModeExtension(ctx, resolvedModel);
|
|
126
150
|
const args = ["--mode", "json", "-p", "--no-extensions"];
|
|
151
|
+
if (childCodexFastModeExtension) args.push("-e", childCodexFastModeExtension);
|
|
127
152
|
if (existsSync(childSafetyExtension)) args.push("-e", childSafetyExtension);
|
|
128
153
|
args.push("--session", sessionPath);
|
|
129
|
-
const model =
|
|
154
|
+
const model = resolvedModel;
|
|
130
155
|
if (model) args.push("--model", model);
|
|
131
156
|
const thinking = resolveChildThinking(agent, thinkingOverride);
|
|
132
157
|
if (thinking) args.push("--thinking", thinking);
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isRecord } from "../core/utils/records.js";
|
|
5
|
+
import { sanitizeDelegationText } from "./delegation-markdown.js";
|
|
6
|
+
|
|
7
|
+
const ACTIVITY_MAX_BYTES = 180_000;
|
|
8
|
+
const DEFAULT_RECENT_LIMIT = 5;
|
|
9
|
+
|
|
10
|
+
export type DelegationActivityStatus = "running" | "success" | "error";
|
|
11
|
+
|
|
12
|
+
export interface DelegationActivity {
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
status: DelegationActivityStatus;
|
|
16
|
+
startedAtMs?: number;
|
|
17
|
+
endedAtMs?: number;
|
|
18
|
+
elapsedMs?: number;
|
|
19
|
+
command?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
target?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
outputSummary?: string;
|
|
24
|
+
errorSummary?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DelegationActivitySnapshot {
|
|
28
|
+
status: "ok" | "missing" | "blocked" | "empty";
|
|
29
|
+
note?: string;
|
|
30
|
+
fingerprint: string;
|
|
31
|
+
current?: DelegationActivity;
|
|
32
|
+
recent: DelegationActivity[];
|
|
33
|
+
lastUpdatedMs?: number;
|
|
34
|
+
quietMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePath(path: string): string {
|
|
38
|
+
return resolve(path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isInside(root: string, candidate: string): boolean {
|
|
42
|
+
const normalizedRoot = normalizePath(root);
|
|
43
|
+
const normalizedCandidate = normalizePath(candidate);
|
|
44
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${sep}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTail(path: string, size: number, maxBytes: number): string {
|
|
48
|
+
if (size <= maxBytes) {
|
|
49
|
+
const buffer = Buffer.alloc(size);
|
|
50
|
+
const fd = openSync(path, "r");
|
|
51
|
+
try {
|
|
52
|
+
readSync(fd, buffer, 0, size, 0);
|
|
53
|
+
} finally {
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
return buffer.toString("utf8");
|
|
57
|
+
}
|
|
58
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
59
|
+
const fd = openSync(path, "r");
|
|
60
|
+
try {
|
|
61
|
+
readSync(fd, buffer, 0, maxBytes, Math.max(0, size - maxBytes));
|
|
62
|
+
} finally {
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
}
|
|
65
|
+
const text = buffer.toString("utf8");
|
|
66
|
+
const firstNewline = text.indexOf("\n");
|
|
67
|
+
const safeTail = firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
|
|
68
|
+
return `{"type":"custom","customType":"zob-activity-tail-capped","data":{"omittedBytes":${size - maxBytes}}}\n${safeTail}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function timestampMs(entry: Record<string, unknown>): number | undefined {
|
|
72
|
+
if (typeof entry.timestamp !== "string") return undefined;
|
|
73
|
+
const parsed = Date.parse(entry.timestamp);
|
|
74
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactWhitespace(value: string): string {
|
|
78
|
+
return value.replace(/\s+/g, " ").trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function redactDelegationCommand(command: string): string {
|
|
82
|
+
return sanitizeDelegationText(command)
|
|
83
|
+
.replace(/\b([A-Z0-9_]*(?:DATABASE_URL|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, "$1=<redacted>")
|
|
84
|
+
.replace(/(Authorization\s*:\s*Bearer\s+)("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
|
|
85
|
+
.replace(/\b(--(?:password|passwd|token|secret|api-key|access-key|private-key|database-url)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi, "$1<redacted>")
|
|
86
|
+
.replace(/\b(-H\s+["']?Authorization\s*:\s*Bearer\s+)([^"'\s]+)/gi, "$1<redacted>");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function capInline(value: string, limit: number): string {
|
|
90
|
+
const compact = compactWhitespace(sanitizeDelegationText(value));
|
|
91
|
+
if (compact.length <= limit) return compact;
|
|
92
|
+
const head = Math.max(8, Math.floor(limit * 0.65));
|
|
93
|
+
const tail = Math.max(4, limit - head - 1);
|
|
94
|
+
return `${compact.slice(0, head)}…${compact.slice(-tail)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function contentText(content: unknown): string {
|
|
98
|
+
if (typeof content === "string") return sanitizeDelegationText(content);
|
|
99
|
+
if (!Array.isArray(content)) return "";
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
for (const part of content) {
|
|
102
|
+
if (!isRecord(part) || typeof part.type !== "string") continue;
|
|
103
|
+
if (part.type === "text" && typeof part.text === "string") parts.push(sanitizeDelegationText(part.text));
|
|
104
|
+
else if (part.type === "image") parts.push("[image]");
|
|
105
|
+
}
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toolArguments(part: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const direct = part.arguments ?? part.args ?? part.input;
|
|
111
|
+
return isRecord(direct) ? direct : {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toolName(part: Record<string, unknown>): string {
|
|
115
|
+
return typeof part.name === "string" ? part.name : typeof part.toolName === "string" ? part.toolName : "tool";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function timeoutMsFromArgs(args: Record<string, unknown>): number | undefined {
|
|
119
|
+
const raw = args.timeout ?? args.timeout_ms ?? args.timeoutMs;
|
|
120
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return undefined;
|
|
121
|
+
// Pi bash tool accepts timeout in seconds; keep millisecond values if callers already use them.
|
|
122
|
+
return raw > 10_000 ? Math.floor(raw) : Math.floor(raw * 1000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function activityFromToolCall(entry: Record<string, unknown>, part: Record<string, unknown>): DelegationActivity | undefined {
|
|
126
|
+
const id = typeof part.id === "string" ? part.id : undefined;
|
|
127
|
+
if (!id) return undefined;
|
|
128
|
+
const name = toolName(part);
|
|
129
|
+
const lower = name.toLowerCase();
|
|
130
|
+
const args = toolArguments(part);
|
|
131
|
+
const startedAtMs = timestampMs(entry);
|
|
132
|
+
const activity: DelegationActivity = {
|
|
133
|
+
toolCallId: id,
|
|
134
|
+
toolName: name,
|
|
135
|
+
status: "running",
|
|
136
|
+
startedAtMs,
|
|
137
|
+
timeoutMs: timeoutMsFromArgs(args),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const path = typeof args.path === "string" ? args.path : typeof args.file === "string" ? args.file : undefined;
|
|
141
|
+
if (lower === "bash") {
|
|
142
|
+
const command = typeof args.command === "string" ? redactDelegationCommand(args.command) : undefined;
|
|
143
|
+
activity.command = command;
|
|
144
|
+
activity.summary = command ? capInline(command, 140) : "shell command";
|
|
145
|
+
} else if (lower === "read") {
|
|
146
|
+
activity.target = path;
|
|
147
|
+
activity.summary = path ? `read ${path}` : "read file";
|
|
148
|
+
} else if (lower === "grep") {
|
|
149
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : typeof args.query === "string" ? args.query : undefined;
|
|
150
|
+
activity.target = path;
|
|
151
|
+
activity.summary = `grep ${pattern ? capInline(pattern, 60) : "pattern"}${path ? ` in ${path}` : ""}`;
|
|
152
|
+
} else if (lower === "find") {
|
|
153
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : "files";
|
|
154
|
+
activity.target = path;
|
|
155
|
+
activity.summary = `find ${capInline(pattern, 60)}${path ? ` in ${path}` : ""}`;
|
|
156
|
+
} else if (lower === "edit" || lower === "write") {
|
|
157
|
+
activity.target = path;
|
|
158
|
+
const edits = Array.isArray(args.edits) ? ` · ${args.edits.length} edit${args.edits.length === 1 ? "" : "s"}` : "";
|
|
159
|
+
activity.summary = `${lower} ${path ?? "file"}${edits}`;
|
|
160
|
+
} else if (lower === "delegate_task" || lower === "delegate_agent") {
|
|
161
|
+
const agent = typeof args.agent === "string" ? args.agent : undefined;
|
|
162
|
+
activity.summary = `${lower}${agent ? ` → ${agent}` : ""}`;
|
|
163
|
+
} else {
|
|
164
|
+
activity.summary = Object.entries(args).length > 0
|
|
165
|
+
? `${name} ${Object.entries(args).slice(0, 3).map(([key, value]) => `${key}=${typeof value === "string" ? capInline(value, 40) : Array.isArray(value) ? `${value.length} items` : String(value)}`).join(" ")}`
|
|
166
|
+
: name;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return activity;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resultSummary(text: string): string | undefined {
|
|
173
|
+
const trimmed = text.trim();
|
|
174
|
+
if (!trimmed) return undefined;
|
|
175
|
+
const compact = compactWhitespace(trimmed);
|
|
176
|
+
const timeout = compact.match(/Command timed out after\s+([0-9.]+)\s+seconds/i);
|
|
177
|
+
if (timeout) return `timed out after ${timeout[1]}s`;
|
|
178
|
+
const lineCount = trimmed.split("\n").length;
|
|
179
|
+
if (lineCount > 1 || trimmed.length > 120) return `${lineCount} line${lineCount === 1 ? "" : "s"} · ${trimmed.length} chars`;
|
|
180
|
+
return capInline(trimmed, 140);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyToolResult(entry: Record<string, unknown>, message: Record<string, unknown>, byId: Map<string, DelegationActivity>, order: DelegationActivity[]): void {
|
|
184
|
+
const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
185
|
+
const name = typeof message.toolName === "string" ? message.toolName : "tool";
|
|
186
|
+
if (!id) return;
|
|
187
|
+
let activity = byId.get(id);
|
|
188
|
+
if (!activity) {
|
|
189
|
+
activity = { toolCallId: id, toolName: name, status: "running" };
|
|
190
|
+
byId.set(id, activity);
|
|
191
|
+
order.push(activity);
|
|
192
|
+
}
|
|
193
|
+
const endedAtMs = timestampMs(entry);
|
|
194
|
+
activity.endedAtMs = endedAtMs;
|
|
195
|
+
activity.elapsedMs = activity.startedAtMs && endedAtMs ? Math.max(0, endedAtMs - activity.startedAtMs) : undefined;
|
|
196
|
+
activity.status = message.isError === true ? "error" : "success";
|
|
197
|
+
const text = contentText(message.content);
|
|
198
|
+
const summary = resultSummary(text);
|
|
199
|
+
if (message.isError === true) activity.errorSummary = summary ?? "tool error";
|
|
200
|
+
else activity.outputSummary = summary ?? "completed";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseActivityText(text: string, nowMs: number): { recent: DelegationActivity[]; current?: DelegationActivity; lastUpdatedMs?: number } {
|
|
204
|
+
const byId = new Map<string, DelegationActivity>();
|
|
205
|
+
const order: DelegationActivity[] = [];
|
|
206
|
+
let lastUpdatedMs: number | undefined;
|
|
207
|
+
|
|
208
|
+
for (const rawLine of text.split("\n")) {
|
|
209
|
+
if (!rawLine.trim()) continue;
|
|
210
|
+
let parsed: unknown;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(rawLine);
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(parsed)) continue;
|
|
217
|
+
const at = timestampMs(parsed);
|
|
218
|
+
if (at !== undefined) lastUpdatedMs = lastUpdatedMs === undefined ? at : Math.max(lastUpdatedMs, at);
|
|
219
|
+
|
|
220
|
+
if (parsed.type === "message" && isRecord(parsed.message)) {
|
|
221
|
+
const message = parsed.message;
|
|
222
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
223
|
+
for (const part of message.content) {
|
|
224
|
+
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
225
|
+
const activity = activityFromToolCall(parsed, part);
|
|
226
|
+
if (!activity) continue;
|
|
227
|
+
byId.set(activity.toolCallId, activity);
|
|
228
|
+
order.push(activity);
|
|
229
|
+
}
|
|
230
|
+
} else if (message.role === "toolResult") {
|
|
231
|
+
applyToolResult(parsed, message, byId, order);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const activity of order) {
|
|
237
|
+
if (activity.status === "running" && activity.startedAtMs !== undefined) activity.elapsedMs = Math.max(0, nowMs - activity.startedAtMs);
|
|
238
|
+
}
|
|
239
|
+
const current = [...order].reverse().find((activity) => activity.status === "running");
|
|
240
|
+
return { recent: order.slice(-DEFAULT_RECENT_LIMIT), current, lastUpdatedMs };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function readDelegationActivitySnapshot(sessionPath: string | undefined, repoRoot: string, nowMs = Date.now()): DelegationActivitySnapshot {
|
|
244
|
+
if (!sessionPath) return { status: "missing", note: "Child session is not captured yet.", fingerprint: "no-session", recent: [] };
|
|
245
|
+
const resolved = resolve(sessionPath);
|
|
246
|
+
if (!isInside(repoRoot, resolved)) {
|
|
247
|
+
return { status: "blocked", note: `Child session path is outside repo root: ${sessionPath}`, fingerprint: `blocked:${sessionPath}`, recent: [] };
|
|
248
|
+
}
|
|
249
|
+
if (!existsSync(resolved)) return { status: "missing", note: `Child session file not found yet: ${sessionPath}`, fingerprint: `missing:${sessionPath}`, recent: [] };
|
|
250
|
+
const stat = statSync(resolved);
|
|
251
|
+
if (stat.size === 0) return { status: "empty", note: "Child session file is empty.", fingerprint: `${resolved}:0:${Math.floor(stat.mtimeMs)}`, recent: [], lastUpdatedMs: Math.floor(stat.mtimeMs), quietMs: Math.max(0, nowMs - stat.mtimeMs) };
|
|
252
|
+
const text = readTail(resolved, stat.size, ACTIVITY_MAX_BYTES);
|
|
253
|
+
const parsed = parseActivityText(text, nowMs);
|
|
254
|
+
const lastUpdatedMs = parsed.lastUpdatedMs ?? Math.floor(stat.mtimeMs);
|
|
255
|
+
const cappedNote = stat.size > ACTIVITY_MAX_BYTES ? `Activity feed read from last ${ACTIVITY_MAX_BYTES} bytes of ${stat.size} bytes.` : undefined;
|
|
256
|
+
return {
|
|
257
|
+
status: "ok",
|
|
258
|
+
note: cappedNote,
|
|
259
|
+
fingerprint: `${resolved}:${stat.size}:${Math.floor(stat.mtimeMs)}`,
|
|
260
|
+
recent: parsed.recent,
|
|
261
|
+
current: parsed.current,
|
|
262
|
+
lastUpdatedMs,
|
|
263
|
+
quietMs: Math.max(0, nowMs - Math.max(lastUpdatedMs, Math.floor(stat.mtimeMs))),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function formatActivityDuration(ms: number | undefined): string | undefined {
|
|
268
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return undefined;
|
|
269
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
270
|
+
const seconds = totalSeconds % 60;
|
|
271
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
272
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
273
|
+
const pad2 = (value: number): string => String(value).padStart(2, "0");
|
|
274
|
+
if (hours > 0) return `${hours}:${pad2(minutes)}:${pad2(seconds)}`;
|
|
275
|
+
return `${pad2(minutes)}:${pad2(seconds)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatActivitySummary(activity: DelegationActivity, options: { includeCommand?: boolean; nowMs?: number } = {}): string[] {
|
|
279
|
+
const icon = activity.status === "running" ? "◉" : activity.status === "success" ? "✓" : "✗";
|
|
280
|
+
const duration = formatActivityDuration(activity.elapsedMs ?? (activity.status === "running" && activity.startedAtMs ? (options.nowMs ?? Date.now()) - activity.startedAtMs : undefined));
|
|
281
|
+
const timeout = formatActivityDuration(activity.timeoutMs);
|
|
282
|
+
const detail = activity.status === "running"
|
|
283
|
+
? [duration ? `running ${duration}` : "running", timeout ? `timeout ${timeout}` : undefined].filter(Boolean).join(" / ")
|
|
284
|
+
: activity.errorSummary ?? activity.outputSummary ?? (activity.status === "success" ? "done" : "error");
|
|
285
|
+
const summary = activity.summary ?? activity.command ?? activity.target ?? activity.toolName;
|
|
286
|
+
const lines = [`${icon} ${activity.toolName}${detail ? ` · ${detail}` : ""}${summary && activity.toolName.toLowerCase() !== summary.toLowerCase() ? ` · ${summary}` : ""}`];
|
|
287
|
+
if (options.includeCommand && activity.command) lines.push(`$ ${activity.command}`);
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Markdown, truncateToWidth, visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
6
6
|
import { delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCostLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDuration, statusIcon, type DelegationRunView } from "./delegation-monitor.js";
|
|
7
|
+
import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
|
|
7
8
|
import { sanitizeDelegationText } from "./delegation-markdown.js";
|
|
8
9
|
import { isRecord } from "../core/utils/records.js";
|
|
9
10
|
|
|
@@ -180,6 +181,31 @@ function actionCard(lines: string[], title: string, details: string[], width: nu
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
function renderLiveActivityCard(run: DelegationRunView, repoRoot: string, lines: string[], width: number, theme: Theme, nowMs = Date.now()): void {
|
|
185
|
+
const snapshot = readDelegationActivitySnapshot(run.sessionPath, repoRoot, nowMs);
|
|
186
|
+
if (snapshot.recent.length === 0) return;
|
|
187
|
+
pushSpacer(lines);
|
|
188
|
+
const bgKey = snapshot.current ? "toolPendingBg" : "toolSuccessBg";
|
|
189
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.current ? "toolTitle" : "success", theme.bold(" Live activity")), width)));
|
|
190
|
+
if (snapshot.current) {
|
|
191
|
+
for (const line of formatActivitySummary(snapshot.current, { includeCommand: true, nowMs })) {
|
|
192
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(line.startsWith("$") ? "toolOutput" : "warning", ` ${line}`), width)));
|
|
193
|
+
}
|
|
194
|
+
if (typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
|
|
195
|
+
const quiet = `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`;
|
|
196
|
+
lines.push(theme.bg(bgKey, padToWidth(theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", ` ${quiet}`), width)));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const recent = snapshot.recent.slice(-5).filter((activity) => activity.toolCallId !== snapshot.current?.toolCallId);
|
|
200
|
+
if (recent.length > 0) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", " last actions"), width)));
|
|
201
|
+
for (const activity of recent) {
|
|
202
|
+
const colorKey = activity.status === "error" ? "error" : activity.status === "running" ? "warning" : "success";
|
|
203
|
+
const first = formatActivitySummary(activity, { nowMs })[0];
|
|
204
|
+
if (first) lines.push(theme.bg(bgKey, padToWidth(theme.fg(colorKey, ` ${first}`), width)));
|
|
205
|
+
}
|
|
206
|
+
if (snapshot.note) lines.push(theme.bg(bgKey, padToWidth(theme.fg("muted", ` ${snapshot.note}`), width)));
|
|
207
|
+
}
|
|
208
|
+
|
|
183
209
|
function failureDetails(run: DelegationRunView, fallback: string): string[] {
|
|
184
210
|
const details: string[] = [];
|
|
185
211
|
if (run.errorMessage) details.push(`message: ${sanitizeDelegationText(run.errorMessage)}`);
|
|
@@ -372,6 +398,7 @@ export function renderDelegationFeedLines(run: DelegationRunView | undefined, re
|
|
|
372
398
|
lines.push(theme.fg("dim", `${statusIcon(run.status)} ${run.agent}${signalText ? ` · ${theme.fg(delegationSignalColor(signalBadge), signalText)}` : ""}${modelLabel ? ` · ${theme.fg("muted", `(${modelLabel})`)}` : ""} · ${run.status}${run.failureKind ? ` · ${run.failureKind}` : ""} · ${formatDuration(delegationDurationMs(run))} · ${formatDelegationCostLabel(run)} · ${formatDelegationContextLabel(run)}`));
|
|
373
399
|
if (run.usage) lines.push(theme.fg("muted", `usage · in ${run.usage.input} · out ${run.usage.output} · cache ${run.usage.cacheRead}/${run.usage.cacheWrite} · context ${run.usage.contextTokens}`));
|
|
374
400
|
if (run.taskPreview) lines.push(theme.fg("muted", `task · ${sanitizeDelegationText(run.taskPreview)}`));
|
|
401
|
+
renderLiveActivityCard(run, repoRoot, lines, safeWidth, theme);
|
|
375
402
|
let renderedFailureSummary = false;
|
|
376
403
|
if (run.failureKind === "preflight" || run.failureKind === "config") {
|
|
377
404
|
renderedFailureSummary = true;
|
|
@@ -40,6 +40,7 @@ export interface DelegationRunView {
|
|
|
40
40
|
childChangedPaths?: ChildResult["childChangedPaths"];
|
|
41
41
|
usage?: ChildResult["usage"];
|
|
42
42
|
model?: string;
|
|
43
|
+
background?: boolean;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface DelegationMonitorState {
|
|
@@ -73,6 +74,8 @@ const TRANSCRIPT_MAX_BYTES = 240_000;
|
|
|
73
74
|
const TRANSCRIPT_MAX_LINES = 1_200;
|
|
74
75
|
const WIDGET_COMPLETE_TTL_MS = 30_000;
|
|
75
76
|
const WIDGET_FAILURE_TTL_MS = 120_000;
|
|
77
|
+
const WIDGET_BACKGROUND_COMPLETE_TTL_MS = 10 * 60_000;
|
|
78
|
+
const WIDGET_BACKGROUND_FAILURE_TTL_MS = 30 * 60_000;
|
|
76
79
|
|
|
77
80
|
function capPreview(text: string | undefined, limit = PREVIEW_LIMIT): string {
|
|
78
81
|
const value = text ?? "";
|
|
@@ -362,6 +365,10 @@ export function shouldShowRunInWidget(run: DelegationRunView, nowMs = Date.now()
|
|
|
362
365
|
if (run.status === "queued" || run.status === "running") return true;
|
|
363
366
|
if (!run.endedAtMs) return false;
|
|
364
367
|
const ageMs = Math.max(0, nowMs - run.endedAtMs);
|
|
368
|
+
if (run.background) {
|
|
369
|
+
if (run.status === "complete") return ageMs <= WIDGET_BACKGROUND_COMPLETE_TTL_MS;
|
|
370
|
+
return ageMs <= WIDGET_BACKGROUND_FAILURE_TTL_MS;
|
|
371
|
+
}
|
|
365
372
|
if (run.status === "complete") return ageMs <= WIDGET_COMPLETE_TTL_MS;
|
|
366
373
|
return ageMs <= WIDGET_FAILURE_TTL_MS;
|
|
367
374
|
}
|
|
@@ -466,6 +473,7 @@ export function updateDelegationRun(state: DelegationMonitorState, id: string, p
|
|
|
466
473
|
if (patch.errorMessage !== undefined) run.errorMessage = patch.errorMessage;
|
|
467
474
|
if (patch.usage !== undefined) run.usage = patch.usage;
|
|
468
475
|
if (patch.model !== undefined) run.model = patch.model;
|
|
476
|
+
if (patch.background !== undefined) run.background = patch.background;
|
|
469
477
|
return run;
|
|
470
478
|
}
|
|
471
479
|
|
|
@@ -132,6 +132,19 @@ function activeZagentState(state: HarnessRuntimeState): ActiveZagentState | unde
|
|
|
132
132
|
return state.zagent.id ? state.zagent as ActiveZagentState : undefined;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function zpeerStableTeamAgentLeaseRequired(zagent: ActiveZagentState | undefined): boolean {
|
|
136
|
+
return Boolean(
|
|
137
|
+
zagent?.id
|
|
138
|
+
|| process.env.ZOB_ZAGENT_ID?.trim()
|
|
139
|
+
|| process.env.ZOB_ZTEAM_ID?.trim()
|
|
140
|
+
|| process.env.ZOB_COMS_ROLE_ID?.trim(),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function withZpeerLeaseMode<T extends NonNullable<HarnessRuntimeState["zobLive"]["peerCard"]>>(peer: T, useStableTeamAgentLease: boolean): T {
|
|
145
|
+
return { ...peer, zpeerAdhoc: useStableTeamAgentLease ? undefined : true } as T;
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
function formatScopedZteamModeLabel(scopedMode: ActiveZagentState["scopedMode"]): string | undefined {
|
|
136
149
|
if (!scopedMode?.active || !scopedMode.teamId || !scopedMode.modeId || !scopedMode.baseMode) return undefined;
|
|
137
150
|
return `zob:${scopedMode.baseMode}@${scopedMode.teamId}/${scopedMode.modeId}`;
|
|
@@ -511,6 +524,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
|
|
|
511
524
|
if (carryoverProfile?.zagentId && state.zagent.id !== carryoverProfile.zagentId) loadActiveZagentById(state, repoRoot, carryoverProfile.zagentId);
|
|
512
525
|
const sharedZpeerProfile = zpeerProfile ? zpeerProfileIdIsSharedFallback(zpeerProfile.profileId) : false;
|
|
513
526
|
const zagent = activeZagentState(state);
|
|
527
|
+
const useStableTeamAgentLease = zpeerStableTeamAgentLeaseRequired(zagent);
|
|
514
528
|
const zagentMemberships = zagent ? zagentRoomMemberships(zagent) : undefined;
|
|
515
529
|
const zagentActiveRoomId = zagent?.activeRoom ?? zagentMemberships?.find((membership) => membership.roomId)?.roomId;
|
|
516
530
|
const zpeerProfileRoomId = zagentActiveRoomId ?? zpeerProfile?.activeRoomId ?? zpeerProfile?.roomId ?? carryoverProfile?.activeRoomId ?? carryoverProfile?.roomId;
|
|
@@ -579,7 +593,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
|
|
|
579
593
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
580
594
|
return buildZobLiveAckEnvelope(envelope);
|
|
581
595
|
});
|
|
582
|
-
const peerCard = ensureZpeerFields(repoRoot, {
|
|
596
|
+
const peerCard = withZpeerLeaseMode(ensureZpeerFields(repoRoot, {
|
|
583
597
|
...basePeer,
|
|
584
598
|
team: zagent?.team ?? basePeer.team,
|
|
585
599
|
roleId: zagent?.id ?? basePeer.roleId,
|
|
@@ -588,58 +602,78 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
|
|
|
588
602
|
endpoint,
|
|
589
603
|
endpointHash: sha256(endpoint),
|
|
590
604
|
status: "online" as const,
|
|
591
|
-
}, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships);
|
|
605
|
+
}, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships), useStableTeamAgentLease);
|
|
592
606
|
state.zobLive.server = server;
|
|
593
|
-
|
|
594
|
-
|
|
607
|
+
if (!useStableTeamAgentLease) {
|
|
608
|
+
releaseZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_adhoc" });
|
|
595
609
|
state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
|
|
596
|
-
state.zobLive.leaseOwned =
|
|
597
|
-
state.zobLive.leaseStatus = "
|
|
610
|
+
state.zobLive.leaseOwned = false;
|
|
611
|
+
state.zobLive.leaseStatus = "unavailable";
|
|
598
612
|
state.zobLive.leaseBlockReason = undefined;
|
|
599
613
|
state.zobLive.lastHeartbeatMs = Date.now();
|
|
600
614
|
scheduleZpeerHeartbeat(pi, state, ctx);
|
|
601
615
|
} else {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
616
|
+
const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_start" });
|
|
617
|
+
if (leaseClaim.ok) {
|
|
618
|
+
state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
|
|
619
|
+
state.zobLive.leaseOwned = true;
|
|
620
|
+
state.zobLive.leaseStatus = "owned";
|
|
621
|
+
state.zobLive.leaseBlockReason = undefined;
|
|
622
|
+
state.zobLive.lastHeartbeatMs = Date.now();
|
|
623
|
+
scheduleZpeerHeartbeat(pi, state, ctx);
|
|
624
|
+
} else {
|
|
625
|
+
state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, server);
|
|
626
|
+
setZpeerLastEvent(state, {
|
|
627
|
+
kind: "blocked",
|
|
628
|
+
roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
|
|
629
|
+
fromAlias: state.zobLive.peerCard.zpeerAlias,
|
|
630
|
+
status: "lease_blocked",
|
|
631
|
+
reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
|
|
632
|
+
});
|
|
633
|
+
}
|
|
610
634
|
}
|
|
611
635
|
try { writeZpeerLocalProfileFromPeer(repoRoot, state.zobLive.peerCard, profileId); } catch { /* best-effort reload continuity; live runtime must remain available */ }
|
|
612
636
|
} else {
|
|
613
|
-
const peerCard = ensureZpeerFields(repoRoot, {
|
|
637
|
+
const peerCard = withZpeerLeaseMode(ensureZpeerFields(repoRoot, {
|
|
614
638
|
...state.zobLive.peerCard,
|
|
615
639
|
team: zagent?.team ?? state.zobLive.peerCard.team,
|
|
616
640
|
roleId: zagent?.id ?? state.zobLive.peerCard.roleId,
|
|
617
641
|
agent: zagent?.id ?? state.zobLive.peerCard.agent,
|
|
618
642
|
heartbeatAt: new Date().toISOString(),
|
|
619
643
|
status: "online",
|
|
620
|
-
}, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships);
|
|
621
|
-
|
|
622
|
-
|
|
644
|
+
}, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships), useStableTeamAgentLease);
|
|
645
|
+
if (!useStableTeamAgentLease) {
|
|
646
|
+
releaseZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_adhoc" });
|
|
623
647
|
state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
|
|
624
|
-
state.zobLive.leaseOwned =
|
|
625
|
-
state.zobLive.leaseStatus = "
|
|
648
|
+
state.zobLive.leaseOwned = false;
|
|
649
|
+
state.zobLive.leaseStatus = "unavailable";
|
|
626
650
|
state.zobLive.leaseBlockReason = undefined;
|
|
627
651
|
state.zobLive.lastHeartbeatMs = Date.now();
|
|
628
652
|
scheduleZpeerHeartbeat(pi, state, ctx);
|
|
629
|
-
} else if (state.zobLive.server) {
|
|
630
|
-
state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, state.zobLive.server);
|
|
631
|
-
setZpeerLastEvent(state, {
|
|
632
|
-
kind: "blocked",
|
|
633
|
-
roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
|
|
634
|
-
fromAlias: state.zobLive.peerCard.zpeerAlias,
|
|
635
|
-
status: "lease_blocked",
|
|
636
|
-
reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
|
|
637
|
-
});
|
|
638
653
|
} else {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
654
|
+
const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_refresh" });
|
|
655
|
+
if (leaseClaim.ok) {
|
|
656
|
+
state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
|
|
657
|
+
state.zobLive.leaseOwned = true;
|
|
658
|
+
state.zobLive.leaseStatus = "owned";
|
|
659
|
+
state.zobLive.leaseBlockReason = undefined;
|
|
660
|
+
state.zobLive.lastHeartbeatMs = Date.now();
|
|
661
|
+
scheduleZpeerHeartbeat(pi, state, ctx);
|
|
662
|
+
} else if (state.zobLive.server) {
|
|
663
|
+
state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, state.zobLive.server);
|
|
664
|
+
setZpeerLastEvent(state, {
|
|
665
|
+
kind: "blocked",
|
|
666
|
+
roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
|
|
667
|
+
fromAlias: state.zobLive.peerCard.zpeerAlias,
|
|
668
|
+
status: "lease_blocked",
|
|
669
|
+
reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
state.zobLive.peerCard = writeZobLivePeerCard(repoRoot, { ...peerCard, heartbeatAt: new Date().toISOString(), status: "offline" });
|
|
673
|
+
state.zobLive.leaseOwned = false;
|
|
674
|
+
state.zobLive.leaseStatus = "blocked";
|
|
675
|
+
state.zobLive.leaseBlockReason = "blocked_live_owner";
|
|
676
|
+
}
|
|
643
677
|
}
|
|
644
678
|
try { writeZpeerLocalProfileFromPeer(repoRoot, state.zobLive.peerCard, profileId); } catch { /* best-effort reload continuity; live runtime must remain available */ }
|
|
645
679
|
}
|
|
@@ -414,6 +414,8 @@ function restoreDelegationRunsFromEntries(state: HarnessRuntimeState, entries: u
|
|
|
414
414
|
errorMessage: Array.isArray(data.errors) ? data.errors.map(String).join("; ") : existing?.errorMessage,
|
|
415
415
|
childChangedPaths: normalizeZcommitChildChangedPathRefs(data.childChangedPathRefs) ?? existing?.childChangedPaths,
|
|
416
416
|
usage: usageField(data) ?? existing?.usage,
|
|
417
|
+
model: stringField(data, "model") ?? existing?.model,
|
|
418
|
+
background: (typeof data.background === "boolean" ? data.background : undefined) ?? existing?.background,
|
|
417
419
|
};
|
|
418
420
|
if (event === "start") restoredRun.endedAtMs = existing?.endedAtMs;
|
|
419
421
|
restored.set(runId, restoredRun);
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
statusIcon,
|
|
41
41
|
updateDelegationRun,
|
|
42
42
|
type DelegationRunMode,
|
|
43
|
+
type DelegationRunView,
|
|
43
44
|
} from "../delegation-monitor.js";
|
|
45
|
+
import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "../delegation-activity.js";
|
|
44
46
|
import { delegateViewLink } from "../delegation-mouse.js";
|
|
45
47
|
import type { BackgroundDelegationRuntimeRun, HarnessRuntimeState } from "../state.js";
|
|
46
48
|
import { strictGoalErrors, strictGoalSpecErrors } from "../state.js";
|
|
@@ -728,6 +730,32 @@ export function formatDelegationCatalogSummary(catalog: Record<string, unknown>)
|
|
|
728
730
|
return lines.join("\n");
|
|
729
731
|
}
|
|
730
732
|
|
|
733
|
+
function renderMiniActivityLines(run: DelegationRunView, nowMs: number, expanded: boolean, theme: { fg: (color: any, text: string) => string }): string[] {
|
|
734
|
+
const lines: string[] = [];
|
|
735
|
+
const snapshot = readDelegationActivitySnapshot(run.sessionPath, process.cwd(), nowMs);
|
|
736
|
+
const recent = snapshot.recent.slice(-5);
|
|
737
|
+
if (recent.length === 0) {
|
|
738
|
+
if ((run.status === "running" || run.status === "queued") && snapshot.note && snapshot.status !== "missing") {
|
|
739
|
+
lines.push(` ${theme.fg("muted", snapshot.note)}`);
|
|
740
|
+
}
|
|
741
|
+
return lines;
|
|
742
|
+
}
|
|
743
|
+
const label = expanded ? "last actions" : "last";
|
|
744
|
+
lines.push(` ${theme.fg("dim", `${label} ${recent.length}`)}`);
|
|
745
|
+
for (const activity of recent) {
|
|
746
|
+
const color = activity.status === "running" ? "warning" : activity.status === "error" ? "error" : "success";
|
|
747
|
+
const includeCommand = activity.status === "running" && activity.toolName.toLowerCase() === "bash";
|
|
748
|
+
const rendered = formatActivitySummary(activity, { includeCommand, nowMs });
|
|
749
|
+
const [first, ...rest] = rendered;
|
|
750
|
+
if (first) lines.push(` ${theme.fg(color, first)}`);
|
|
751
|
+
for (const extra of rest.slice(0, expanded ? 2 : 1)) lines.push(` ${theme.fg("muted", extra)}`);
|
|
752
|
+
}
|
|
753
|
+
if (snapshot.current && typeof snapshot.quietMs === "number" && snapshot.quietMs >= 120_000) {
|
|
754
|
+
lines.push(` ${theme.fg(snapshot.quietMs >= 300_000 ? "warning" : "muted", `quiet ${formatActivityDuration(snapshot.quietMs)} since last transcript update`)}`);
|
|
755
|
+
}
|
|
756
|
+
return lines;
|
|
757
|
+
}
|
|
758
|
+
|
|
731
759
|
export function renderDelegationToolResultText(source: "delegate_agent" | "delegate_task", details: DelegationDetails | undefined, state: HarnessRuntimeState, toolCallId: string | undefined, isPartial: boolean, expanded: boolean, theme: { fg: (color: any, text: string) => string; bold: (text: string) => string }): string {
|
|
732
760
|
hydrateDelegationRunsFromDetails(source, details, state, toolCallId);
|
|
733
761
|
const nowMs = Date.now();
|
|
@@ -754,11 +782,13 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
|
|
|
754
782
|
const prefix = index === visibleRuns.length - 1 && !hasMore ? "└─" : "├─";
|
|
755
783
|
const viewHint = delegateViewLink(run.id);
|
|
756
784
|
const kind = run.failureKind ? ` ${run.failureKind}` : "";
|
|
785
|
+
const background = run.background ? " bg" : "";
|
|
757
786
|
const badge = delegationSignalBadge(run);
|
|
758
787
|
const badgeText = formatDelegationSignalBadge(badge);
|
|
759
788
|
const modelLabel = formatDelegationModelLabel(run);
|
|
760
789
|
const modelSuffix = modelLabel ? ` ${theme.fg("muted", `(${modelLabel})`)}` : "";
|
|
761
|
-
lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
|
|
790
|
+
lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}${background}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
|
|
791
|
+
lines.push(...renderMiniActivityLines(run, nowMs, expanded, theme));
|
|
762
792
|
if (expanded && (run.errorMessage || run.stopReason || run.gateErrors?.length)) lines.push(` ${theme.fg("dim", run.errorMessage ?? run.gateErrors?.join("; ") ?? run.stopReason ?? "")}`);
|
|
763
793
|
}
|
|
764
794
|
if (hasMore) lines.push(theme.fg("dim", `└─ … ${monitoredRuns.length - maxRows} more child run(s)`));
|
|
@@ -827,6 +827,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
|
|
|
827
827
|
taskHash: sha256(structuredTask),
|
|
828
828
|
originalUserAskHash: sha256(params.original_user_ask ?? state.activeGoal?.originalUserAsk ?? ""),
|
|
829
829
|
outputContract,
|
|
830
|
+
background: params.run_in_background === true,
|
|
830
831
|
startedAt,
|
|
831
832
|
});
|
|
832
833
|
|
|
@@ -975,6 +976,8 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
|
|
|
975
976
|
};
|
|
976
977
|
|
|
977
978
|
if (params.run_in_background) {
|
|
979
|
+
updateDelegationRun(state.delegations, runId, { background: true });
|
|
980
|
+
renderDelegationMonitor();
|
|
978
981
|
const backgroundController = new AbortController();
|
|
979
982
|
const backgroundPromise = runDelegateTaskChild(backgroundController.signal, false);
|
|
980
983
|
const backgroundRun: BackgroundDelegationRuntimeRun = { runId, startedAtMs, promise: backgroundPromise, abortController: backgroundController };
|
|
@@ -8,7 +8,8 @@ import { isRecord } from "../core/utils/records.js";
|
|
|
8
8
|
import type { AssistantLikeMessage, ModeName } from "../types.js";
|
|
9
9
|
import { readHarnessReadinessWidgetData } from "../domains/orchestration/widget-readers.js";
|
|
10
10
|
import { buildZpeerPeerRoomSummaries, type ZpeerPeerRoomSummary } from "../domains/coms/coms-v2/zpeer.js";
|
|
11
|
-
import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, summarizeDelegations } from "./delegation-monitor.js";
|
|
11
|
+
import { delegationCost, delegationDurationMs, formatDelegationCost, formatDuration, listWidgetDelegationRuns, summarizeDelegations } from "./delegation-monitor.js";
|
|
12
|
+
import { formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
|
|
12
13
|
import { disposeDelegationMouseSupport } from "./delegation-mouse.js";
|
|
13
14
|
import { formatZcompactHudLine } from "./auto-compaction.js";
|
|
14
15
|
import type { HarnessRuntimeState } from "./state.js";
|
|
@@ -339,9 +340,18 @@ export function renderHarnessWidget(pi: ExtensionAPI, state: HarnessRuntimeState
|
|
|
339
340
|
? "working"
|
|
340
341
|
: "idle";
|
|
341
342
|
const assistantsCount = delegationSummary.running + delegationSummary.queued + (runtimeTodoSummary?.delegated ?? 0);
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
const widgetDelegationRuns = listWidgetDelegationRuns(state.delegations, renderNowMs);
|
|
344
|
+
const activeDelegationRun = widgetDelegationRuns.find((run) => run.status === "running" || run.status === "queued") ?? widgetDelegationRuns[0];
|
|
345
|
+
const activeDelegationSnapshot = activeDelegationRun ? readDelegationActivitySnapshot(activeDelegationRun.sessionPath, ctx.cwd, renderNowMs) : undefined;
|
|
346
|
+
const activeDelegationActivity = activeDelegationSnapshot?.current ?? activeDelegationSnapshot?.recent.at(-1);
|
|
347
|
+
const activeDelegationLine = activeDelegationRun && activeDelegationActivity
|
|
348
|
+
? `${activeDelegationRun.agent}: ${formatActivitySummary(activeDelegationActivity, { nowMs: renderNowMs })[0] ?? activeDelegationActivity.toolName}`
|
|
349
|
+
: undefined;
|
|
350
|
+
const assistantsState = activeDelegationLine
|
|
351
|
+
? truncateToWidth(`${assistantsCount > 0 ? `${assistantsCount} active` : "recent"} · ${activeDelegationLine}`, 96, "…")
|
|
352
|
+
: assistantsCount > 0
|
|
353
|
+
? `${assistantsCount} active`
|
|
354
|
+
: "none";
|
|
345
355
|
const zpeerRoomSummaries = cachedZpeerRoomSummaries(state, ctx, renderNowMs);
|
|
346
356
|
const zpeerLast = state.zobLive.lastEvent
|
|
347
357
|
? `${state.zobLive.lastEvent.kind} ${state.zobLive.lastEvent.fromAlias ? `@${state.zobLive.lastEvent.fromAlias}` : "?"}→${state.zobLive.lastEvent.toAlias ? `@${state.zobLive.lastEvent.toAlias}` : "?"} ${state.zobLive.lastEvent.status}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zob-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
@@ -200,6 +200,8 @@ async function main() {
|
|
|
200
200
|
const gammaEndpoint = join(root, 'gamma.sock');
|
|
201
201
|
const workerOneEndpoint = join(root, 'worker-one.sock');
|
|
202
202
|
const workerTwoEndpoint = join(root, 'worker-two.sock');
|
|
203
|
+
const adhocOneEndpoint = join(root, 'adhoc-one.sock');
|
|
204
|
+
const adhocTwoEndpoint = join(root, 'adhoc-two.sock');
|
|
203
205
|
const pendingReplies = new Map();
|
|
204
206
|
const receivedPrompts = [];
|
|
205
207
|
const receivedResponses = [];
|
|
@@ -244,6 +246,11 @@ async function main() {
|
|
|
244
246
|
receivedPrompts.push(incoming);
|
|
245
247
|
return envelope.buildZobLiveAckEnvelope(incoming);
|
|
246
248
|
}));
|
|
249
|
+
servers.push(await localTransport.bindZobLocalEndpoint(adhocOneEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
|
|
250
|
+
servers.push(await localTransport.bindZobLocalEndpoint(adhocTwoEndpoint, async (incoming) => {
|
|
251
|
+
receivedPrompts.push(incoming);
|
|
252
|
+
return envelope.buildZobLiveAckEnvelope(incoming);
|
|
253
|
+
}));
|
|
247
254
|
|
|
248
255
|
const oldHeartbeatAt = new Date(Date.now() - 180_000).toISOString();
|
|
249
256
|
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');
|
|
@@ -279,6 +286,17 @@ async function main() {
|
|
|
279
286
|
});
|
|
280
287
|
});
|
|
281
288
|
|
|
289
|
+
const adhocOne = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'adhocone', roomId: 'adhoc-room', endpoint: adhocOneEndpoint, endpointHash: hashing.sha256(adhocOneEndpoint), sha256: hashing.sha256 }), 'adhoc-room', 'adhocone'), zpeerAdhoc: true });
|
|
290
|
+
const adhocTwo = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'adhoctwo', roomId: 'adhoc-room', endpoint: adhocTwoEndpoint, endpointHash: hashing.sha256(adhocTwoEndpoint), sha256: hashing.sha256 }), 'adhoc-room', 'adhoctwo'), zpeerAdhoc: true });
|
|
291
|
+
assert(adhocOne.zpeerAdhoc === true && adhocTwo.zpeerAdhoc === true, 'direct/ad-hoc zpeer peers must carry the ad-hoc marker');
|
|
292
|
+
const adhocSummary = zpeer.buildZpeerRoomSummary(repoRoot, adhocOne, 'adhoc-room');
|
|
293
|
+
assert(adhocSummary.peerCount === 2 && adhocSummary.online === 2, `ad-hoc room summary expected 2 online peers even with lease domain present, got ${adhocSummary.online}/${adhocSummary.peerCount}`);
|
|
294
|
+
assert(adhocSummary.aliases.includes('adhocone') && adhocSummary.aliases.includes('adhoctwo'), 'ad-hoc room summary must include both direct peers');
|
|
295
|
+
const adhocPromptCountBefore = receivedPrompts.length;
|
|
296
|
+
const adhocAsync = await zpeer.sendZpeerPrompt(repoRoot, adhocOne, 'adhoctwo', rawPrompt, waitForReply, { mode: 'async' });
|
|
297
|
+
assert(adhocAsync.status === 'waiting', `ad-hoc same-room send expected waiting after ACK, got ${adhocAsync.status}${adhocAsync.reason ? `: ${adhocAsync.reason}` : ''}`);
|
|
298
|
+
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');
|
|
299
|
+
|
|
282
300
|
const joinedAlpha = await zpeer.joinZpeerRoom(repoRoot, alpha, 'shared-room', 'sharedalpha', 'bridge');
|
|
283
301
|
assert(joinedAlpha.ok === true, `alpha multi-room join expected ok, got ${joinedAlpha.reason ?? 'not ok'}`);
|
|
284
302
|
alpha = joinedAlpha.peer;
|
|
@@ -34,6 +34,7 @@ const files = [
|
|
|
34
34
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts',
|
|
35
35
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts',
|
|
36
36
|
'.pi/extensions/zob-harness/src/domains/coms/coms-v2/transcript-capture.ts',
|
|
37
|
+
'.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts',
|
|
37
38
|
'.pi/extensions/zob-harness/src/runtime/commands.ts',
|
|
38
39
|
'.pi/extensions/zob-harness/src/runtime/tools-coms.ts',
|
|
39
40
|
'.pi/extensions/zob-harness/src/runtime/events.ts',
|
|
@@ -82,7 +83,7 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'o
|
|
|
82
83
|
const zpeer = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts'];
|
|
83
84
|
const liveRegistry = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts'];
|
|
84
85
|
const zpeerProfile = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts'];
|
|
85
|
-
for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping']) {
|
|
86
|
+
for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping', 'zpeerAdhoc']) {
|
|
86
87
|
if (!zpeer.includes(needle)) failures.push(`zpeer missing ${needle}`);
|
|
87
88
|
}
|
|
88
89
|
for (const needle of ['peer-messages.jsonl', 'peer-status.jsonl', 'appendZpeerPeerRecords', 'reasonHash', 'bodyStored: false']) {
|
|
@@ -96,7 +97,8 @@ if (!zpeer.includes('const bothPeersAreWorkers = self.roleType === "worker" && t
|
|
|
96
97
|
if (hardTeamMismatchIndex !== -1 && (roomFirstTopologyIndex === -1 || hardTeamMismatchIndex < roomFirstTopologyIndex)) failures.push('zpeer topology must not hard-block cross-team peers before same-room allowance');
|
|
97
98
|
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');
|
|
98
99
|
if (!liveRegistry.includes('readZobLiveRegistryAllProjectsSnapshot') || !liveRegistry.includes('join(projectsDir, entry.name, "agents")')) failures.push('live registry must expose all-project agents room discovery helper');
|
|
99
|
-
|
|
100
|
+
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');
|
|
101
|
+
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']) {
|
|
100
102
|
if (!liveRegistry.includes(needle)) failures.push(`live registry stable lease support missing ${needle}`);
|
|
101
103
|
}
|
|
102
104
|
if (!zpeer.includes('readZobLiveRegistryAllProjectsSnapshot(repoRoot)')) failures.push('zpeer room discovery must use all-project registry snapshots');
|
|
@@ -172,7 +174,7 @@ for (const needle of ['readZpeerNewCarryoverProfile(repoRoot)', 'const carryover
|
|
|
172
174
|
}
|
|
173
175
|
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');
|
|
174
176
|
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');
|
|
175
|
-
for (const needle of ['ZPEER AWARENESS (transient, rebuilt each turn)', 'buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard)', '- rooms:', 'explicit roomId', 'zpeer_ask with mode=\\"async\\"', 'Passive wait rule', 'stop the turn and remain idle', 'avoid spam', 'Raw ZPeer bodies are transient', 'registerMessageRenderer("zob-zpeer-event"', 'scheduleZpeerHeartbeat', 'clearZpeerHeartbeatTimer', 'refreshZpeerSelf(repoRoot', 'kind: "response_sent"', 'kind: "inbound"']) {
|
|
177
|
+
for (const needle of ['ZPEER AWARENESS (transient, rebuilt each turn)', 'buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard)', '- rooms:', 'explicit roomId', 'zpeer_ask with mode=\\"async\\"', 'Passive wait rule', 'stop the turn and remain idle', 'avoid spam', 'Raw ZPeer bodies are transient', 'registerMessageRenderer("zob-zpeer-event"', 'scheduleZpeerHeartbeat', 'clearZpeerHeartbeatTimer', 'refreshZpeerSelf(repoRoot', 'kind: "response_sent"', 'kind: "inbound"', 'zpeerStableTeamAgentLeaseRequired', 'withZpeerLeaseMode', 'leaseStatus = "unavailable"', 'runtime_adhoc']) {
|
|
176
178
|
if (!events.includes(needle)) failures.push(`runtime missing zpeer awareness/event support ${needle}`);
|
|
177
179
|
}
|
|
178
180
|
const responseSentBlock = events.match(/setZpeerLastEvent\(state, \{\s*kind: "response_sent"[\s\S]*?customType: "zob-zpeer-event"[\s\S]*?triggerTurn: false[\s\S]*?\}\);/)?.[0] ?? '';
|
|
@@ -212,6 +214,11 @@ const goalRuntime = contents['.pi/extensions/zob-harness/src/runtime/goal-runtim
|
|
|
212
214
|
for (const needle of ['state.zobLive.passivePeerWait?.suppressGoalContinuation === true', 'clearRuntimeGoalContinuationTimer(state)', 'return;']) {
|
|
213
215
|
if (!goalRuntime.includes(needle)) failures.push(`goal runtime missing passive peer continuation suppression ${needle}`);
|
|
214
216
|
}
|
|
217
|
+
|
|
218
|
+
const childRunner = contents['.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts'];
|
|
219
|
+
for (const needle of ['childModelPattern', 'ctx.model', 'resolveCodexFastModeExtension', 'getAgentDir()', 'codex-fast-mode.ts', 'childCodexFastModeExtension', 'args.push("-e", childCodexFastModeExtension)', 'const model = resolvedModel']) {
|
|
220
|
+
if (!childRunner.includes(needle)) failures.push(`delegation child runner missing Codex auto/model inheritance support ${needle}`);
|
|
221
|
+
}
|
|
215
222
|
for (const needle of ['updatePassivePeerWaitState(state, result', 'result.status !== "waiting"', 'state.zobLive.passivePeerWait = undefined', 'schema: "zob.passive-peer-wait.v1"', 'source: "zpeer_ask"', 'suppressGoalContinuation: true', 'bodyStored: false', 'localOnly: true', 'networkEnabled: false']) {
|
|
216
223
|
if (!toolsComs.includes(needle)) failures.push(`zpeer_ask missing passive peer wait state handling ${needle}`);
|
|
217
224
|
}
|