zob-harness 0.1.1 → 0.2.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/capabilities/zob-public-runtime-capabilities.json +34 -0
- package/.pi/extensions/zob-harness/src/coms-v2/registry.ts +61 -27
- package/.pi/extensions/zob-harness/src/coms-v2/zpeer-profile.ts +139 -16
- package/.pi/extensions/zob-harness/src/coms-v2/zpeer.ts +100 -25
- package/.pi/extensions/zob-harness/src/goal-runtime.ts +4 -0
- package/.pi/extensions/zob-harness/src/runtime/commands.ts +359 -13
- package/.pi/extensions/zob-harness/src/runtime/delegation-overlay.ts +22 -4
- package/.pi/extensions/zob-harness/src/runtime/events.ts +282 -22
- package/.pi/extensions/zob-harness/src/runtime/state.ts +35 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-coms.ts +30 -3
- package/.pi/extensions/zob-harness/src/runtime/widget.ts +117 -32
- package/.pi/extensions/zob-harness/src/zagents.ts +534 -0
- package/.pi/skills/zob-zagent-creator/SKILL.md +85 -0
- package/package.json +3 -1
- package/scripts/zagent-static-smoke.mjs +155 -0
- package/scripts/zpeer-local-e2e-smoke.mjs +88 -20
- package/scripts/zpeer-static-smoke.mjs +78 -9
|
@@ -1520,6 +1520,40 @@
|
|
|
1520
1520
|
],
|
|
1521
1521
|
"noShipNotes": "Local-only multi-room-scoped peer UX with active-room compatibility; transient prompt/response over local_socket, persisted command/mission metadata is hash/body-free."
|
|
1522
1522
|
},
|
|
1523
|
+
{
|
|
1524
|
+
"name": "zagent",
|
|
1525
|
+
"family": "zagent",
|
|
1526
|
+
"modes": [
|
|
1527
|
+
"all"
|
|
1528
|
+
],
|
|
1529
|
+
"skillRefs": [
|
|
1530
|
+
".pi/skills/zob-zagent-creator/SKILL.md",
|
|
1531
|
+
".pi/skills/zob-coms-v2-live/SKILL.md",
|
|
1532
|
+
".pi/skills/zob-harness/SKILL.md"
|
|
1533
|
+
],
|
|
1534
|
+
"docRefs": [
|
|
1535
|
+
"AGENTS.md",
|
|
1536
|
+
".pi/skills/zob-zagent-creator/SKILL.md"
|
|
1537
|
+
],
|
|
1538
|
+
"noShipNotes": "Project-local ZAgent definition UX for .pi/zagents/ only. ZAgents are full-session Pi/ZPeer identities tied to live coordination, not delegate subagents; launch-plan is plan/metadata only and must not spawn processes. Persisted command metadata must remain hash/body-free."
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
"name": "zteam",
|
|
1542
|
+
"family": "zagent",
|
|
1543
|
+
"modes": [
|
|
1544
|
+
"all"
|
|
1545
|
+
],
|
|
1546
|
+
"skillRefs": [
|
|
1547
|
+
".pi/skills/zob-zagent-creator/SKILL.md",
|
|
1548
|
+
".pi/skills/zob-coms-v2-live/SKILL.md",
|
|
1549
|
+
".pi/skills/zob-harness/SKILL.md"
|
|
1550
|
+
],
|
|
1551
|
+
"docRefs": [
|
|
1552
|
+
"AGENTS.md",
|
|
1553
|
+
".pi/skills/zob-zagent-creator/SKILL.md"
|
|
1554
|
+
],
|
|
1555
|
+
"noShipNotes": "Project-local ZTeam bundle UX for .pi/zteams/ only. ZTeam members are full-session Pi/ZPeer identities tied to live coordination, not delegate subagents; launch-plan is plan/metadata only and must not spawn processes. Persisted command metadata must remain hash/body-free."
|
|
1556
|
+
},
|
|
1523
1557
|
{
|
|
1524
1558
|
"name": "rules_status",
|
|
1525
1559
|
"family": "safety",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { loadTeamDefinition, validateTeamDefinition } from "../topology/teams.js";
|
|
6
6
|
import { isRecord } from "../utils/records.js";
|
|
@@ -34,6 +34,50 @@ function peerPath(repoRoot: string, peer: Pick<ZobLivePeerCard, "roleId" | "sess
|
|
|
34
34
|
return join(dir, `${safeFileStem(`${peer.roleId}-${peer.sessionHash.slice(0, 12)}`)}.json`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function peerPathForProjectId(projectId: string, peer: Pick<ZobLivePeerCard, "roleId" | "sessionHash">): string {
|
|
38
|
+
return join(registryRoot().path, "projects", safeFileStem(projectId), "agents", `${safeFileStem(`${peer.roleId}-${peer.sessionHash.slice(0, 12)}`)}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readPeerCardsFromAgentsDir(dir: string, nowMs: number, teamName?: string): ZobLivePeerCard[] {
|
|
42
|
+
if (!existsSync(dir)) return [];
|
|
43
|
+
return readdirSync(dir)
|
|
44
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
45
|
+
.map((entry) => {
|
|
46
|
+
try {
|
|
47
|
+
return parsePeerCard(JSON.parse(readFileSync(join(dir, entry), "utf8")) as unknown);
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.filter((peer): peer is ZobLivePeerCard => Boolean(peer))
|
|
53
|
+
.filter((peer) => !teamName || peer.team === teamName)
|
|
54
|
+
.map((peer) => ({ ...peer, status: derivePeerStatus(peer, nowMs) }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function allProjectAgentsDirs(): string[] {
|
|
58
|
+
const root = registryRoot();
|
|
59
|
+
const projectsDir = join(root.path, "projects");
|
|
60
|
+
if (!existsSync(projectsDir)) return [];
|
|
61
|
+
return readdirSync(projectsDir, { withFileTypes: true })
|
|
62
|
+
.filter((entry) => entry.isDirectory())
|
|
63
|
+
.map((entry) => join(projectsDir, entry.name, "agents"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildSnapshot(projectId: string, kind: "user_runtime" | "env_override", peers: ZobLivePeerCard[], teamName?: string): ZobLiveRegistrySnapshot {
|
|
67
|
+
const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
|
|
68
|
+
for (const peer of peers) counts[peer.status] += 1;
|
|
69
|
+
return {
|
|
70
|
+
schema: "zob.live-registry-snapshot.v1",
|
|
71
|
+
projectId,
|
|
72
|
+
registry: kind,
|
|
73
|
+
team: teamName,
|
|
74
|
+
generatedAt: new Date().toISOString(),
|
|
75
|
+
peers,
|
|
76
|
+
counts,
|
|
77
|
+
bodyStored: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
37
81
|
function parsePeerCard(value: unknown): ZobLivePeerCard | undefined {
|
|
38
82
|
if (!isRecord(value) || value.schema !== "zob.live-peer-card.v1") return undefined;
|
|
39
83
|
if (hasForbiddenPersistedKey(value) || value.bodyStored !== false) return undefined;
|
|
@@ -59,6 +103,15 @@ export function writeZobLivePeerCard(repoRoot: string, peer: ZobLivePeerCard): Z
|
|
|
59
103
|
return peer;
|
|
60
104
|
}
|
|
61
105
|
|
|
106
|
+
export function writeZobLivePeerCardToProjectId(peer: ZobLivePeerCard): ZobLivePeerCard {
|
|
107
|
+
if (hasForbiddenPersistedKey(peer)) throw new Error("Refusing to persist ZOB live peer card with forbidden body-like keys");
|
|
108
|
+
if (peer.bodyStored !== false) throw new Error("ZOB live peer card bodyStored must be false");
|
|
109
|
+
const filePath = peerPathForProjectId(peer.projectId, peer);
|
|
110
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
111
|
+
writeFileSync(filePath, `${JSON.stringify(peer, null, 2)}\n`, "utf8");
|
|
112
|
+
return peer;
|
|
113
|
+
}
|
|
114
|
+
|
|
62
115
|
export function registerCurrentZobLivePeer(repoRoot: string, teamName = "zob-core"): ZobLivePeerCard | undefined {
|
|
63
116
|
const policy = readZobComsV2Policy(repoRoot);
|
|
64
117
|
if (!zobComsRegistryEnabled(policy)) return undefined;
|
|
@@ -83,31 +136,12 @@ export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-c
|
|
|
83
136
|
|
|
84
137
|
export function readZobLiveRegistrySnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
|
|
85
138
|
const { dir, projectId, kind } = projectAgentsDir(repoRoot);
|
|
139
|
+
return buildSnapshot(projectId, kind, readPeerCardsFromAgentsDir(dir, Date.now(), teamName), teamName);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function readZobLiveRegistryAllProjectsSnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
|
|
143
|
+
const { projectId, kind } = projectAgentsDir(repoRoot);
|
|
86
144
|
const nowMs = Date.now();
|
|
87
|
-
const peers =
|
|
88
|
-
|
|
89
|
-
.filter((entry) => entry.endsWith(".json"))
|
|
90
|
-
.map((entry) => {
|
|
91
|
-
try {
|
|
92
|
-
return parsePeerCard(JSON.parse(readFileSync(join(dir, entry), "utf8")) as unknown);
|
|
93
|
-
} catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.filter((peer): peer is ZobLivePeerCard => Boolean(peer))
|
|
98
|
-
.filter((peer) => !teamName || peer.team === teamName)
|
|
99
|
-
.map((peer) => ({ ...peer, status: derivePeerStatus(peer, nowMs) }))
|
|
100
|
-
: [];
|
|
101
|
-
const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
|
|
102
|
-
for (const peer of peers) counts[peer.status] += 1;
|
|
103
|
-
return {
|
|
104
|
-
schema: "zob.live-registry-snapshot.v1",
|
|
105
|
-
projectId,
|
|
106
|
-
registry: kind,
|
|
107
|
-
team: teamName,
|
|
108
|
-
generatedAt: new Date().toISOString(),
|
|
109
|
-
peers,
|
|
110
|
-
counts,
|
|
111
|
-
bodyStored: false,
|
|
112
|
-
};
|
|
145
|
+
const peers = allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
|
|
146
|
+
return buildSnapshot(projectId, kind, peers, teamName);
|
|
113
147
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -9,6 +9,8 @@ import { isRecord } from "../utils/records.js";
|
|
|
9
9
|
import { safeFileStem } from "../utils/paths.js";
|
|
10
10
|
|
|
11
11
|
const PROFILE_SCHEMA = "zob.zpeer-local-profile.v1";
|
|
12
|
+
const NEW_CARRYOVER_SCHEMA = "zob.zpeer-new-carryover.v1";
|
|
13
|
+
const DEFAULT_NEW_CARRYOVER_TTL_MS = 30 * 60 * 1000;
|
|
12
14
|
const FORBIDDEN_PROFILE_KEYS = new Set(["body", "task", "prompt", "output", "content", "message", "response", "rationale", "text", "diff", "patch"]);
|
|
13
15
|
|
|
14
16
|
export interface ZpeerLocalProfile {
|
|
@@ -26,6 +28,31 @@ export interface ZpeerLocalProfile {
|
|
|
26
28
|
bodyStored: false;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export interface ZpeerNewCarryoverProfile {
|
|
32
|
+
schema: typeof NEW_CARRYOVER_SCHEMA;
|
|
33
|
+
projectId: string;
|
|
34
|
+
alias?: string;
|
|
35
|
+
roomId?: string;
|
|
36
|
+
activeRoomId?: string;
|
|
37
|
+
memberships?: ZpeerRoomMembership[];
|
|
38
|
+
zagentId?: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
expiresAt: string;
|
|
42
|
+
localOnly: true;
|
|
43
|
+
networkEnabled: false;
|
|
44
|
+
bodyStored: false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ZpeerNewCarryoverInput {
|
|
48
|
+
alias?: string;
|
|
49
|
+
roomId?: string;
|
|
50
|
+
activeRoomId?: string;
|
|
51
|
+
memberships?: ZpeerRoomMembership[];
|
|
52
|
+
zagentId?: string;
|
|
53
|
+
ttlMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
function registryRoot(): string {
|
|
30
57
|
const override = process.env.ZOB_COMS_REGISTRY_ROOT;
|
|
31
58
|
if (override && override.trim().length > 0) return override;
|
|
@@ -59,11 +86,17 @@ function derivedSessionSeed(repoRoot: string): string | undefined {
|
|
|
59
86
|
export function resolveZpeerProfileId(repoRoot: string): string {
|
|
60
87
|
const explicit = firstEnvValue(["ZOB_ZPEER_PROFILE_ID", "ZPEER_PROFILE"]);
|
|
61
88
|
if (explicit) return safeFileStem(`explicit-${explicit.value}`).slice(0, 80) || `explicit-${sha256(explicit.value).slice(0, 16)}`;
|
|
62
|
-
const comsSession = firstEnvValue(["ZOB_COMS_SESSION_ID"]);
|
|
63
|
-
if (comsSession) return safeFileStem(`coms-${comsSession.value}`).slice(0, 80) || `coms-${sha256(comsSession.value).slice(0, 16)}`;
|
|
64
89
|
const terminalSeed = derivedSessionSeed(repoRoot);
|
|
65
90
|
if (terminalSeed) return `terminal-${sha256(terminalSeed).slice(0, 20)}`;
|
|
66
|
-
|
|
91
|
+
const comsSession = firstEnvValue(["ZOB_COMS_SESSION_ID"]);
|
|
92
|
+
if (comsSession) return safeFileStem(`coms-${comsSession.value}`).slice(0, 80) || `coms-${sha256(comsSession.value).slice(0, 16)}`;
|
|
93
|
+
const role = firstEnvValue(["ZOB_COMS_ROLE_ID"]);
|
|
94
|
+
const roleId = role?.value ?? "zob-orchestrator";
|
|
95
|
+
return `role-${safeFileStem(roleId).slice(0, 40) || sha256(roleId).slice(0, 16)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function zpeerProfileIdIsSharedFallback(profileId = resolveZpeerProfileId("")): boolean {
|
|
99
|
+
return profileId.startsWith("role-");
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
function zpeerProfileDir(repoRoot: string): { dir: string; projectId: string } {
|
|
@@ -76,12 +109,32 @@ export function zpeerProfilePath(repoRoot: string, profileId = resolveZpeerProfi
|
|
|
76
109
|
return join(dir, `${safeFileStem(sha256(profileId).slice(0, 32))}.json`);
|
|
77
110
|
}
|
|
78
111
|
|
|
112
|
+
function zpeerNewCarryoverProfilePath(repoRoot: string): string {
|
|
113
|
+
const { dir } = zpeerProfileDir(repoRoot);
|
|
114
|
+
return join(dir, "new-carryover.json");
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
function hasForbiddenProfileKey(value: unknown): boolean {
|
|
80
118
|
if (!value || typeof value !== "object") return false;
|
|
81
119
|
if (Array.isArray(value)) return value.some(hasForbiddenProfileKey);
|
|
82
120
|
return Object.entries(value).some(([key, child]) => FORBIDDEN_PROFILE_KEYS.has(key) || hasForbiddenProfileKey(child));
|
|
83
121
|
}
|
|
84
122
|
|
|
123
|
+
function validZpeerMemberships(value: unknown): value is ZpeerRoomMembership[] {
|
|
124
|
+
if (!Array.isArray(value)) return false;
|
|
125
|
+
for (const membership of value) {
|
|
126
|
+
if (!isRecord(membership)) return false;
|
|
127
|
+
if (hasForbiddenProfileKey(membership)) return false;
|
|
128
|
+
if (typeof membership.roomId !== "string" || typeof membership.alias !== "string" || typeof membership.role !== "string") return false;
|
|
129
|
+
if (typeof membership.joinedAt !== "string" || membership.localOnly !== true || membership.networkEnabled !== false || membership.bodyStored !== false) return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validIsoDate(value: unknown): value is string {
|
|
135
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
136
|
+
}
|
|
137
|
+
|
|
85
138
|
function parseZpeerLocalProfile(value: unknown, repoRoot: string): ZpeerLocalProfile | undefined {
|
|
86
139
|
if (!isRecord(value) || value.schema !== PROFILE_SCHEMA) return undefined;
|
|
87
140
|
if (hasForbiddenProfileKey(value) || value.bodyStored !== false || value.localOnly !== true || value.networkEnabled !== false) return undefined;
|
|
@@ -89,18 +142,25 @@ function parseZpeerLocalProfile(value: unknown, repoRoot: string): ZpeerLocalPro
|
|
|
89
142
|
if (value.alias !== undefined && typeof value.alias !== "string") return undefined;
|
|
90
143
|
if (value.roomId !== undefined && typeof value.roomId !== "string") return undefined;
|
|
91
144
|
if (value.activeRoomId !== undefined && typeof value.activeRoomId !== "string") return undefined;
|
|
92
|
-
if (value.memberships !== undefined)
|
|
93
|
-
if (!Array.isArray(value.memberships)) return undefined;
|
|
94
|
-
for (const membership of value.memberships) {
|
|
95
|
-
if (!isRecord(membership)) return undefined;
|
|
96
|
-
if (typeof membership.roomId !== "string" || typeof membership.alias !== "string" || typeof membership.role !== "string") return undefined;
|
|
97
|
-
if (typeof membership.joinedAt !== "string" || membership.localOnly !== true || membership.networkEnabled !== false || membership.bodyStored !== false) return undefined;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
145
|
+
if (value.memberships !== undefined && !validZpeerMemberships(value.memberships)) return undefined;
|
|
100
146
|
if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") return undefined;
|
|
101
147
|
return value as unknown as ZpeerLocalProfile;
|
|
102
148
|
}
|
|
103
149
|
|
|
150
|
+
function parseZpeerNewCarryoverProfile(value: unknown, repoRoot: string, nowMs = Date.now()): ZpeerNewCarryoverProfile | undefined {
|
|
151
|
+
if (!isRecord(value) || value.schema !== NEW_CARRYOVER_SCHEMA) return undefined;
|
|
152
|
+
if (hasForbiddenProfileKey(value) || value.bodyStored !== false || value.localOnly !== true || value.networkEnabled !== false) return undefined;
|
|
153
|
+
if (value.projectId !== buildZobComsProjectId(repoRoot)) return undefined;
|
|
154
|
+
if (value.alias !== undefined && typeof value.alias !== "string") return undefined;
|
|
155
|
+
if (value.roomId !== undefined && typeof value.roomId !== "string") return undefined;
|
|
156
|
+
if (value.activeRoomId !== undefined && typeof value.activeRoomId !== "string") return undefined;
|
|
157
|
+
if (value.zagentId !== undefined && typeof value.zagentId !== "string") return undefined;
|
|
158
|
+
if (value.memberships !== undefined && !validZpeerMemberships(value.memberships)) return undefined;
|
|
159
|
+
if (!validIsoDate(value.createdAt) || !validIsoDate(value.updatedAt) || !validIsoDate(value.expiresAt)) return undefined;
|
|
160
|
+
if (Date.parse(value.expiresAt) <= nowMs) return undefined;
|
|
161
|
+
return value as unknown as ZpeerNewCarryoverProfile;
|
|
162
|
+
}
|
|
163
|
+
|
|
104
164
|
export function readZpeerLocalProfile(repoRoot: string, profileId = resolveZpeerProfileId(repoRoot)): ZpeerLocalProfile | undefined {
|
|
105
165
|
const path = zpeerProfilePath(repoRoot, profileId);
|
|
106
166
|
if (!existsSync(path)) return undefined;
|
|
@@ -115,14 +175,15 @@ export function writeZpeerLocalProfile(repoRoot: string, input: { alias?: string
|
|
|
115
175
|
const { dir, projectId } = zpeerProfileDir(repoRoot);
|
|
116
176
|
const existing = readZpeerLocalProfile(repoRoot, profileId);
|
|
117
177
|
const now = new Date().toISOString();
|
|
178
|
+
const sharedFallback = zpeerProfileIdIsSharedFallback(profileId);
|
|
118
179
|
const profile: ZpeerLocalProfile = {
|
|
119
180
|
schema: PROFILE_SCHEMA,
|
|
120
181
|
profileId,
|
|
121
182
|
projectId,
|
|
122
|
-
alias: input.alias ?? existing?.alias,
|
|
183
|
+
alias: sharedFallback ? undefined : input.alias ?? existing?.alias,
|
|
123
184
|
roomId: input.roomId ?? existing?.roomId,
|
|
124
185
|
activeRoomId: input.activeRoomId ?? input.roomId ?? existing?.activeRoomId,
|
|
125
|
-
memberships: input.memberships ?? existing?.memberships,
|
|
186
|
+
memberships: sharedFallback ? undefined : input.memberships ?? existing?.memberships,
|
|
126
187
|
createdAt: existing?.createdAt ?? now,
|
|
127
188
|
updatedAt: now,
|
|
128
189
|
localOnly: true,
|
|
@@ -135,6 +196,68 @@ export function writeZpeerLocalProfile(repoRoot: string, input: { alias?: string
|
|
|
135
196
|
return profile;
|
|
136
197
|
}
|
|
137
198
|
|
|
138
|
-
export function
|
|
139
|
-
|
|
199
|
+
export function readZpeerNewCarryoverProfile(repoRoot: string): ZpeerNewCarryoverProfile | undefined {
|
|
200
|
+
const path = zpeerNewCarryoverProfilePath(repoRoot);
|
|
201
|
+
if (!existsSync(path)) return undefined;
|
|
202
|
+
try {
|
|
203
|
+
const profile = parseZpeerNewCarryoverProfile(JSON.parse(readFileSync(path, "utf8")) as unknown, repoRoot);
|
|
204
|
+
if (!profile) clearZpeerNewCarryoverProfile(repoRoot);
|
|
205
|
+
return profile;
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function writeZpeerNewCarryoverProfile(repoRoot: string, input: ZpeerNewCarryoverInput): ZpeerNewCarryoverProfile {
|
|
212
|
+
if (hasForbiddenProfileKey(input)) throw new Error("Refusing to persist ZPeer /new carryover profile with forbidden body-like keys");
|
|
213
|
+
if (input.memberships !== undefined && !validZpeerMemberships(input.memberships)) throw new Error("Refusing to persist ZPeer /new carryover profile with invalid memberships");
|
|
214
|
+
const { dir, projectId } = zpeerProfileDir(repoRoot);
|
|
215
|
+
const existing = readZpeerNewCarryoverProfile(repoRoot);
|
|
216
|
+
const nowMs = Date.now();
|
|
217
|
+
const now = new Date(nowMs).toISOString();
|
|
218
|
+
const ttlMs = input.ttlMs !== undefined && Number.isFinite(input.ttlMs) && input.ttlMs > 0 ? input.ttlMs : DEFAULT_NEW_CARRYOVER_TTL_MS;
|
|
219
|
+
const profile: ZpeerNewCarryoverProfile = {
|
|
220
|
+
schema: NEW_CARRYOVER_SCHEMA,
|
|
221
|
+
projectId,
|
|
222
|
+
alias: input.alias ?? existing?.alias,
|
|
223
|
+
roomId: input.roomId ?? existing?.roomId,
|
|
224
|
+
activeRoomId: input.activeRoomId ?? input.roomId ?? existing?.activeRoomId,
|
|
225
|
+
memberships: input.memberships ?? existing?.memberships,
|
|
226
|
+
zagentId: input.zagentId ?? existing?.zagentId,
|
|
227
|
+
createdAt: existing?.createdAt ?? now,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
expiresAt: new Date(nowMs + ttlMs).toISOString(),
|
|
230
|
+
localOnly: true,
|
|
231
|
+
networkEnabled: false,
|
|
232
|
+
bodyStored: false,
|
|
233
|
+
};
|
|
234
|
+
if (hasForbiddenProfileKey(profile)) throw new Error("Refusing to persist ZPeer /new carryover profile with forbidden body-like keys");
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
236
|
+
writeFileSync(zpeerNewCarryoverProfilePath(repoRoot), `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
237
|
+
return profile;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function clearZpeerNewCarryoverProfile(repoRoot: string): void {
|
|
241
|
+
const path = zpeerNewCarryoverProfilePath(repoRoot);
|
|
242
|
+
if (existsSync(path)) unlinkSync(path);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function generatedAliasForPeer(peer: Pick<ZobLivePeerCard, "roleId" | "sessionHash">): string | undefined {
|
|
246
|
+
const roleAlias = `${peer.roleId}-${peer.sessionHash.slice(0, 6)}`;
|
|
247
|
+
if (/^[a-zA-Z][a-zA-Z0-9_-]{1,31}$/.test(roleAlias)) return roleAlias;
|
|
248
|
+
return `peer-${peer.sessionHash.slice(0, 8)}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function activeMembershipAlias(peer: Pick<ZobLivePeerCard, "zpeerRoomId" | "zpeerActiveRoomId" | "zpeerMemberships">): string | undefined {
|
|
252
|
+
const activeRoomId = peer.zpeerActiveRoomId ?? peer.zpeerRoomId;
|
|
253
|
+
return peer.zpeerMemberships?.find((membership) => membership.roomId === activeRoomId)?.alias;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function writeZpeerLocalProfileFromPeer(repoRoot: string, peer: Pick<ZobLivePeerCard, "roleId" | "sessionHash" | "zpeerAlias" | "zpeerRoomId" | "zpeerActiveRoomId" | "zpeerMemberships">, profileId = resolveZpeerProfileId(repoRoot)): ZpeerLocalProfile {
|
|
257
|
+
const existing = readZpeerLocalProfile(repoRoot, profileId);
|
|
258
|
+
const generatedAlias = generatedAliasForPeer(peer);
|
|
259
|
+
const membershipAlias = activeMembershipAlias(peer);
|
|
260
|
+
const candidateAlias = membershipAlias ?? peer.zpeerAlias;
|
|
261
|
+
const alias = candidateAlias && candidateAlias !== generatedAlias ? candidateAlias : existing?.alias ?? candidateAlias;
|
|
262
|
+
return writeZpeerLocalProfile(repoRoot, { alias, roomId: peer.zpeerRoomId, activeRoomId: peer.zpeerActiveRoomId, memberships: peer.zpeerMemberships }, profileId);
|
|
140
263
|
}
|
|
@@ -4,10 +4,11 @@ import { join } from "node:path";
|
|
|
4
4
|
import { buildZobComsProjectId } from "./identity.js";
|
|
5
5
|
import { buildZobLiveEnvelope } from "./envelope.js";
|
|
6
6
|
import { sendZobLocalEnvelope } from "./local-transport.js";
|
|
7
|
-
import {
|
|
7
|
+
import { readZobLiveRegistryAllProjectsSnapshot, writeZobLivePeerCard, writeZobLivePeerCardToProjectId } from "./registry.js";
|
|
8
8
|
import type { ZobLivePeerCard, ZobLivePeerStatus, ZpeerRoomMembership, ZpeerRoomMembershipRole } from "./types.js";
|
|
9
9
|
import { validateZobComsEdge } from "../topology/coms.js";
|
|
10
10
|
import { loadTeamDefinition, validateTeamDefinition } from "../topology/teams.js";
|
|
11
|
+
import { loadZteamManifest, zteamAllowsZpeerContact } from "../zagents.js";
|
|
11
12
|
import { sha256 } from "../utils/hashing.js";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_ROOM_ID = "default";
|
|
@@ -180,6 +181,34 @@ function zpeerReachableStatus(peer: ZobLivePeerCard): ZobLivePeerStatus {
|
|
|
180
181
|
return "offline";
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
async function peerRespondsToAliasPing(peer: ZobLivePeerCard): Promise<boolean> {
|
|
185
|
+
if (zpeerReachableStatus(peer) !== "online") return false;
|
|
186
|
+
try {
|
|
187
|
+
const response = await sendZobLocalEnvelope(peer.endpoint, {
|
|
188
|
+
schema: "zob.live-envelope.v1",
|
|
189
|
+
type: "ping",
|
|
190
|
+
msgId: `zpeer-alias-ping:${Date.now()}`,
|
|
191
|
+
hops: 0,
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
bodyStored: false,
|
|
194
|
+
}, { timeoutMs: 1_000 });
|
|
195
|
+
return response.type === "pong" || response.type === "ack";
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function activeAliasCollision(repoRoot: string, self: ZobLivePeerCard, roomId: string, alias: string): Promise<ZpeerRoomPeer | undefined> {
|
|
202
|
+
for (const entry of peersInRoom(repoRoot, roomId)) {
|
|
203
|
+
if (entry.peer.sessionHash === self.sessionHash || entry.membership.alias !== alias) continue;
|
|
204
|
+
if (await peerRespondsToAliasPing(entry.peer)) return entry;
|
|
205
|
+
if (zpeerReachableStatus(entry.peer) === "online") {
|
|
206
|
+
try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort stale alias release */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
183
212
|
export function ensureZpeerFields(repoRoot: string, peer: ZobLivePeerCard, roomId?: string, alias?: string, restoredMemberships?: ZpeerRoomMembership[]): ZobLivePeerCard {
|
|
184
213
|
const hasRestoredMemberships = Boolean(restoredMemberships && restoredMemberships.length > 0);
|
|
185
214
|
const baseMemberships = hasRestoredMemberships
|
|
@@ -204,25 +233,27 @@ export function refreshZpeerSelf(repoRoot: string, peer: ZobLivePeerCard, roomId
|
|
|
204
233
|
return writeZobLivePeerCard(repoRoot, { ...ensured, heartbeatAt: new Date().toISOString(), status: "online" });
|
|
205
234
|
}
|
|
206
235
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
236
|
+
type ZobLiveRegistrySnapshotValue = ReturnType<typeof readZobLiveRegistryAllProjectsSnapshot>;
|
|
237
|
+
|
|
238
|
+
function peersInRoomFromSnapshot(snapshot: ZobLiveRegistrySnapshotValue, roomId: string): ZpeerRoomPeer[] {
|
|
239
|
+
return snapshot.peers
|
|
211
240
|
.flatMap((peer) => zpeerMembershipsForPeer(peer)
|
|
212
241
|
.filter((membership) => membership.roomId === roomId)
|
|
213
242
|
.map((membership) => ({ peer, membership })));
|
|
214
243
|
}
|
|
215
244
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
245
|
+
function peersInRoom(repoRoot: string, roomId: string): ZpeerRoomPeer[] {
|
|
246
|
+
return peersInRoomFromSnapshot(readZobLiveRegistryAllProjectsSnapshot(repoRoot), roomId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard | undefined, roomId: string, peers: ZpeerRoomPeer[]): ZpeerRoomSummary {
|
|
219
250
|
const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
|
|
220
251
|
const aliases = peers.map((entry) => entry.membership.alias).sort();
|
|
221
252
|
for (const entry of peers) counts[zpeerReachableStatus(entry.peer)] += 1;
|
|
222
253
|
const duplicateAliases = aliases.filter((alias, index) => aliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
|
|
223
254
|
return {
|
|
224
255
|
schema: "zob.zpeer-room-summary.v1",
|
|
225
|
-
projectId
|
|
256
|
+
projectId,
|
|
226
257
|
roomId,
|
|
227
258
|
selfAlias: self ? peerAliasInRoom(self, roomId) : undefined,
|
|
228
259
|
peerCount: peers.length,
|
|
@@ -238,10 +269,17 @@ export function buildZpeerRoomSummary(repoRoot: string, self?: ZobLivePeerCard,
|
|
|
238
269
|
};
|
|
239
270
|
}
|
|
240
271
|
|
|
272
|
+
export function buildZpeerRoomSummary(repoRoot: string, self?: ZobLivePeerCard, requestedRoomId?: string): ZpeerRoomSummary {
|
|
273
|
+
const snapshot = readZobLiveRegistryAllProjectsSnapshot(repoRoot);
|
|
274
|
+
const roomId = safeZpeerRoomId(requestedRoomId) ?? (self ? activeZpeerRoomId(self) : DEFAULT_ROOM_ID);
|
|
275
|
+
return buildZpeerRoomSummaryFromPeers(snapshot.projectId, self, roomId, peersInRoomFromSnapshot(snapshot, roomId));
|
|
276
|
+
}
|
|
277
|
+
|
|
241
278
|
export function buildZpeerPeerRoomSummaries(repoRoot: string, self: ZobLivePeerCard): ZpeerPeerRoomSummary[] {
|
|
279
|
+
const snapshot = readZobLiveRegistryAllProjectsSnapshot(repoRoot);
|
|
242
280
|
const activeRoomId = activeZpeerRoomId(self);
|
|
243
281
|
return zpeerMembershipsForPeer(self).map((membership) => ({
|
|
244
|
-
...
|
|
282
|
+
...buildZpeerRoomSummaryFromPeers(snapshot.projectId, self, membership.roomId, peersInRoomFromSnapshot(snapshot, membership.roomId)),
|
|
245
283
|
active: membership.roomId === activeRoomId,
|
|
246
284
|
})).sort((left, right) => {
|
|
247
285
|
if (left.active !== right.active) return left.active ? -1 : 1;
|
|
@@ -249,26 +287,26 @@ export function buildZpeerPeerRoomSummaries(repoRoot: string, self: ZobLivePeerC
|
|
|
249
287
|
});
|
|
250
288
|
}
|
|
251
289
|
|
|
252
|
-
export function changeZpeerAlias(repoRoot: string, self: ZobLivePeerCard, requestedAlias: string, requestedRoomId?: string): { ok: true; peer: ZobLivePeerCard } | { ok: false; reason: string } {
|
|
290
|
+
export async function changeZpeerAlias(repoRoot: string, self: ZobLivePeerCard, requestedAlias: string, requestedRoomId?: string): Promise<{ ok: true; peer: ZobLivePeerCard } | { ok: false; reason: string }> {
|
|
253
291
|
const alias = safeZpeerAlias(requestedAlias);
|
|
254
292
|
if (!alias) return { ok: false, reason: "alias must match [a-zA-Z][a-zA-Z0-9_-]{1,31}" };
|
|
255
293
|
const roomId = safeZpeerRoomId(requestedRoomId) ?? activeZpeerRoomId(self);
|
|
256
294
|
const memberships = zpeerMembershipsForPeer(self);
|
|
257
295
|
const current = memberships.find((membership) => membership.roomId === roomId);
|
|
258
296
|
if (!current) return { ok: false, reason: `current peer is not a member of room '${roomId}'` };
|
|
259
|
-
const collision =
|
|
260
|
-
if (collision) return { ok: false, reason: `alias '${alias}' is already used in room '${roomId}'` };
|
|
297
|
+
const collision = await activeAliasCollision(repoRoot, self, roomId, alias);
|
|
298
|
+
if (collision) return { ok: false, reason: `alias '${alias}' is already used by a live peer in room '${roomId}'` };
|
|
261
299
|
return { ok: true, peer: refreshZpeerSelf(repoRoot, withZpeerMembershipState(repoRoot, self, upsertMembership(memberships, { ...current, alias }), activeZpeerRoomId(self))) };
|
|
262
300
|
}
|
|
263
301
|
|
|
264
|
-
export function joinZpeerRoom(repoRoot: string, self: ZobLivePeerCard, requestedRoom: string, requestedAlias?: string, role: ZpeerRoomMembershipRole = "member"): { ok: true; peer: ZobLivePeerCard } | { ok: false; reason: string } {
|
|
302
|
+
export async function joinZpeerRoom(repoRoot: string, self: ZobLivePeerCard, requestedRoom: string, requestedAlias?: string, role: ZpeerRoomMembershipRole = "member"): Promise<{ ok: true; peer: ZobLivePeerCard } | { ok: false; reason: string }> {
|
|
265
303
|
const roomId = safeZpeerRoomId(requestedRoom);
|
|
266
304
|
if (!roomId) return { ok: false, reason: "room must match [a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}" };
|
|
267
305
|
const memberships = zpeerMembershipsForPeer(self);
|
|
268
306
|
const existing = memberships.find((membership) => membership.roomId === roomId);
|
|
269
307
|
const alias = safeZpeerAlias(requestedAlias ?? existing?.alias ?? self.zpeerAlias) ?? generatedZpeerAlias(self);
|
|
270
|
-
const collision =
|
|
271
|
-
if (collision) return { ok: false, reason: `alias '${alias}' already exists in target room '${roomId}'; rename first or use 'as <alias>'` };
|
|
308
|
+
const collision = await activeAliasCollision(repoRoot, self, roomId, alias);
|
|
309
|
+
if (collision) return { ok: false, reason: `alias '${alias}' already exists on a live peer in target room '${roomId}'; rename first or use 'as <alias>'` };
|
|
272
310
|
const next = buildMembership({ roomId, alias, role, joinedAt: existing?.joinedAt ?? new Date().toISOString() });
|
|
273
311
|
return { ok: true, peer: refreshZpeerSelf(repoRoot, withZpeerMembershipState(repoRoot, self, upsertMembership(memberships, next), activeZpeerRoomId(self))) };
|
|
274
312
|
}
|
|
@@ -292,12 +330,37 @@ export function useZpeerRoom(repoRoot: string, self: ZobLivePeerCard, requestedR
|
|
|
292
330
|
return { ok: true, peer: refreshZpeerSelf(repoRoot, withZpeerMembershipState(repoRoot, self, memberships, roomId)) };
|
|
293
331
|
}
|
|
294
332
|
|
|
295
|
-
export function
|
|
333
|
+
export function clearZpeerRoom(repoRoot: string, self: ZobLivePeerCard, requestedRoom: string): { ok: true; roomId: string; cleared: number; preservedSelf: true } | { ok: false; reason: string } {
|
|
334
|
+
const roomId = safeZpeerRoomId(requestedRoom);
|
|
335
|
+
if (!roomId) return { ok: false, reason: "room must match [a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}" };
|
|
336
|
+
let cleared = 0;
|
|
337
|
+
for (const entry of peersInRoom(repoRoot, roomId)) {
|
|
338
|
+
if (entry.peer.sessionHash === self.sessionHash) continue;
|
|
339
|
+
const remaining = zpeerMembershipsForPeer(entry.peer).filter((membership) => membership.roomId !== roomId);
|
|
340
|
+
const fallbackRoomId = `cleared-${entry.peer.sessionHash.slice(0, 8)}`;
|
|
341
|
+
const fallbackAlias = safeZpeerAlias(entry.peer.zpeerAlias) ?? generatedZpeerAlias(entry.peer);
|
|
342
|
+
const nextMemberships = remaining.length > 0 ? remaining : [buildMembership({ roomId: fallbackRoomId, alias: fallbackAlias, role: "observer", joinedAt: new Date().toISOString() })];
|
|
343
|
+
const active = nextMemberships[0];
|
|
344
|
+
writeZobLivePeerCardToProjectId({
|
|
345
|
+
...entry.peer,
|
|
346
|
+
heartbeatAt: new Date().toISOString(),
|
|
347
|
+
status: "offline",
|
|
348
|
+
zpeerRoomId: active.roomId,
|
|
349
|
+
zpeerActiveRoomId: active.roomId,
|
|
350
|
+
zpeerAlias: active.alias,
|
|
351
|
+
zpeerMemberships: nextMemberships,
|
|
352
|
+
});
|
|
353
|
+
cleared += 1;
|
|
354
|
+
}
|
|
355
|
+
return { ok: true, roomId, cleared, preservedSelf: true };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function changeZpeerRoom(repoRoot: string, self: ZobLivePeerCard, requestedRoom: string): Promise<{ ok: true; peer: ZobLivePeerCard } | { ok: false; reason: string }> {
|
|
296
359
|
const roomId = safeZpeerRoomId(requestedRoom);
|
|
297
360
|
if (!roomId) return { ok: false, reason: "room must match [a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}" };
|
|
298
361
|
const memberships = zpeerMembershipsForPeer(self);
|
|
299
362
|
if (!memberships.some((membership) => membership.roomId === roomId)) {
|
|
300
|
-
const joined = joinZpeerRoom(repoRoot, self, roomId, self.zpeerAlias ?? generatedZpeerAlias(self));
|
|
363
|
+
const joined = await joinZpeerRoom(repoRoot, self, roomId, self.zpeerAlias ?? generatedZpeerAlias(self));
|
|
301
364
|
if (!joined.ok) return joined;
|
|
302
365
|
return useZpeerRoom(repoRoot, joined.peer, roomId);
|
|
303
366
|
}
|
|
@@ -345,8 +408,12 @@ function appendZpeerPeerRecords(repoRoot: string, record: {
|
|
|
345
408
|
appendHashOnlyZpeerJsonl(repoRoot, "peer-status.jsonl", { ...base, schema: "zob.zpeer-peer-status.v1" });
|
|
346
409
|
}
|
|
347
410
|
|
|
348
|
-
function validateZpeerTopology(repoRoot: string, self: ZobLivePeerCard, target: ZobLivePeerCard): string | undefined {
|
|
411
|
+
function validateZpeerTopology(repoRoot: string, self: ZobLivePeerCard, target: ZobLivePeerCard, roomId: string, selfAlias?: string, targetAlias?: string): string | undefined {
|
|
349
412
|
if (self.roleId === target.roleId && self.roleType === "orchestrator" && target.roleType === "orchestrator") return undefined;
|
|
413
|
+
const selfInRequestedRoom = isPeerInZpeerRoom(self, roomId);
|
|
414
|
+
const targetInRequestedRoom = isPeerInZpeerRoom(target, roomId);
|
|
415
|
+
const bothPeersAreWorkers = self.roleType === "worker" && target.roleType === "worker";
|
|
416
|
+
if (selfInRequestedRoom && targetInRequestedRoom && !bothPeersAreWorkers) return undefined;
|
|
350
417
|
const teamName = self.team || target.team || "zob-core";
|
|
351
418
|
if (target.team !== teamName) return "zpeer topology blocked: peers are in different teams";
|
|
352
419
|
let topologyRoot = repoRoot;
|
|
@@ -355,11 +422,19 @@ function validateZpeerTopology(repoRoot: string, self: ZobLivePeerCard, target:
|
|
|
355
422
|
topologyRoot = process.cwd();
|
|
356
423
|
loaded = loadTeamDefinition(topologyRoot, teamName);
|
|
357
424
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
425
|
+
if (loaded.definition) {
|
|
426
|
+
const definitionErrors = validateTeamDefinition(topologyRoot, loaded.definition);
|
|
427
|
+
if (definitionErrors.length > 0) return `zpeer topology blocked: ${definitionErrors.join("; ")}`;
|
|
428
|
+
const edgeErrors = validateZobComsEdge(loaded.definition, self.roleId, target.roleId);
|
|
429
|
+
if (edgeErrors.length === 0) return undefined;
|
|
430
|
+
if (!edgeErrors.some((error) => error.startsWith("Unknown coms sender") || error.startsWith("Unknown coms receiver"))) return `zpeer topology blocked: ${edgeErrors.join("; ")}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!self.team || self.team !== target.team) return "zpeer topology blocked: peers are not in the same zteam";
|
|
434
|
+
const zteam = loadZteamManifest(repoRoot, self.team);
|
|
435
|
+
if (zteam.errors.length > 0) return `zpeer topology blocked: ${loaded.errors.join("; ") || "legacy topology rejected ZAgent role ids"}; zteam fallback blocked: ${zteam.errors.join("; ")}`;
|
|
436
|
+
if (!zteamAllowsZpeerContact(zteam.manifest, self.roleId, roomId, selfAlias)) return `zpeer topology blocked: zteam '${self.team}' does not allow ${self.roleId} in room '${roomId}'`;
|
|
437
|
+
if (!zteamAllowsZpeerContact(zteam.manifest, target.roleId, roomId, targetAlias)) return `zpeer topology blocked: zteam '${target.team}' does not allow ${target.roleId} in room '${roomId}'`;
|
|
363
438
|
return undefined;
|
|
364
439
|
}
|
|
365
440
|
|
|
@@ -400,7 +475,7 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
|
|
|
400
475
|
const target = candidates[0];
|
|
401
476
|
const targetReachableStatus = zpeerReachableStatus(target.peer);
|
|
402
477
|
if (targetReachableStatus !== "online") return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is ${targetReachableStatus}`, targetAlias, taskHash, bodyStored: false }, 1);
|
|
403
|
-
const topologyBlocker = validateZpeerTopology(repoRoot, self, target.peer);
|
|
478
|
+
const topologyBlocker = validateZpeerTopology(repoRoot, self, target.peer, roomId, senderAlias, target.membership.alias);
|
|
404
479
|
if (topologyBlocker) return finish("attempt", { status: "blocked", reason: topologyBlocker, targetAlias, taskHash, bodyStored: false }, 1);
|
|
405
480
|
if (target.peer.transport !== "local_socket" || target.peer.endpoint.startsWith("pending-") || target.peer.endpoint === "observe-only") return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is not reachable by local_socket`, targetAlias, taskHash, bodyStored: false }, 1);
|
|
406
481
|
if (self.transport !== "local_socket" || !self.endpoint || self.endpoint.startsWith("pending-") || self.endpoint === "observe-only") return finish("attempt", { status: "blocked", reason: "current session has no local_socket reply endpoint", targetAlias, taskHash, bodyStored: false }, 1);
|
|
@@ -560,6 +560,10 @@ export function resumeRuntimeGoal(goal: RuntimeGoal, requestedAdditionalTurns?:
|
|
|
560
560
|
export function queueRuntimeGoalContinuation(pi: ExtensionAPI, state: HarnessRuntimeState, ctx: ExtensionContext, options: { userVisible?: boolean; retryMs?: number } = {}): void {
|
|
561
561
|
const goal = state.runtimeGoal;
|
|
562
562
|
if (!canContinue(goal)) return;
|
|
563
|
+
if (state.zobLive.passivePeerWait?.suppressGoalContinuation === true) {
|
|
564
|
+
clearRuntimeGoalContinuationTimer(state);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
563
567
|
const humanDecision = pauseIfHumanDecisionRequired(pi, state, goal);
|
|
564
568
|
if (humanDecision) {
|
|
565
569
|
ctx.ui.notify(`ZOB /goal paused: ${goal.oracle.blockerSummary}`, "warning");
|