@askexenow/exe-os 0.8.36 → 0.8.37
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/bin/backfill-conversations.js +9 -1
- package/dist/bin/backfill-responses.js +9 -1
- package/dist/bin/cli.js +255 -31
- package/dist/bin/exe-assign.js +9 -1
- package/dist/bin/exe-boot.js +554 -23
- package/dist/bin/exe-dispatch.js +43 -3
- package/dist/bin/exe-export-behaviors.js +7 -0
- package/dist/bin/exe-gateway.js +57 -9
- package/dist/bin/exe-heartbeat.js +2 -1
- package/dist/bin/exe-kill.js +7 -0
- package/dist/bin/exe-launch-agent.js +8 -1
- package/dist/bin/exe-link.js +503 -12
- package/dist/bin/exe-pending-messages.js +2 -1
- package/dist/bin/exe-pending-reviews.js +2 -1
- package/dist/bin/exe-review.js +9 -1
- package/dist/bin/exe-search.js +9 -1
- package/dist/bin/exe-session-cleanup.js +11 -2
- package/dist/bin/git-sweep.js +9 -1
- package/dist/bin/graph-backfill.js +7 -0
- package/dist/bin/graph-export.js +7 -0
- package/dist/bin/install.js +35 -5
- package/dist/bin/scan-tasks.js +9 -1
- package/dist/bin/shard-migrate.js +7 -0
- package/dist/bin/wiki-sync.js +7 -0
- package/dist/gateway/index.js +57 -9
- package/dist/hooks/bug-report-worker.js +45 -5
- package/dist/hooks/commit-complete.js +9 -1
- package/dist/hooks/error-recall.js +10 -2
- package/dist/hooks/exe-heartbeat-hook.js +1 -1
- package/dist/hooks/ingest-worker.js +56 -8
- package/dist/hooks/ingest.js +1 -1
- package/dist/hooks/instructions-loaded.js +10 -2
- package/dist/hooks/notification.js +10 -2
- package/dist/hooks/post-compact.js +10 -2
- package/dist/hooks/pre-compact.js +10 -2
- package/dist/hooks/pre-tool-use.js +10 -2
- package/dist/hooks/prompt-ingest-worker.js +9 -1
- package/dist/hooks/prompt-submit.js +56 -8
- package/dist/hooks/response-ingest-worker.js +9 -1
- package/dist/hooks/session-end.js +10 -2
- package/dist/hooks/session-start.js +10 -2
- package/dist/hooks/stop.js +10 -2
- package/dist/hooks/subagent-stop.js +10 -2
- package/dist/hooks/summary-worker.js +512 -13
- package/dist/index.js +65 -15
- package/dist/lib/cloud-sync.js +502 -11
- package/dist/lib/exe-daemon.js +73 -23
- package/dist/lib/hybrid-search.js +9 -1
- package/dist/lib/messaging.js +43 -3
- package/dist/lib/store.js +9 -1
- package/dist/lib/tasks.js +47 -7
- package/dist/lib/tmux-routing.js +45 -3
- package/dist/mcp/server.js +73 -16
- package/dist/mcp/tools/create-task.js +48 -8
- package/dist/mcp/tools/deactivate-behavior.js +1 -1
- package/dist/mcp/tools/list-tasks.js +2 -1
- package/dist/mcp/tools/send-message.js +46 -6
- package/dist/mcp/tools/update-task.js +3 -2
- package/dist/runtime/index.js +54 -4
- package/dist/tui/App.js +54 -4
- package/package.json +2 -2
- package/src/commands/exe/afk.md +116 -0
package/dist/lib/exe-daemon.js
CHANGED
|
@@ -1651,7 +1651,8 @@ async function writeMemory(record) {
|
|
|
1651
1651
|
has_error: record.has_error ? 1 : 0,
|
|
1652
1652
|
raw_text: record.raw_text,
|
|
1653
1653
|
vector: record.vector,
|
|
1654
|
-
version:
|
|
1654
|
+
version: 0,
|
|
1655
|
+
// Placeholder — assigned atomically at flush time
|
|
1655
1656
|
task_id: record.task_id ?? null,
|
|
1656
1657
|
importance: record.importance ?? 5,
|
|
1657
1658
|
status: record.status ?? "active",
|
|
@@ -1685,6 +1686,13 @@ async function flushBatch() {
|
|
|
1685
1686
|
_flushing = true;
|
|
1686
1687
|
try {
|
|
1687
1688
|
const batch = _pendingRecords.slice(0);
|
|
1689
|
+
const client = getClient();
|
|
1690
|
+
const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
|
|
1691
|
+
let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
1692
|
+
for (const row of batch) {
|
|
1693
|
+
row.version = baseVersion++;
|
|
1694
|
+
}
|
|
1695
|
+
_nextVersion = baseVersion;
|
|
1688
1696
|
const buildStmt = (row) => {
|
|
1689
1697
|
const hasVector = row.vector !== null;
|
|
1690
1698
|
const taskId = row.task_id ?? null;
|
|
@@ -4354,6 +4362,7 @@ var init_capacity_monitor = __esm({
|
|
|
4354
4362
|
// src/lib/tmux-routing.ts
|
|
4355
4363
|
var tmux_routing_exports = {};
|
|
4356
4364
|
__export(tmux_routing_exports, {
|
|
4365
|
+
acquireSpawnLock: () => acquireSpawnLock,
|
|
4357
4366
|
employeeSessionName: () => employeeSessionName,
|
|
4358
4367
|
ensureEmployee: () => ensureEmployee,
|
|
4359
4368
|
extractRootExe: () => extractRootExe,
|
|
@@ -4368,6 +4377,7 @@ __export(tmux_routing_exports, {
|
|
|
4368
4377
|
notifyParentExe: () => notifyParentExe,
|
|
4369
4378
|
parseParentExe: () => parseParentExe,
|
|
4370
4379
|
registerParentExe: () => registerParentExe,
|
|
4380
|
+
releaseSpawnLock: () => releaseSpawnLock,
|
|
4371
4381
|
resolveExeSession: () => resolveExeSession,
|
|
4372
4382
|
sendIntercom: () => sendIntercom,
|
|
4373
4383
|
spawnEmployee: () => spawnEmployee,
|
|
@@ -4378,6 +4388,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSy
|
|
|
4378
4388
|
import path15 from "path";
|
|
4379
4389
|
import os5 from "os";
|
|
4380
4390
|
import { fileURLToPath } from "url";
|
|
4391
|
+
import { unlinkSync as unlinkSync4 } from "fs";
|
|
4392
|
+
function spawnLockPath(sessionName) {
|
|
4393
|
+
return path15.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
4394
|
+
}
|
|
4395
|
+
function isProcessAlive(pid) {
|
|
4396
|
+
try {
|
|
4397
|
+
process.kill(pid, 0);
|
|
4398
|
+
return true;
|
|
4399
|
+
} catch {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
function acquireSpawnLock(sessionName) {
|
|
4404
|
+
if (!existsSync12(SPAWN_LOCK_DIR)) {
|
|
4405
|
+
mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
|
|
4406
|
+
}
|
|
4407
|
+
const lockFile = spawnLockPath(sessionName);
|
|
4408
|
+
if (existsSync12(lockFile)) {
|
|
4409
|
+
try {
|
|
4410
|
+
const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
|
|
4411
|
+
const age = Date.now() - lock.timestamp;
|
|
4412
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
4413
|
+
return false;
|
|
4414
|
+
}
|
|
4415
|
+
} catch {
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
4419
|
+
return true;
|
|
4420
|
+
}
|
|
4421
|
+
function releaseSpawnLock(sessionName) {
|
|
4422
|
+
try {
|
|
4423
|
+
unlinkSync4(spawnLockPath(sessionName));
|
|
4424
|
+
} catch {
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4381
4427
|
function resolveBehaviorsExporterScript() {
|
|
4382
4428
|
try {
|
|
4383
4429
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -4476,10 +4522,10 @@ function isEmployeeAlive(sessionName) {
|
|
|
4476
4522
|
}
|
|
4477
4523
|
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
4478
4524
|
const base = employeeSessionName(employeeName, exeSession);
|
|
4479
|
-
if (!isAlive(base)) return 0;
|
|
4525
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
4480
4526
|
for (let i = 2; i <= maxInstances; i++) {
|
|
4481
4527
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
4482
|
-
if (!isAlive(candidate)) return i;
|
|
4528
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
4483
4529
|
}
|
|
4484
4530
|
return null;
|
|
4485
4531
|
}
|
|
@@ -4843,6 +4889,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4843
4889
|
command: spawnCommand
|
|
4844
4890
|
});
|
|
4845
4891
|
if (spawnResult.error) {
|
|
4892
|
+
releaseSpawnLock(sessionName);
|
|
4846
4893
|
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
4847
4894
|
}
|
|
4848
4895
|
transport.pipeLog(sessionName, logFile);
|
|
@@ -4880,6 +4927,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4880
4927
|
}
|
|
4881
4928
|
}
|
|
4882
4929
|
if (!booted) {
|
|
4930
|
+
releaseSpawnLock(sessionName);
|
|
4883
4931
|
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
4884
4932
|
}
|
|
4885
4933
|
if (!useExeAgent) {
|
|
@@ -4896,9 +4944,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
4896
4944
|
pid: 0,
|
|
4897
4945
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4898
4946
|
});
|
|
4947
|
+
releaseSpawnLock(sessionName);
|
|
4899
4948
|
return { sessionName };
|
|
4900
4949
|
}
|
|
4901
|
-
var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
4950
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
4902
4951
|
var init_tmux_routing = __esm({
|
|
4903
4952
|
"src/lib/tmux-routing.ts"() {
|
|
4904
4953
|
"use strict";
|
|
@@ -4910,6 +4959,7 @@ var init_tmux_routing = __esm({
|
|
|
4910
4959
|
init_provider_table();
|
|
4911
4960
|
init_intercom_queue();
|
|
4912
4961
|
init_plan_limits();
|
|
4962
|
+
SPAWN_LOCK_DIR = path15.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
4913
4963
|
SESSION_CACHE = path15.join(os5.homedir(), ".exe-os", "session-cache");
|
|
4914
4964
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
4915
4965
|
VERIFY_PANE_LINES = 200;
|
|
@@ -6130,7 +6180,7 @@ var init_consolidation = __esm({
|
|
|
6130
6180
|
import net from "net";
|
|
6131
6181
|
import { spawn } from "child_process";
|
|
6132
6182
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6133
|
-
import { existsSync as existsSync14, unlinkSync as
|
|
6183
|
+
import { existsSync as existsSync14, unlinkSync as unlinkSync5, readFileSync as readFileSync11, openSync, closeSync, statSync } from "fs";
|
|
6134
6184
|
import path17 from "path";
|
|
6135
6185
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6136
6186
|
function handleData(chunk) {
|
|
@@ -6166,11 +6216,11 @@ function cleanupStaleFiles() {
|
|
|
6166
6216
|
} catch {
|
|
6167
6217
|
}
|
|
6168
6218
|
try {
|
|
6169
|
-
|
|
6219
|
+
unlinkSync5(PID_PATH);
|
|
6170
6220
|
} catch {
|
|
6171
6221
|
}
|
|
6172
6222
|
try {
|
|
6173
|
-
|
|
6223
|
+
unlinkSync5(SOCKET_PATH);
|
|
6174
6224
|
} catch {
|
|
6175
6225
|
}
|
|
6176
6226
|
}
|
|
@@ -6222,7 +6272,7 @@ function spawnDaemon() {
|
|
|
6222
6272
|
}
|
|
6223
6273
|
}
|
|
6224
6274
|
}
|
|
6225
|
-
function
|
|
6275
|
+
function acquireSpawnLock2() {
|
|
6226
6276
|
try {
|
|
6227
6277
|
const fd = openSync(SPAWN_LOCK_PATH, "wx");
|
|
6228
6278
|
closeSync(fd);
|
|
@@ -6232,7 +6282,7 @@ function acquireSpawnLock() {
|
|
|
6232
6282
|
const stat = statSync(SPAWN_LOCK_PATH);
|
|
6233
6283
|
if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
|
|
6234
6284
|
try {
|
|
6235
|
-
|
|
6285
|
+
unlinkSync5(SPAWN_LOCK_PATH);
|
|
6236
6286
|
} catch {
|
|
6237
6287
|
}
|
|
6238
6288
|
try {
|
|
@@ -6247,9 +6297,9 @@ function acquireSpawnLock() {
|
|
|
6247
6297
|
return false;
|
|
6248
6298
|
}
|
|
6249
6299
|
}
|
|
6250
|
-
function
|
|
6300
|
+
function releaseSpawnLock2() {
|
|
6251
6301
|
try {
|
|
6252
|
-
|
|
6302
|
+
unlinkSync5(SPAWN_LOCK_PATH);
|
|
6253
6303
|
} catch {
|
|
6254
6304
|
}
|
|
6255
6305
|
}
|
|
@@ -6294,12 +6344,12 @@ function connectToSocket() {
|
|
|
6294
6344
|
async function connectEmbedDaemon() {
|
|
6295
6345
|
if (_socket && _connected) return true;
|
|
6296
6346
|
if (await connectToSocket()) return true;
|
|
6297
|
-
if (
|
|
6347
|
+
if (acquireSpawnLock2()) {
|
|
6298
6348
|
try {
|
|
6299
6349
|
cleanupStaleFiles();
|
|
6300
6350
|
spawnDaemon();
|
|
6301
6351
|
} finally {
|
|
6302
|
-
|
|
6352
|
+
releaseSpawnLock2();
|
|
6303
6353
|
}
|
|
6304
6354
|
}
|
|
6305
6355
|
const start = Date.now();
|
|
@@ -6380,11 +6430,11 @@ function killAndRespawnDaemon() {
|
|
|
6380
6430
|
_connected = false;
|
|
6381
6431
|
_buffer = "";
|
|
6382
6432
|
try {
|
|
6383
|
-
|
|
6433
|
+
unlinkSync5(PID_PATH);
|
|
6384
6434
|
} catch {
|
|
6385
6435
|
}
|
|
6386
6436
|
try {
|
|
6387
|
-
|
|
6437
|
+
unlinkSync5(SOCKET_PATH);
|
|
6388
6438
|
} catch {
|
|
6389
6439
|
}
|
|
6390
6440
|
spawnDaemon();
|
|
@@ -7891,7 +7941,7 @@ var init_messaging = __esm({
|
|
|
7891
7941
|
init_config();
|
|
7892
7942
|
init_memory();
|
|
7893
7943
|
import net2 from "net";
|
|
7894
|
-
import { writeFileSync as writeFileSync7, unlinkSync as
|
|
7944
|
+
import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync6, mkdirSync as mkdirSync8, existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
7895
7945
|
import path20 from "path";
|
|
7896
7946
|
import { getLlama } from "node-llama-cpp";
|
|
7897
7947
|
var SOCKET_PATH2 = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path20.join(EXE_AI_DIR, "exed.sock");
|
|
@@ -7996,11 +8046,11 @@ async function shutdown() {
|
|
|
7996
8046
|
}
|
|
7997
8047
|
_llama = null;
|
|
7998
8048
|
try {
|
|
7999
|
-
|
|
8049
|
+
unlinkSync6(SOCKET_PATH2);
|
|
8000
8050
|
} catch {
|
|
8001
8051
|
}
|
|
8002
8052
|
try {
|
|
8003
|
-
|
|
8053
|
+
unlinkSync6(PID_PATH2);
|
|
8004
8054
|
} catch {
|
|
8005
8055
|
}
|
|
8006
8056
|
process.stderr.write("[exed] Shutdown complete.\n");
|
|
@@ -8044,12 +8094,12 @@ function startServer() {
|
|
|
8044
8094
|
} catch {
|
|
8045
8095
|
}
|
|
8046
8096
|
}
|
|
8047
|
-
|
|
8097
|
+
unlinkSync6(oldPath);
|
|
8048
8098
|
} catch {
|
|
8049
8099
|
}
|
|
8050
8100
|
}
|
|
8051
8101
|
try {
|
|
8052
|
-
|
|
8102
|
+
unlinkSync6(SOCKET_PATH2);
|
|
8053
8103
|
} catch {
|
|
8054
8104
|
}
|
|
8055
8105
|
const server = net2.createServer((socket) => {
|
|
@@ -8475,7 +8525,7 @@ function checkExistingDaemon() {
|
|
|
8475
8525
|
return true;
|
|
8476
8526
|
}
|
|
8477
8527
|
try {
|
|
8478
|
-
|
|
8528
|
+
unlinkSync6(PID_PATH2);
|
|
8479
8529
|
} catch {
|
|
8480
8530
|
}
|
|
8481
8531
|
return false;
|
|
@@ -8638,11 +8688,11 @@ try {
|
|
|
8638
8688
|
process.stderr.write(`[exed] FATAL: ${err instanceof Error ? err.message : String(err)}
|
|
8639
8689
|
`);
|
|
8640
8690
|
try {
|
|
8641
|
-
|
|
8691
|
+
unlinkSync6(SOCKET_PATH2);
|
|
8642
8692
|
} catch {
|
|
8643
8693
|
}
|
|
8644
8694
|
try {
|
|
8645
|
-
|
|
8695
|
+
unlinkSync6(PID_PATH2);
|
|
8646
8696
|
} catch {
|
|
8647
8697
|
}
|
|
8648
8698
|
process.exit(1);
|
|
@@ -1529,7 +1529,8 @@ async function writeMemory(record) {
|
|
|
1529
1529
|
has_error: record.has_error ? 1 : 0,
|
|
1530
1530
|
raw_text: record.raw_text,
|
|
1531
1531
|
vector: record.vector,
|
|
1532
|
-
version:
|
|
1532
|
+
version: 0,
|
|
1533
|
+
// Placeholder — assigned atomically at flush time
|
|
1533
1534
|
task_id: record.task_id ?? null,
|
|
1534
1535
|
importance: record.importance ?? 5,
|
|
1535
1536
|
status: record.status ?? "active",
|
|
@@ -1563,6 +1564,13 @@ async function flushBatch() {
|
|
|
1563
1564
|
_flushing = true;
|
|
1564
1565
|
try {
|
|
1565
1566
|
const batch = _pendingRecords.slice(0);
|
|
1567
|
+
const client = getClient();
|
|
1568
|
+
const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
|
|
1569
|
+
let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
1570
|
+
for (const row of batch) {
|
|
1571
|
+
row.version = baseVersion++;
|
|
1572
|
+
}
|
|
1573
|
+
_nextVersion = baseVersion;
|
|
1566
1574
|
const buildStmt = (row) => {
|
|
1567
1575
|
const hasVector = row.vector !== null;
|
|
1568
1576
|
const taskId = row.task_id ?? null;
|
package/dist/lib/messaging.js
CHANGED
|
@@ -559,6 +559,42 @@ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSy
|
|
|
559
559
|
import path7 from "path";
|
|
560
560
|
import os4 from "os";
|
|
561
561
|
import { fileURLToPath } from "url";
|
|
562
|
+
import { unlinkSync } from "fs";
|
|
563
|
+
function spawnLockPath(sessionName) {
|
|
564
|
+
return path7.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
565
|
+
}
|
|
566
|
+
function isProcessAlive(pid) {
|
|
567
|
+
try {
|
|
568
|
+
process.kill(pid, 0);
|
|
569
|
+
return true;
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function acquireSpawnLock(sessionName) {
|
|
575
|
+
if (!existsSync7(SPAWN_LOCK_DIR)) {
|
|
576
|
+
mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
|
|
577
|
+
}
|
|
578
|
+
const lockFile = spawnLockPath(sessionName);
|
|
579
|
+
if (existsSync7(lockFile)) {
|
|
580
|
+
try {
|
|
581
|
+
const lock = JSON.parse(readFileSync7(lockFile, "utf8"));
|
|
582
|
+
const age = Date.now() - lock.timestamp;
|
|
583
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
function releaseSpawnLock(sessionName) {
|
|
593
|
+
try {
|
|
594
|
+
unlinkSync(spawnLockPath(sessionName));
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
562
598
|
function resolveBehaviorsExporterScript() {
|
|
563
599
|
try {
|
|
564
600
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -628,10 +664,10 @@ function isEmployeeAlive(sessionName) {
|
|
|
628
664
|
}
|
|
629
665
|
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
630
666
|
const base = employeeSessionName(employeeName, exeSession);
|
|
631
|
-
if (!isAlive(base)) return 0;
|
|
667
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
632
668
|
for (let i = 2; i <= maxInstances; i++) {
|
|
633
669
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
634
|
-
if (!isAlive(candidate)) return i;
|
|
670
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
635
671
|
}
|
|
636
672
|
return null;
|
|
637
673
|
}
|
|
@@ -943,6 +979,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
943
979
|
command: spawnCommand
|
|
944
980
|
});
|
|
945
981
|
if (spawnResult.error) {
|
|
982
|
+
releaseSpawnLock(sessionName);
|
|
946
983
|
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
947
984
|
}
|
|
948
985
|
transport.pipeLog(sessionName, logFile);
|
|
@@ -980,6 +1017,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
980
1017
|
}
|
|
981
1018
|
}
|
|
982
1019
|
if (!booted) {
|
|
1020
|
+
releaseSpawnLock(sessionName);
|
|
983
1021
|
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
984
1022
|
}
|
|
985
1023
|
if (!useExeAgent) {
|
|
@@ -996,9 +1034,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
996
1034
|
pid: 0,
|
|
997
1035
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
998
1036
|
});
|
|
1037
|
+
releaseSpawnLock(sessionName);
|
|
999
1038
|
return { sessionName };
|
|
1000
1039
|
}
|
|
1001
|
-
var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1040
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1002
1041
|
var init_tmux_routing = __esm({
|
|
1003
1042
|
"src/lib/tmux-routing.ts"() {
|
|
1004
1043
|
"use strict";
|
|
@@ -1010,6 +1049,7 @@ var init_tmux_routing = __esm({
|
|
|
1010
1049
|
init_provider_table();
|
|
1011
1050
|
init_intercom_queue();
|
|
1012
1051
|
init_plan_limits();
|
|
1052
|
+
SPAWN_LOCK_DIR = path7.join(os4.homedir(), ".exe-os", "spawn-locks");
|
|
1013
1053
|
SESSION_CACHE = path7.join(os4.homedir(), ".exe-os", "session-cache");
|
|
1014
1054
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
1015
1055
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
package/dist/lib/store.js
CHANGED
|
@@ -1443,7 +1443,8 @@ async function writeMemory(record) {
|
|
|
1443
1443
|
has_error: record.has_error ? 1 : 0,
|
|
1444
1444
|
raw_text: record.raw_text,
|
|
1445
1445
|
vector: record.vector,
|
|
1446
|
-
version:
|
|
1446
|
+
version: 0,
|
|
1447
|
+
// Placeholder — assigned atomically at flush time
|
|
1447
1448
|
task_id: record.task_id ?? null,
|
|
1448
1449
|
importance: record.importance ?? 5,
|
|
1449
1450
|
status: record.status ?? "active",
|
|
@@ -1477,6 +1478,13 @@ async function flushBatch() {
|
|
|
1477
1478
|
_flushing = true;
|
|
1478
1479
|
try {
|
|
1479
1480
|
const batch = _pendingRecords.slice(0);
|
|
1481
|
+
const client = getClient();
|
|
1482
|
+
const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
|
|
1483
|
+
let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
1484
|
+
for (const row of batch) {
|
|
1485
|
+
row.version = baseVersion++;
|
|
1486
|
+
}
|
|
1487
|
+
_nextVersion = baseVersion;
|
|
1480
1488
|
const buildStmt = (row) => {
|
|
1481
1489
|
const hasVector = row.vector !== null;
|
|
1482
1490
|
const taskId = row.task_id ?? null;
|
package/dist/lib/tasks.js
CHANGED
|
@@ -1122,6 +1122,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSy
|
|
|
1122
1122
|
import path9 from "path";
|
|
1123
1123
|
import os5 from "os";
|
|
1124
1124
|
import { fileURLToPath } from "url";
|
|
1125
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
1126
|
+
function spawnLockPath(sessionName) {
|
|
1127
|
+
return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1128
|
+
}
|
|
1129
|
+
function isProcessAlive(pid) {
|
|
1130
|
+
try {
|
|
1131
|
+
process.kill(pid, 0);
|
|
1132
|
+
return true;
|
|
1133
|
+
} catch {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function acquireSpawnLock(sessionName) {
|
|
1138
|
+
if (!existsSync9(SPAWN_LOCK_DIR)) {
|
|
1139
|
+
mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
|
|
1140
|
+
}
|
|
1141
|
+
const lockFile = spawnLockPath(sessionName);
|
|
1142
|
+
if (existsSync9(lockFile)) {
|
|
1143
|
+
try {
|
|
1144
|
+
const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
|
|
1145
|
+
const age = Date.now() - lock.timestamp;
|
|
1146
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
function releaseSpawnLock(sessionName) {
|
|
1156
|
+
try {
|
|
1157
|
+
unlinkSync2(spawnLockPath(sessionName));
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1125
1161
|
function resolveBehaviorsExporterScript() {
|
|
1126
1162
|
try {
|
|
1127
1163
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -1202,10 +1238,10 @@ function isEmployeeAlive(sessionName) {
|
|
|
1202
1238
|
}
|
|
1203
1239
|
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
1204
1240
|
const base = employeeSessionName(employeeName, exeSession);
|
|
1205
|
-
if (!isAlive(base)) return 0;
|
|
1241
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
1206
1242
|
for (let i = 2; i <= maxInstances; i++) {
|
|
1207
1243
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
1208
|
-
if (!isAlive(candidate)) return i;
|
|
1244
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
1209
1245
|
}
|
|
1210
1246
|
return null;
|
|
1211
1247
|
}
|
|
@@ -1539,6 +1575,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1539
1575
|
command: spawnCommand
|
|
1540
1576
|
});
|
|
1541
1577
|
if (spawnResult.error) {
|
|
1578
|
+
releaseSpawnLock(sessionName);
|
|
1542
1579
|
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
1543
1580
|
}
|
|
1544
1581
|
transport.pipeLog(sessionName, logFile);
|
|
@@ -1576,6 +1613,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1576
1613
|
}
|
|
1577
1614
|
}
|
|
1578
1615
|
if (!booted) {
|
|
1616
|
+
releaseSpawnLock(sessionName);
|
|
1579
1617
|
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
1580
1618
|
}
|
|
1581
1619
|
if (!useExeAgent) {
|
|
@@ -1592,9 +1630,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
1592
1630
|
pid: 0,
|
|
1593
1631
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1594
1632
|
});
|
|
1633
|
+
releaseSpawnLock(sessionName);
|
|
1595
1634
|
return { sessionName };
|
|
1596
1635
|
}
|
|
1597
|
-
var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1636
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1598
1637
|
var init_tmux_routing = __esm({
|
|
1599
1638
|
"src/lib/tmux-routing.ts"() {
|
|
1600
1639
|
"use strict";
|
|
@@ -1606,6 +1645,7 @@ var init_tmux_routing = __esm({
|
|
|
1606
1645
|
init_provider_table();
|
|
1607
1646
|
init_intercom_queue();
|
|
1608
1647
|
init_plan_limits();
|
|
1648
|
+
SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
1609
1649
|
SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
|
|
1610
1650
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
1611
1651
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
@@ -1618,7 +1658,7 @@ var init_tmux_routing = __esm({
|
|
|
1618
1658
|
|
|
1619
1659
|
// src/lib/tasks-review.ts
|
|
1620
1660
|
import path10 from "path";
|
|
1621
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as
|
|
1661
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
1622
1662
|
async function countPendingReviews() {
|
|
1623
1663
|
const client = getClient();
|
|
1624
1664
|
const result = await client.execute({
|
|
@@ -1742,7 +1782,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
1742
1782
|
if (existsSync10(cacheDir)) {
|
|
1743
1783
|
for (const f of readdirSync2(cacheDir)) {
|
|
1744
1784
|
if (f.startsWith("review-notified-")) {
|
|
1745
|
-
|
|
1785
|
+
unlinkSync3(path10.join(cacheDir, f));
|
|
1746
1786
|
}
|
|
1747
1787
|
}
|
|
1748
1788
|
}
|
|
@@ -2324,7 +2364,7 @@ var init_skill_learning = __esm({
|
|
|
2324
2364
|
|
|
2325
2365
|
// src/lib/tasks.ts
|
|
2326
2366
|
import path13 from "path";
|
|
2327
|
-
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as
|
|
2367
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
|
|
2328
2368
|
async function createTask(input) {
|
|
2329
2369
|
const result = await createTaskCore(input);
|
|
2330
2370
|
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
@@ -2350,7 +2390,7 @@ async function updateTask(input) {
|
|
|
2350
2390
|
writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
2351
2391
|
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
2352
2392
|
try {
|
|
2353
|
-
|
|
2393
|
+
unlinkSync4(cachePath);
|
|
2354
2394
|
} catch {
|
|
2355
2395
|
}
|
|
2356
2396
|
}
|
package/dist/lib/tmux-routing.js
CHANGED
|
@@ -2299,6 +2299,42 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSy
|
|
|
2299
2299
|
import path13 from "path";
|
|
2300
2300
|
import os5 from "os";
|
|
2301
2301
|
import { fileURLToPath } from "url";
|
|
2302
|
+
import { unlinkSync as unlinkSync4 } from "fs";
|
|
2303
|
+
function spawnLockPath(sessionName) {
|
|
2304
|
+
return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
2305
|
+
}
|
|
2306
|
+
function isProcessAlive(pid) {
|
|
2307
|
+
try {
|
|
2308
|
+
process.kill(pid, 0);
|
|
2309
|
+
return true;
|
|
2310
|
+
} catch {
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
function acquireSpawnLock(sessionName) {
|
|
2315
|
+
if (!existsSync10(SPAWN_LOCK_DIR)) {
|
|
2316
|
+
mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
|
|
2317
|
+
}
|
|
2318
|
+
const lockFile = spawnLockPath(sessionName);
|
|
2319
|
+
if (existsSync10(lockFile)) {
|
|
2320
|
+
try {
|
|
2321
|
+
const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
|
|
2322
|
+
const age = Date.now() - lock.timestamp;
|
|
2323
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
2324
|
+
return false;
|
|
2325
|
+
}
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
2330
|
+
return true;
|
|
2331
|
+
}
|
|
2332
|
+
function releaseSpawnLock(sessionName) {
|
|
2333
|
+
try {
|
|
2334
|
+
unlinkSync4(spawnLockPath(sessionName));
|
|
2335
|
+
} catch {
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2302
2338
|
function resolveBehaviorsExporterScript() {
|
|
2303
2339
|
try {
|
|
2304
2340
|
const thisFile = fileURLToPath(import.meta.url);
|
|
@@ -2397,10 +2433,10 @@ function isEmployeeAlive(sessionName) {
|
|
|
2397
2433
|
}
|
|
2398
2434
|
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
2399
2435
|
const base = employeeSessionName(employeeName, exeSession);
|
|
2400
|
-
if (!isAlive(base)) return 0;
|
|
2436
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
2401
2437
|
for (let i = 2; i <= maxInstances; i++) {
|
|
2402
2438
|
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
2403
|
-
if (!isAlive(candidate)) return i;
|
|
2439
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
2404
2440
|
}
|
|
2405
2441
|
return null;
|
|
2406
2442
|
}
|
|
@@ -2764,6 +2800,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
2764
2800
|
command: spawnCommand
|
|
2765
2801
|
});
|
|
2766
2802
|
if (spawnResult.error) {
|
|
2803
|
+
releaseSpawnLock(sessionName);
|
|
2767
2804
|
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
2768
2805
|
}
|
|
2769
2806
|
transport.pipeLog(sessionName, logFile);
|
|
@@ -2801,6 +2838,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
2801
2838
|
}
|
|
2802
2839
|
}
|
|
2803
2840
|
if (!booted) {
|
|
2841
|
+
releaseSpawnLock(sessionName);
|
|
2804
2842
|
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
2805
2843
|
}
|
|
2806
2844
|
if (!useExeAgent) {
|
|
@@ -2817,9 +2855,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
2817
2855
|
pid: 0,
|
|
2818
2856
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2819
2857
|
});
|
|
2858
|
+
releaseSpawnLock(sessionName);
|
|
2820
2859
|
return { sessionName };
|
|
2821
2860
|
}
|
|
2822
|
-
var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
2861
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
2823
2862
|
var init_tmux_routing = __esm({
|
|
2824
2863
|
"src/lib/tmux-routing.ts"() {
|
|
2825
2864
|
init_session_registry();
|
|
@@ -2830,6 +2869,7 @@ var init_tmux_routing = __esm({
|
|
|
2830
2869
|
init_provider_table();
|
|
2831
2870
|
init_intercom_queue();
|
|
2832
2871
|
init_plan_limits();
|
|
2872
|
+
SPAWN_LOCK_DIR = path13.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
2833
2873
|
SESSION_CACHE = path13.join(os5.homedir(), ".exe-os", "session-cache");
|
|
2834
2874
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
2835
2875
|
VERIFY_PANE_LINES = 200;
|
|
@@ -2842,6 +2882,7 @@ var init_tmux_routing = __esm({
|
|
|
2842
2882
|
});
|
|
2843
2883
|
init_tmux_routing();
|
|
2844
2884
|
export {
|
|
2885
|
+
acquireSpawnLock,
|
|
2845
2886
|
employeeSessionName,
|
|
2846
2887
|
ensureEmployee,
|
|
2847
2888
|
extractRootExe,
|
|
@@ -2856,6 +2897,7 @@ export {
|
|
|
2856
2897
|
notifyParentExe,
|
|
2857
2898
|
parseParentExe,
|
|
2858
2899
|
registerParentExe,
|
|
2900
|
+
releaseSpawnLock,
|
|
2859
2901
|
resolveExeSession,
|
|
2860
2902
|
sendIntercom,
|
|
2861
2903
|
spawnEmployee,
|