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.
- package/extensions/index.ts +83 -5
- package/package.json +1 -1
- package/src/adapters/cmux-adapter.test.ts +309 -0
- package/src/adapters/cmux-adapter.ts +5 -1
- package/src/adapters/terminal-registry.ts +13 -9
- package/src/adapters/tmux-adapter.test.ts +231 -0
- package/src/adapters/tmux-adapter.ts +70 -6
- package/src/adapters/windows-adapter.test.ts +86 -155
- package/src/utils/paths.ts +4 -0
- package/src/utils/runtime.test.ts +171 -0
- package/src/utils/runtime.ts +168 -0
- package/src/utils/terminal-adapter.ts +2 -0
|
@@ -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
|
/**
|