pi-teams 0.8.7 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { withLock } from "./lock";
4
+ import { runtimeStatusPath, teamDir } from "./paths";
5
+
6
+ /**
7
+ * Runtime constants for health checking.
8
+ * Exported for configurability and testing.
9
+ */
10
+ export const HEARTBEAT_STALE_MS = 90000; // 90 seconds
11
+ export const STARTUP_STALL_MS = 60000; // 60 seconds
12
+ export const RUNTIME_STALE_MS = 300000; // 5 minutes - files older than this are considered stale
13
+
14
+ /**
15
+ * Structured error information for better diagnostics.
16
+ */
17
+ export interface RuntimeError {
18
+ message: string;
19
+ timestamp: number;
20
+ }
21
+
22
+ export interface AgentRuntimeStatus {
23
+ teamName: string;
24
+ agentName: string;
25
+ pid?: number;
26
+ startedAt?: number;
27
+ lastHeartbeatAt?: number;
28
+ lastInboxReadAt?: number;
29
+ ready?: boolean;
30
+ lastError?: RuntimeError;
31
+ }
32
+
33
+ /**
34
+ * Write runtime status for an agent. Merges with existing status.
35
+ */
36
+ export async function writeRuntimeStatus(
37
+ teamName: string,
38
+ agentName: string,
39
+ updates: Partial<AgentRuntimeStatus>
40
+ ): Promise<AgentRuntimeStatus> {
41
+ const p = runtimeStatusPath(teamName, agentName);
42
+ const dir = path.dirname(p);
43
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
44
+
45
+ return await withLock(p, async () => {
46
+ let current: AgentRuntimeStatus = {
47
+ teamName,
48
+ agentName,
49
+ };
50
+
51
+ if (fs.existsSync(p)) {
52
+ try {
53
+ current = JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
54
+ } catch {
55
+ // Corrupted file, start fresh
56
+ current = { teamName, agentName };
57
+ }
58
+ }
59
+
60
+ const next: AgentRuntimeStatus = {
61
+ ...current,
62
+ ...updates,
63
+ teamName,
64
+ agentName,
65
+ };
66
+
67
+ fs.writeFileSync(p, JSON.stringify(next, null, 2));
68
+ return next;
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Read runtime status for an agent. Returns null if not found.
74
+ */
75
+ export async function readRuntimeStatus(
76
+ teamName: string,
77
+ agentName: string
78
+ ): Promise<AgentRuntimeStatus | null> {
79
+ const p = runtimeStatusPath(teamName, agentName);
80
+ if (!fs.existsSync(p)) return null;
81
+
82
+ return await withLock(p, async () => {
83
+ if (!fs.existsSync(p)) return null;
84
+ try {
85
+ return JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
86
+ } catch {
87
+ // Corrupted file
88
+ return null;
89
+ }
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Delete runtime status for an agent. Called during shutdown.
95
+ */
96
+ export async function deleteRuntimeStatus(
97
+ teamName: string,
98
+ agentName: string
99
+ ): Promise<boolean> {
100
+ const p = runtimeStatusPath(teamName, agentName);
101
+ if (!fs.existsSync(p)) return false;
102
+
103
+ return await withLock(p, async () => {
104
+ if (!fs.existsSync(p)) return false;
105
+ try {
106
+ fs.unlinkSync(p);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Clean up stale runtime files for a team.
116
+ * Removes files older than RUNTIME_STALE_MS that have no recent heartbeat.
117
+ * Returns the number of files cleaned up.
118
+ */
119
+ export async function cleanupStaleRuntimeFiles(
120
+ teamName: string,
121
+ now: number = Date.now()
122
+ ): Promise<number> {
123
+ const runtimeDir = path.join(teamDir(teamName), "runtime");
124
+ if (!fs.existsSync(runtimeDir)) return 0;
125
+
126
+ let cleaned = 0;
127
+ const files = fs.readdirSync(runtimeDir).filter(f => f.endsWith(".json"));
128
+
129
+ for (const file of files) {
130
+ const p = path.join(runtimeDir, file);
131
+ try {
132
+ const status = JSON.parse(fs.readFileSync(p, "utf-8")) as AgentRuntimeStatus;
133
+
134
+ // Check if the file is stale
135
+ const lastActivity = status.lastHeartbeatAt || status.startedAt || 0;
136
+ const isStale = (now - lastActivity) > RUNTIME_STALE_MS;
137
+
138
+ if (isStale) {
139
+ await withLock(p, async () => {
140
+ if (fs.existsSync(p)) {
141
+ fs.unlinkSync(p);
142
+ cleaned++;
143
+ }
144
+ });
145
+ }
146
+ } catch {
147
+ // Corrupted file, remove it
148
+ try {
149
+ fs.unlinkSync(p);
150
+ cleaned++;
151
+ } catch {
152
+ // Ignore removal errors
153
+ }
154
+ }
155
+ }
156
+
157
+ return cleaned;
158
+ }
159
+
160
+ /**
161
+ * Create a structured error object from an error.
162
+ */
163
+ export function createRuntimeError(error: unknown): RuntimeError {
164
+ return {
165
+ message: error instanceof Error ? error.message : String(error),
166
+ timestamp: Date.now(),
167
+ };
168
+ }
@@ -21,6 +21,8 @@ export interface SpawnOptions {
21
21
  env: Record<string, string>;
22
22
  /** Team name for window title formatting (e.g., "team: agent") */
23
23
  teamName?: string;
24
+ /** Optional pane ID to anchor pane-based layouts to a specific origin pane */
25
+ anchorPaneId?: string;
24
26
  }
25
27
 
26
28
  /**