zob-harness 0.7.1 → 0.9.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 +43 -1
- package/.pi/extensions/zob-harness/src/core/constants.ts +5 -4
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +363 -22
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts +40 -0
- package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +6 -2
- package/.pi/extensions/zob-harness/src/domains/coms/zagents.ts +304 -0
- package/.pi/extensions/zob-harness/src/runtime/commands.ts +1078 -10
- package/.pi/extensions/zob-harness/src/runtime/events.ts +163 -15
- package/.pi/extensions/zob-harness/src/runtime/schemas.ts +37 -0
- package/.pi/extensions/zob-harness/src/runtime/state.ts +22 -0
- package/.pi/extensions/zob-harness/src/runtime/tools-zagent.ts +975 -0
- package/.pi/extensions/zob-harness/src/runtime/widget.ts +35 -2
- package/.pi/extensions/zob-harness/src/runtime/zobHarness.ts +3 -0
- package/.pi/skills/zob-factory/SKILL.md +4 -2
- package/.pi/skills/zob-zagent-creator/SKILL.md +106 -14
- package/.pi/zagents/harness-architect.json +67 -0
- package/.pi/zagents/harness-chief.json +73 -0
- package/.pi/zagents/harness-coms-steward.json +64 -0
- package/.pi/zagents/harness-factory-engineer.json +61 -0
- package/.pi/zagents/harness-implementer.json +69 -0
- package/.pi/zagents/harness-interlocutor.json +61 -0
- package/.pi/zagents/harness-oracle.json +70 -0
- package/.pi/zagents/hot-add-readonly-test-scout.json +102 -0
- package/.pi/zagents/prompts/harness-architect.md +82 -0
- package/.pi/zagents/prompts/harness-chief.md +92 -0
- package/.pi/zagents/prompts/harness-coms-steward.md +80 -0
- package/.pi/zagents/prompts/harness-factory-engineer.md +79 -0
- package/.pi/zagents/prompts/harness-implementer.md +87 -0
- package/.pi/zagents/prompts/harness-interlocutor.md +92 -0
- package/.pi/zagents/prompts/harness-oracle.md +83 -0
- package/.pi/zagents/prompts/hot-add-readonly-test-scout.md +51 -0
- package/.pi/zteams/agent-factory-pacman-multiplayer.tmux.sh +54 -0
- package/.pi/zteams/zob-harness-devs.json +249 -0
- package/.pi/zteams/zob-harness-devs.modes.json +120 -0
- package/.pi/zteams/zob-harness-devs.tmux.sh +270 -0
- package/package.json +7 -1
- package/scripts/zagent-static-smoke.mjs +300 -1
- package/scripts/zpeer-local-e2e-smoke.mjs +9 -7
- package/scripts/zpeer-static-smoke.mjs +5 -0
- package/scripts/zteam-hot-add/smoke.mjs +77 -0
- package/scripts/zteam-lifecycle-smoke.mjs +154 -0
- package/scripts/zteam-tools/smoke.mjs +180 -0
|
@@ -1404,6 +1404,48 @@
|
|
|
1404
1404
|
],
|
|
1405
1405
|
"noShipNotes": "Factory-mode only; requires manifest/checkpoint/sentinel validation."
|
|
1406
1406
|
},
|
|
1407
|
+
{
|
|
1408
|
+
"name": "zob_zteam_hot_add",
|
|
1409
|
+
"family": "zteam/zagent",
|
|
1410
|
+
"modes": [
|
|
1411
|
+
"plan",
|
|
1412
|
+
"implement",
|
|
1413
|
+
"orchestrator",
|
|
1414
|
+
"factory"
|
|
1415
|
+
],
|
|
1416
|
+
"skillRefs": [
|
|
1417
|
+
".pi/skills/zob-zagent-creator/SKILL.md",
|
|
1418
|
+
".pi/skills/zob-coms-v2-live/SKILL.md",
|
|
1419
|
+
".pi/skills/zob-harness/SKILL.md"
|
|
1420
|
+
],
|
|
1421
|
+
"docRefs": [
|
|
1422
|
+
".pi/extensions/zob-harness/src/runtime/tools-zagent.ts",
|
|
1423
|
+
".pi/extensions/zob-harness/src/runtime/schemas.ts",
|
|
1424
|
+
".pi/extensions/zob-harness/src/domains/coms/zagents.ts"
|
|
1425
|
+
],
|
|
1426
|
+
"noShipNotes": "Agent-executable governed ZTeam/ZAgent hot-add. Defaults to plan-only/no-spawn with action=plan and spawnCount=0; action=apply still writes only promptRef/prompt, ZAgent manifest, and ZTeam membership after exact apply_confirmation=<team_id> and never launches. action=launch is explicit and requires exact launch_confirmation_phrase: LAUNCH ZTEAM <team_id> ZAGENT <zagent_id> IN TMUX <session_name>; it requires existing ZTeam/ZAgent manifests, existing membership, existing tmux session, absent target window, safe ids/window/session, and bounded cwd. Launch creates only one scoped tmux new-window in the existing session and send-keys a single ZOB_ZTEAM_ID/ZOB_ZAGENT_ID pi command for the target ZAgent; it never creates sessions, kills/closes/reloads windows, or touches unrelated agents. It polls local ZPeer lease/registry presence with bounded timeout and returns liveProofBlocked unless presence is online. Durable ledgers remain local-only/hash-only/body-free; raw request, generated prompt body, launch command, output, and diffs are not persisted."
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
"name": "zob_zteam_remove",
|
|
1430
|
+
"family": "zteam/zagent",
|
|
1431
|
+
"modes": [
|
|
1432
|
+
"plan",
|
|
1433
|
+
"implement",
|
|
1434
|
+
"orchestrator",
|
|
1435
|
+
"factory"
|
|
1436
|
+
],
|
|
1437
|
+
"skillRefs": [
|
|
1438
|
+
".pi/skills/zob-zagent-creator/SKILL.md",
|
|
1439
|
+
".pi/skills/zob-coms-v2-live/SKILL.md",
|
|
1440
|
+
".pi/skills/zob-harness/SKILL.md"
|
|
1441
|
+
],
|
|
1442
|
+
"docRefs": [
|
|
1443
|
+
".pi/extensions/zob-harness/src/runtime/tools-zagent.ts",
|
|
1444
|
+
".pi/extensions/zob-harness/src/runtime/schemas.ts",
|
|
1445
|
+
".pi/extensions/zob-harness/src/domains/coms/zagents.ts"
|
|
1446
|
+
],
|
|
1447
|
+
"noShipNotes": "Agent-executable governed ZTeam/ZAgent remove/delete/close planner. Defaults to plan-only/no-spawn with action=plan, spawnCount=0, closeCount=0, and bodyStored=false/promptBodiesStored=false/outputBodiesStored=false in durable records. action=apply preserves existing file-delete semantics and requires exact confirmation phrase: REMOVE ZTEAM <team_id> ZAGENT <zagent_id> SCOPE <membership|manifest|prompt|manifest_and_prompt>. action=close_tmux is explicit and requires exact close_confirmation_phrase: CLOSE ZTEAM <team_id> ZAGENT <zagent_id> TMUX WINDOW <session_name>; it validates safe team/agent/session/window ids, bounded cwd paths, existing tmux session, and existing target window. It sends only scoped tmux send-keys C-u /quit C-m to the selected window, waits bounded for local ZPeer presence offline/none and/or target window disappearance, and if force_close_window=true may use targeted tmux kill-window only for that selected window. It never creates sessions, kills sessions, uses broad process termination, reloads, closes all, or touches unrelated windows/agents. Durable ledgers remain local-only/hash-only/body-free; raw tmux commands, raw output, diffs, prompts, and secrets are not persisted."
|
|
1448
|
+
},
|
|
1407
1449
|
{
|
|
1408
1450
|
"name": "zob_zcommit_run",
|
|
1409
1451
|
"family": "git/commit",
|
|
@@ -1626,7 +1668,7 @@
|
|
|
1626
1668
|
"AGENTS.md",
|
|
1627
1669
|
".pi/skills/zob-zagent-creator/SKILL.md"
|
|
1628
1670
|
],
|
|
1629
|
-
"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
|
|
1671
|
+
"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 and hot-add default to plan/metadata only and must not spawn processes. Hot-add supports explicit team ids or current-context inference from ZOB_ZTEAM_ID, active ZAgent, ZPeer team/active room, or repo conventions. Hot-add apply requires --apply --confirm <team-id>; optional tmux-window launch planning requires --tmux-window --launch-confirm <team-id>; raw natural-language ask is hashed only in durable records; generated ZAgents include promptRef/prompt, explicit tools/paths/gates; live presence checks use local lease/registry evidence and tmux windows are not presence proof; persisted command metadata must remain hash/body-free. Reset sends Pi /new and reload sends Pi /reload to existing scoped tmux agent windows without close/start; quit calls only the scoped launcher close for the current or explicit team."
|
|
1630
1672
|
},
|
|
1631
1673
|
{
|
|
1632
1674
|
"name": "rules_status",
|
|
@@ -53,6 +53,7 @@ export const ZOB_WORKSPACE_CLAIM_TOOLS = ["zob_workspace_claim", "zob_workspace_
|
|
|
53
53
|
export const ZOB_WORKER_POOL_TOOLS = ["zob_worker_pool_plan", "zob_worker_pool_status", "zob_worker_pool_owner_request", "zob_worker_pool_owner_decision"] as const;
|
|
54
54
|
export const ZOB_MERGE_QUEUE_TOOLS = ["zob_merge_candidate_submit", "zob_merge_queue_decide", "zob_merge_queue_list"] as const;
|
|
55
55
|
export const ZOB_ZCOMMIT_TOOLS = ["zob_zcommit_run"] as const;
|
|
56
|
+
export const ZOB_ZAGENT_TOOLS = ["zob_zteam_hot_add", "zob_zteam_remove"] as const;
|
|
56
57
|
export const ZOB_DELEGATION_READ_TOOLS = ["zob_delegation_catalog", "get_delegation_run", "await_delegation_run"] as const;
|
|
57
58
|
export const ZOB_MISSION_CONTROL_READ_TOOLS = ["zob_coms_readiness", "zob_mission_control_snapshot"] as const;
|
|
58
59
|
export const ZOB_MISSION_CONTROL_PROPOSAL_TOOLS = ["zob_mission_control_propose_command"] as const;
|
|
@@ -68,11 +69,11 @@ export const ZOB_AUTONOMOUS_FACTORY_TOOLS = ["zob_autonomous_dry_run", "zob_auto
|
|
|
68
69
|
|
|
69
70
|
export const MODE_TOOLS: Record<ModeName, string[]> = {
|
|
70
71
|
explore: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
71
|
-
plan: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
72
|
-
implement: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
72
|
+
plan: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
73
|
+
implement: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
73
74
|
oracle: ["read", "grep", "find", "ls", "bash", "delegate_agent", "delegate_task", "zob_coms_list", "zob_coms_get", "zob_coms_await", "zpeer_ask", "zob_goal_room_list", "zob_workspace_claims_list", "zob_worker_pool_status", "zob_merge_queue_list", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS],
|
|
74
|
-
orchestrator: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS],
|
|
75
|
-
factory: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "factory_run", "factory_quarantine_review", "factory_quarantine_activate", "factory_quarantine_verify_activation", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_AUTONOMOUS_FACTORY_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
75
|
+
orchestrator: ["read", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS],
|
|
76
|
+
factory: ["read", "bash", "edit", "write", "grep", "find", "ls", "delegate_agent", "delegate_task", "orchestrate_run", "factory_run", "factory_quarantine_review", "factory_quarantine_activate", "factory_quarantine_verify_activation", "chain_run", ...ZOB_RUNTIME_GOAL_TOOLS, ...ZOB_DELEGATION_READ_TOOLS, ...ZOB_ZCOMMIT_TOOLS, ...ZOB_ZAGENT_TOOLS, ...ZOB_AUTONOMOUS_READ_TOOLS, ...ZOB_AUTONOMOUS_FACTORY_TOOLS, ...ZOB_COMS_TOOLS, ...ZOB_GOAL_ROOM_TOOLS, ...ZOB_GOVERNED_REQUEST_TOOLS, ...ZOB_WORKSPACE_CLAIM_TOOLS, ...ZOB_WORKER_POOL_TOOLS, ...ZOB_MERGE_QUEUE_TOOLS, ...ZOB_MISSION_CONTROL_READ_TOOLS, ...ZOB_MISSION_CONTROL_PROPOSAL_TOOLS, ...ZOB_CONTEXT_READ_TOOLS, ...ZOB_CONTEXT_PROPOSAL_TOOLS, ...ZOB_COMPUTE_READ_TOOLS, ...ZOB_COMPUTE_REPORT_TOOLS, ...ZOB_PROJECT_DNA_READ_TOOLS, ...ZOB_PROJECT_DNA_PROPOSAL_TOOLS],
|
|
76
77
|
// Vanilla is handled specially by applyMode: all currently available Pi tools are enabled.
|
|
77
78
|
vanilla: [],
|
|
78
79
|
};
|
|
@@ -5,13 +5,16 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { loadTeamDefinition, validateTeamDefinition } from "../../topology/teams.js";
|
|
6
6
|
import { isRecord } from "../../../core/utils/records.js";
|
|
7
7
|
import { safeFileStem } from "../../../core/utils/paths.js";
|
|
8
|
+
import { sha256 } from "../../../core/utils/hashing.js";
|
|
8
9
|
import { buildCurrentZobLivePeerCard, buildZobComsProjectId } from "./identity.js";
|
|
10
|
+
import { pingZobLocalEndpoint } from "./local-transport.js";
|
|
9
11
|
import { readZobComsV2Policy, zobComsRegistryEnabled } from "./policy.js";
|
|
10
|
-
import type { ZobLivePeerCard, ZobLivePeerStatus, ZobLiveRegistrySnapshot } from "./types.js";
|
|
12
|
+
import type { ZobLivePeerCard, ZobLivePeerStatus, ZobLiveRegistrySnapshot, ZobLiveTeamAgentLease } from "./types.js";
|
|
11
13
|
|
|
12
14
|
const FORBIDDEN_PERSISTED_KEYS = new Set(["body", "task", "prompt", "output", "content", "message", "rationale", "text", "diff", "patch"]);
|
|
13
15
|
const DEFAULT_OFFLINE_PEER_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
14
16
|
const MIN_OFFLINE_PEER_RETENTION_MS = 5 * 60 * 1000;
|
|
17
|
+
const MIN_LEASE_TTL_MS = 30_000;
|
|
15
18
|
|
|
16
19
|
function registryRoot(): { path: string; kind: "user_runtime" | "env_override" } {
|
|
17
20
|
const override = process.env.ZOB_COMS_REGISTRY_ROOT;
|
|
@@ -31,6 +34,12 @@ function projectAgentsDir(repoRoot: string): { dir: string; projectId: string; k
|
|
|
31
34
|
return { dir: join(root.path, "projects", projectId, "agents"), projectId, kind: root.kind };
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
function projectLeasesDir(repoRoot: string): { dir: string; projectId: string; kind: "user_runtime" | "env_override" } {
|
|
38
|
+
const root = registryRoot();
|
|
39
|
+
const projectId = buildZobComsProjectId(repoRoot);
|
|
40
|
+
return { dir: join(root.path, "projects", projectId, "leases"), projectId, kind: root.kind };
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function peerPath(repoRoot: string, peer: Pick<ZobLivePeerCard, "roleId" | "sessionHash">): string {
|
|
35
44
|
const { dir } = projectAgentsDir(repoRoot);
|
|
36
45
|
return join(dir, `${safeFileStem(`${peer.roleId}-${peer.sessionHash.slice(0, 12)}`)}.json`);
|
|
@@ -40,6 +49,22 @@ function peerPathForProjectId(projectId: string, peer: Pick<ZobLivePeerCard, "ro
|
|
|
40
49
|
return join(registryRoot().path, "projects", safeFileStem(projectId), "agents", `${safeFileStem(`${peer.roleId}-${peer.sessionHash.slice(0, 12)}`)}.json`);
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
function stableLeaseStem(input: Pick<ZobLivePeerCard, "team" | "roleId"> | Pick<ZobLiveTeamAgentLease, "teamId" | "agentId">): string {
|
|
53
|
+
const teamId = "team" in input ? input.team : input.teamId;
|
|
54
|
+
const agentId = "roleId" in input ? input.roleId : input.agentId;
|
|
55
|
+
return safeFileStem(`${teamId}-${agentId}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function leasePath(repoRoot: string, peer: Pick<ZobLivePeerCard, "team" | "roleId">): string {
|
|
59
|
+
const { dir } = projectLeasesDir(repoRoot);
|
|
60
|
+
return join(dir, `${stableLeaseStem(peer)}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function leasePathForTeamAgent(repoRoot: string, teamId: string, agentId: string): string {
|
|
64
|
+
const { dir } = projectLeasesDir(repoRoot);
|
|
65
|
+
return join(dir, `${safeFileStem(`${teamId}-${agentId}`)}.json`);
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
function readPeerCardsFromAgentsDir(dir: string, nowMs: number, teamName?: string): ZobLivePeerCard[] {
|
|
44
69
|
if (!existsSync(dir)) return [];
|
|
45
70
|
return readdirSync(dir)
|
|
@@ -56,6 +81,22 @@ function readPeerCardsFromAgentsDir(dir: string, nowMs: number, teamName?: strin
|
|
|
56
81
|
.map((peer) => ({ ...peer, status: derivePeerStatus(peer, nowMs) }));
|
|
57
82
|
}
|
|
58
83
|
|
|
84
|
+
function readLeasesFromDir(dir: string, nowMs: number, teamName?: string): ZobLivePeerCard[] {
|
|
85
|
+
if (!existsSync(dir)) return [];
|
|
86
|
+
return readdirSync(dir)
|
|
87
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
88
|
+
.map((entry) => {
|
|
89
|
+
try {
|
|
90
|
+
return parseTeamAgentLease(JSON.parse(readFileSync(join(dir, entry), "utf8")) as unknown);
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.filter((lease): lease is ZobLiveTeamAgentLease => Boolean(lease))
|
|
96
|
+
.filter((lease) => !teamName || lease.teamId === teamName)
|
|
97
|
+
.map((lease) => leaseToPeerCard(lease, nowMs));
|
|
98
|
+
}
|
|
99
|
+
|
|
59
100
|
function boundedOfflinePeerRetentionMs(value: number | undefined): number {
|
|
60
101
|
const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
|
|
61
102
|
const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
|
|
@@ -69,6 +110,12 @@ function offlinePeerExpired(peer: ZobLivePeerCard, nowMs: number, retentionMs: n
|
|
|
69
110
|
return nowMs - heartbeatMs >= Math.max(peer.offlineAfterMs, retentionMs);
|
|
70
111
|
}
|
|
71
112
|
|
|
113
|
+
function leaseExpired(lease: ZobLiveTeamAgentLease, nowMs: number, retentionMs = 0): boolean {
|
|
114
|
+
const expiresMs = Date.parse(lease.expiresAt);
|
|
115
|
+
if (!Number.isFinite(expiresMs)) return true;
|
|
116
|
+
return nowMs >= expiresMs + Math.max(0, retentionMs);
|
|
117
|
+
}
|
|
118
|
+
|
|
72
119
|
function allProjectAgentsDirs(): string[] {
|
|
73
120
|
const root = registryRoot();
|
|
74
121
|
const projectsDir = join(root.path, "projects");
|
|
@@ -78,6 +125,15 @@ function allProjectAgentsDirs(): string[] {
|
|
|
78
125
|
.map((entry) => join(projectsDir, entry.name, "agents"));
|
|
79
126
|
}
|
|
80
127
|
|
|
128
|
+
function allProjectLeasesDirs(): string[] {
|
|
129
|
+
const root = registryRoot();
|
|
130
|
+
const projectsDir = join(root.path, "projects");
|
|
131
|
+
if (!existsSync(projectsDir)) return [];
|
|
132
|
+
return readdirSync(projectsDir, { withFileTypes: true })
|
|
133
|
+
.filter((entry) => entry.isDirectory())
|
|
134
|
+
.map((entry) => join(projectsDir, entry.name, "leases"));
|
|
135
|
+
}
|
|
136
|
+
|
|
81
137
|
function buildSnapshot(projectId: string, kind: "user_runtime" | "env_override", peers: ZobLivePeerCard[], teamName?: string): ZobLiveRegistrySnapshot {
|
|
82
138
|
const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
|
|
83
139
|
for (const peer of peers) counts[peer.status] += 1;
|
|
@@ -100,6 +156,15 @@ function parsePeerCard(value: unknown): ZobLivePeerCard | undefined {
|
|
|
100
156
|
return value as unknown as ZobLivePeerCard;
|
|
101
157
|
}
|
|
102
158
|
|
|
159
|
+
function parseTeamAgentLease(value: unknown): ZobLiveTeamAgentLease | undefined {
|
|
160
|
+
if (!isRecord(value) || value.schema !== "zob.live-team-agent-lease.v1") return undefined;
|
|
161
|
+
if (hasForbiddenPersistedKey(value) || value.bodyStored !== false || value.localOnly !== true || value.networkEnabled !== false) return undefined;
|
|
162
|
+
if (value.stableLease !== true || value.exclusiveBy !== "teamId+agentId") return undefined;
|
|
163
|
+
if (typeof value.teamId !== "string" || typeof value.agentId !== "string" || typeof value.sessionHash !== "string") return undefined;
|
|
164
|
+
if (typeof value.leaseOwnerId !== "string" || typeof value.endpoint !== "string" || typeof value.endpointHash !== "string") return undefined;
|
|
165
|
+
return value as unknown as ZobLiveTeamAgentLease;
|
|
166
|
+
}
|
|
167
|
+
|
|
103
168
|
function derivePeerStatus(peer: ZobLivePeerCard, nowMs: number): ZobLivePeerStatus {
|
|
104
169
|
if (peer.status === "offline") return "offline";
|
|
105
170
|
const heartbeatMs = Date.parse(peer.heartbeatAt);
|
|
@@ -109,32 +174,206 @@ function derivePeerStatus(peer: ZobLivePeerCard, nowMs: number): ZobLivePeerStat
|
|
|
109
174
|
return "online";
|
|
110
175
|
}
|
|
111
176
|
|
|
177
|
+
function deriveLeaseStatus(lease: ZobLiveTeamAgentLease, nowMs: number): ZobLivePeerStatus {
|
|
178
|
+
if (lease.status === "offline" || leaseExpired(lease, nowMs)) return "offline";
|
|
179
|
+
const heartbeatMs = Date.parse(lease.heartbeatAt);
|
|
180
|
+
if (!Number.isFinite(heartbeatMs)) return "stale";
|
|
181
|
+
if (nowMs - heartbeatMs >= lease.offlineAfterMs) return "offline";
|
|
182
|
+
if (nowMs - heartbeatMs >= lease.staleAfterMs) return "stale";
|
|
183
|
+
return "online";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function leaseOwnerId(peer: Pick<ZobLivePeerCard, "team" | "roleId" | "sessionHash" | "endpointHash">): string {
|
|
187
|
+
return `${peer.team}:${peer.roleId}:${peer.sessionHash}:${peer.endpointHash}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sameLeaseOwner(lease: ZobLiveTeamAgentLease, peer: Pick<ZobLivePeerCard, "team" | "roleId" | "sessionHash" | "endpointHash">): boolean {
|
|
191
|
+
return lease.teamId === peer.team
|
|
192
|
+
&& lease.agentId === peer.roleId
|
|
193
|
+
&& lease.sessionHash === peer.sessionHash
|
|
194
|
+
&& lease.endpointHash === peer.endpointHash
|
|
195
|
+
&& lease.leaseOwnerId === leaseOwnerId(peer);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function leaseExpiresAt(peer: Pick<ZobLivePeerCard, "offlineAfterMs" | "staleAfterMs">, nowMs: number): string {
|
|
199
|
+
const ttlMs = Math.max(MIN_LEASE_TTL_MS, Math.floor(Math.max(peer.offlineAfterMs, peer.staleAfterMs * 2)));
|
|
200
|
+
return new Date(nowMs + ttlMs).toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { nowMs?: number } = {}): ZobLiveTeamAgentLease {
|
|
204
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
205
|
+
const now = new Date(nowMs).toISOString();
|
|
206
|
+
const projectId = buildZobComsProjectId(repoRoot);
|
|
207
|
+
const ownerId = leaseOwnerId(peer);
|
|
208
|
+
return {
|
|
209
|
+
schema: "zob.live-team-agent-lease.v1",
|
|
210
|
+
projectId,
|
|
211
|
+
teamId: peer.team,
|
|
212
|
+
agentId: peer.roleId,
|
|
213
|
+
roleId: peer.roleId,
|
|
214
|
+
roleType: peer.roleType,
|
|
215
|
+
leadId: peer.leadId,
|
|
216
|
+
agent: peer.agent,
|
|
217
|
+
sessionId: peer.sessionId,
|
|
218
|
+
sessionHash: peer.sessionHash,
|
|
219
|
+
leaseOwnerId: ownerId,
|
|
220
|
+
leaseOwnerHash: sha256(ownerId),
|
|
221
|
+
transport: peer.transport,
|
|
222
|
+
endpoint: peer.endpoint,
|
|
223
|
+
endpointHash: peer.endpointHash,
|
|
224
|
+
cwdHash: peer.cwdHash,
|
|
225
|
+
pid: peer.pid,
|
|
226
|
+
startedAt: peer.startedAt,
|
|
227
|
+
heartbeatAt: now,
|
|
228
|
+
leasedAt: now,
|
|
229
|
+
renewedAt: now,
|
|
230
|
+
expiresAt: leaseExpiresAt(peer, nowMs),
|
|
231
|
+
contextUsedPct: peer.contextUsedPct,
|
|
232
|
+
queueDepth: peer.queueDepth,
|
|
233
|
+
status: peer.status === "offline" ? "offline" : "online",
|
|
234
|
+
zpeerRoomId: peer.zpeerRoomId,
|
|
235
|
+
zpeerAlias: peer.zpeerAlias,
|
|
236
|
+
zpeerActiveRoomId: peer.zpeerActiveRoomId,
|
|
237
|
+
zpeerMemberships: peer.zpeerMemberships,
|
|
238
|
+
zpeerLocalOnly: peer.zpeerLocalOnly,
|
|
239
|
+
staleAfterMs: peer.staleAfterMs,
|
|
240
|
+
offlineAfterMs: peer.offlineAfterMs,
|
|
241
|
+
stableLease: true,
|
|
242
|
+
exclusiveBy: "teamId+agentId",
|
|
243
|
+
localOnly: true,
|
|
244
|
+
networkEnabled: false,
|
|
245
|
+
bodyStored: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renewTeamAgentLease(repoRoot: string, existing: ZobLiveTeamAgentLease | undefined, peer: ZobLivePeerCard, input: { nowMs?: number } = {}): ZobLiveTeamAgentLease {
|
|
250
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
251
|
+
const now = new Date(nowMs).toISOString();
|
|
252
|
+
const next = buildTeamAgentLease(repoRoot, peer, { nowMs });
|
|
253
|
+
return existing && sameLeaseOwner(existing, peer)
|
|
254
|
+
? { ...next, leasedAt: existing.leasedAt, renewedAt: now }
|
|
255
|
+
: next;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function leaseToPeerCard(lease: ZobLiveTeamAgentLease, nowMs: number): ZobLivePeerCard {
|
|
259
|
+
return {
|
|
260
|
+
schema: "zob.live-peer-card.v1",
|
|
261
|
+
projectId: lease.projectId,
|
|
262
|
+
team: lease.teamId,
|
|
263
|
+
roleId: lease.roleId,
|
|
264
|
+
roleType: lease.roleType,
|
|
265
|
+
leadId: lease.leadId,
|
|
266
|
+
agent: lease.agent,
|
|
267
|
+
sessionId: lease.sessionId,
|
|
268
|
+
sessionHash: lease.sessionHash,
|
|
269
|
+
transport: lease.transport,
|
|
270
|
+
endpoint: lease.endpoint,
|
|
271
|
+
endpointHash: lease.endpointHash,
|
|
272
|
+
cwdHash: lease.cwdHash,
|
|
273
|
+
pid: lease.pid,
|
|
274
|
+
startedAt: lease.startedAt,
|
|
275
|
+
heartbeatAt: lease.heartbeatAt,
|
|
276
|
+
contextUsedPct: lease.contextUsedPct,
|
|
277
|
+
queueDepth: lease.queueDepth,
|
|
278
|
+
status: deriveLeaseStatus(lease, nowMs),
|
|
279
|
+
zpeerRoomId: lease.zpeerRoomId,
|
|
280
|
+
zpeerAlias: lease.zpeerAlias,
|
|
281
|
+
zpeerActiveRoomId: lease.zpeerActiveRoomId,
|
|
282
|
+
zpeerMemberships: lease.zpeerMemberships,
|
|
283
|
+
zpeerLocalOnly: lease.zpeerLocalOnly,
|
|
284
|
+
staleAfterMs: lease.staleAfterMs,
|
|
285
|
+
offlineAfterMs: lease.offlineAfterMs,
|
|
286
|
+
bodyStored: false,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readLeaseAtPath(filePath: string): ZobLiveTeamAgentLease | undefined {
|
|
291
|
+
if (!existsSync(filePath)) return undefined;
|
|
292
|
+
try {
|
|
293
|
+
return parseTeamAgentLease(JSON.parse(readFileSync(filePath, "utf8")) as unknown);
|
|
294
|
+
} catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function writeLeaseAtPath(filePath: string, lease: ZobLiveTeamAgentLease): ZobLiveTeamAgentLease {
|
|
300
|
+
if (hasForbiddenPersistedKey(lease)) throw new Error("Refusing to persist ZOB live team-agent lease with forbidden body-like keys");
|
|
301
|
+
if (lease.bodyStored !== false || lease.localOnly !== true || lease.networkEnabled !== false) throw new Error("ZOB live team-agent lease must be localOnly=true networkEnabled=false bodyStored=false");
|
|
302
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
303
|
+
writeFileSync(filePath, `${JSON.stringify(lease, null, 2)}\n`, "utf8");
|
|
304
|
+
return lease;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function leaseHasLocalSocketEndpoint(lease: ZobLiveTeamAgentLease): boolean {
|
|
308
|
+
return lease.transport === "local_socket"
|
|
309
|
+
&& Boolean(lease.endpoint)
|
|
310
|
+
&& !lease.endpoint.startsWith("pending-")
|
|
311
|
+
&& lease.endpoint !== "observe-only"
|
|
312
|
+
&& existsSync(lease.endpoint);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function leaseRespondsToPing(lease: ZobLiveTeamAgentLease): Promise<boolean> {
|
|
316
|
+
if (!leaseHasLocalSocketEndpoint(lease)) return false;
|
|
317
|
+
try {
|
|
318
|
+
const response = await pingZobLocalEndpoint(lease.endpoint, `zteam-agent-lease-ping:${lease.teamId}:${lease.agentId}:${Date.now()}`);
|
|
319
|
+
return response.type === "pong" || response.type === "ack";
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
112
325
|
export function pruneExpiredZobLivePeers(repoRoot: string, input: { teamName?: string; nowMs?: number; retentionMs?: number } = {}): { schema: "zob.live-registry-prune.v1"; pruned: number; retained: number; retentionMs: number; bodyStored: false } {
|
|
113
326
|
const { dir } = projectAgentsDir(repoRoot);
|
|
114
327
|
const nowMs = input.nowMs ?? Date.now();
|
|
115
328
|
const retentionMs = boundedOfflinePeerRetentionMs(input.retentionMs);
|
|
116
329
|
let pruned = 0;
|
|
117
330
|
let retained = 0;
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
331
|
+
if (existsSync(dir)) {
|
|
332
|
+
for (const entry of readdirSync(dir).filter((name) => name.endsWith(".json"))) {
|
|
333
|
+
const filePath = join(dir, entry);
|
|
334
|
+
try {
|
|
335
|
+
const peer = parsePeerCard(JSON.parse(readFileSync(filePath, "utf8")) as unknown);
|
|
336
|
+
if (!peer || (input.teamName && peer.team !== input.teamName)) {
|
|
337
|
+
retained += 1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (offlinePeerExpired(peer, nowMs, retentionMs)) {
|
|
341
|
+
unlinkSync(filePath);
|
|
342
|
+
pruned += 1;
|
|
343
|
+
} else {
|
|
344
|
+
retained += 1;
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
131
347
|
retained += 1;
|
|
132
348
|
}
|
|
133
|
-
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const leasePrune = pruneExpiredZobLiveTeamAgentLeases(repoRoot, input);
|
|
352
|
+
return { schema: "zob.live-registry-prune.v1", pruned: pruned + leasePrune.pruned, retained: retained + leasePrune.retained, retentionMs, bodyStored: false };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function pruneExpiredZobLiveTeamAgentLeases(repoRoot: string, input: { teamName?: string; nowMs?: number; retentionMs?: number } = {}): { schema: "zob.live-team-agent-lease-prune.v1"; pruned: number; retained: number; retentionMs: number; bodyStored: false } {
|
|
356
|
+
const { dir } = projectLeasesDir(repoRoot);
|
|
357
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
358
|
+
const retentionMs = boundedOfflinePeerRetentionMs(input.retentionMs);
|
|
359
|
+
let pruned = 0;
|
|
360
|
+
let retained = 0;
|
|
361
|
+
if (!existsSync(dir)) return { schema: "zob.live-team-agent-lease-prune.v1", pruned, retained, retentionMs, bodyStored: false };
|
|
362
|
+
for (const entry of readdirSync(dir).filter((name) => name.endsWith(".json"))) {
|
|
363
|
+
const filePath = join(dir, entry);
|
|
364
|
+
const lease = readLeaseAtPath(filePath);
|
|
365
|
+
if (!lease || (input.teamName && lease.teamId !== input.teamName)) {
|
|
366
|
+
retained += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (leaseExpired(lease, nowMs, retentionMs)) {
|
|
370
|
+
unlinkSync(filePath);
|
|
371
|
+
pruned += 1;
|
|
372
|
+
} else {
|
|
134
373
|
retained += 1;
|
|
135
374
|
}
|
|
136
375
|
}
|
|
137
|
-
return { schema: "zob.live-
|
|
376
|
+
return { schema: "zob.live-team-agent-lease-prune.v1", pruned, retained, retentionMs, bodyStored: false };
|
|
138
377
|
}
|
|
139
378
|
|
|
140
379
|
export function writeZobLivePeerCard(repoRoot: string, peer: ZobLivePeerCard): ZobLivePeerCard {
|
|
@@ -142,7 +381,7 @@ export function writeZobLivePeerCard(repoRoot: string, peer: ZobLivePeerCard): Z
|
|
|
142
381
|
if (peer.bodyStored !== false) throw new Error("ZOB live peer card bodyStored must be false");
|
|
143
382
|
const { dir } = projectAgentsDir(repoRoot);
|
|
144
383
|
mkdirSync(dir, { recursive: true });
|
|
145
|
-
writeFileSync(peerPath(repoRoot, peer), `${JSON.stringify(peer, null, 2)}\n`, "utf8");
|
|
384
|
+
writeFileSync(peerPath(repoRoot, peer), `${JSON.stringify({ ...peer, projectId: buildZobComsProjectId(repoRoot) }, null, 2)}\n`, "utf8");
|
|
146
385
|
return peer;
|
|
147
386
|
}
|
|
148
387
|
|
|
@@ -155,18 +394,112 @@ export function writeZobLivePeerCardToProjectId(peer: ZobLivePeerCard): ZobLiveP
|
|
|
155
394
|
return peer;
|
|
156
395
|
}
|
|
157
396
|
|
|
397
|
+
export function writeZobLiveTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { reason?: string; nowMs?: number } = {}): ZobLiveTeamAgentLease {
|
|
398
|
+
const filePath = leasePath(repoRoot, peer);
|
|
399
|
+
const existing = readLeaseAtPath(filePath);
|
|
400
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
401
|
+
if (existing && !sameLeaseOwner(existing, peer) && !leaseExpired(existing, nowMs)) return existing;
|
|
402
|
+
const ownedExisting = existing && sameLeaseOwner(existing, peer) ? existing : undefined;
|
|
403
|
+
return writeLeaseAtPath(filePath, renewTeamAgentLease(repoRoot, ownedExisting, peer, { nowMs }));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function claimZobLiveTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { reason?: string; nowMs?: number } = {}): Promise<
|
|
407
|
+
| { ok: true; status: "acquired" | "renewed" | "reclaimed"; lease: ZobLiveTeamAgentLease; previousOwnerHash?: string; pingChecked: boolean; bodyStored: false }
|
|
408
|
+
| { ok: false; status: "blocked_live_owner"; lease: ZobLiveTeamAgentLease; ownerHash: string; reason: string; pingChecked: true; bodyStored: false }
|
|
409
|
+
> {
|
|
410
|
+
const filePath = leasePath(repoRoot, peer);
|
|
411
|
+
const existing = readLeaseAtPath(filePath);
|
|
412
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
413
|
+
if (!existing) {
|
|
414
|
+
const lease = writeLeaseAtPath(filePath, buildTeamAgentLease(repoRoot, peer, { nowMs }));
|
|
415
|
+
return { ok: true, status: "acquired", lease, pingChecked: false, bodyStored: false };
|
|
416
|
+
}
|
|
417
|
+
if (sameLeaseOwner(existing, peer)) {
|
|
418
|
+
const lease = writeLeaseAtPath(filePath, renewTeamAgentLease(repoRoot, existing, peer, { nowMs }));
|
|
419
|
+
return { ok: true, status: "renewed", lease, pingChecked: false, bodyStored: false };
|
|
420
|
+
}
|
|
421
|
+
const responsive = await leaseRespondsToPing(existing);
|
|
422
|
+
if (responsive) {
|
|
423
|
+
return {
|
|
424
|
+
ok: false,
|
|
425
|
+
status: "blocked_live_owner",
|
|
426
|
+
lease: existing,
|
|
427
|
+
ownerHash: existing.leaseOwnerHash,
|
|
428
|
+
reason: `stable team-agent lease for ${existing.teamId}/${existing.agentId} is held by a responsive live endpoint`,
|
|
429
|
+
pingChecked: true,
|
|
430
|
+
bodyStored: false,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const lease = writeLeaseAtPath(filePath, buildTeamAgentLease(repoRoot, peer, { nowMs }));
|
|
434
|
+
return { ok: true, status: "reclaimed", lease, previousOwnerHash: existing.leaseOwnerHash, pingChecked: true, bodyStored: false };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function releaseZobLiveTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { reason?: string } = {}): { schema: "zob.live-team-agent-lease-release.v1"; released: boolean; reason: "released" | "not_found" | "owner_mismatch"; teamId: string; agentId: string; leaseOwnerHash?: string; bodyStored: false } {
|
|
438
|
+
void input;
|
|
439
|
+
const filePath = leasePath(repoRoot, peer);
|
|
440
|
+
const existing = readLeaseAtPath(filePath);
|
|
441
|
+
if (!existing) return { schema: "zob.live-team-agent-lease-release.v1", released: false, reason: "not_found", teamId: peer.team, agentId: peer.roleId, bodyStored: false };
|
|
442
|
+
if (!sameLeaseOwner(existing, peer)) return { schema: "zob.live-team-agent-lease-release.v1", released: false, reason: "owner_mismatch", teamId: peer.team, agentId: peer.roleId, leaseOwnerHash: existing.leaseOwnerHash, bodyStored: false };
|
|
443
|
+
unlinkSync(filePath);
|
|
444
|
+
return { schema: "zob.live-team-agent-lease-release.v1", released: true, reason: "released", teamId: peer.team, agentId: peer.roleId, leaseOwnerHash: existing.leaseOwnerHash, bodyStored: false };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function ownsZobLiveTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard): { schema: "zob.live-team-agent-lease-ownership.v1"; owned: boolean; reason: "owned" | "not_found" | "owner_mismatch" | "expired"; teamId: string; agentId: string; leaseOwnerHash?: string; bodyStored: false } {
|
|
448
|
+
const filePath = leasePath(repoRoot, peer);
|
|
449
|
+
const existing = readLeaseAtPath(filePath);
|
|
450
|
+
if (!existing) return { schema: "zob.live-team-agent-lease-ownership.v1", owned: false, reason: "not_found", teamId: peer.team, agentId: peer.roleId, bodyStored: false };
|
|
451
|
+
if (leaseExpired(existing, Date.now())) return { schema: "zob.live-team-agent-lease-ownership.v1", owned: false, reason: "expired", teamId: peer.team, agentId: peer.roleId, leaseOwnerHash: existing.leaseOwnerHash, bodyStored: false };
|
|
452
|
+
if (!sameLeaseOwner(existing, peer)) return { schema: "zob.live-team-agent-lease-ownership.v1", owned: false, reason: "owner_mismatch", teamId: peer.team, agentId: peer.roleId, leaseOwnerHash: existing.leaseOwnerHash, bodyStored: false };
|
|
453
|
+
return { schema: "zob.live-team-agent-lease-ownership.v1", owned: true, reason: "owned", teamId: peer.team, agentId: peer.roleId, leaseOwnerHash: existing.leaseOwnerHash, bodyStored: false };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function retireInactiveZobLiveTeamAgentLeases(repoRoot: string, input: { teamName: string; agentIds: string[]; nowMs?: number }): Promise<{ schema: "zob.live-team-agent-lease-retire.v1"; teamName: string; checked: number; retired: number; retainedLive: number; missing: number; errorHashes: string[]; bodyStored: false }> {
|
|
457
|
+
const errors: string[] = [];
|
|
458
|
+
let checked = 0;
|
|
459
|
+
let retired = 0;
|
|
460
|
+
let retainedLive = 0;
|
|
461
|
+
let missing = 0;
|
|
462
|
+
const uniqueAgentIds = [...new Set(input.agentIds.filter((agentId) => agentId.trim().length > 0))];
|
|
463
|
+
for (const agentId of uniqueAgentIds) {
|
|
464
|
+
const filePath = leasePathForTeamAgent(repoRoot, input.teamName, agentId);
|
|
465
|
+
const lease = readLeaseAtPath(filePath);
|
|
466
|
+
if (!lease) {
|
|
467
|
+
missing += 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
checked += 1;
|
|
471
|
+
try {
|
|
472
|
+
const live = await leaseRespondsToPing(lease);
|
|
473
|
+
if (live) {
|
|
474
|
+
retainedLive += 1;
|
|
475
|
+
} else {
|
|
476
|
+
unlinkSync(filePath);
|
|
477
|
+
retired += 1;
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { schema: "zob.live-team-agent-lease-retire.v1", teamName: input.teamName, checked, retired, retainedLive, missing, errorHashes: errors.map((error) => sha256(error)), bodyStored: false };
|
|
484
|
+
}
|
|
485
|
+
|
|
158
486
|
export function registerCurrentZobLivePeer(repoRoot: string, teamName = "zob-core"): ZobLivePeerCard | undefined {
|
|
159
487
|
const policy = readZobComsV2Policy(repoRoot);
|
|
160
488
|
if (!zobComsRegistryEnabled(policy)) return undefined;
|
|
161
489
|
const team = loadTeamDefinition(repoRoot, teamName);
|
|
162
490
|
const errors = [...team.errors, ...validateTeamDefinition(repoRoot, team.definition)];
|
|
163
491
|
if (errors.length > 0 || !team.definition) throw new Error(`Cannot register ZOB live peer: ${errors.join("; ")}`);
|
|
164
|
-
|
|
492
|
+
const peer = writeZobLivePeerCard(repoRoot, buildCurrentZobLivePeerCard(repoRoot, team.definition, policy));
|
|
493
|
+
writeZobLiveTeamAgentLease(repoRoot, peer, { reason: "register_current" });
|
|
494
|
+
return peer;
|
|
165
495
|
}
|
|
166
496
|
|
|
167
497
|
export function touchCurrentZobLivePeer(repoRoot: string, teamName = "zob-core"): ZobLivePeerCard | undefined {
|
|
168
498
|
const peer = registerCurrentZobLivePeer(repoRoot, teamName);
|
|
169
|
-
|
|
499
|
+
if (!peer) return undefined;
|
|
500
|
+
const touched = writeZobLivePeerCard(repoRoot, { ...peer, heartbeatAt: new Date().toISOString(), status: "online" });
|
|
501
|
+
writeZobLiveTeamAgentLease(repoRoot, touched, { reason: "touch_current" });
|
|
502
|
+
return touched;
|
|
170
503
|
}
|
|
171
504
|
|
|
172
505
|
export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-core"): ZobLivePeerCard | undefined {
|
|
@@ -174,17 +507,25 @@ export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-c
|
|
|
174
507
|
const team = loadTeamDefinition(repoRoot, teamName);
|
|
175
508
|
if (!team.definition || !zobComsRegistryEnabled(policy)) return undefined;
|
|
176
509
|
const peer = buildCurrentZobLivePeerCard(repoRoot, team.definition, policy);
|
|
510
|
+
releaseZobLiveTeamAgentLease(repoRoot, peer, { reason: "unregister_current" });
|
|
177
511
|
return writeZobLivePeerCard(repoRoot, { ...peer, heartbeatAt: new Date().toISOString(), status: "offline" });
|
|
178
512
|
}
|
|
179
513
|
|
|
180
514
|
export function readZobLiveRegistrySnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
|
|
181
|
-
const { dir, projectId, kind } =
|
|
182
|
-
|
|
515
|
+
const { dir, projectId, kind } = projectLeasesDir(repoRoot);
|
|
516
|
+
const nowMs = Date.now();
|
|
517
|
+
if (existsSync(dir)) return buildSnapshot(projectId, kind, readLeasesFromDir(dir, nowMs, teamName), teamName);
|
|
518
|
+
const agents = projectAgentsDir(repoRoot);
|
|
519
|
+
return buildSnapshot(agents.projectId, agents.kind, readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName), teamName);
|
|
183
520
|
}
|
|
184
521
|
|
|
185
522
|
export function readZobLiveRegistryAllProjectsSnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
|
|
186
523
|
const { projectId, kind } = projectAgentsDir(repoRoot);
|
|
187
524
|
const nowMs = Date.now();
|
|
188
|
-
const
|
|
525
|
+
const leaseDirs = allProjectLeasesDirs();
|
|
526
|
+
const hasLeaseDomain = leaseDirs.some((dir) => existsSync(dir));
|
|
527
|
+
const peers = hasLeaseDomain
|
|
528
|
+
? leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName))
|
|
529
|
+
: allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
|
|
189
530
|
return buildSnapshot(projectId, kind, peers, teamName);
|
|
190
531
|
}
|
|
@@ -114,6 +114,46 @@ export interface ZobLivePeerCard {
|
|
|
114
114
|
bodyStored: false;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export interface ZobLiveTeamAgentLease {
|
|
118
|
+
schema: "zob.live-team-agent-lease.v1";
|
|
119
|
+
projectId: string;
|
|
120
|
+
teamId: string;
|
|
121
|
+
agentId: string;
|
|
122
|
+
roleId: string;
|
|
123
|
+
roleType: ZobLiveRoleType;
|
|
124
|
+
leadId?: string;
|
|
125
|
+
agent: string;
|
|
126
|
+
sessionId: string;
|
|
127
|
+
sessionHash: string;
|
|
128
|
+
leaseOwnerId: string;
|
|
129
|
+
leaseOwnerHash: string;
|
|
130
|
+
transport: ZobLiveTransportKind;
|
|
131
|
+
endpoint: string;
|
|
132
|
+
endpointHash: string;
|
|
133
|
+
cwdHash: string;
|
|
134
|
+
pid?: number;
|
|
135
|
+
startedAt: string;
|
|
136
|
+
heartbeatAt: string;
|
|
137
|
+
leasedAt: string;
|
|
138
|
+
renewedAt: string;
|
|
139
|
+
expiresAt: string;
|
|
140
|
+
contextUsedPct: number;
|
|
141
|
+
queueDepth: number;
|
|
142
|
+
status: ZobLivePeerStatus;
|
|
143
|
+
zpeerRoomId?: string;
|
|
144
|
+
zpeerAlias?: string;
|
|
145
|
+
zpeerActiveRoomId?: string;
|
|
146
|
+
zpeerMemberships?: ZpeerRoomMembership[];
|
|
147
|
+
zpeerLocalOnly?: true;
|
|
148
|
+
staleAfterMs: number;
|
|
149
|
+
offlineAfterMs: number;
|
|
150
|
+
stableLease: true;
|
|
151
|
+
exclusiveBy: "teamId+agentId";
|
|
152
|
+
localOnly: true;
|
|
153
|
+
networkEnabled: false;
|
|
154
|
+
bodyStored: false;
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
export interface ZobLiveRegistrySnapshot {
|
|
118
158
|
schema: "zob.live-registry-snapshot.v1";
|
|
119
159
|
projectId: string;
|