@wrongstack/core 0.301.0 → 0.302.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/dist/agent-status-tracker.d.ts +6 -2
- package/dist/chronicle/index.js +1836 -1645
- package/dist/chronicle/metrics-store.d.ts +14 -0
- package/dist/chronicle/project-server-protocol.d.ts +13 -0
- package/dist/chronicle/project-server.js +1759 -1583
- package/dist/chronicle/rollup-adapter.d.ts +2 -0
- package/dist/chronicle/sqlite-journal.d.ts +59 -0
- package/dist/coordination/index.js +791 -249
- package/dist/coordination/mail-tools.d.ts +2 -2
- package/dist/core/continue-intent.d.ts +2 -0
- package/dist/core/conversation-state.d.ts +5 -0
- package/dist/core/index.js +120 -19
- package/dist/defaults/index.js +928 -374
- package/dist/execution/index.js +28 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8763 -6781
- package/dist/infrastructure/index.js +722 -672
- package/dist/kernel/events/memory-events.d.ts +62 -0
- package/dist/plugin/index.js +2154 -1979
- package/dist/session-catalog/client.d.ts +62 -0
- package/dist/session-catalog/endpoint.d.ts +6 -0
- package/dist/session-catalog/index.d.ts +6 -0
- package/dist/session-catalog/index.js +1978 -0
- package/dist/session-catalog/project-server.d.ts +3 -0
- package/dist/session-catalog/project-server.js +1838 -0
- package/dist/session-catalog/protocol.d.ts +275 -0
- package/dist/session-catalog/registry.d.ts +59 -0
- package/dist/session-catalog/store.d.ts +55 -0
- package/dist/session-registry-types.d.ts +17 -0
- package/dist/session-registry.d.ts +1 -1
- package/dist/storage/index.d.ts +42 -38
- package/dist/storage/index.js +14279 -13393
- package/dist/storage/session-event-bridge.d.ts +2 -2
- package/dist/storage/session-store.d.ts +6 -0
- package/dist/tools/index.js +8 -2
- package/dist/types/context-evidence.d.ts +2 -0
- package/dist/types/messages.d.ts +8 -0
- package/dist/types/session.d.ts +19 -0
- package/dist/utils/context-evidence.d.ts +13 -1
- package/dist/utils/index.js +26 -2
- package/instructions/system-lite.md +11 -2
- package/instructions/system-pro.md +14 -0
- package/instructions/system.md +14 -0
- package/package.json +7 -3
|
@@ -13,703 +13,753 @@ var __export = (target, all) => {
|
|
|
13
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
// src/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
process.kill(pid, 0);
|
|
22
|
-
return true;
|
|
23
|
-
} catch (err) {
|
|
24
|
-
const code = err.code;
|
|
25
|
-
if (code === "EPERM") return true;
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
16
|
+
// src/session-catalog/protocol.ts
|
|
17
|
+
function encodeSessionCatalogMessage(message) {
|
|
18
|
+
return `${JSON.stringify(message)}
|
|
19
|
+
`;
|
|
28
20
|
}
|
|
29
|
-
var
|
|
30
|
-
|
|
21
|
+
var SESSION_CATALOG_PROTOCOL_VERSION, SESSION_CATALOG_MAX_FRAME_CHARS;
|
|
22
|
+
var init_protocol = __esm({
|
|
23
|
+
"src/session-catalog/protocol.ts"() {
|
|
31
24
|
"use strict";
|
|
25
|
+
SESSION_CATALOG_PROTOCOL_VERSION = 1;
|
|
26
|
+
SESSION_CATALOG_MAX_FRAME_CHARS = 4 * 1024 * 1024;
|
|
32
27
|
}
|
|
33
28
|
});
|
|
34
29
|
|
|
35
|
-
// src/session-
|
|
36
|
-
import {
|
|
37
|
-
import * as
|
|
38
|
-
import { hostname } from "node:os";
|
|
30
|
+
// src/session-catalog/endpoint.ts
|
|
31
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
32
|
+
import * as os3 from "node:os";
|
|
39
33
|
import * as path8 from "node:path";
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async function maybeUnlinkOwnedLock(lockPath) {
|
|
44
|
-
const owner = await fs6.readFile(lockPath, "utf8").catch(() => null);
|
|
45
|
-
if (owner === null || owner.trim() !== lockOwnerStamp()) return;
|
|
46
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
47
|
-
try {
|
|
48
|
-
await fs6.unlink(lockPath);
|
|
49
|
-
return;
|
|
50
|
-
} catch {
|
|
51
|
-
if (attempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
async function breakStaleLock(lockPath) {
|
|
56
|
-
try {
|
|
57
|
-
const content = await fs6.readFile(lockPath, "utf8").catch(() => "");
|
|
58
|
-
const trimmed = content.trim();
|
|
59
|
-
const colonIdx = trimmed.indexOf(":");
|
|
60
|
-
if (colonIdx === -1) {
|
|
61
|
-
const barePid = Number.parseInt(trimmed, 10);
|
|
62
|
-
if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
|
|
63
|
-
return await breakStaleLockVerified(lockPath, async () => {
|
|
64
|
-
const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
|
|
65
|
-
return reread.trim() === trimmed;
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return await breakStaleLockVerified(lockPath, async () => {
|
|
69
|
-
const st = await fs6.stat(lockPath);
|
|
70
|
-
return Date.now() - st.mtimeMs > STALE_LOCK_MS;
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
const ownerHost = trimmed.slice(0, colonIdx);
|
|
74
|
-
const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
|
|
75
|
-
if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
76
|
-
if (isPidAlive(ownerPid)) {
|
|
77
|
-
const stat5 = await fs6.stat(lockPath);
|
|
78
|
-
if (Date.now() - stat5.mtimeMs > SAME_HOST_STALE_MS) {
|
|
79
|
-
return await breakStaleLockVerified(lockPath, async () => {
|
|
80
|
-
const st = await fs6.stat(lockPath);
|
|
81
|
-
return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
return await breakStaleLockVerified(lockPath, async () => {
|
|
87
|
-
const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
|
|
88
|
-
return reread.trim() === trimmed;
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
return await breakStaleLockVerified(lockPath, async () => {
|
|
92
|
-
const st = await fs6.stat(lockPath);
|
|
93
|
-
return Date.now() - st.mtimeMs > STALE_LOCK_MS;
|
|
94
|
-
});
|
|
95
|
-
} catch {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
34
|
+
function normalizedPath(value) {
|
|
35
|
+
const resolved = path8.resolve(value);
|
|
36
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
98
37
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return await breakLockAtomically(lockPath) === true;
|
|
38
|
+
function sessionCatalogProjectServerKey(projectDir) {
|
|
39
|
+
return createHash2("sha256").update(normalizedPath(projectDir)).digest("hex").slice(0, 24);
|
|
102
40
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
void fs6.unlink(tombstone).catch(() => void 0);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
async function writeAtomicFile(filePath, registry) {
|
|
114
|
-
const tmp = path8.join(
|
|
115
|
-
path8.dirname(filePath),
|
|
116
|
-
`.${path8.basename(filePath)}.${randomUUID().slice(0, 8)}.tmp`
|
|
117
|
-
);
|
|
118
|
-
let tmpPersisted = false;
|
|
119
|
-
try {
|
|
120
|
-
const handle = await fs6.open(tmp, "w");
|
|
121
|
-
try {
|
|
122
|
-
await handle.writeFile(JSON.stringify(registry, null, 2), "utf8");
|
|
123
|
-
await handle.sync().catch(() => void 0);
|
|
124
|
-
} finally {
|
|
125
|
-
await handle.close();
|
|
126
|
-
}
|
|
127
|
-
tmpPersisted = true;
|
|
128
|
-
try {
|
|
129
|
-
await fs6.rename(tmp, filePath);
|
|
130
|
-
tmpPersisted = false;
|
|
131
|
-
} catch (renameErr) {
|
|
132
|
-
const code = renameErr?.code;
|
|
133
|
-
if (code === "EPERM" || code === "EBUSY" || code === "EACCES") {
|
|
134
|
-
await fs6.copyFile(tmp, filePath);
|
|
135
|
-
try {
|
|
136
|
-
const destHandle = await fs6.open(filePath, "r+");
|
|
137
|
-
try {
|
|
138
|
-
await destHandle.sync().catch(() => void 0);
|
|
139
|
-
} finally {
|
|
140
|
-
await destHandle.close();
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
}
|
|
144
|
-
await fs6.unlink(tmp).catch(() => void 0);
|
|
145
|
-
tmpPersisted = false;
|
|
146
|
-
} else {
|
|
147
|
-
throw renameErr;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
if (tmpPersisted) await fs6.unlink(tmp).catch(() => void 0);
|
|
152
|
-
throw err;
|
|
41
|
+
function sessionCatalogProjectServerEndpoint(projectDir) {
|
|
42
|
+
const key = sessionCatalogProjectServerKey(projectDir);
|
|
43
|
+
if (process.platform === "win32") {
|
|
44
|
+
return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
|
|
153
45
|
}
|
|
46
|
+
return path8.join(os3.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
|
|
154
47
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const dir = path8.dirname(filePath);
|
|
158
|
-
const base = path8.basename(filePath);
|
|
159
|
-
const now = Date.now();
|
|
160
|
-
const stale = [];
|
|
161
|
-
for (const name of await fs6.readdir(dir)) {
|
|
162
|
-
const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
|
|
163
|
-
if (!isTemp) continue;
|
|
164
|
-
const stat5 = await fs6.stat(path8.join(dir, name)).catch(() => null);
|
|
165
|
-
if (!stat5) continue;
|
|
166
|
-
if (now - stat5.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat5.mtimeMs });
|
|
167
|
-
}
|
|
168
|
-
stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
169
|
-
await Promise.all(
|
|
170
|
-
stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
|
|
171
|
-
await fs6.unlink(path8.join(dir, name)).catch(() => void 0);
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
} catch {
|
|
175
|
-
}
|
|
48
|
+
function sessionCatalogProjectServerMetadataPath(projectDir) {
|
|
49
|
+
return path8.join(projectDir, SESSION_CATALOG_METADATA_FILE);
|
|
176
50
|
}
|
|
177
|
-
var
|
|
178
|
-
var
|
|
179
|
-
"src/session-
|
|
51
|
+
var SESSION_CATALOG_METADATA_FILE;
|
|
52
|
+
var init_endpoint = __esm({
|
|
53
|
+
"src/session-catalog/endpoint.ts"() {
|
|
180
54
|
"use strict";
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
|
|
184
|
-
STALE_TMP_MS = 6e4;
|
|
185
|
-
MAX_STALE_TMP_FILES = 20;
|
|
186
|
-
HOST_NAME = hostname();
|
|
55
|
+
init_protocol();
|
|
56
|
+
SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
|
|
187
57
|
}
|
|
188
58
|
});
|
|
189
59
|
|
|
190
|
-
// src/session-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
getSessionRegistry: () => getSessionRegistry,
|
|
195
|
-
hasSessionRegistry: () => hasSessionRegistry
|
|
196
|
-
});
|
|
197
|
-
import * as fs7 from "node:fs/promises";
|
|
60
|
+
// src/session-catalog/client.ts
|
|
61
|
+
import { spawn } from "node:child_process";
|
|
62
|
+
import * as fs6 from "node:fs";
|
|
63
|
+
import * as net from "node:net";
|
|
198
64
|
import * as path9 from "node:path";
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
65
|
+
import { fileURLToPath } from "node:url";
|
|
66
|
+
function locateServer(moduleUrl, exists) {
|
|
67
|
+
for (const candidate of [
|
|
68
|
+
"./project-server.js",
|
|
69
|
+
"../session-catalog/project-server.js",
|
|
70
|
+
"./session-catalog/project-server.js",
|
|
71
|
+
"../../dist/session-catalog/project-server.js"
|
|
72
|
+
]) {
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(candidate, moduleUrl);
|
|
75
|
+
if (url.protocol === "file:" && exists(fileURLToPath(url))) return url;
|
|
76
|
+
} catch {
|
|
207
77
|
}
|
|
208
|
-
} catch {
|
|
209
78
|
}
|
|
210
|
-
return
|
|
79
|
+
return null;
|
|
211
80
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
return
|
|
81
|
+
function resolveSessionCatalogDaemonAvailability(moduleUrl = import.meta.url, exists = fs6.existsSync) {
|
|
82
|
+
if (process.env["WRONGSTACK_SESSION_CATALOG_INLINE"] || process.env["WRONGSTACK_SESSION_CATALOG_SERVER"] === "0")
|
|
83
|
+
return { kind: "inline-requested" };
|
|
84
|
+
const url = locateServer(moduleUrl, exists);
|
|
85
|
+
return url ? { kind: "available", url } : { kind: "missing-build" };
|
|
217
86
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return _instance;
|
|
87
|
+
function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exists = fs6.existsSync) {
|
|
88
|
+
const availability = resolveSessionCatalogDaemonAvailability(moduleUrl, exists);
|
|
89
|
+
return availability.kind === "available" ? availability.url : null;
|
|
90
|
+
}
|
|
91
|
+
function normalize2(value) {
|
|
92
|
+
const resolved = path9.resolve(value);
|
|
93
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
226
94
|
}
|
|
227
|
-
function
|
|
228
|
-
return
|
|
95
|
+
function delay(ms) {
|
|
96
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
229
97
|
}
|
|
230
|
-
var
|
|
231
|
-
var
|
|
232
|
-
"src/session-
|
|
98
|
+
var CONNECT_TIMEOUT_MS, START_TIMEOUT_MS, CALL_TIMEOUT_MS, MAX_PENDING_REQUESTS, MAX_EVENT_LISTENERS, SessionCatalogProjectClient;
|
|
99
|
+
var init_client = __esm({
|
|
100
|
+
"src/session-catalog/client.ts"() {
|
|
233
101
|
"use strict";
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
agentsFlushTimer = null;
|
|
264
|
-
lastAgentsWriteAt = 0;
|
|
265
|
-
/**
|
|
266
|
-
* Last full entry this process registered. Kept so the heartbeat can
|
|
267
|
-
* re-create our entry if it ever goes missing because the file was reset or
|
|
268
|
-
* a reader pruned a stale snapshot.
|
|
269
|
-
*/
|
|
270
|
-
lastEntry = null;
|
|
271
|
-
ownershipLockWaitMs;
|
|
272
|
-
constructor(globalRoot, options = {}) {
|
|
273
|
-
this.filePath = path9.join(globalRoot, REGISTRY_FILE);
|
|
274
|
-
this.ownershipLockWaitMs = Math.max(0, options.ownershipLockWaitMs ?? OWNERSHIP_LOCK_WAIT_MS);
|
|
102
|
+
init_endpoint();
|
|
103
|
+
init_protocol();
|
|
104
|
+
CONNECT_TIMEOUT_MS = 750;
|
|
105
|
+
START_TIMEOUT_MS = 1e4;
|
|
106
|
+
CALL_TIMEOUT_MS = 3e4;
|
|
107
|
+
MAX_PENDING_REQUESTS = 1024;
|
|
108
|
+
MAX_EVENT_LISTENERS = 64;
|
|
109
|
+
SessionCatalogProjectClient = class {
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.options = options;
|
|
112
|
+
this.endpoint = sessionCatalogProjectServerEndpoint(options.projectDir);
|
|
113
|
+
}
|
|
114
|
+
options;
|
|
115
|
+
endpoint;
|
|
116
|
+
socket = null;
|
|
117
|
+
info = null;
|
|
118
|
+
buffer = "";
|
|
119
|
+
connecting = null;
|
|
120
|
+
connectResolve = null;
|
|
121
|
+
connectReject = null;
|
|
122
|
+
authToken;
|
|
123
|
+
nextId = 1;
|
|
124
|
+
pending = /* @__PURE__ */ new Map();
|
|
125
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
126
|
+
reconnectTimer;
|
|
127
|
+
explicitlyClosed = false;
|
|
128
|
+
async call(op, args, options = {}) {
|
|
129
|
+
await this.ensureConnected(true);
|
|
130
|
+
return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
|
|
275
131
|
}
|
|
276
|
-
// ── Public API ──────────────────────────────────────────────────────────
|
|
277
132
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
133
|
+
* Call an already-running project daemon without starting one.
|
|
134
|
+
*
|
|
135
|
+
* Cross-project discovery must use this path: observing another project is
|
|
136
|
+
* never sufficient authority to wake that project's IPC owner.
|
|
280
137
|
*/
|
|
281
|
-
async
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
138
|
+
async callExisting(op, args, options = {}) {
|
|
139
|
+
await this.ensureConnected(false);
|
|
140
|
+
return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
|
|
141
|
+
}
|
|
142
|
+
ping() {
|
|
143
|
+
return this.call("ping", {}, { timeoutMs: 3e3 });
|
|
144
|
+
}
|
|
145
|
+
async subscribe(listener) {
|
|
146
|
+
if (!this.eventListeners.has(listener) && this.eventListeners.size >= MAX_EVENT_LISTENERS) {
|
|
147
|
+
throw new Error(`Session Catalog listener limit reached (${MAX_EVENT_LISTENERS})`);
|
|
148
|
+
}
|
|
149
|
+
this.explicitlyClosed = false;
|
|
150
|
+
this.eventListeners.add(listener);
|
|
151
|
+
try {
|
|
152
|
+
await this.call("subscribe", {});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.eventListeners.delete(listener);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return async () => {
|
|
158
|
+
this.eventListeners.delete(listener);
|
|
159
|
+
if (this.eventListeners.size === 0)
|
|
160
|
+
await this.callExisting("unsubscribe", {}).catch(() => void 0);
|
|
288
161
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
162
|
+
}
|
|
163
|
+
async shutdown(reason) {
|
|
164
|
+
try {
|
|
165
|
+
await this.ensureConnected(false);
|
|
166
|
+
} catch {
|
|
167
|
+
return { stopped: false };
|
|
295
168
|
}
|
|
296
|
-
await this.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
169
|
+
const result = await this.request({ type: "shutdown", ...reason !== void 0 ? { reason } : {} }, 5e3).catch(
|
|
170
|
+
() => ({ stopped: false })
|
|
171
|
+
);
|
|
172
|
+
if (result.stopped) {
|
|
173
|
+
const metadataPath = sessionCatalogProjectServerMetadataPath(this.options.projectDir);
|
|
174
|
+
const deadline = Date.now() + 5e3;
|
|
175
|
+
while (fs6.existsSync(metadataPath) && Date.now() < deadline) await delay(20);
|
|
176
|
+
if (result.pid && result.pid !== process.pid) {
|
|
177
|
+
while (Date.now() < deadline) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(result.pid, 0);
|
|
180
|
+
await delay(20);
|
|
181
|
+
} catch {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
306
184
|
}
|
|
307
185
|
}
|
|
308
|
-
const lockedOwner = registry[entry.sessionId];
|
|
309
|
-
if (lockedOwner && lockedOwner.pid !== entry.pid && pidAlive(lockedOwner.pid)) {
|
|
310
|
-
throw new SessionOwnershipConflictError(
|
|
311
|
-
`Session ${entry.sessionId} is already open in another running wstack (pid ${lockedOwner.pid}).`
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
registry[entry.sessionId] = full;
|
|
315
|
-
}, true);
|
|
316
|
-
this.cancelPendingAgentsFlush();
|
|
317
|
-
if (this.heartbeatTimer) {
|
|
318
|
-
clearInterval(this.heartbeatTimer);
|
|
319
|
-
this.heartbeatTimer = null;
|
|
320
186
|
}
|
|
321
|
-
|
|
322
|
-
this.currentSessionId = entry.sessionId;
|
|
323
|
-
this.lastEntry = full;
|
|
324
|
-
this.heartbeatTimer = setInterval(() => {
|
|
325
|
-
void this.heartbeat();
|
|
326
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
327
|
-
if (this.heartbeatTimer.unref) this.heartbeatTimer.unref();
|
|
187
|
+
return result;
|
|
328
188
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!this.currentSessionId) return;
|
|
341
|
-
this.pendingAgents = agents;
|
|
342
|
-
if (this.lastEntry) {
|
|
343
|
-
this.lastEntry.agents = agents;
|
|
344
|
-
this.lastEntry.agentCount = agents.length;
|
|
345
|
-
this.lastEntry.status = deriveSessionStatus(agents);
|
|
346
|
-
this.lastEntry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
347
|
-
}
|
|
348
|
-
const sinceLastWrite = Date.now() - this.lastAgentsWriteAt;
|
|
349
|
-
if (!this.agentsFlushTimer && sinceLastWrite >= AGENTS_WRITE_THROTTLE_MS) {
|
|
350
|
-
await this.writeAgentsSnapshot();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (!this.agentsFlushTimer) {
|
|
354
|
-
const delay = Math.max(0, AGENTS_WRITE_THROTTLE_MS - sinceLastWrite);
|
|
355
|
-
this.pendingAgentsFlush = new Promise((resolve5, reject) => {
|
|
356
|
-
this.pendingAgentsResolve = resolve5;
|
|
357
|
-
const timer = setTimeout(() => {
|
|
358
|
-
this.agentsFlushTimer = null;
|
|
359
|
-
this.pendingAgentsFlush = null;
|
|
360
|
-
this.pendingAgentsResolve = null;
|
|
361
|
-
this.writeAgentsSnapshot().then(resolve5, reject);
|
|
362
|
-
}, delay);
|
|
363
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
364
|
-
this.agentsFlushTimer = timer;
|
|
189
|
+
async close() {
|
|
190
|
+
this.explicitlyClosed = true;
|
|
191
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
192
|
+
this.reconnectTimer = void 0;
|
|
193
|
+
const socket = this.socket;
|
|
194
|
+
this.socket = null;
|
|
195
|
+
this.info = null;
|
|
196
|
+
if (socket && !socket.destroyed)
|
|
197
|
+
await new Promise((resolve8) => {
|
|
198
|
+
socket.once("close", resolve8);
|
|
199
|
+
socket.end();
|
|
365
200
|
});
|
|
366
|
-
}
|
|
367
|
-
await this.pendingAgentsFlush;
|
|
368
201
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
202
|
+
async ensureConnected(spawnIfMissing) {
|
|
203
|
+
if (this.socket && !this.socket.destroyed && this.info) return;
|
|
204
|
+
if (this.connecting) return this.connecting;
|
|
205
|
+
this.connecting = this.connectWithElection(spawnIfMissing).finally(() => {
|
|
206
|
+
this.connecting = null;
|
|
207
|
+
});
|
|
208
|
+
return this.connecting;
|
|
209
|
+
}
|
|
210
|
+
async connectWithElection(spawnIfMissing) {
|
|
211
|
+
const deadline = Date.now() + (spawnIfMissing ? START_TIMEOUT_MS : CONNECT_TIMEOUT_MS);
|
|
212
|
+
let spawned = false;
|
|
213
|
+
let lastError = new Error("Session Catalog project server unavailable");
|
|
214
|
+
while (Date.now() < deadline) {
|
|
215
|
+
try {
|
|
216
|
+
await this.connectOnce();
|
|
217
|
+
return;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
lastError = error;
|
|
220
|
+
}
|
|
221
|
+
if (!spawnIfMissing) break;
|
|
222
|
+
if (!spawned) {
|
|
223
|
+
this.spawnDetached();
|
|
224
|
+
spawned = true;
|
|
386
225
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
226
|
+
await delay(75);
|
|
227
|
+
}
|
|
228
|
+
throw lastError;
|
|
229
|
+
}
|
|
230
|
+
connectOnce() {
|
|
231
|
+
this.socket?.destroy();
|
|
232
|
+
this.socket = null;
|
|
233
|
+
this.info = null;
|
|
234
|
+
this.authToken = void 0;
|
|
235
|
+
this.buffer = "";
|
|
236
|
+
return new Promise((resolve8, reject) => {
|
|
237
|
+
const socket = net.createConnection(this.endpoint);
|
|
238
|
+
this.socket = socket;
|
|
239
|
+
socket.setEncoding("utf8");
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
reject(new Error("Session Catalog handshake timed out"));
|
|
242
|
+
socket.destroy();
|
|
243
|
+
}, CONNECT_TIMEOUT_MS);
|
|
244
|
+
timer.unref?.();
|
|
245
|
+
this.connectResolve = () => {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
this.connectResolve = null;
|
|
248
|
+
this.connectReject = null;
|
|
249
|
+
resolve8();
|
|
250
|
+
};
|
|
251
|
+
this.connectReject = (error) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
this.connectResolve = null;
|
|
254
|
+
this.connectReject = null;
|
|
255
|
+
reject(error);
|
|
256
|
+
};
|
|
257
|
+
socket.on("data", (chunk) => this.onData(socket, chunk));
|
|
258
|
+
socket.on("error", (error) => {
|
|
259
|
+
if (!this.info) this.connectReject?.(error);
|
|
260
|
+
});
|
|
261
|
+
socket.on("close", () => this.onClose(socket));
|
|
391
262
|
});
|
|
392
263
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
264
|
+
currentAuthToken() {
|
|
265
|
+
if (this.authToken === void 0) {
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(
|
|
268
|
+
fs6.readFileSync(sessionCatalogProjectServerMetadataPath(this.options.projectDir), "utf8")
|
|
269
|
+
);
|
|
270
|
+
if (typeof parsed.authToken === "string" && parsed.authToken)
|
|
271
|
+
this.authToken = parsed.authToken;
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
402
274
|
}
|
|
403
|
-
this.
|
|
404
|
-
this.pendingAgentsResolve = null;
|
|
405
|
-
this.pendingAgentsFlush = null;
|
|
406
|
-
const pending = this.pendingAgents;
|
|
407
|
-
this.pendingAgents = null;
|
|
408
|
-
return pending;
|
|
275
|
+
return this.authToken;
|
|
409
276
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (this.
|
|
416
|
-
|
|
417
|
-
|
|
277
|
+
request(message, timeoutMs) {
|
|
278
|
+
const socket = this.socket;
|
|
279
|
+
if (!socket || socket.destroyed)
|
|
280
|
+
return Promise.reject(new Error("Session Catalog connection is unavailable"));
|
|
281
|
+
const id = this.nextId++;
|
|
282
|
+
if (this.pending.size >= MAX_PENDING_REQUESTS) {
|
|
283
|
+
return Promise.reject(
|
|
284
|
+
new Error(`Session Catalog pending request limit reached (${MAX_PENDING_REQUESTS})`)
|
|
285
|
+
);
|
|
418
286
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
287
|
+
const encoded = encodeSessionCatalogMessage({
|
|
288
|
+
...message,
|
|
289
|
+
id,
|
|
290
|
+
authToken: this.currentAuthToken()
|
|
291
|
+
});
|
|
292
|
+
if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
|
|
293
|
+
return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
|
|
294
|
+
return new Promise((resolve8, reject) => {
|
|
295
|
+
const timer = setTimeout(() => {
|
|
296
|
+
const pending = this.pending.get(id);
|
|
297
|
+
if (!pending) return;
|
|
298
|
+
this.pending.delete(id);
|
|
299
|
+
pending.reject(
|
|
300
|
+
new Error(
|
|
301
|
+
`Session Catalog ${message.type === "request" ? message.op : message.type} timed out`
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}, timeoutMs);
|
|
305
|
+
timer.unref?.();
|
|
306
|
+
this.pending.set(id, { resolve: resolve8, reject, timer });
|
|
307
|
+
socket.write(encoded);
|
|
433
308
|
});
|
|
434
309
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.heartbeatTimer = null;
|
|
310
|
+
onData(socket, chunk) {
|
|
311
|
+
if (socket !== this.socket) return;
|
|
312
|
+
this.buffer += chunk;
|
|
313
|
+
if (this.buffer.length > SESSION_CATALOG_MAX_FRAME_CHARS) {
|
|
314
|
+
socket.destroy(new Error("Session Catalog response exceeded frame limit"));
|
|
315
|
+
return;
|
|
442
316
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
317
|
+
while (true) {
|
|
318
|
+
const newline = this.buffer.indexOf("\n");
|
|
319
|
+
if (newline < 0) return;
|
|
320
|
+
const line = this.buffer.slice(0, newline);
|
|
321
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
322
|
+
if (!line) continue;
|
|
323
|
+
try {
|
|
324
|
+
this.onMessage(JSON.parse(line));
|
|
325
|
+
} catch {
|
|
326
|
+
socket.destroy(new Error("Invalid Session Catalog response"));
|
|
327
|
+
return;
|
|
453
328
|
}
|
|
454
|
-
}
|
|
329
|
+
}
|
|
455
330
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
331
|
+
onMessage(message) {
|
|
332
|
+
if (message.type === "hello") {
|
|
333
|
+
if (message.protocolVersion !== SESSION_CATALOG_PROTOCOL_VERSION) {
|
|
334
|
+
this.connectReject?.(
|
|
335
|
+
new Error(
|
|
336
|
+
`Session Catalog protocol mismatch: client=${SESSION_CATALOG_PROTOCOL_VERSION}, server=${message.protocolVersion}`
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
this.socket?.destroy();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (normalize2(message.projectDir) !== normalize2(this.options.projectDir) || normalize2(message.projectRoot) !== normalize2(this.options.projectRoot)) {
|
|
343
|
+
this.connectReject?.(new Error("Session Catalog project identity mismatch"));
|
|
344
|
+
this.socket?.destroy();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.info = message;
|
|
348
|
+
this.connectResolve?.();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (message.type === "event") {
|
|
352
|
+
for (const listener of this.eventListeners) {
|
|
353
|
+
try {
|
|
354
|
+
listener(message.event);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const pending = this.pending.get(message.id);
|
|
361
|
+
if (!pending) return;
|
|
362
|
+
this.pending.delete(message.id);
|
|
363
|
+
clearTimeout(pending.timer);
|
|
364
|
+
if (message.ok) pending.resolve(message.result);
|
|
365
|
+
else {
|
|
366
|
+
const error = new Error(message.error);
|
|
367
|
+
if (message.errorName) error.name = message.errorName;
|
|
368
|
+
pending.reject(error);
|
|
369
|
+
}
|
|
462
370
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
371
|
+
onClose(socket) {
|
|
372
|
+
if (socket !== this.socket) return;
|
|
373
|
+
this.socket = null;
|
|
374
|
+
this.info = null;
|
|
375
|
+
this.authToken = void 0;
|
|
376
|
+
const error = new Error("Session Catalog connection closed");
|
|
377
|
+
this.connectReject?.(error);
|
|
378
|
+
this.connectResolve = null;
|
|
379
|
+
this.connectReject = null;
|
|
380
|
+
for (const pending of this.pending.values()) {
|
|
381
|
+
clearTimeout(pending.timer);
|
|
382
|
+
pending.reject(error);
|
|
383
|
+
}
|
|
384
|
+
this.pending.clear();
|
|
385
|
+
this.scheduleSubscriptionReconnect();
|
|
469
386
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
387
|
+
scheduleSubscriptionReconnect() {
|
|
388
|
+
if (this.explicitlyClosed || this.eventListeners.size === 0 || this.reconnectTimer) return;
|
|
389
|
+
this.reconnectTimer = setTimeout(() => {
|
|
390
|
+
this.reconnectTimer = void 0;
|
|
391
|
+
void this.call("subscribe", {}).catch(() => this.scheduleSubscriptionReconnect());
|
|
392
|
+
}, 250);
|
|
393
|
+
this.reconnectTimer.unref?.();
|
|
476
394
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
395
|
+
spawnDetached() {
|
|
396
|
+
const url = resolveSessionCatalogProjectServerUrl();
|
|
397
|
+
if (!url) throw new Error("Built Session Catalog project server is unavailable");
|
|
398
|
+
const child = spawn(
|
|
399
|
+
process.execPath,
|
|
400
|
+
[
|
|
401
|
+
fileURLToPath(url),
|
|
402
|
+
"--project-dir",
|
|
403
|
+
this.options.projectDir,
|
|
404
|
+
"--project-root",
|
|
405
|
+
this.options.projectRoot
|
|
406
|
+
],
|
|
407
|
+
{ detached: true, stdio: "ignore", windowsHide: true, env: process.env }
|
|
408
|
+
);
|
|
409
|
+
child.unref();
|
|
482
410
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// src/session-catalog/registry.ts
|
|
416
|
+
var registry_exports = {};
|
|
417
|
+
__export(registry_exports, {
|
|
418
|
+
ProjectSessionRegistry: () => ProjectSessionRegistry,
|
|
419
|
+
getProjectSessionRegistry: () => getProjectSessionRegistry,
|
|
420
|
+
hasProjectSessionRegistry: () => hasProjectSessionRegistry
|
|
421
|
+
});
|
|
422
|
+
import { randomUUID } from "node:crypto";
|
|
423
|
+
import * as fs7 from "node:fs/promises";
|
|
424
|
+
import * as path10 from "node:path";
|
|
425
|
+
function getProjectSessionRegistry(globalRoot) {
|
|
426
|
+
const key = globalRoot !== void 0 ? path10.resolve(globalRoot) : lastRegistryKey;
|
|
427
|
+
if (!key)
|
|
428
|
+
throw new Error("SessionRegistry not initialized. Call getSessionRegistry(globalRoot) first.");
|
|
429
|
+
let registry = registries.get(key);
|
|
430
|
+
if (!registry) {
|
|
431
|
+
if (registries.size >= MAX_GLOBAL_ROOTS) {
|
|
432
|
+
const idle = [...registries.entries()].find(([, candidate]) => !candidate.ownsSession());
|
|
433
|
+
if (!idle)
|
|
434
|
+
throw new Error(`Session Registry global-root limit reached (${MAX_GLOBAL_ROOTS})`);
|
|
435
|
+
registries.delete(idle[0]);
|
|
436
|
+
void idle[1].dispose();
|
|
437
|
+
}
|
|
438
|
+
registry = new ProjectSessionRegistry(key);
|
|
439
|
+
registries.set(key, registry);
|
|
440
|
+
} else {
|
|
441
|
+
registries.delete(key);
|
|
442
|
+
registries.set(key, registry);
|
|
443
|
+
}
|
|
444
|
+
lastRegistryKey = key;
|
|
445
|
+
return registry;
|
|
446
|
+
}
|
|
447
|
+
function hasProjectSessionRegistry(globalRoot) {
|
|
448
|
+
if (globalRoot === void 0) return registries.size > 0;
|
|
449
|
+
return registries.has(path10.resolve(globalRoot));
|
|
450
|
+
}
|
|
451
|
+
var HEARTBEAT_INTERVAL_MS, AGENT_WRITE_THROTTLE_MS, MAX_PROJECT_CLIENTS, MAX_GLOBAL_ROOTS, ProjectSessionRegistry, registries, lastRegistryKey;
|
|
452
|
+
var init_registry = __esm({
|
|
453
|
+
"src/session-catalog/registry.ts"() {
|
|
454
|
+
"use strict";
|
|
455
|
+
init_client();
|
|
456
|
+
init_endpoint();
|
|
457
|
+
HEARTBEAT_INTERVAL_MS = 5e3;
|
|
458
|
+
AGENT_WRITE_THROTTLE_MS = 300;
|
|
459
|
+
MAX_PROJECT_CLIENTS = 128;
|
|
460
|
+
MAX_GLOBAL_ROOTS = 16;
|
|
461
|
+
ProjectSessionRegistry = class {
|
|
462
|
+
constructor(globalRoot) {
|
|
463
|
+
this.globalRoot = globalRoot;
|
|
464
|
+
}
|
|
465
|
+
globalRoot;
|
|
466
|
+
instanceId = randomUUID();
|
|
467
|
+
clients = /* @__PURE__ */ new Map();
|
|
468
|
+
current;
|
|
469
|
+
heartbeatTimer;
|
|
470
|
+
agentRevision = 0;
|
|
471
|
+
pendingAgents;
|
|
472
|
+
agentTimer;
|
|
473
|
+
lastAgentWriteAt = 0;
|
|
474
|
+
bindingKey(projectDir) {
|
|
475
|
+
const resolved = path10.resolve(projectDir);
|
|
476
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
477
|
+
}
|
|
478
|
+
async closeBinding(binding) {
|
|
479
|
+
if (this.current?.binding === binding) return;
|
|
480
|
+
const key = this.bindingKey(binding.projectDir);
|
|
481
|
+
if (this.clients.get(key) === binding) this.clients.delete(key);
|
|
482
|
+
await binding.client.close();
|
|
483
|
+
}
|
|
484
|
+
binding(projectSlug2, projectRoot) {
|
|
485
|
+
const projectDir = path10.join(this.globalRoot, "projects", projectSlug2);
|
|
486
|
+
const key = this.bindingKey(projectDir);
|
|
487
|
+
let binding = this.clients.get(key);
|
|
488
|
+
if (!binding) {
|
|
489
|
+
if (this.clients.size >= MAX_PROJECT_CLIENTS) {
|
|
490
|
+
const oldest = [...this.clients.entries()].find(
|
|
491
|
+
([, candidate]) => candidate !== this.current?.binding
|
|
492
|
+
);
|
|
493
|
+
if (oldest) {
|
|
494
|
+
this.clients.delete(oldest[0]);
|
|
495
|
+
void oldest[1].client.close();
|
|
504
496
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
497
|
+
}
|
|
498
|
+
binding = {
|
|
499
|
+
projectDir,
|
|
500
|
+
projectRoot: path10.resolve(projectRoot),
|
|
501
|
+
client: new SessionCatalogProjectClient({ projectDir, projectRoot })
|
|
502
|
+
};
|
|
503
|
+
this.clients.set(key, binding);
|
|
504
|
+
} else {
|
|
505
|
+
this.clients.delete(key);
|
|
506
|
+
this.clients.set(key, binding);
|
|
508
507
|
}
|
|
508
|
+
return binding;
|
|
509
509
|
}
|
|
510
|
-
|
|
510
|
+
fullEntry(entry) {
|
|
511
|
+
const agents = entry.agents ?? [];
|
|
512
|
+
return {
|
|
513
|
+
...entry,
|
|
514
|
+
status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
|
|
515
|
+
lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
516
|
+
agentCount: agents.length,
|
|
517
|
+
agents
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async register(entry) {
|
|
521
|
+
const full = this.fullEntry(entry);
|
|
522
|
+
if (this.current?.entry.sessionId === full.sessionId && this.current.entry.pid === full.pid) {
|
|
523
|
+
this.current.entry = full;
|
|
524
|
+
this.current.credential = await this.current.binding.client.call("heartbeat", {
|
|
525
|
+
credential: this.current.credential,
|
|
526
|
+
status: full.status
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const nextBinding = this.binding(full.projectSlug, full.projectRoot);
|
|
531
|
+
let nextCredential;
|
|
511
532
|
try {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
const heartbeatAge = now - heartbeatAt;
|
|
530
|
-
const agents = Array.isArray(entry.agents) ? entry.agents : [];
|
|
531
|
-
const liveAgents = agents.filter((agent) => {
|
|
532
|
-
if (agent.id === "leader") return true;
|
|
533
|
-
const lastActivityAt = Date.parse(agent.lastActivityAt);
|
|
534
|
-
return Number.isFinite(lastActivityAt) && now - lastActivityAt <= AGENT_STALE_MS;
|
|
535
|
-
});
|
|
536
|
-
if (liveAgents.length !== agents.length || entry.agentCount !== liveAgents.length) {
|
|
537
|
-
entry.agents = liveAgents;
|
|
538
|
-
entry.agentCount = liveAgents.length;
|
|
539
|
-
pruned = true;
|
|
540
|
-
}
|
|
541
|
-
if (entry.status === "closing" && heartbeatAge > CLOSING_GRACE_MS) {
|
|
542
|
-
if (!pidAlive(entry.pid)) {
|
|
543
|
-
delete registry[id];
|
|
544
|
-
pruned = true;
|
|
545
|
-
}
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive(entry.pid)) {
|
|
549
|
-
delete registry[id];
|
|
550
|
-
pruned = true;
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
if (heartbeatAge <= STALE_TIMEOUT_MS) continue;
|
|
554
|
-
if (entry.status !== "lost") {
|
|
555
|
-
entry.status = "lost";
|
|
556
|
-
pruned = true;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (pruned) {
|
|
560
|
-
await this.atomicUpdate((current) => {
|
|
561
|
-
for (const [id, version] of observed) {
|
|
562
|
-
const latest = current[id];
|
|
563
|
-
if (!latest || latest.pid !== version.pid || latest.lastHeartbeatAt !== version.lastHeartbeatAt) {
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
const next = registry[id];
|
|
567
|
-
if (next) current[id] = next;
|
|
568
|
-
else delete current[id];
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
return registry;
|
|
573
|
-
} catch {
|
|
574
|
-
return {};
|
|
533
|
+
nextCredential = await nextBinding.client.call("claim_new", {
|
|
534
|
+
entry: full,
|
|
535
|
+
ownerInstanceId: this.instanceId
|
|
536
|
+
});
|
|
537
|
+
} catch (error) {
|
|
538
|
+
await this.closeBinding(nextBinding).catch(() => void 0);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
const previous = this.current;
|
|
542
|
+
this.current = { binding: nextBinding, credential: nextCredential, entry: full };
|
|
543
|
+
this.agentRevision = 0;
|
|
544
|
+
this.cancelAgentTimer();
|
|
545
|
+
this.startHeartbeat();
|
|
546
|
+
if (previous) {
|
|
547
|
+
await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
|
|
548
|
+
if (previous.binding !== nextBinding) await this.closeBinding(previous.binding);
|
|
575
549
|
}
|
|
576
550
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
let attempt = 0;
|
|
551
|
+
/** Reserve before transcript hydration; activation swaps ownership only after the writer opened. */
|
|
552
|
+
async reserveResume(target) {
|
|
553
|
+
const binding = this.binding(target.projectSlug, target.projectRoot);
|
|
554
|
+
let reservation;
|
|
582
555
|
try {
|
|
583
|
-
await
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return;
|
|
556
|
+
reservation = await binding.client.call("reserve_resume", {
|
|
557
|
+
targetSessionId: target.sessionId,
|
|
558
|
+
requesterInstanceId: this.instanceId,
|
|
559
|
+
...this.current ? { currentSessionId: this.current.entry.sessionId } : {}
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
await this.closeBinding(binding).catch(() => void 0);
|
|
563
|
+
throw error;
|
|
592
564
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
565
|
+
let settled = false;
|
|
566
|
+
return {
|
|
567
|
+
reservation,
|
|
568
|
+
activate: async (registration) => {
|
|
569
|
+
if (settled) throw new Error("Resume reservation is already settled");
|
|
570
|
+
const entry = this.fullEntry(registration);
|
|
571
|
+
const credential = await binding.client.call("activate_reservation", {
|
|
572
|
+
reservation,
|
|
573
|
+
entry
|
|
574
|
+
});
|
|
575
|
+
const previous = this.current;
|
|
576
|
+
this.current = { binding, credential, entry };
|
|
577
|
+
settled = true;
|
|
578
|
+
this.agentRevision = 0;
|
|
579
|
+
this.cancelAgentTimer();
|
|
580
|
+
this.startHeartbeat();
|
|
581
|
+
if (previous) {
|
|
582
|
+
await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
|
|
583
|
+
if (previous.binding !== binding) await this.closeBinding(previous.binding);
|
|
610
584
|
}
|
|
611
|
-
|
|
585
|
+
},
|
|
586
|
+
cancel: async () => {
|
|
587
|
+
if (settled) return;
|
|
588
|
+
settled = true;
|
|
612
589
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
stamped = true;
|
|
618
|
-
} catch {
|
|
619
|
-
if (stampAttempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
if (!stamped) {
|
|
623
|
-
stampFailed = true;
|
|
624
|
-
throw new Error("failed to stamp session-registry lock owner");
|
|
625
|
-
}
|
|
626
|
-
const raw = await fs7.readFile(this.filePath, "utf8").catch(() => "{}");
|
|
627
|
-
const registry = parseRegistry(raw);
|
|
628
|
-
fn(registry);
|
|
629
|
-
await this.writeAtomicWithRetry(registry);
|
|
630
|
-
return;
|
|
590
|
+
await binding.client.call("cancel_reservation", {
|
|
591
|
+
reservationId: reservation.reservationId,
|
|
592
|
+
requesterInstanceId: this.instanceId
|
|
593
|
+
}).catch(() => void 0);
|
|
631
594
|
} finally {
|
|
632
|
-
await
|
|
633
|
-
if (stampFailed) {
|
|
634
|
-
await fs7.unlink(lockPath).catch(() => void 0);
|
|
635
|
-
} else {
|
|
636
|
-
await maybeUnlinkOwnedLock(lockPath);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
} catch (err) {
|
|
640
|
-
if (err instanceof SessionOwnershipConflictError) throw err;
|
|
641
|
-
if (required) {
|
|
642
|
-
throw new SessionRegistryWriteError(
|
|
643
|
-
`Session registry ownership update failed: ${err instanceof Error ? err.message : String(err)}`
|
|
644
|
-
);
|
|
595
|
+
await this.closeBinding(binding).catch(() => void 0);
|
|
645
596
|
}
|
|
646
|
-
return;
|
|
647
597
|
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async updateAgents(agents) {
|
|
601
|
+
if (!this.current) return;
|
|
602
|
+
this.pendingAgents = agents;
|
|
603
|
+
this.current.entry = {
|
|
604
|
+
...this.current.entry,
|
|
605
|
+
agents,
|
|
606
|
+
agentCount: agents.length,
|
|
607
|
+
status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
|
|
608
|
+
lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
609
|
+
};
|
|
610
|
+
const elapsed = Date.now() - this.lastAgentWriteAt;
|
|
611
|
+
if (!this.agentTimer && elapsed >= AGENT_WRITE_THROTTLE_MS) {
|
|
612
|
+
await this.flushAgents();
|
|
613
|
+
return;
|
|
648
614
|
}
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
615
|
+
if (!this.agentTimer) {
|
|
616
|
+
this.agentTimer = setTimeout(
|
|
617
|
+
() => {
|
|
618
|
+
this.agentTimer = void 0;
|
|
619
|
+
void this.flushAgents();
|
|
620
|
+
},
|
|
621
|
+
Math.max(0, AGENT_WRITE_THROTTLE_MS - elapsed)
|
|
652
622
|
);
|
|
623
|
+
this.agentTimer.unref?.();
|
|
653
624
|
}
|
|
654
625
|
}
|
|
655
|
-
async
|
|
656
|
-
|
|
626
|
+
async flushAgents() {
|
|
627
|
+
const agents = this.pendingAgents;
|
|
628
|
+
const current = this.current;
|
|
629
|
+
if (!agents || !current) return;
|
|
630
|
+
this.pendingAgents = void 0;
|
|
631
|
+
this.lastAgentWriteAt = Date.now();
|
|
632
|
+
const revision = ++this.agentRevision;
|
|
633
|
+
await current.binding.client.call("publish_agents", {
|
|
634
|
+
credential: current.credential,
|
|
635
|
+
revision,
|
|
636
|
+
agents
|
|
637
|
+
});
|
|
657
638
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const maxAttempts = 5;
|
|
673
|
-
let lastErr;
|
|
674
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
639
|
+
async markClosing() {
|
|
640
|
+
this.stopHeartbeat();
|
|
641
|
+
this.cancelAgentTimer();
|
|
642
|
+
if (this.current)
|
|
643
|
+
await this.current.binding.client.call("mark_closing", {
|
|
644
|
+
credential: this.current.credential
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async unregister() {
|
|
648
|
+
this.stopHeartbeat();
|
|
649
|
+
this.cancelAgentTimer();
|
|
650
|
+
const current = this.current;
|
|
651
|
+
this.current = void 0;
|
|
652
|
+
if (current) {
|
|
675
653
|
try {
|
|
676
|
-
await
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
lastErr = err;
|
|
680
|
-
const code = err?.code;
|
|
681
|
-
if (!transientCodes.has(String(code ?? "")) || attempt === maxAttempts) {
|
|
682
|
-
throw err;
|
|
683
|
-
}
|
|
684
|
-
const delayMs = Math.min(80, 5 * attempt);
|
|
685
|
-
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
654
|
+
await current.binding.client.call("release", { credential: current.credential });
|
|
655
|
+
} finally {
|
|
656
|
+
await this.closeBinding(current.binding);
|
|
686
657
|
}
|
|
687
658
|
}
|
|
688
|
-
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
689
659
|
}
|
|
690
|
-
async
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
660
|
+
async list() {
|
|
661
|
+
const projectsDir = path10.join(this.globalRoot, "projects");
|
|
662
|
+
let directories = [];
|
|
663
|
+
try {
|
|
664
|
+
directories = (await fs7.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).slice(0, 1e3).map((entry) => entry.name);
|
|
665
|
+
} catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
const snapshots = await Promise.all(
|
|
669
|
+
directories.map(async (slug) => {
|
|
670
|
+
const projectDir = path10.join(projectsDir, slug);
|
|
671
|
+
let client;
|
|
672
|
+
try {
|
|
673
|
+
const metadata = JSON.parse(
|
|
674
|
+
await fs7.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
|
|
675
|
+
);
|
|
676
|
+
if (typeof metadata.projectRoot !== "string" || !metadata.projectRoot) return [];
|
|
677
|
+
client = new SessionCatalogProjectClient({
|
|
678
|
+
projectDir,
|
|
679
|
+
projectRoot: metadata.projectRoot
|
|
680
|
+
});
|
|
681
|
+
return await client.callExisting("list_live", {});
|
|
682
|
+
} catch {
|
|
683
|
+
return [];
|
|
684
|
+
} finally {
|
|
685
|
+
await client?.close().catch(() => void 0);
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
return snapshots.flat();
|
|
690
|
+
}
|
|
691
|
+
async listByProject(projectSlug2) {
|
|
692
|
+
const current = this.current;
|
|
693
|
+
if (current && path10.basename(current.binding.projectDir) === projectSlug2) {
|
|
694
|
+
return current.binding.client.call("list_live", {}).catch(() => []);
|
|
694
695
|
}
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
this.lastTempPruneAt = now;
|
|
698
|
-
this.tempPrunePromise = this.pruneStaleTempFiles();
|
|
696
|
+
const projectDir = path10.join(this.globalRoot, "projects", projectSlug2);
|
|
697
|
+
let client;
|
|
699
698
|
try {
|
|
700
|
-
|
|
699
|
+
const metadata = JSON.parse(
|
|
700
|
+
await fs7.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
|
|
701
|
+
);
|
|
702
|
+
if (typeof metadata.projectRoot !== "string") return [];
|
|
703
|
+
client = new SessionCatalogProjectClient({ projectDir, projectRoot: metadata.projectRoot });
|
|
704
|
+
return await client.callExisting("list_live", {}).catch(() => []);
|
|
705
|
+
} catch {
|
|
706
|
+
return [];
|
|
701
707
|
} finally {
|
|
702
|
-
|
|
708
|
+
await client?.close().catch(() => void 0);
|
|
703
709
|
}
|
|
704
710
|
}
|
|
705
|
-
async
|
|
706
|
-
await
|
|
711
|
+
async get(sessionId) {
|
|
712
|
+
return (await this.list()).find((entry) => entry.sessionId === sessionId);
|
|
707
713
|
}
|
|
708
|
-
|
|
709
|
-
|
|
714
|
+
subscribeProject(projectSlug2, projectRoot, listener) {
|
|
715
|
+
const binding = this.binding(projectSlug2, projectRoot);
|
|
716
|
+
return binding.client.subscribe(listener).then((unsubscribe) => async () => {
|
|
717
|
+
await unsubscribe();
|
|
718
|
+
await this.closeBinding(binding);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
get registryPath() {
|
|
722
|
+
return path10.join(this.globalRoot, "projects");
|
|
723
|
+
}
|
|
724
|
+
async dispose() {
|
|
725
|
+
await this.unregister().catch(() => void 0);
|
|
726
|
+
await Promise.all([...this.clients.values()].map((binding) => binding.client.close()));
|
|
727
|
+
this.clients.clear();
|
|
728
|
+
}
|
|
729
|
+
/** Whether this facade currently owns a live session lease. */
|
|
730
|
+
ownsSession() {
|
|
731
|
+
return this.current !== void 0;
|
|
732
|
+
}
|
|
733
|
+
startHeartbeat() {
|
|
734
|
+
this.stopHeartbeat();
|
|
735
|
+
this.heartbeatTimer = setInterval(() => {
|
|
736
|
+
void this.heartbeat();
|
|
737
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
738
|
+
this.heartbeatTimer.unref?.();
|
|
739
|
+
}
|
|
740
|
+
stopHeartbeat() {
|
|
741
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
742
|
+
this.heartbeatTimer = void 0;
|
|
743
|
+
}
|
|
744
|
+
cancelAgentTimer() {
|
|
745
|
+
if (this.agentTimer) clearTimeout(this.agentTimer);
|
|
746
|
+
this.agentTimer = void 0;
|
|
747
|
+
this.pendingAgents = void 0;
|
|
748
|
+
}
|
|
749
|
+
async heartbeat() {
|
|
750
|
+
const current = this.current;
|
|
751
|
+
if (!current) return;
|
|
752
|
+
try {
|
|
753
|
+
const credential = await current.binding.client.call("heartbeat", {
|
|
754
|
+
credential: current.credential,
|
|
755
|
+
status: current.entry.status
|
|
756
|
+
});
|
|
757
|
+
if (this.current === current) current.credential = credential;
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
710
760
|
}
|
|
711
761
|
};
|
|
712
|
-
|
|
762
|
+
registries = /* @__PURE__ */ new Map();
|
|
713
763
|
}
|
|
714
764
|
});
|
|
715
765
|
|
|
@@ -757,7 +807,7 @@ function envFlag(value) {
|
|
|
757
807
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
758
808
|
}
|
|
759
809
|
var COLOR = isColorTty();
|
|
760
|
-
var wrap = (
|
|
810
|
+
var wrap = (open, close) => (s) => COLOR ? `\x1B[${open}m${s}\x1B[${close}m` : s;
|
|
761
811
|
var color = {
|
|
762
812
|
reset: wrap("0", "0"),
|
|
763
813
|
bold: wrap("1", "22"),
|
|
@@ -2143,8 +2193,8 @@ var contextManagerTool = createContextManagerTool();
|
|
|
2143
2193
|
|
|
2144
2194
|
// src/boot.ts
|
|
2145
2195
|
import * as fs8 from "node:fs/promises";
|
|
2146
|
-
import * as
|
|
2147
|
-
import * as
|
|
2196
|
+
import * as os4 from "node:os";
|
|
2197
|
+
import * as path11 from "node:path";
|
|
2148
2198
|
|
|
2149
2199
|
// src/security/secret-vault.ts
|
|
2150
2200
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
@@ -2525,8 +2575,8 @@ function checkKeyFilePermissions(keyFile, opts) {
|
|
|
2525
2575
|
if (process.platform === "win32") return;
|
|
2526
2576
|
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
2527
2577
|
try {
|
|
2528
|
-
const
|
|
2529
|
-
const actualMode =
|
|
2578
|
+
const stat4 = fs2.statSync(keyFile);
|
|
2579
|
+
const actualMode = stat4.mode & 511;
|
|
2530
2580
|
if (actualMode !== KEY_FILE_MODE) {
|
|
2531
2581
|
void restrictFilePermissions(keyFile, {
|
|
2532
2582
|
label: "secret-vault",
|
|
@@ -3583,8 +3633,8 @@ var IN_PROJECT_DENIED_PATHS = [
|
|
|
3583
3633
|
reason: "The other half of the filesystem confinement switch."
|
|
3584
3634
|
}
|
|
3585
3635
|
];
|
|
3586
|
-
function deleteNestedPath(target,
|
|
3587
|
-
const segments =
|
|
3636
|
+
function deleteNestedPath(target, path12) {
|
|
3637
|
+
const segments = path12.split(".");
|
|
3588
3638
|
const last = segments[segments.length - 1];
|
|
3589
3639
|
if (last === void 0) return false;
|
|
3590
3640
|
let cursor = target;
|
|
@@ -3641,16 +3691,16 @@ function assertInProjectAllowListComplete() {
|
|
|
3641
3691
|
);
|
|
3642
3692
|
}
|
|
3643
3693
|
const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
|
|
3644
|
-
({ path:
|
|
3645
|
-
).map(({ path:
|
|
3694
|
+
({ path: path12 }) => !IN_PROJECT_ALLOWED_KEYS.has(path12.split(".")[0] ?? "")
|
|
3695
|
+
).map(({ path: path12 }) => path12);
|
|
3646
3696
|
if (orphanedPaths.length > 0) {
|
|
3647
3697
|
problems.push(
|
|
3648
3698
|
`IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
|
|
3649
3699
|
);
|
|
3650
3700
|
}
|
|
3651
3701
|
const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
|
|
3652
|
-
({ path:
|
|
3653
|
-
).map(({ path:
|
|
3702
|
+
({ path: path12 }) => path12.split(".").length < 2 || path12.split(".").some((s) => s.length === 0)
|
|
3703
|
+
).map(({ path: path12 }) => path12);
|
|
3654
3704
|
if (malformedPaths.length > 0) {
|
|
3655
3705
|
problems.push(
|
|
3656
3706
|
`IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
|
|
@@ -3678,8 +3728,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
|
|
|
3678
3728
|
}
|
|
3679
3729
|
stripped.push(k);
|
|
3680
3730
|
}
|
|
3681
|
-
for (const { path:
|
|
3682
|
-
if (deleteNestedPath(out,
|
|
3731
|
+
for (const { path: path12 } of IN_PROJECT_DENIED_PATHS) {
|
|
3732
|
+
if (deleteNestedPath(out, path12)) stripped.push(path12);
|
|
3683
3733
|
}
|
|
3684
3734
|
if (stripped.length > 0) {
|
|
3685
3735
|
warn(
|
|
@@ -4288,8 +4338,8 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
|
|
|
4288
4338
|
const t0 = Date.now();
|
|
4289
4339
|
let mtimeMs = null;
|
|
4290
4340
|
try {
|
|
4291
|
-
const
|
|
4292
|
-
mtimeMs =
|
|
4341
|
+
const stat4 = await fs5.stat(file);
|
|
4342
|
+
mtimeMs = stat4.mtimeMs;
|
|
4293
4343
|
const cached = this.jsonCache.get(file);
|
|
4294
4344
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
4295
4345
|
return structuredClone(cached.value);
|
|
@@ -4588,11 +4638,11 @@ async function bootConfig(options = {}) {
|
|
|
4588
4638
|
loadSyncConfig = true,
|
|
4589
4639
|
skipIdentityValidation = false
|
|
4590
4640
|
} = options;
|
|
4591
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
4641
|
+
const cwd = typeof flags["cwd"] === "string" ? path11.resolve(flags["cwd"]) : process.cwd();
|
|
4592
4642
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
4593
4643
|
const projectRoot = pathResolver.projectRoot;
|
|
4594
4644
|
const projectIdentityRoot = canonicalProjectRoot(projectRoot);
|
|
4595
|
-
const userHome =
|
|
4645
|
+
const userHome = os4.homedir();
|
|
4596
4646
|
const wpaths = resolveWstackPaths({ projectRoot });
|
|
4597
4647
|
assertProjectRootOutsideStateDir(projectRoot, wpaths.globalRoot);
|
|
4598
4648
|
await fs8.mkdir(wpaths.globalRoot, { recursive: true });
|
|
@@ -4701,8 +4751,8 @@ async function bootConfig(options = {}) {
|
|
|
4701
4751
|
}
|
|
4702
4752
|
const logger = new DefaultLogger({ level: config.log?.level ?? "info", file: wpaths.logFile });
|
|
4703
4753
|
try {
|
|
4704
|
-
const {
|
|
4705
|
-
|
|
4754
|
+
const { getProjectSessionRegistry: getProjectSessionRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
4755
|
+
getProjectSessionRegistry2(wpaths.globalRoot);
|
|
4706
4756
|
} catch {
|
|
4707
4757
|
}
|
|
4708
4758
|
return {
|
|
@@ -4791,7 +4841,7 @@ async function migrateLegacyConfig(wpaths) {
|
|
|
4791
4841
|
} catch (err) {
|
|
4792
4842
|
if (err.code !== "ENOENT") return;
|
|
4793
4843
|
}
|
|
4794
|
-
await fs8.mkdir(
|
|
4844
|
+
await fs8.mkdir(path11.dirname(profileFp), { recursive: true });
|
|
4795
4845
|
const profileContent = { ...result.value };
|
|
4796
4846
|
delete profileContent["activeProfile"];
|
|
4797
4847
|
await atomicWrite(profileFp, JSON.stringify(profileContent, null, 2), { mode: 384 });
|
|
@@ -4803,57 +4853,57 @@ async function migrateLegacyConfig(wpaths) {
|
|
|
4803
4853
|
}
|
|
4804
4854
|
var PROFILE_STATE_PAIRS = [
|
|
4805
4855
|
{
|
|
4806
|
-
globalSrc: (w) =>
|
|
4856
|
+
globalSrc: (w) => path11.join(w.globalRoot, "statusline.json"),
|
|
4807
4857
|
profileDst: (w, n) => w.profileStatuslineConfig(n)
|
|
4808
4858
|
},
|
|
4809
4859
|
{
|
|
4810
|
-
globalSrc: (w) =>
|
|
4860
|
+
globalSrc: (w) => path11.join(w.globalRoot, "mode.json"),
|
|
4811
4861
|
profileDst: (w, n) => w.profileModeConfig(n)
|
|
4812
4862
|
},
|
|
4813
4863
|
{
|
|
4814
|
-
globalSrc: (w) =>
|
|
4864
|
+
globalSrc: (w) => path11.join(w.globalRoot, "provider-status.json"),
|
|
4815
4865
|
profileDst: (w, n) => w.profileProviderStatus(n)
|
|
4816
4866
|
},
|
|
4817
4867
|
{
|
|
4818
|
-
globalSrc: (w) =>
|
|
4868
|
+
globalSrc: (w) => path11.join(w.globalRoot, "update-cache.json"),
|
|
4819
4869
|
profileDst: (w, n) => w.profileUpdateCache(n)
|
|
4820
4870
|
},
|
|
4821
4871
|
{
|
|
4822
|
-
globalSrc: (w) =>
|
|
4823
|
-
profileDst: (w, n) =>
|
|
4872
|
+
globalSrc: (w) => path11.join(w.globalRoot, "memory.md"),
|
|
4873
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "memory.md")
|
|
4824
4874
|
},
|
|
4825
4875
|
{
|
|
4826
|
-
globalSrc: (w) =>
|
|
4827
|
-
profileDst: (w, n) =>
|
|
4876
|
+
globalSrc: (w) => path11.join(w.globalRoot, "history"),
|
|
4877
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "history")
|
|
4828
4878
|
},
|
|
4829
4879
|
{
|
|
4830
|
-
globalSrc: (w) =>
|
|
4831
|
-
profileDst: (w, n) =>
|
|
4880
|
+
globalSrc: (w) => path11.join(w.globalRoot, "sync.json"),
|
|
4881
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "sync.json")
|
|
4832
4882
|
},
|
|
4833
4883
|
{
|
|
4834
|
-
globalSrc: (w) =>
|
|
4835
|
-
profileDst: (w, n) =>
|
|
4884
|
+
globalSrc: (w) => path11.join(w.globalRoot, "sync-state.json"),
|
|
4885
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "sync-state.json")
|
|
4836
4886
|
},
|
|
4837
4887
|
{
|
|
4838
|
-
globalSrc: (w) =>
|
|
4839
|
-
profileDst: (w, n) =>
|
|
4888
|
+
globalSrc: (w) => path11.join(w.globalRoot, "prompt-usage.json"),
|
|
4889
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "prompt-usage.json")
|
|
4840
4890
|
},
|
|
4841
4891
|
{
|
|
4842
|
-
globalSrc: (w) =>
|
|
4843
|
-
profileDst: (w, n) =>
|
|
4892
|
+
globalSrc: (w) => path11.join(w.globalRoot, "custom-context-modes.json"),
|
|
4893
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "custom-context-modes.json")
|
|
4844
4894
|
},
|
|
4845
4895
|
{
|
|
4846
|
-
globalSrc: (w) =>
|
|
4847
|
-
profileDst: (w, n) =>
|
|
4896
|
+
globalSrc: (w) => path11.join(w.globalRoot, "desktop.json"),
|
|
4897
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "desktop.json")
|
|
4848
4898
|
},
|
|
4849
4899
|
{
|
|
4850
|
-
globalSrc: (w) =>
|
|
4851
|
-
profileDst: (w, n) =>
|
|
4900
|
+
globalSrc: (w) => path11.join(w.globalRoot, "installed-skills.json"),
|
|
4901
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, "installed-skills.json")
|
|
4852
4902
|
},
|
|
4853
4903
|
...["skills", "prompts", "instructions", "design-kits", "desktop", "settings"].map(
|
|
4854
4904
|
(directory) => ({
|
|
4855
|
-
globalSrc: (w) =>
|
|
4856
|
-
profileDst: (w, n) =>
|
|
4905
|
+
globalSrc: (w) => path11.join(w.globalRoot, directory),
|
|
4906
|
+
profileDst: (w, n) => path11.join(w.profilesDir, n, directory)
|
|
4857
4907
|
})
|
|
4858
4908
|
)
|
|
4859
4909
|
];
|
|
@@ -4868,7 +4918,7 @@ async function migrateProfileFiles(wpaths) {
|
|
|
4868
4918
|
} catch {
|
|
4869
4919
|
}
|
|
4870
4920
|
try {
|
|
4871
|
-
await fs8.mkdir(
|
|
4921
|
+
await fs8.mkdir(path11.join(wpaths.profilesDir, profileName), { recursive: true });
|
|
4872
4922
|
} catch {
|
|
4873
4923
|
return;
|
|
4874
4924
|
}
|
|
@@ -4907,9 +4957,9 @@ async function migrateProfileFiles(wpaths) {
|
|
|
4907
4957
|
}
|
|
4908
4958
|
}
|
|
4909
4959
|
function assertProjectRootOutsideStateDir(projectRoot, globalRoot) {
|
|
4910
|
-
const stateNamespace =
|
|
4911
|
-
const rel =
|
|
4912
|
-
if (rel.startsWith("..") ||
|
|
4960
|
+
const stateNamespace = path11.resolve(globalRoot, "projects");
|
|
4961
|
+
const rel = path11.relative(stateNamespace, path11.resolve(projectRoot));
|
|
4962
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) return;
|
|
4913
4963
|
throw new Error(
|
|
4914
4964
|
`Refusing to start: the resolved project root is inside WrongStack's per-project state directory.
|
|
4915
4965
|
project root: ${projectRoot}
|
|
@@ -4932,7 +4982,7 @@ async function writeProjectMeta(paths, projectRoot, projectId) {
|
|
|
4932
4982
|
}
|
|
4933
4983
|
}
|
|
4934
4984
|
async function registerProjectInManifest(paths, projectRoot, events, workingDir, projectId) {
|
|
4935
|
-
const manifestPath =
|
|
4985
|
+
const manifestPath = path11.join(paths.globalRoot, "projects.json");
|
|
4936
4986
|
try {
|
|
4937
4987
|
const t0 = Date.now();
|
|
4938
4988
|
const raw = await fs8.readFile(manifestPath, "utf8");
|
|
@@ -4974,7 +5024,7 @@ async function registerProjectInManifest(paths, projectRoot, events, workingDir,
|
|
|
4974
5024
|
if (workingDir) existing.lastWorkingDir = workingDir;
|
|
4975
5025
|
} else {
|
|
4976
5026
|
const slug = paths.projectSlug;
|
|
4977
|
-
const name =
|
|
5027
|
+
const name = path11.basename(projectRoot);
|
|
4978
5028
|
const entry = {
|
|
4979
5029
|
name,
|
|
4980
5030
|
root: projectRoot,
|
|
@@ -5008,24 +5058,24 @@ async function registerProjectInManifest(paths, projectRoot, events, workingDir,
|
|
|
5008
5058
|
}
|
|
5009
5059
|
}
|
|
5010
5060
|
async function cleanupStaleProjects(wpaths) {
|
|
5011
|
-
const projectsRoot =
|
|
5061
|
+
const projectsRoot = path11.dirname(wpaths.projectDir);
|
|
5012
5062
|
let entries;
|
|
5013
5063
|
try {
|
|
5014
5064
|
entries = await fs8.readdir(projectsRoot, { withFileTypes: true });
|
|
5015
5065
|
} catch {
|
|
5016
5066
|
return;
|
|
5017
5067
|
}
|
|
5018
|
-
const stateNamespace =
|
|
5068
|
+
const stateNamespace = path11.resolve(projectsRoot);
|
|
5019
5069
|
for (const entry of entries) {
|
|
5020
5070
|
if (!entry.isDirectory()) continue;
|
|
5021
|
-
const projectPath =
|
|
5022
|
-
const metaPath =
|
|
5071
|
+
const projectPath = path11.join(projectsRoot, entry.name);
|
|
5072
|
+
const metaPath = path11.join(projectPath, "meta.json");
|
|
5023
5073
|
try {
|
|
5024
5074
|
const raw = await fs8.readFile(metaPath, "utf8");
|
|
5025
5075
|
const meta = JSON.parse(raw);
|
|
5026
5076
|
if (typeof meta.root !== "string") continue;
|
|
5027
|
-
const rel =
|
|
5028
|
-
const nested = rel !== "" && !rel.startsWith("..") && !
|
|
5077
|
+
const rel = path11.relative(stateNamespace, path11.resolve(meta.root));
|
|
5078
|
+
const nested = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
5029
5079
|
if (nested) {
|
|
5030
5080
|
await fs8.rm(projectPath, { recursive: true, force: true });
|
|
5031
5081
|
continue;
|