@askexenow/exe-os 0.8.40 → 0.8.42
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 +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1376 -659
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2549 -1784
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +12 -2
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +791 -634
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2501 -1846
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +44 -12
- package/dist/bin/exe-new-employee.js +6 -2
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +176 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +157 -4
- package/dist/bin/exe-session-cleanup.js +2487 -403
- package/dist/bin/exe-settings.js +2 -1
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2691 -151
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +50 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2542 -1887
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2690 -150
- package/dist/hooks/error-recall.js +157 -4
- package/dist/hooks/ingest-worker.js +1455 -803
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1670 -1509
- package/dist/hooks/prompt-submit.js +2650 -1074
- package/dist/hooks/response-ingest-worker.js +154 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +157 -4
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +155 -2
- package/dist/hooks/summary-worker.js +190 -21
- package/dist/index.js +326 -102
- package/dist/lib/cloud-sync.js +31 -10
- package/dist/lib/config.js +2 -0
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/embedder.js +3 -1
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +285 -18
- package/dist/lib/hybrid-search.js +157 -4
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1485 -918
- package/dist/lib/tmux-routing.js +194 -10
- package/dist/mcp/server.js +1643 -924
- package/dist/mcp/tools/create-task.js +2283 -829
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +79 -0
- package/dist/runtime/index.js +280 -68
- package/dist/tui/App.js +1485 -645
- package/package.json +3 -2
package/dist/gateway/index.js
CHANGED
|
@@ -19,6 +19,61 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
19
19
|
};
|
|
20
20
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
21
|
|
|
22
|
+
// src/lib/state-bus.ts
|
|
23
|
+
var StateBus, orgBus;
|
|
24
|
+
var init_state_bus = __esm({
|
|
25
|
+
"src/lib/state-bus.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
StateBus = class {
|
|
28
|
+
handlers = /* @__PURE__ */ new Map();
|
|
29
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
30
|
+
/** Emit an event to all subscribers */
|
|
31
|
+
emit(event) {
|
|
32
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
33
|
+
if (typeHandlers) {
|
|
34
|
+
for (const handler of typeHandlers) {
|
|
35
|
+
try {
|
|
36
|
+
handler(event);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const handler of this.globalHandlers) {
|
|
42
|
+
try {
|
|
43
|
+
handler(event);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Subscribe to a specific event type */
|
|
49
|
+
on(type, handler) {
|
|
50
|
+
if (!this.handlers.has(type)) {
|
|
51
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
52
|
+
}
|
|
53
|
+
this.handlers.get(type).add(handler);
|
|
54
|
+
}
|
|
55
|
+
/** Subscribe to ALL events */
|
|
56
|
+
onAny(handler) {
|
|
57
|
+
this.globalHandlers.add(handler);
|
|
58
|
+
}
|
|
59
|
+
/** Unsubscribe from a specific event type */
|
|
60
|
+
off(type, handler) {
|
|
61
|
+
this.handlers.get(type)?.delete(handler);
|
|
62
|
+
}
|
|
63
|
+
/** Unsubscribe from ALL events */
|
|
64
|
+
offAny(handler) {
|
|
65
|
+
this.globalHandlers.delete(handler);
|
|
66
|
+
}
|
|
67
|
+
/** Remove all listeners */
|
|
68
|
+
clear() {
|
|
69
|
+
this.handlers.clear();
|
|
70
|
+
this.globalHandlers.clear();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
orgBus = new StateBus();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
22
77
|
// src/gateway/crm-bridge.ts
|
|
23
78
|
var crm_bridge_exports = {};
|
|
24
79
|
__export(crm_bridge_exports, {
|
|
@@ -568,6 +623,13 @@ async function ensureSchema() {
|
|
|
568
623
|
});
|
|
569
624
|
} catch {
|
|
570
625
|
}
|
|
626
|
+
try {
|
|
627
|
+
await client.execute({
|
|
628
|
+
sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
|
|
629
|
+
args: []
|
|
630
|
+
});
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
571
633
|
try {
|
|
572
634
|
await client.execute({
|
|
573
635
|
sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
|
|
@@ -1014,6 +1076,18 @@ async function ensureSchema() {
|
|
|
1014
1076
|
CREATE INDEX IF NOT EXISTS idx_session_kills_agent
|
|
1015
1077
|
ON session_kills(agent_id);
|
|
1016
1078
|
`);
|
|
1079
|
+
await client.execute(`
|
|
1080
|
+
CREATE TABLE IF NOT EXISTS global_procedures (
|
|
1081
|
+
id TEXT PRIMARY KEY,
|
|
1082
|
+
title TEXT NOT NULL,
|
|
1083
|
+
content TEXT NOT NULL,
|
|
1084
|
+
priority TEXT NOT NULL DEFAULT 'p0',
|
|
1085
|
+
domain TEXT,
|
|
1086
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
1087
|
+
created_at TEXT NOT NULL,
|
|
1088
|
+
updated_at TEXT NOT NULL
|
|
1089
|
+
)
|
|
1090
|
+
`);
|
|
1017
1091
|
await client.executeMultiple(`
|
|
1018
1092
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
1019
1093
|
id TEXT PRIMARY KEY,
|
|
@@ -1185,6 +1259,7 @@ var config_exports = {};
|
|
|
1185
1259
|
__export(config_exports, {
|
|
1186
1260
|
CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
|
|
1187
1261
|
CONFIG_PATH: () => CONFIG_PATH,
|
|
1262
|
+
COO_AGENT_NAME: () => COO_AGENT_NAME,
|
|
1188
1263
|
CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
|
|
1189
1264
|
DB_PATH: () => DB_PATH,
|
|
1190
1265
|
EXE_AI_DIR: () => EXE_AI_DIR,
|
|
@@ -1340,7 +1415,7 @@ async function loadConfigFrom(configPath) {
|
|
|
1340
1415
|
return { ...DEFAULT_CONFIG };
|
|
1341
1416
|
}
|
|
1342
1417
|
}
|
|
1343
|
-
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1418
|
+
var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
|
|
1344
1419
|
var init_config = __esm({
|
|
1345
1420
|
"src/lib/config.ts"() {
|
|
1346
1421
|
"use strict";
|
|
@@ -1348,6 +1423,7 @@ var init_config = __esm({
|
|
|
1348
1423
|
DB_PATH = path.join(EXE_AI_DIR, "memories.db");
|
|
1349
1424
|
MODELS_DIR = path.join(EXE_AI_DIR, "models");
|
|
1350
1425
|
CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
|
|
1426
|
+
COO_AGENT_NAME = "exe";
|
|
1351
1427
|
LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
|
|
1352
1428
|
CURRENT_CONFIG_VERSION = 1;
|
|
1353
1429
|
DEFAULT_CONFIG = {
|
|
@@ -2126,6 +2202,71 @@ var init_shard_manager = __esm({
|
|
|
2126
2202
|
}
|
|
2127
2203
|
});
|
|
2128
2204
|
|
|
2205
|
+
// src/lib/global-procedures.ts
|
|
2206
|
+
var global_procedures_exports = {};
|
|
2207
|
+
__export(global_procedures_exports, {
|
|
2208
|
+
deactivateGlobalProcedure: () => deactivateGlobalProcedure,
|
|
2209
|
+
getGlobalProceduresBlock: () => getGlobalProceduresBlock,
|
|
2210
|
+
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
2211
|
+
storeGlobalProcedure: () => storeGlobalProcedure
|
|
2212
|
+
});
|
|
2213
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2214
|
+
async function loadGlobalProcedures() {
|
|
2215
|
+
const client = getClient();
|
|
2216
|
+
const result = await client.execute({
|
|
2217
|
+
sql: "SELECT * FROM global_procedures WHERE active = 1 ORDER BY priority ASC, created_at ASC",
|
|
2218
|
+
args: []
|
|
2219
|
+
});
|
|
2220
|
+
const procedures = result.rows;
|
|
2221
|
+
if (procedures.length > 0) {
|
|
2222
|
+
_cache = procedures.map((p) => `### ${p.title}
|
|
2223
|
+
${p.content}`).join("\n\n");
|
|
2224
|
+
} else {
|
|
2225
|
+
_cache = "";
|
|
2226
|
+
}
|
|
2227
|
+
_cacheLoaded = true;
|
|
2228
|
+
return procedures;
|
|
2229
|
+
}
|
|
2230
|
+
function getGlobalProceduresBlock() {
|
|
2231
|
+
if (!_cacheLoaded) return "";
|
|
2232
|
+
if (!_cache) return "";
|
|
2233
|
+
return `## Organization-Wide Procedures (MANDATORY \u2014 supersedes all other rules)
|
|
2234
|
+
|
|
2235
|
+
${_cache}
|
|
2236
|
+
`;
|
|
2237
|
+
}
|
|
2238
|
+
async function storeGlobalProcedure(input) {
|
|
2239
|
+
const id = randomUUID2();
|
|
2240
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2241
|
+
const client = getClient();
|
|
2242
|
+
await client.execute({
|
|
2243
|
+
sql: `INSERT INTO global_procedures (id, title, content, priority, domain, active, created_at, updated_at)
|
|
2244
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
2245
|
+
args: [id, input.title, input.content, input.priority ?? "p0", input.domain ?? null, now, now]
|
|
2246
|
+
});
|
|
2247
|
+
await loadGlobalProcedures();
|
|
2248
|
+
return id;
|
|
2249
|
+
}
|
|
2250
|
+
async function deactivateGlobalProcedure(id) {
|
|
2251
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2252
|
+
const client = getClient();
|
|
2253
|
+
const result = await client.execute({
|
|
2254
|
+
sql: "UPDATE global_procedures SET active = 0, updated_at = ? WHERE id = ?",
|
|
2255
|
+
args: [now, id]
|
|
2256
|
+
});
|
|
2257
|
+
await loadGlobalProcedures();
|
|
2258
|
+
return result.rowsAffected > 0;
|
|
2259
|
+
}
|
|
2260
|
+
var _cache, _cacheLoaded;
|
|
2261
|
+
var init_global_procedures = __esm({
|
|
2262
|
+
"src/lib/global-procedures.ts"() {
|
|
2263
|
+
"use strict";
|
|
2264
|
+
init_database();
|
|
2265
|
+
_cache = "";
|
|
2266
|
+
_cacheLoaded = false;
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2129
2270
|
// src/lib/store.ts
|
|
2130
2271
|
var store_exports = {};
|
|
2131
2272
|
__export(store_exports, {
|
|
@@ -2205,6 +2346,11 @@ async function initStore(options) {
|
|
|
2205
2346
|
"version-query"
|
|
2206
2347
|
);
|
|
2207
2348
|
_nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
|
|
2349
|
+
try {
|
|
2350
|
+
const { loadGlobalProcedures: loadGlobalProcedures2 } = await Promise.resolve().then(() => (init_global_procedures(), global_procedures_exports));
|
|
2351
|
+
await loadGlobalProcedures2();
|
|
2352
|
+
} catch {
|
|
2353
|
+
}
|
|
2208
2354
|
}
|
|
2209
2355
|
function classifyTier(record) {
|
|
2210
2356
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -2246,6 +2392,12 @@ async function writeMemory(record) {
|
|
|
2246
2392
|
supersedes_id: record.supersedes_id ?? null
|
|
2247
2393
|
};
|
|
2248
2394
|
_pendingRecords.push(dbRow);
|
|
2395
|
+
orgBus.emit({
|
|
2396
|
+
type: "memory_stored",
|
|
2397
|
+
agentId: record.agent_id,
|
|
2398
|
+
project: record.project_name,
|
|
2399
|
+
timestamp: record.timestamp
|
|
2400
|
+
});
|
|
2249
2401
|
const MAX_PENDING = 1e3;
|
|
2250
2402
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
2251
2403
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -2591,6 +2743,7 @@ var init_store = __esm({
|
|
|
2591
2743
|
init_database();
|
|
2592
2744
|
init_keychain();
|
|
2593
2745
|
init_config();
|
|
2746
|
+
init_state_bus();
|
|
2594
2747
|
INIT_MAX_RETRIES = 3;
|
|
2595
2748
|
INIT_RETRY_DELAY_MS = 1e3;
|
|
2596
2749
|
_pendingRecords = [];
|
|
@@ -3130,7 +3283,7 @@ var init_employees = __esm({
|
|
|
3130
3283
|
|
|
3131
3284
|
// src/lib/license.ts
|
|
3132
3285
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
3133
|
-
import { randomUUID as
|
|
3286
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
3134
3287
|
import path9 from "path";
|
|
3135
3288
|
import { jwtVerify, importSPKI } from "jose";
|
|
3136
3289
|
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
@@ -3229,2112 +3382,2607 @@ var init_plan_limits = __esm({
|
|
|
3229
3382
|
}
|
|
3230
3383
|
});
|
|
3231
3384
|
|
|
3232
|
-
// src/lib/
|
|
3233
|
-
import
|
|
3234
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync10, appendFileSync } from "fs";
|
|
3385
|
+
// src/lib/notifications.ts
|
|
3386
|
+
import crypto3 from "crypto";
|
|
3235
3387
|
import path11 from "path";
|
|
3236
3388
|
import os6 from "os";
|
|
3237
|
-
import {
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3389
|
+
import {
|
|
3390
|
+
readFileSync as readFileSync9,
|
|
3391
|
+
readdirSync as readdirSync2,
|
|
3392
|
+
unlinkSync as unlinkSync2,
|
|
3393
|
+
existsSync as existsSync10,
|
|
3394
|
+
rmdirSync
|
|
3395
|
+
} from "fs";
|
|
3396
|
+
async function writeNotification(notification) {
|
|
3243
3397
|
try {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3398
|
+
const client = getClient();
|
|
3399
|
+
const id = crypto3.randomUUID();
|
|
3400
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3401
|
+
await client.execute({
|
|
3402
|
+
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
3403
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
3404
|
+
args: [
|
|
3405
|
+
id,
|
|
3406
|
+
notification.agentId,
|
|
3407
|
+
notification.agentRole,
|
|
3408
|
+
notification.event,
|
|
3409
|
+
notification.project,
|
|
3410
|
+
notification.summary,
|
|
3411
|
+
notification.taskFile ?? null,
|
|
3412
|
+
now
|
|
3413
|
+
]
|
|
3414
|
+
});
|
|
3415
|
+
} catch (err) {
|
|
3416
|
+
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
3417
|
+
`);
|
|
3264
3418
|
}
|
|
3265
|
-
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
3266
|
-
return true;
|
|
3267
3419
|
}
|
|
3268
|
-
function
|
|
3420
|
+
async function markAsReadByTaskFile(taskFile) {
|
|
3269
3421
|
try {
|
|
3270
|
-
|
|
3422
|
+
const client = getClient();
|
|
3423
|
+
await client.execute({
|
|
3424
|
+
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
3425
|
+
args: [taskFile]
|
|
3426
|
+
});
|
|
3271
3427
|
} catch {
|
|
3272
3428
|
}
|
|
3273
3429
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
path11.dirname(thisFile),
|
|
3279
|
-
"..",
|
|
3280
|
-
"bin",
|
|
3281
|
-
"exe-export-behaviors.js"
|
|
3282
|
-
);
|
|
3283
|
-
return existsSync10(scriptPath) ? scriptPath : null;
|
|
3284
|
-
} catch {
|
|
3285
|
-
return null;
|
|
3430
|
+
var init_notifications = __esm({
|
|
3431
|
+
"src/lib/notifications.ts"() {
|
|
3432
|
+
"use strict";
|
|
3433
|
+
init_database();
|
|
3286
3434
|
}
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
// src/lib/session-kill-telemetry.ts
|
|
3438
|
+
import crypto4 from "crypto";
|
|
3439
|
+
async function recordSessionKill(input) {
|
|
3291
3440
|
try {
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3441
|
+
const client = getClient();
|
|
3442
|
+
await client.execute({
|
|
3443
|
+
sql: `INSERT INTO session_kills
|
|
3444
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
3445
|
+
ticks_idle, estimated_tokens_saved)
|
|
3446
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
3447
|
+
args: [
|
|
3448
|
+
crypto4.randomUUID(),
|
|
3449
|
+
input.sessionName,
|
|
3450
|
+
input.agentId,
|
|
3451
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
3452
|
+
input.reason,
|
|
3453
|
+
input.ticksIdle ?? null,
|
|
3454
|
+
input.estimatedTokensSaved ?? null
|
|
3455
|
+
]
|
|
3456
|
+
});
|
|
3298
3457
|
} catch (err) {
|
|
3299
3458
|
process.stderr.write(
|
|
3300
|
-
`[
|
|
3459
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
3301
3460
|
`
|
|
3302
3461
|
);
|
|
3303
|
-
return null;
|
|
3304
|
-
}
|
|
3305
|
-
}
|
|
3306
|
-
function getMySession() {
|
|
3307
|
-
return getTransport().getMySession();
|
|
3308
|
-
}
|
|
3309
|
-
function employeeSessionName(employee, exeSession, instance) {
|
|
3310
|
-
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
3311
|
-
return `${employee}${suffix}-${exeSession}`;
|
|
3312
|
-
}
|
|
3313
|
-
function extractRootExe(name) {
|
|
3314
|
-
const match = name.match(/(exe\d+)$/);
|
|
3315
|
-
return match?.[1] ?? null;
|
|
3316
|
-
}
|
|
3317
|
-
function getParentExe(sessionKey) {
|
|
3318
|
-
try {
|
|
3319
|
-
const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
3320
|
-
return data.parentExe || null;
|
|
3321
|
-
} catch {
|
|
3322
|
-
return null;
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
function getDispatchedBy(sessionKey) {
|
|
3326
|
-
try {
|
|
3327
|
-
const data = JSON.parse(readFileSync9(
|
|
3328
|
-
path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
3329
|
-
"utf8"
|
|
3330
|
-
));
|
|
3331
|
-
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
3332
|
-
} catch {
|
|
3333
|
-
return null;
|
|
3334
3462
|
}
|
|
3335
3463
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
const key = getSessionKey();
|
|
3341
|
-
const parentExe = getParentExe(key);
|
|
3342
|
-
if (parentExe) {
|
|
3343
|
-
return extractRootExe(parentExe) ?? parentExe;
|
|
3344
|
-
}
|
|
3345
|
-
} catch {
|
|
3464
|
+
var init_session_kill_telemetry = __esm({
|
|
3465
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
3466
|
+
"use strict";
|
|
3467
|
+
init_database();
|
|
3346
3468
|
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
// src/lib/tasks-crud.ts
|
|
3472
|
+
import crypto5 from "crypto";
|
|
3473
|
+
import path12 from "path";
|
|
3474
|
+
import { execSync as execSync4 } from "child_process";
|
|
3475
|
+
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
3476
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
3477
|
+
async function writeCheckpoint(input) {
|
|
3478
|
+
const client = getClient();
|
|
3479
|
+
const row = await resolveTask(client, input.taskId);
|
|
3480
|
+
const taskId = String(row.id);
|
|
3481
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3482
|
+
const blockedByIds = [];
|
|
3483
|
+
if (row.blocked_by) {
|
|
3484
|
+
blockedByIds.push(String(row.blocked_by));
|
|
3358
3485
|
}
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3486
|
+
const checkpoint = {
|
|
3487
|
+
step: input.step,
|
|
3488
|
+
context_summary: input.contextSummary,
|
|
3489
|
+
files_touched: input.filesTouched ?? [],
|
|
3490
|
+
blocked_by_ids: blockedByIds,
|
|
3491
|
+
last_checkpoint_at: now
|
|
3492
|
+
};
|
|
3493
|
+
const result = await client.execute({
|
|
3494
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
3495
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
3496
|
+
});
|
|
3497
|
+
if (result.rowsAffected === 0) {
|
|
3498
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
3367
3499
|
}
|
|
3500
|
+
const countResult = await client.execute({
|
|
3501
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
3502
|
+
args: [taskId]
|
|
3503
|
+
});
|
|
3504
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
3505
|
+
return { checkpointCount };
|
|
3368
3506
|
}
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3507
|
+
function extractParentFromContext(contextBody) {
|
|
3508
|
+
if (!contextBody) return null;
|
|
3509
|
+
const match = contextBody.match(
|
|
3510
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
3511
|
+
);
|
|
3512
|
+
return match ? match[1].toLowerCase() : null;
|
|
3375
3513
|
}
|
|
3376
|
-
function
|
|
3377
|
-
|
|
3378
|
-
const lastSent = state[targetSession] ?? 0;
|
|
3379
|
-
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
3514
|
+
function slugify(title) {
|
|
3515
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3380
3516
|
}
|
|
3381
|
-
function
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3517
|
+
async function resolveTask(client, identifier) {
|
|
3518
|
+
let result = await client.execute({
|
|
3519
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
3520
|
+
args: [identifier]
|
|
3521
|
+
});
|
|
3522
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3523
|
+
result = await client.execute({
|
|
3524
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
3525
|
+
args: [`%${identifier}%`]
|
|
3526
|
+
});
|
|
3527
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3528
|
+
if (result.rows.length > 1) {
|
|
3529
|
+
const exact = result.rows.filter(
|
|
3530
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
3531
|
+
);
|
|
3532
|
+
if (exact.length === 1) return exact[0];
|
|
3533
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
3534
|
+
const active = candidates.filter(
|
|
3535
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3536
|
+
);
|
|
3537
|
+
if (active.length === 1) return active[0];
|
|
3538
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3539
|
+
throw new Error(
|
|
3540
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3541
|
+
);
|
|
3387
3542
|
}
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3543
|
+
result = await client.execute({
|
|
3544
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
3545
|
+
args: [`%${identifier}%`]
|
|
3546
|
+
});
|
|
3547
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
3548
|
+
if (result.rows.length > 1) {
|
|
3549
|
+
const active = result.rows.filter(
|
|
3550
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
3551
|
+
);
|
|
3552
|
+
if (active.length === 1) return active[0];
|
|
3553
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
3554
|
+
throw new Error(
|
|
3555
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
3556
|
+
);
|
|
3398
3557
|
}
|
|
3558
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
3399
3559
|
}
|
|
3400
|
-
function
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3560
|
+
async function createTaskCore(input) {
|
|
3561
|
+
const client = getClient();
|
|
3562
|
+
const id = crypto5.randomUUID();
|
|
3563
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3564
|
+
const slug = slugify(input.title);
|
|
3565
|
+
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
3566
|
+
let blockedById = null;
|
|
3567
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
3568
|
+
if (input.blockedBy) {
|
|
3569
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
3570
|
+
blockedById = String(blocker.id);
|
|
3571
|
+
}
|
|
3572
|
+
let parentTaskId = null;
|
|
3573
|
+
let parentRef = input.parentTaskId;
|
|
3574
|
+
if (!parentRef) {
|
|
3575
|
+
const extracted = extractParentFromContext(input.context);
|
|
3576
|
+
if (extracted) {
|
|
3577
|
+
parentRef = extracted;
|
|
3578
|
+
process.stderr.write(
|
|
3579
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
if (parentRef) {
|
|
3584
|
+
try {
|
|
3585
|
+
const parent = await resolveTask(client, parentRef);
|
|
3586
|
+
parentTaskId = String(parent.id);
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
if (!input.parentTaskId) {
|
|
3589
|
+
throw new Error(
|
|
3590
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
3591
|
+
);
|
|
3408
3592
|
}
|
|
3593
|
+
throw err;
|
|
3409
3594
|
}
|
|
3410
|
-
if (/Running…/.test(pane)) return "tool";
|
|
3411
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3412
|
-
return "idle";
|
|
3413
|
-
} catch {
|
|
3414
|
-
return "offline";
|
|
3415
3595
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
3424
|
-
return "skipped_exe";
|
|
3596
|
+
let warning;
|
|
3597
|
+
const dupCheck = await client.execute({
|
|
3598
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
3599
|
+
args: [input.title, input.assignedTo]
|
|
3600
|
+
});
|
|
3601
|
+
if (dupCheck.rows.length > 0) {
|
|
3602
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
3425
3603
|
}
|
|
3426
|
-
if (
|
|
3427
|
-
|
|
3428
|
-
|
|
3604
|
+
if (input.baseDir) {
|
|
3605
|
+
try {
|
|
3606
|
+
await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
3607
|
+
await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
3608
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
3609
|
+
await ensureGitignoreExe(input.baseDir);
|
|
3610
|
+
} catch {
|
|
3611
|
+
}
|
|
3429
3612
|
}
|
|
3613
|
+
const complexity = input.complexity ?? "standard";
|
|
3614
|
+
let sessionScope = null;
|
|
3430
3615
|
try {
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3433
|
-
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
3434
|
-
return "failed";
|
|
3435
|
-
}
|
|
3436
|
-
const sessionState = getSessionState(targetSession);
|
|
3437
|
-
if (sessionState === "no_claude") {
|
|
3438
|
-
queueIntercom(targetSession, "claude not running in session");
|
|
3439
|
-
recordDebounce(targetSession);
|
|
3440
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
3441
|
-
return "queued";
|
|
3442
|
-
}
|
|
3443
|
-
if (sessionState === "thinking" || sessionState === "tool") {
|
|
3444
|
-
queueIntercom(targetSession, "session busy at send time");
|
|
3445
|
-
recordDebounce(targetSession);
|
|
3446
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3447
|
-
return "queued";
|
|
3448
|
-
}
|
|
3449
|
-
if (transport.isPaneInCopyMode(targetSession)) {
|
|
3450
|
-
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
3451
|
-
transport.sendKeys(targetSession, "q");
|
|
3452
|
-
}
|
|
3453
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3454
|
-
recordDebounce(targetSession);
|
|
3455
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3456
|
-
return "delivered";
|
|
3616
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3617
|
+
sessionScope = resolveExeSession2();
|
|
3457
3618
|
} catch {
|
|
3458
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3459
|
-
return "failed";
|
|
3460
3619
|
}
|
|
3620
|
+
await client.execute({
|
|
3621
|
+
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
|
|
3622
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3623
|
+
args: [
|
|
3624
|
+
id,
|
|
3625
|
+
input.title,
|
|
3626
|
+
input.assignedTo,
|
|
3627
|
+
input.assignedBy,
|
|
3628
|
+
input.projectName,
|
|
3629
|
+
input.priority,
|
|
3630
|
+
initialStatus,
|
|
3631
|
+
taskFile,
|
|
3632
|
+
blockedById,
|
|
3633
|
+
parentTaskId,
|
|
3634
|
+
input.reviewer ?? null,
|
|
3635
|
+
input.context,
|
|
3636
|
+
complexity,
|
|
3637
|
+
input.budgetTokens ?? null,
|
|
3638
|
+
input.budgetFallbackModel ?? null,
|
|
3639
|
+
0,
|
|
3640
|
+
null,
|
|
3641
|
+
sessionScope,
|
|
3642
|
+
now,
|
|
3643
|
+
now
|
|
3644
|
+
]
|
|
3645
|
+
});
|
|
3646
|
+
return {
|
|
3647
|
+
id,
|
|
3648
|
+
title: input.title,
|
|
3649
|
+
assignedTo: input.assignedTo,
|
|
3650
|
+
assignedBy: input.assignedBy,
|
|
3651
|
+
projectName: input.projectName,
|
|
3652
|
+
priority: input.priority,
|
|
3653
|
+
status: initialStatus,
|
|
3654
|
+
taskFile,
|
|
3655
|
+
createdAt: now,
|
|
3656
|
+
updatedAt: now,
|
|
3657
|
+
warning,
|
|
3658
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
3659
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
3660
|
+
tokensUsed: 0,
|
|
3661
|
+
tokensWarnedAt: null
|
|
3662
|
+
};
|
|
3461
3663
|
}
|
|
3462
|
-
function
|
|
3463
|
-
const
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3664
|
+
async function listTasks(input) {
|
|
3665
|
+
const client = getClient();
|
|
3666
|
+
const conditions = [];
|
|
3667
|
+
const args = [];
|
|
3668
|
+
if (input.assignedTo) {
|
|
3669
|
+
conditions.push("assigned_to = ?");
|
|
3670
|
+
args.push(input.assignedTo);
|
|
3468
3671
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
if (rootExe && rootExe !== target) {
|
|
3475
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3476
|
-
`);
|
|
3477
|
-
const fallback = sendIntercom(rootExe);
|
|
3478
|
-
return fallback !== "failed";
|
|
3479
|
-
}
|
|
3480
|
-
return false;
|
|
3672
|
+
if (input.status) {
|
|
3673
|
+
conditions.push("status = ?");
|
|
3674
|
+
args.push(input.status);
|
|
3675
|
+
} else {
|
|
3676
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
3481
3677
|
}
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3678
|
+
if (input.projectName) {
|
|
3679
|
+
conditions.push("project_name = ?");
|
|
3680
|
+
args.push(input.projectName);
|
|
3681
|
+
}
|
|
3682
|
+
if (input.priority) {
|
|
3683
|
+
conditions.push("priority = ?");
|
|
3684
|
+
args.push(input.priority);
|
|
3487
3685
|
}
|
|
3488
3686
|
try {
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
if (
|
|
3492
|
-
|
|
3687
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3688
|
+
const session = resolveExeSession2();
|
|
3689
|
+
if (session) {
|
|
3690
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
3691
|
+
args.push(session);
|
|
3493
3692
|
}
|
|
3693
|
+
} catch {
|
|
3494
3694
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
)
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
3518
|
-
}
|
|
3519
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3520
|
-
if (isEmployeeAlive(sessionName)) {
|
|
3521
|
-
const result2 = sendIntercom(sessionName);
|
|
3522
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3523
|
-
return { status: "intercom_sent", sessionName };
|
|
3524
|
-
}
|
|
3525
|
-
if (result2 === "delivered") {
|
|
3526
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
3527
|
-
}
|
|
3528
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3529
|
-
}
|
|
3530
|
-
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
3531
|
-
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
3532
|
-
if (result.error) {
|
|
3533
|
-
return { status: "failed", sessionName, error: result.error };
|
|
3534
|
-
}
|
|
3535
|
-
return { status: "spawned", sessionName };
|
|
3695
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3696
|
+
const result = await client.execute({
|
|
3697
|
+
sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
|
|
3698
|
+
args
|
|
3699
|
+
});
|
|
3700
|
+
return result.rows.map((r) => ({
|
|
3701
|
+
id: String(r.id),
|
|
3702
|
+
title: String(r.title),
|
|
3703
|
+
assignedTo: String(r.assigned_to),
|
|
3704
|
+
assignedBy: String(r.assigned_by),
|
|
3705
|
+
projectName: String(r.project_name),
|
|
3706
|
+
priority: String(r.priority),
|
|
3707
|
+
status: String(r.status),
|
|
3708
|
+
taskFile: String(r.task_file),
|
|
3709
|
+
createdAt: String(r.created_at),
|
|
3710
|
+
updatedAt: String(r.updated_at),
|
|
3711
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
3712
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
3713
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
3714
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
3715
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
3716
|
+
}));
|
|
3536
3717
|
}
|
|
3537
|
-
function
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3541
|
-
const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
|
|
3542
|
-
const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3543
|
-
if (!existsSync10(logDir)) {
|
|
3544
|
-
mkdirSync6(logDir, { recursive: true });
|
|
3545
|
-
}
|
|
3546
|
-
transport.kill(sessionName);
|
|
3547
|
-
let cleanupSuffix = "";
|
|
3548
|
-
try {
|
|
3549
|
-
const thisFile = fileURLToPath2(import.meta.url);
|
|
3550
|
-
const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
3551
|
-
if (existsSync10(cleanupScript)) {
|
|
3552
|
-
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
3553
|
-
}
|
|
3554
|
-
} catch {
|
|
3555
|
-
}
|
|
3556
|
-
try {
|
|
3557
|
-
const claudeJsonPath = path11.join(os6.homedir(), ".claude.json");
|
|
3558
|
-
let claudeJson = {};
|
|
3559
|
-
try {
|
|
3560
|
-
claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
|
|
3561
|
-
} catch {
|
|
3562
|
-
}
|
|
3563
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3564
|
-
const projects = claudeJson.projects;
|
|
3565
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
3566
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3567
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3568
|
-
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3569
|
-
} catch {
|
|
3570
|
-
}
|
|
3718
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3719
|
+
if (!taskContext) return null;
|
|
3720
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
3571
3721
|
try {
|
|
3572
|
-
const
|
|
3573
|
-
const
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
const
|
|
3583
|
-
|
|
3584
|
-
"
|
|
3585
|
-
"store_memory",
|
|
3586
|
-
"create_task",
|
|
3587
|
-
"update_task",
|
|
3588
|
-
"list_tasks",
|
|
3589
|
-
"get_task",
|
|
3590
|
-
"ask_team_memory",
|
|
3591
|
-
"store_behavior",
|
|
3592
|
-
"get_identity",
|
|
3593
|
-
"send_message"
|
|
3594
|
-
];
|
|
3595
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3596
|
-
let changed = false;
|
|
3597
|
-
for (const tool of requiredTools) {
|
|
3598
|
-
if (!allow.includes(tool)) {
|
|
3599
|
-
allow.push(tool);
|
|
3600
|
-
changed = true;
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
if (changed) {
|
|
3604
|
-
perms.allow = allow;
|
|
3605
|
-
settings.permissions = perms;
|
|
3606
|
-
mkdirSync6(projSettingsDir, { recursive: true });
|
|
3607
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3722
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
3723
|
+
const branch = execSync4(
|
|
3724
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
3725
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
3726
|
+
).trim();
|
|
3727
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
3728
|
+
const commitCount = execSync4(
|
|
3729
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
3730
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3731
|
+
).trim();
|
|
3732
|
+
const count = parseInt(commitCount, 10);
|
|
3733
|
+
if (count === 0) {
|
|
3734
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
3608
3735
|
}
|
|
3736
|
+
return null;
|
|
3609
3737
|
} catch {
|
|
3738
|
+
return null;
|
|
3610
3739
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
const
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
if (
|
|
3619
|
-
const identityPath = path11.join(
|
|
3620
|
-
os6.homedir(),
|
|
3621
|
-
".exe-os",
|
|
3622
|
-
"identity",
|
|
3623
|
-
`${employeeName}.md`
|
|
3624
|
-
);
|
|
3625
|
-
_resetCcAgentSupportCache();
|
|
3626
|
-
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
3627
|
-
if (hasAgentFlag) {
|
|
3628
|
-
identityFlag = ` --agent ${employeeName}`;
|
|
3629
|
-
} else if (existsSync10(identityPath)) {
|
|
3630
|
-
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
3631
|
-
legacyFallbackWarned = true;
|
|
3632
|
-
}
|
|
3633
|
-
const behaviorsFile = exportBehaviorsSync(
|
|
3634
|
-
employeeName,
|
|
3635
|
-
path11.basename(spawnCwd),
|
|
3636
|
-
sessionName
|
|
3637
|
-
);
|
|
3638
|
-
if (behaviorsFile) {
|
|
3639
|
-
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
if (legacyFallbackWarned) {
|
|
3740
|
+
}
|
|
3741
|
+
async function updateTaskStatus(input) {
|
|
3742
|
+
const client = getClient();
|
|
3743
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3744
|
+
const row = await resolveTask(client, input.taskId);
|
|
3745
|
+
const taskId = String(row.id);
|
|
3746
|
+
const taskFile = String(row.task_file);
|
|
3747
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
3643
3748
|
process.stderr.write(
|
|
3644
|
-
`[
|
|
3749
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3645
3750
|
`
|
|
3646
3751
|
);
|
|
3647
3752
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
}
|
|
3663
|
-
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
3664
|
-
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3665
|
-
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3666
|
-
if (cfg?.apiKeyEnv) {
|
|
3667
|
-
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3668
|
-
if (keyVal) {
|
|
3669
|
-
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3753
|
+
if (input.status === "done") {
|
|
3754
|
+
const existingRow = await client.execute({
|
|
3755
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
3756
|
+
args: [taskId]
|
|
3757
|
+
});
|
|
3758
|
+
if (existingRow.rows.length > 0) {
|
|
3759
|
+
const ctx = existingRow.rows[0];
|
|
3760
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
3761
|
+
if (warning) {
|
|
3762
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
3763
|
+
|
|
3764
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
3765
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
3766
|
+
`);
|
|
3670
3767
|
}
|
|
3671
3768
|
}
|
|
3672
3769
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
)
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3770
|
+
if (input.status === "in_progress") {
|
|
3771
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
3772
|
+
const claim = await client.execute({
|
|
3773
|
+
sql: `UPDATE tasks
|
|
3774
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
3775
|
+
WHERE id = ? AND status = 'open'`,
|
|
3776
|
+
args: [tmuxSession, now, taskId]
|
|
3777
|
+
});
|
|
3778
|
+
if (claim.rowsAffected === 0) {
|
|
3779
|
+
const current = await client.execute({
|
|
3780
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
3781
|
+
args: [taskId]
|
|
3782
|
+
});
|
|
3783
|
+
const cur = current.rows[0];
|
|
3784
|
+
const status = cur?.status ?? "unknown";
|
|
3785
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
3786
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
3787
|
+
}
|
|
3788
|
+
try {
|
|
3789
|
+
await writeCheckpoint({
|
|
3790
|
+
taskId,
|
|
3791
|
+
step: "claimed",
|
|
3792
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3793
|
+
});
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
return { row, taskFile, now, taskId };
|
|
3685
3797
|
}
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3798
|
+
if (input.result) {
|
|
3799
|
+
await client.execute({
|
|
3800
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
3801
|
+
args: [input.status, input.result, now, taskId]
|
|
3802
|
+
});
|
|
3803
|
+
} else {
|
|
3804
|
+
await client.execute({
|
|
3805
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
3806
|
+
args: [input.status, now, taskId]
|
|
3807
|
+
});
|
|
3693
3808
|
}
|
|
3694
|
-
transport.pipeLog(sessionName, logFile);
|
|
3695
3809
|
try {
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
3702
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3703
|
-
}));
|
|
3810
|
+
await writeCheckpoint({
|
|
3811
|
+
taskId,
|
|
3812
|
+
step: `status_transition:${input.status}`,
|
|
3813
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
3814
|
+
});
|
|
3704
3815
|
} catch {
|
|
3705
3816
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
}
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3817
|
+
return { row, taskFile, now, taskId };
|
|
3818
|
+
}
|
|
3819
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
3820
|
+
const client = getClient();
|
|
3821
|
+
const row = await resolveTask(client, taskId);
|
|
3822
|
+
const id = String(row.id);
|
|
3823
|
+
const taskFile = String(row.task_file);
|
|
3824
|
+
const assignedTo = String(row.assigned_to);
|
|
3825
|
+
const assignedBy = String(row.assigned_by);
|
|
3826
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
3827
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
3828
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
3829
|
+
}
|
|
3830
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
3831
|
+
const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
3832
|
+
try {
|
|
3833
|
+
if (existsSync11(archPath)) return;
|
|
3834
|
+
const template = [
|
|
3835
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
3836
|
+
"",
|
|
3837
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
3838
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
3839
|
+
"",
|
|
3840
|
+
"## Overview",
|
|
3841
|
+
"",
|
|
3842
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
3843
|
+
"",
|
|
3844
|
+
"## Key Components",
|
|
3845
|
+
"",
|
|
3846
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
3847
|
+
"",
|
|
3848
|
+
"## Data Flow",
|
|
3849
|
+
"",
|
|
3850
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
3851
|
+
"",
|
|
3852
|
+
"## Invariants",
|
|
3853
|
+
"",
|
|
3854
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
3855
|
+
"",
|
|
3856
|
+
"## Dependencies",
|
|
3857
|
+
"",
|
|
3858
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
3859
|
+
""
|
|
3860
|
+
].join("\n");
|
|
3861
|
+
await writeFile4(archPath, template, "utf-8");
|
|
3862
|
+
} catch {
|
|
3731
3863
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3864
|
+
}
|
|
3865
|
+
async function ensureGitignoreExe(baseDir) {
|
|
3866
|
+
const gitignorePath = path12.join(baseDir, ".gitignore");
|
|
3867
|
+
try {
|
|
3868
|
+
if (existsSync11(gitignorePath)) {
|
|
3869
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
3870
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
3871
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
3872
|
+
} else {
|
|
3873
|
+
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
3736
3874
|
}
|
|
3875
|
+
} catch {
|
|
3737
3876
|
}
|
|
3738
|
-
registerSession({
|
|
3739
|
-
windowName: sessionName,
|
|
3740
|
-
agentId: employeeName,
|
|
3741
|
-
projectDir: spawnCwd,
|
|
3742
|
-
parentExe: exeSession,
|
|
3743
|
-
pid: 0,
|
|
3744
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3745
|
-
});
|
|
3746
|
-
releaseSpawnLock2(sessionName);
|
|
3747
|
-
return { sessionName };
|
|
3748
3877
|
}
|
|
3749
|
-
var
|
|
3750
|
-
var
|
|
3751
|
-
"src/lib/
|
|
3878
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
3879
|
+
var init_tasks_crud = __esm({
|
|
3880
|
+
"src/lib/tasks-crud.ts"() {
|
|
3752
3881
|
"use strict";
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
init_cc_agent_support();
|
|
3757
|
-
init_mcp_prefix();
|
|
3758
|
-
init_provider_table();
|
|
3759
|
-
init_intercom_queue();
|
|
3760
|
-
init_plan_limits();
|
|
3761
|
-
SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3762
|
-
SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3763
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3764
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3765
|
-
INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3766
|
-
DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3767
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3768
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
3882
|
+
init_database();
|
|
3883
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
3884
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3769
3885
|
}
|
|
3770
3886
|
});
|
|
3771
3887
|
|
|
3772
|
-
// src/lib/
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
getFailedMessages: () => getFailedMessages,
|
|
3777
|
-
getMessageStatus: () => getMessageStatus,
|
|
3778
|
-
getPendingMessages: () => getPendingMessages,
|
|
3779
|
-
getReadMessages: () => getReadMessages,
|
|
3780
|
-
getUnacknowledgedMessages: () => getUnacknowledgedMessages,
|
|
3781
|
-
markAcknowledged: () => markAcknowledged,
|
|
3782
|
-
markFailed: () => markFailed,
|
|
3783
|
-
markProcessed: () => markProcessed,
|
|
3784
|
-
markRead: () => markRead,
|
|
3785
|
-
retryPendingMessages: () => retryPendingMessages,
|
|
3786
|
-
sendMessage: () => sendMessage,
|
|
3787
|
-
setWsClientSend: () => setWsClientSend
|
|
3788
|
-
});
|
|
3789
|
-
import crypto3 from "crypto";
|
|
3790
|
-
function generateUlid() {
|
|
3791
|
-
const timestamp = Date.now().toString(36).padStart(10, "0");
|
|
3792
|
-
const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
|
|
3793
|
-
return (timestamp + random).toUpperCase();
|
|
3794
|
-
}
|
|
3795
|
-
function rowToMessage(row) {
|
|
3796
|
-
return {
|
|
3797
|
-
id: row.id,
|
|
3798
|
-
fromAgent: row.from_agent,
|
|
3799
|
-
fromDevice: row.from_device,
|
|
3800
|
-
targetAgent: row.target_agent,
|
|
3801
|
-
targetProject: row.target_project ?? null,
|
|
3802
|
-
targetDevice: row.target_device,
|
|
3803
|
-
content: row.content,
|
|
3804
|
-
priority: row.priority ?? "normal",
|
|
3805
|
-
status: row.status ?? "pending",
|
|
3806
|
-
serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
|
|
3807
|
-
retryCount: Number(row.retry_count ?? 0),
|
|
3808
|
-
createdAt: row.created_at,
|
|
3809
|
-
deliveredAt: row.delivered_at ?? null,
|
|
3810
|
-
processedAt: row.processed_at ?? null,
|
|
3811
|
-
failedAt: row.failed_at ?? null,
|
|
3812
|
-
failureReason: row.failure_reason ?? null
|
|
3813
|
-
};
|
|
3814
|
-
}
|
|
3815
|
-
async function sendMessage(input) {
|
|
3888
|
+
// src/lib/tasks-review.ts
|
|
3889
|
+
import path13 from "path";
|
|
3890
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
3891
|
+
async function countPendingReviews() {
|
|
3816
3892
|
const client = getClient();
|
|
3817
|
-
const id = generateUlid();
|
|
3818
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3819
|
-
const targetDevice = input.targetDevice ?? "local";
|
|
3820
|
-
await client.execute({
|
|
3821
|
-
sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
|
|
3822
|
-
VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
3823
|
-
args: [
|
|
3824
|
-
id,
|
|
3825
|
-
input.fromAgent,
|
|
3826
|
-
input.targetAgent,
|
|
3827
|
-
input.targetProject ?? null,
|
|
3828
|
-
targetDevice,
|
|
3829
|
-
input.content,
|
|
3830
|
-
input.priority ?? "normal",
|
|
3831
|
-
now
|
|
3832
|
-
]
|
|
3833
|
-
});
|
|
3834
|
-
try {
|
|
3835
|
-
if (targetDevice !== "local") {
|
|
3836
|
-
await deliverCrossMachineMessage(id, targetDevice);
|
|
3837
|
-
} else {
|
|
3838
|
-
await deliverLocalMessage(id);
|
|
3839
|
-
}
|
|
3840
|
-
} catch {
|
|
3841
|
-
}
|
|
3842
3893
|
const result = await client.execute({
|
|
3843
|
-
sql: "SELECT * FROM
|
|
3844
|
-
args: [
|
|
3894
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
3895
|
+
args: []
|
|
3845
3896
|
});
|
|
3846
|
-
return
|
|
3847
|
-
}
|
|
3848
|
-
function setWsClientSend(fn) {
|
|
3849
|
-
_wsClientSend = fn;
|
|
3897
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3850
3898
|
}
|
|
3851
|
-
async function
|
|
3899
|
+
async function countNewPendingReviewsSince(sinceIso) {
|
|
3852
3900
|
const client = getClient();
|
|
3853
3901
|
const result = await client.execute({
|
|
3854
|
-
sql:
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
if (result.rows.length === 0) return false;
|
|
3858
|
-
const msg = rowToMessage(result.rows[0]);
|
|
3859
|
-
if (msg.status !== "pending") return false;
|
|
3860
|
-
if (!_wsClientSend) {
|
|
3861
|
-
return false;
|
|
3862
|
-
}
|
|
3863
|
-
const payload = JSON.stringify({
|
|
3864
|
-
id: msg.id,
|
|
3865
|
-
fromAgent: msg.fromAgent,
|
|
3866
|
-
targetAgent: msg.targetAgent,
|
|
3867
|
-
targetProject: msg.targetProject,
|
|
3868
|
-
content: msg.content,
|
|
3869
|
-
priority: msg.priority,
|
|
3870
|
-
createdAt: msg.createdAt
|
|
3902
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3903
|
+
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
3904
|
+
args: [sinceIso]
|
|
3871
3905
|
});
|
|
3872
|
-
|
|
3873
|
-
if (sent) {
|
|
3874
|
-
await client.execute({
|
|
3875
|
-
sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
|
|
3876
|
-
args: [messageId]
|
|
3877
|
-
});
|
|
3878
|
-
return true;
|
|
3879
|
-
}
|
|
3880
|
-
return false;
|
|
3906
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3881
3907
|
}
|
|
3882
|
-
async function
|
|
3908
|
+
async function listPendingReviews(limit) {
|
|
3883
3909
|
const client = getClient();
|
|
3884
3910
|
const result = await client.execute({
|
|
3885
|
-
sql:
|
|
3886
|
-
|
|
3911
|
+
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
3912
|
+
WHERE status = 'needs_review'
|
|
3913
|
+
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
3914
|
+
args: [limit]
|
|
3887
3915
|
});
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
const
|
|
3916
|
+
return result.rows;
|
|
3917
|
+
}
|
|
3918
|
+
async function cleanupOrphanedReviews() {
|
|
3919
|
+
const client = getClient();
|
|
3892
3920
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
throw new Error(ensureResult.error ?? "ensureEmployee failed");
|
|
3901
|
-
}
|
|
3902
|
-
await client.execute({
|
|
3903
|
-
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
3904
|
-
args: [now, messageId]
|
|
3905
|
-
});
|
|
3906
|
-
return true;
|
|
3907
|
-
} catch {
|
|
3908
|
-
const newRetryCount = msg.retryCount + 1;
|
|
3909
|
-
if (newRetryCount >= MAX_RETRIES2) {
|
|
3910
|
-
await markFailed(messageId, "session unavailable after 10 retries");
|
|
3911
|
-
} else {
|
|
3912
|
-
await client.execute({
|
|
3913
|
-
sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
|
|
3914
|
-
args: [newRetryCount, messageId]
|
|
3915
|
-
});
|
|
3916
|
-
}
|
|
3917
|
-
return false;
|
|
3918
|
-
}
|
|
3919
|
-
}
|
|
3920
|
-
async function getPendingMessages(targetAgent) {
|
|
3921
|
-
const client = getClient();
|
|
3922
|
-
const result = await client.execute({
|
|
3923
|
-
sql: `SELECT * FROM messages
|
|
3924
|
-
WHERE target_agent = ? AND status IN ('pending', 'delivered')
|
|
3925
|
-
ORDER BY id`,
|
|
3926
|
-
args: [targetAgent]
|
|
3927
|
-
});
|
|
3928
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3929
|
-
}
|
|
3930
|
-
async function markRead(messageId) {
|
|
3931
|
-
const client = getClient();
|
|
3932
|
-
await client.execute({
|
|
3933
|
-
sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
|
|
3934
|
-
args: [messageId]
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3937
|
-
async function markAcknowledged(messageId) {
|
|
3938
|
-
const client = getClient();
|
|
3939
|
-
await client.execute({
|
|
3940
|
-
sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
|
|
3941
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
3942
|
-
});
|
|
3943
|
-
}
|
|
3944
|
-
async function markProcessed(messageId) {
|
|
3945
|
-
const client = getClient();
|
|
3946
|
-
await client.execute({
|
|
3947
|
-
sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
|
|
3948
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
3949
|
-
});
|
|
3950
|
-
}
|
|
3951
|
-
async function getMessageStatus(messageId) {
|
|
3952
|
-
const client = getClient();
|
|
3953
|
-
const result = await client.execute({
|
|
3954
|
-
sql: "SELECT status FROM messages WHERE id = ?",
|
|
3955
|
-
args: [messageId]
|
|
3956
|
-
});
|
|
3957
|
-
return result.rows[0]?.status ?? null;
|
|
3958
|
-
}
|
|
3959
|
-
async function getUnacknowledgedMessages(targetAgent) {
|
|
3960
|
-
const client = getClient();
|
|
3961
|
-
const result = await client.execute({
|
|
3962
|
-
sql: `SELECT * FROM messages
|
|
3963
|
-
WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
|
|
3964
|
-
ORDER BY id`,
|
|
3965
|
-
args: [targetAgent]
|
|
3966
|
-
});
|
|
3967
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3968
|
-
}
|
|
3969
|
-
async function getReadMessages(targetAgent) {
|
|
3970
|
-
const client = getClient();
|
|
3971
|
-
const result = await client.execute({
|
|
3972
|
-
sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
|
|
3973
|
-
args: [targetAgent]
|
|
3974
|
-
});
|
|
3975
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3976
|
-
}
|
|
3977
|
-
async function markFailed(messageId, reason) {
|
|
3978
|
-
const client = getClient();
|
|
3979
|
-
await client.execute({
|
|
3980
|
-
sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
|
|
3981
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
|
|
3982
|
-
});
|
|
3983
|
-
}
|
|
3984
|
-
async function getFailedMessages() {
|
|
3985
|
-
const client = getClient();
|
|
3986
|
-
const result = await client.execute({
|
|
3987
|
-
sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
|
|
3988
|
-
args: []
|
|
3921
|
+
const r1 = await client.execute({
|
|
3922
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3923
|
+
WHERE status = 'needs_review'
|
|
3924
|
+
AND assigned_by = 'system'
|
|
3925
|
+
AND title LIKE 'Review:%'
|
|
3926
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
3927
|
+
args: [now]
|
|
3989
3928
|
});
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
ORDER BY id`,
|
|
3998
|
-
args: [MAX_RETRIES2]
|
|
3929
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
3930
|
+
const r2 = await client.execute({
|
|
3931
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3932
|
+
WHERE status = 'needs_review'
|
|
3933
|
+
AND result IS NOT NULL
|
|
3934
|
+
AND updated_at < ?`,
|
|
3935
|
+
args: [now, staleThreshold]
|
|
3999
3936
|
});
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
} catch {
|
|
4007
|
-
}
|
|
3937
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
3938
|
+
if (total > 0) {
|
|
3939
|
+
process.stderr.write(
|
|
3940
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
3941
|
+
`
|
|
3942
|
+
);
|
|
4008
3943
|
}
|
|
4009
|
-
return
|
|
3944
|
+
return total;
|
|
4010
3945
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
3946
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
3947
|
+
const roleLower = role.toLowerCase();
|
|
3948
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
3949
|
+
return {
|
|
3950
|
+
lens: "Code Quality (Engineer)",
|
|
3951
|
+
checklist: [
|
|
3952
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
3953
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
3954
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
3955
|
+
"4. Any regressions in the test suite?"
|
|
3956
|
+
]
|
|
3957
|
+
};
|
|
4019
3958
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
|
|
3959
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
3960
|
+
return {
|
|
3961
|
+
lens: "Architecture (CTO)",
|
|
3962
|
+
checklist: [
|
|
3963
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
3964
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
3965
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
3966
|
+
"4. Security implications? Any new attack surface?",
|
|
3967
|
+
"5. Does it scale? Performance considerations?",
|
|
3968
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3969
|
+
]
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3973
|
+
return {
|
|
3974
|
+
lens: "Strategic (COO)",
|
|
3975
|
+
checklist: [
|
|
3976
|
+
"1. Does this serve the project mission?",
|
|
3977
|
+
"2. Is this the right work at the right time?",
|
|
3978
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
3979
|
+
"4. Any cross-project implications?"
|
|
3980
|
+
]
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
return {
|
|
3984
|
+
lens: "General",
|
|
3985
|
+
checklist: [
|
|
3986
|
+
"1. Read the original task's acceptance criteria",
|
|
3987
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3988
|
+
"3. Verify code changes match requirements",
|
|
3989
|
+
"4. Check if tests were added/updated",
|
|
3990
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3991
|
+
]
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3995
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4034
3996
|
try {
|
|
4035
3997
|
const client = getClient();
|
|
4036
|
-
const id = crypto4.randomUUID();
|
|
4037
3998
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
3999
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
4000
|
+
if (parentId) {
|
|
4001
|
+
const result = await client.execute({
|
|
4002
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
4003
|
+
args: [now, parentId]
|
|
4004
|
+
});
|
|
4005
|
+
if (result.rowsAffected > 0) {
|
|
4006
|
+
process.stderr.write(
|
|
4007
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
4008
|
+
`
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
} else {
|
|
4012
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
4013
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
4014
|
+
const parts = reviewPrefix.split("-");
|
|
4015
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
4016
|
+
const agent = parts[1];
|
|
4017
|
+
const slug = parts.slice(2).join("-");
|
|
4018
|
+
const originalTaskFile = `exe/${agent}/${slug}.md`;
|
|
4019
|
+
const result = await client.execute({
|
|
4020
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
4021
|
+
args: [now, originalTaskFile]
|
|
4022
|
+
});
|
|
4023
|
+
if (result.rowsAffected > 0) {
|
|
4024
|
+
process.stderr.write(
|
|
4025
|
+
`[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
|
|
4026
|
+
`
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4052
4031
|
} catch (err) {
|
|
4053
|
-
process.stderr.write(
|
|
4054
|
-
`)
|
|
4032
|
+
process.stderr.write(
|
|
4033
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
4034
|
+
`
|
|
4035
|
+
);
|
|
4055
4036
|
}
|
|
4056
|
-
}
|
|
4057
|
-
async function markAsReadByTaskFile(taskFile) {
|
|
4058
4037
|
try {
|
|
4059
|
-
const
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4038
|
+
const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
|
|
4039
|
+
if (existsSync12(cacheDir)) {
|
|
4040
|
+
for (const f of readdirSync3(cacheDir)) {
|
|
4041
|
+
if (f.startsWith("review-notified-")) {
|
|
4042
|
+
unlinkSync3(path13.join(cacheDir, f));
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4064
4046
|
} catch {
|
|
4065
4047
|
}
|
|
4066
4048
|
}
|
|
4067
|
-
var
|
|
4068
|
-
"src/lib/
|
|
4049
|
+
var init_tasks_review = __esm({
|
|
4050
|
+
"src/lib/tasks-review.ts"() {
|
|
4069
4051
|
"use strict";
|
|
4070
4052
|
init_database();
|
|
4053
|
+
init_config();
|
|
4054
|
+
init_employees();
|
|
4055
|
+
init_notifications();
|
|
4056
|
+
init_tasks_crud();
|
|
4057
|
+
init_tmux_routing();
|
|
4058
|
+
init_session_key();
|
|
4059
|
+
init_state_bus();
|
|
4071
4060
|
}
|
|
4072
4061
|
});
|
|
4073
4062
|
|
|
4074
|
-
// src/lib/tasks-
|
|
4075
|
-
import
|
|
4076
|
-
import
|
|
4077
|
-
|
|
4078
|
-
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
4079
|
-
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
4080
|
-
async function writeCheckpoint(input) {
|
|
4063
|
+
// src/lib/tasks-chain.ts
|
|
4064
|
+
import path14 from "path";
|
|
4065
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
4066
|
+
async function cascadeUnblock(taskId, baseDir, now) {
|
|
4081
4067
|
const client = getClient();
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
if (row.blocked_by) {
|
|
4087
|
-
blockedByIds.push(String(row.blocked_by));
|
|
4088
|
-
}
|
|
4089
|
-
const checkpoint = {
|
|
4090
|
-
step: input.step,
|
|
4091
|
-
context_summary: input.contextSummary,
|
|
4092
|
-
files_touched: input.filesTouched ?? [],
|
|
4093
|
-
blocked_by_ids: blockedByIds,
|
|
4094
|
-
last_checkpoint_at: now
|
|
4095
|
-
};
|
|
4096
|
-
const result = await client.execute({
|
|
4097
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
4098
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
4068
|
+
const unblocked = await client.execute({
|
|
4069
|
+
sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
|
|
4070
|
+
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
4071
|
+
args: [now, taskId]
|
|
4099
4072
|
});
|
|
4100
|
-
if (
|
|
4101
|
-
|
|
4073
|
+
if (baseDir && unblocked.rowsAffected > 0) {
|
|
4074
|
+
const unblockedRows = await client.execute({
|
|
4075
|
+
sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?`,
|
|
4076
|
+
args: [now]
|
|
4077
|
+
});
|
|
4078
|
+
for (const ur of unblockedRows.rows) {
|
|
4079
|
+
try {
|
|
4080
|
+
const ubFile = path14.join(baseDir, String(ur.task_file));
|
|
4081
|
+
let ubContent = await readFile4(ubFile, "utf-8");
|
|
4082
|
+
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
4083
|
+
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
4084
|
+
await writeFile5(ubFile, ubContent, "utf-8");
|
|
4085
|
+
} catch {
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4102
4088
|
}
|
|
4103
|
-
const countResult = await client.execute({
|
|
4104
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
4105
|
-
args: [taskId]
|
|
4106
|
-
});
|
|
4107
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
4108
|
-
return { checkpointCount };
|
|
4109
|
-
}
|
|
4110
|
-
function extractParentFromContext(contextBody) {
|
|
4111
|
-
if (!contextBody) return null;
|
|
4112
|
-
const match = contextBody.match(
|
|
4113
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
4114
|
-
);
|
|
4115
|
-
return match ? match[1].toLowerCase() : null;
|
|
4116
|
-
}
|
|
4117
|
-
function slugify(title) {
|
|
4118
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4119
4089
|
}
|
|
4120
|
-
async function
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
args: [`%${identifier}%`]
|
|
4129
|
-
});
|
|
4130
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
4131
|
-
if (result.rows.length > 1) {
|
|
4132
|
-
const exact = result.rows.filter(
|
|
4133
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
4134
|
-
);
|
|
4135
|
-
if (exact.length === 1) return exact[0];
|
|
4136
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
4137
|
-
const active = candidates.filter(
|
|
4138
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
4139
|
-
);
|
|
4140
|
-
if (active.length === 1) return active[0];
|
|
4141
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
4142
|
-
throw new Error(
|
|
4143
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
4144
|
-
);
|
|
4145
|
-
}
|
|
4146
|
-
result = await client.execute({
|
|
4147
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
4148
|
-
args: [`%${identifier}%`]
|
|
4090
|
+
async function findNextTask(assignedTo) {
|
|
4091
|
+
const client = getClient();
|
|
4092
|
+
const nextResult = await client.execute({
|
|
4093
|
+
sql: `SELECT title, task_file, priority FROM tasks
|
|
4094
|
+
WHERE assigned_to = ? AND status = 'open'
|
|
4095
|
+
ORDER BY priority ASC, created_at ASC
|
|
4096
|
+
LIMIT 1`,
|
|
4097
|
+
args: [assignedTo]
|
|
4149
4098
|
});
|
|
4150
|
-
if (
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
throw new Error(
|
|
4158
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
4159
|
-
);
|
|
4099
|
+
if (nextResult.rows.length === 1) {
|
|
4100
|
+
const nr = nextResult.rows[0];
|
|
4101
|
+
return {
|
|
4102
|
+
title: String(nr.title),
|
|
4103
|
+
priority: String(nr.priority),
|
|
4104
|
+
taskFile: String(nr.task_file)
|
|
4105
|
+
};
|
|
4160
4106
|
}
|
|
4161
|
-
|
|
4107
|
+
return void 0;
|
|
4162
4108
|
}
|
|
4163
|
-
async function
|
|
4109
|
+
async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
4164
4110
|
const client = getClient();
|
|
4165
|
-
const
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
const
|
|
4171
|
-
if (
|
|
4172
|
-
const
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
try {
|
|
4188
|
-
const parent = await resolveTask(client, parentRef);
|
|
4189
|
-
parentTaskId = String(parent.id);
|
|
4190
|
-
} catch (err) {
|
|
4191
|
-
if (!input.parentTaskId) {
|
|
4192
|
-
throw new Error(
|
|
4193
|
-
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
4194
|
-
);
|
|
4195
|
-
}
|
|
4196
|
-
throw err;
|
|
4111
|
+
const remaining = await client.execute({
|
|
4112
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
4113
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')`,
|
|
4114
|
+
args: [parentTaskId]
|
|
4115
|
+
});
|
|
4116
|
+
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
4117
|
+
if (cnt === 0) {
|
|
4118
|
+
const parentRow = await client.execute({
|
|
4119
|
+
sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
|
|
4120
|
+
args: [parentTaskId]
|
|
4121
|
+
});
|
|
4122
|
+
if (parentRow.rows.length === 1) {
|
|
4123
|
+
const pr = parentRow.rows[0];
|
|
4124
|
+
const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
|
|
4125
|
+
await writeNotification({
|
|
4126
|
+
agentId: String(pr.assigned_to),
|
|
4127
|
+
agentRole: "system",
|
|
4128
|
+
event: "subtasks_complete",
|
|
4129
|
+
project: parentProject,
|
|
4130
|
+
summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
|
|
4131
|
+
taskFile: String(pr.task_file)
|
|
4132
|
+
});
|
|
4197
4133
|
}
|
|
4198
4134
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
4135
|
+
}
|
|
4136
|
+
var init_tasks_chain = __esm({
|
|
4137
|
+
"src/lib/tasks-chain.ts"() {
|
|
4138
|
+
"use strict";
|
|
4139
|
+
init_database();
|
|
4140
|
+
init_notifications();
|
|
4206
4141
|
}
|
|
4207
|
-
|
|
4142
|
+
});
|
|
4143
|
+
|
|
4144
|
+
// src/lib/project-name.ts
|
|
4145
|
+
import { execSync as execSync5 } from "child_process";
|
|
4146
|
+
import path15 from "path";
|
|
4147
|
+
function getProjectName(cwd) {
|
|
4148
|
+
const dir = cwd ?? process.cwd();
|
|
4149
|
+
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
4150
|
+
try {
|
|
4151
|
+
let repoRoot;
|
|
4208
4152
|
try {
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4153
|
+
const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
|
|
4154
|
+
cwd: dir,
|
|
4155
|
+
encoding: "utf8",
|
|
4156
|
+
timeout: 2e3,
|
|
4157
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4158
|
+
}).trim();
|
|
4159
|
+
repoRoot = path15.dirname(gitCommonDir);
|
|
4213
4160
|
} catch {
|
|
4161
|
+
repoRoot = execSync5("git rev-parse --show-toplevel", {
|
|
4162
|
+
cwd: dir,
|
|
4163
|
+
encoding: "utf8",
|
|
4164
|
+
timeout: 2e3,
|
|
4165
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4166
|
+
}).trim();
|
|
4214
4167
|
}
|
|
4168
|
+
_cached2 = path15.basename(repoRoot);
|
|
4169
|
+
_cachedCwd = dir;
|
|
4170
|
+
return _cached2;
|
|
4171
|
+
} catch {
|
|
4172
|
+
_cached2 = path15.basename(dir);
|
|
4173
|
+
_cachedCwd = dir;
|
|
4174
|
+
return _cached2;
|
|
4215
4175
|
}
|
|
4216
|
-
const complexity = input.complexity ?? "standard";
|
|
4217
|
-
await client.execute({
|
|
4218
|
-
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, created_at, updated_at)
|
|
4219
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4220
|
-
args: [
|
|
4221
|
-
id,
|
|
4222
|
-
input.title,
|
|
4223
|
-
input.assignedTo,
|
|
4224
|
-
input.assignedBy,
|
|
4225
|
-
input.projectName,
|
|
4226
|
-
input.priority,
|
|
4227
|
-
initialStatus,
|
|
4228
|
-
taskFile,
|
|
4229
|
-
blockedById,
|
|
4230
|
-
parentTaskId,
|
|
4231
|
-
input.reviewer ?? null,
|
|
4232
|
-
input.context,
|
|
4233
|
-
complexity,
|
|
4234
|
-
input.budgetTokens ?? null,
|
|
4235
|
-
input.budgetFallbackModel ?? null,
|
|
4236
|
-
0,
|
|
4237
|
-
null,
|
|
4238
|
-
now,
|
|
4239
|
-
now
|
|
4240
|
-
]
|
|
4241
|
-
});
|
|
4242
|
-
return {
|
|
4243
|
-
id,
|
|
4244
|
-
title: input.title,
|
|
4245
|
-
assignedTo: input.assignedTo,
|
|
4246
|
-
assignedBy: input.assignedBy,
|
|
4247
|
-
projectName: input.projectName,
|
|
4248
|
-
priority: input.priority,
|
|
4249
|
-
status: initialStatus,
|
|
4250
|
-
taskFile,
|
|
4251
|
-
createdAt: now,
|
|
4252
|
-
updatedAt: now,
|
|
4253
|
-
warning,
|
|
4254
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
4255
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
4256
|
-
tokensUsed: 0,
|
|
4257
|
-
tokensWarnedAt: null
|
|
4258
|
-
};
|
|
4259
4176
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
args.push(input.assignedTo);
|
|
4177
|
+
var _cached2, _cachedCwd;
|
|
4178
|
+
var init_project_name = __esm({
|
|
4179
|
+
"src/lib/project-name.ts"() {
|
|
4180
|
+
"use strict";
|
|
4181
|
+
_cached2 = null;
|
|
4182
|
+
_cachedCwd = null;
|
|
4267
4183
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4184
|
+
});
|
|
4185
|
+
|
|
4186
|
+
// src/lib/session-scope.ts
|
|
4187
|
+
var session_scope_exports = {};
|
|
4188
|
+
__export(session_scope_exports, {
|
|
4189
|
+
assertSessionScope: () => assertSessionScope,
|
|
4190
|
+
findSessionForProject: () => findSessionForProject,
|
|
4191
|
+
getSessionProject: () => getSessionProject
|
|
4192
|
+
});
|
|
4193
|
+
function getSessionProject(sessionName) {
|
|
4194
|
+
const sessions = listSessions();
|
|
4195
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
4196
|
+
if (!entry) return null;
|
|
4197
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
4198
|
+
return parts[parts.length - 1] ?? null;
|
|
4199
|
+
}
|
|
4200
|
+
function findSessionForProject(projectName) {
|
|
4201
|
+
const sessions = listSessions();
|
|
4202
|
+
for (const s of sessions) {
|
|
4203
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
4204
|
+
if (proj === projectName && s.agentId === "exe") return s;
|
|
4281
4205
|
}
|
|
4282
|
-
|
|
4283
|
-
const result = await client.execute({
|
|
4284
|
-
sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC`,
|
|
4285
|
-
args
|
|
4286
|
-
});
|
|
4287
|
-
return result.rows.map((r) => ({
|
|
4288
|
-
id: String(r.id),
|
|
4289
|
-
title: String(r.title),
|
|
4290
|
-
assignedTo: String(r.assigned_to),
|
|
4291
|
-
assignedBy: String(r.assigned_by),
|
|
4292
|
-
projectName: String(r.project_name),
|
|
4293
|
-
priority: String(r.priority),
|
|
4294
|
-
status: String(r.status),
|
|
4295
|
-
taskFile: String(r.task_file),
|
|
4296
|
-
createdAt: String(r.created_at),
|
|
4297
|
-
updatedAt: String(r.updated_at),
|
|
4298
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
4299
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
4300
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
4301
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
4302
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
4303
|
-
}));
|
|
4206
|
+
return null;
|
|
4304
4207
|
}
|
|
4305
|
-
function
|
|
4306
|
-
if (!taskContext) return null;
|
|
4307
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
4208
|
+
function assertSessionScope(actionType, targetProject) {
|
|
4308
4209
|
try {
|
|
4309
|
-
const
|
|
4310
|
-
const
|
|
4311
|
-
|
|
4312
|
-
{
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
4210
|
+
const currentProject = getProjectName();
|
|
4211
|
+
const exeSession = resolveExeSession();
|
|
4212
|
+
if (!exeSession) {
|
|
4213
|
+
return { allowed: true, reason: "no_session" };
|
|
4214
|
+
}
|
|
4215
|
+
if (currentProject === targetProject) {
|
|
4216
|
+
return {
|
|
4217
|
+
allowed: true,
|
|
4218
|
+
reason: "same_session",
|
|
4219
|
+
currentProject,
|
|
4220
|
+
targetProject
|
|
4221
|
+
};
|
|
4322
4222
|
}
|
|
4323
|
-
return null;
|
|
4324
|
-
} catch {
|
|
4325
|
-
return null;
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
async function updateTaskStatus(input) {
|
|
4329
|
-
const client = getClient();
|
|
4330
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4331
|
-
const row = await resolveTask(client, input.taskId);
|
|
4332
|
-
const taskId = String(row.id);
|
|
4333
|
-
const taskFile = String(row.task_file);
|
|
4334
|
-
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
4335
4223
|
process.stderr.write(
|
|
4336
|
-
`[
|
|
4224
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
4337
4225
|
`
|
|
4338
4226
|
);
|
|
4227
|
+
return {
|
|
4228
|
+
allowed: false,
|
|
4229
|
+
reason: "cross_session_denied",
|
|
4230
|
+
currentProject,
|
|
4231
|
+
targetProject,
|
|
4232
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
4233
|
+
};
|
|
4234
|
+
} catch {
|
|
4235
|
+
return { allowed: true, reason: "no_session" };
|
|
4339
4236
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
4348
|
-
if (warning) {
|
|
4349
|
-
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
4350
|
-
|
|
4351
|
-
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
4352
|
-
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
4353
|
-
`);
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4237
|
+
}
|
|
4238
|
+
var init_session_scope = __esm({
|
|
4239
|
+
"src/lib/session-scope.ts"() {
|
|
4240
|
+
"use strict";
|
|
4241
|
+
init_session_registry();
|
|
4242
|
+
init_project_name();
|
|
4243
|
+
init_tmux_routing();
|
|
4356
4244
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
});
|
|
4365
|
-
if (claim.rowsAffected === 0) {
|
|
4366
|
-
const current = await client.execute({
|
|
4367
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
4368
|
-
args: [taskId]
|
|
4369
|
-
});
|
|
4370
|
-
const cur = current.rows[0];
|
|
4371
|
-
const status = cur?.status ?? "unknown";
|
|
4372
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
4373
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
4374
|
-
}
|
|
4245
|
+
});
|
|
4246
|
+
|
|
4247
|
+
// src/lib/tasks-notify.ts
|
|
4248
|
+
async function dispatchTaskToEmployee(input) {
|
|
4249
|
+
if (input.assignedTo === "exe") return { dispatched: "skipped" };
|
|
4250
|
+
let crossProject = false;
|
|
4251
|
+
if (input.projectName) {
|
|
4375
4252
|
try {
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4253
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
4254
|
+
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
4255
|
+
if (check.reason === "cross_session_denied") {
|
|
4256
|
+
crossProject = true;
|
|
4257
|
+
return { dispatched: "skipped", crossProject: true };
|
|
4258
|
+
}
|
|
4381
4259
|
} catch {
|
|
4382
4260
|
}
|
|
4383
|
-
return { row, taskFile, now, taskId };
|
|
4384
|
-
}
|
|
4385
|
-
if (input.result) {
|
|
4386
|
-
await client.execute({
|
|
4387
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
4388
|
-
args: [input.status, input.result, now, taskId]
|
|
4389
|
-
});
|
|
4390
|
-
} else {
|
|
4391
|
-
await client.execute({
|
|
4392
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
4393
|
-
args: [input.status, now, taskId]
|
|
4394
|
-
});
|
|
4395
4261
|
}
|
|
4396
4262
|
try {
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4263
|
+
const transport = getTransport();
|
|
4264
|
+
const exeSession = resolveExeSession();
|
|
4265
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
4266
|
+
const sessionName = employeeSessionName(input.assignedTo, exeSession);
|
|
4267
|
+
if (transport.isAlive(sessionName)) {
|
|
4268
|
+
const result = sendIntercom(sessionName);
|
|
4269
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
4270
|
+
return { dispatched, session: sessionName, crossProject };
|
|
4271
|
+
} else {
|
|
4272
|
+
const projectDir = input.projectDir ?? process.cwd();
|
|
4273
|
+
const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
|
|
4274
|
+
autoInstance: isMultiInstance(input.assignedTo)
|
|
4275
|
+
});
|
|
4276
|
+
if (result.status === "failed") {
|
|
4277
|
+
process.stderr.write(
|
|
4278
|
+
`[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
|
|
4279
|
+
`
|
|
4280
|
+
);
|
|
4281
|
+
return { dispatched: "session_missing" };
|
|
4282
|
+
}
|
|
4283
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
4284
|
+
}
|
|
4402
4285
|
} catch {
|
|
4286
|
+
return { dispatched: "session_missing" };
|
|
4403
4287
|
}
|
|
4404
|
-
return { row, taskFile, now, taskId };
|
|
4405
|
-
}
|
|
4406
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
4407
|
-
const client = getClient();
|
|
4408
|
-
const row = await resolveTask(client, taskId);
|
|
4409
|
-
const id = String(row.id);
|
|
4410
|
-
const taskFile = String(row.task_file);
|
|
4411
|
-
const assignedTo = String(row.assigned_to);
|
|
4412
|
-
const assignedBy = String(row.assigned_by);
|
|
4413
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
4414
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
4415
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
4416
4288
|
}
|
|
4417
|
-
|
|
4418
|
-
const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
4289
|
+
function notifyTaskDone() {
|
|
4419
4290
|
try {
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
4423
|
-
"",
|
|
4424
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
4425
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
4426
|
-
"",
|
|
4427
|
-
"## Overview",
|
|
4428
|
-
"",
|
|
4429
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
4430
|
-
"",
|
|
4431
|
-
"## Key Components",
|
|
4432
|
-
"",
|
|
4433
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
4434
|
-
"",
|
|
4435
|
-
"## Data Flow",
|
|
4436
|
-
"",
|
|
4437
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
4438
|
-
"",
|
|
4439
|
-
"## Invariants",
|
|
4440
|
-
"",
|
|
4441
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
4442
|
-
"",
|
|
4443
|
-
"## Dependencies",
|
|
4444
|
-
"",
|
|
4445
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
4446
|
-
""
|
|
4447
|
-
].join("\n");
|
|
4448
|
-
await writeFile4(archPath, template, "utf-8");
|
|
4291
|
+
const key = getSessionKey();
|
|
4292
|
+
if (key && !process.env.VITEST) notifyParentExe(key);
|
|
4449
4293
|
} catch {
|
|
4450
4294
|
}
|
|
4451
4295
|
}
|
|
4452
|
-
async function
|
|
4453
|
-
const gitignorePath = path13.join(baseDir, ".gitignore");
|
|
4296
|
+
async function markTaskNotificationsRead(taskFile) {
|
|
4454
4297
|
try {
|
|
4455
|
-
|
|
4456
|
-
const content = readFileSync11(gitignorePath, "utf-8");
|
|
4457
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
4458
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
4459
|
-
} else {
|
|
4460
|
-
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
4461
|
-
}
|
|
4298
|
+
await markAsReadByTaskFile(taskFile);
|
|
4462
4299
|
} catch {
|
|
4463
4300
|
}
|
|
4464
4301
|
}
|
|
4465
|
-
var
|
|
4466
|
-
|
|
4467
|
-
"src/lib/tasks-crud.ts"() {
|
|
4302
|
+
var init_tasks_notify = __esm({
|
|
4303
|
+
"src/lib/tasks-notify.ts"() {
|
|
4468
4304
|
"use strict";
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4305
|
+
init_tmux_routing();
|
|
4306
|
+
init_session_key();
|
|
4307
|
+
init_notifications();
|
|
4308
|
+
init_transport();
|
|
4309
|
+
init_employees();
|
|
4472
4310
|
}
|
|
4473
4311
|
});
|
|
4474
4312
|
|
|
4475
|
-
// src/lib/
|
|
4476
|
-
import
|
|
4477
|
-
|
|
4478
|
-
async function countPendingReviews() {
|
|
4479
|
-
const client = getClient();
|
|
4480
|
-
const result = await client.execute({
|
|
4481
|
-
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
4482
|
-
args: []
|
|
4483
|
-
});
|
|
4484
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
4485
|
-
}
|
|
4486
|
-
async function countNewPendingReviewsSince(sinceIso) {
|
|
4487
|
-
const client = getClient();
|
|
4488
|
-
const result = await client.execute({
|
|
4489
|
-
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
4490
|
-
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
4491
|
-
args: [sinceIso]
|
|
4492
|
-
});
|
|
4493
|
-
return Number(result.rows[0]?.cnt) || 0;
|
|
4494
|
-
}
|
|
4495
|
-
async function listPendingReviews(limit) {
|
|
4313
|
+
// src/lib/behaviors.ts
|
|
4314
|
+
import crypto6 from "crypto";
|
|
4315
|
+
async function storeBehavior(opts) {
|
|
4496
4316
|
const client = getClient();
|
|
4497
|
-
const
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4317
|
+
const id = crypto6.randomUUID();
|
|
4318
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4319
|
+
await client.execute({
|
|
4320
|
+
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
4321
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
4322
|
+
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
4502
4323
|
});
|
|
4503
|
-
return
|
|
4504
|
-
}
|
|
4505
|
-
function getReviewChecklist(role, agent, taskSlug) {
|
|
4506
|
-
const roleLower = role.toLowerCase();
|
|
4507
|
-
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
4508
|
-
return {
|
|
4509
|
-
lens: "Code Quality (Engineer)",
|
|
4510
|
-
checklist: [
|
|
4511
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
4512
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
4513
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
4514
|
-
"4. Any regressions in the test suite?"
|
|
4515
|
-
]
|
|
4516
|
-
};
|
|
4517
|
-
}
|
|
4518
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
4519
|
-
return {
|
|
4520
|
-
lens: "Architecture (CTO)",
|
|
4521
|
-
checklist: [
|
|
4522
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
4523
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
4524
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
4525
|
-
"4. Security implications? Any new attack surface?",
|
|
4526
|
-
"5. Does it scale? Performance considerations?",
|
|
4527
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
4528
|
-
]
|
|
4529
|
-
};
|
|
4530
|
-
}
|
|
4531
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
4532
|
-
return {
|
|
4533
|
-
lens: "Strategic (COO)",
|
|
4534
|
-
checklist: [
|
|
4535
|
-
"1. Does this serve the project mission?",
|
|
4536
|
-
"2. Is this the right work at the right time?",
|
|
4537
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
4538
|
-
"4. Any cross-project implications?"
|
|
4539
|
-
]
|
|
4540
|
-
};
|
|
4541
|
-
}
|
|
4542
|
-
return {
|
|
4543
|
-
lens: "General",
|
|
4544
|
-
checklist: [
|
|
4545
|
-
"1. Read the original task's acceptance criteria",
|
|
4546
|
-
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
4547
|
-
"3. Verify code changes match requirements",
|
|
4548
|
-
"4. Check if tests were added/updated",
|
|
4549
|
-
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
4550
|
-
]
|
|
4551
|
-
};
|
|
4552
|
-
}
|
|
4553
|
-
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
4554
|
-
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
4555
|
-
try {
|
|
4556
|
-
const client = getClient();
|
|
4557
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4558
|
-
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
4559
|
-
if (parentId) {
|
|
4560
|
-
const result = await client.execute({
|
|
4561
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
4562
|
-
args: [now, parentId]
|
|
4563
|
-
});
|
|
4564
|
-
if (result.rowsAffected > 0) {
|
|
4565
|
-
process.stderr.write(
|
|
4566
|
-
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
4567
|
-
`
|
|
4568
|
-
);
|
|
4569
|
-
}
|
|
4570
|
-
} else {
|
|
4571
|
-
const fileName = taskFile.split("/").pop() ?? "";
|
|
4572
|
-
const reviewPrefix = fileName.replace(".md", "");
|
|
4573
|
-
const parts = reviewPrefix.split("-");
|
|
4574
|
-
if (parts.length >= 3 && parts[0] === "review") {
|
|
4575
|
-
const agent = parts[1];
|
|
4576
|
-
const slug = parts.slice(2).join("-");
|
|
4577
|
-
const originalTaskFile = `exe/${agent}/${slug}.md`;
|
|
4578
|
-
const result = await client.execute({
|
|
4579
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
4580
|
-
args: [now, originalTaskFile]
|
|
4581
|
-
});
|
|
4582
|
-
if (result.rowsAffected > 0) {
|
|
4583
|
-
process.stderr.write(
|
|
4584
|
-
`[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
|
|
4585
|
-
`
|
|
4586
|
-
);
|
|
4587
|
-
}
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4590
|
-
} catch (err) {
|
|
4591
|
-
process.stderr.write(
|
|
4592
|
-
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
4593
|
-
`
|
|
4594
|
-
);
|
|
4595
|
-
}
|
|
4596
|
-
try {
|
|
4597
|
-
const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
|
|
4598
|
-
if (existsSync13(cacheDir)) {
|
|
4599
|
-
for (const f of readdirSync3(cacheDir)) {
|
|
4600
|
-
if (f.startsWith("review-notified-")) {
|
|
4601
|
-
unlinkSync4(path14.join(cacheDir, f));
|
|
4602
|
-
}
|
|
4603
|
-
}
|
|
4604
|
-
}
|
|
4605
|
-
} catch {
|
|
4606
|
-
}
|
|
4324
|
+
return id;
|
|
4607
4325
|
}
|
|
4608
|
-
var
|
|
4609
|
-
"src/lib/
|
|
4326
|
+
var init_behaviors = __esm({
|
|
4327
|
+
"src/lib/behaviors.ts"() {
|
|
4610
4328
|
"use strict";
|
|
4611
4329
|
init_database();
|
|
4612
|
-
init_config();
|
|
4613
|
-
init_employees();
|
|
4614
|
-
init_notifications();
|
|
4615
|
-
init_tasks_crud();
|
|
4616
|
-
init_tmux_routing();
|
|
4617
|
-
init_session_key();
|
|
4618
4330
|
}
|
|
4619
4331
|
});
|
|
4620
4332
|
|
|
4621
|
-
// src/lib/
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4333
|
+
// src/lib/skill-learning.ts
|
|
4334
|
+
var skill_learning_exports = {};
|
|
4335
|
+
__export(skill_learning_exports, {
|
|
4336
|
+
captureAndLearn: () => captureAndLearn,
|
|
4337
|
+
captureTrajectory: () => captureTrajectory,
|
|
4338
|
+
editDistance: () => editDistance,
|
|
4339
|
+
extractSkill: () => extractSkill,
|
|
4340
|
+
extractTrajectory: () => extractTrajectory,
|
|
4341
|
+
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
4342
|
+
hashSignature: () => hashSignature,
|
|
4343
|
+
storeTrajectory: () => storeTrajectory,
|
|
4344
|
+
sweepTrajectories: () => sweepTrajectories
|
|
4345
|
+
});
|
|
4346
|
+
import crypto7 from "crypto";
|
|
4347
|
+
async function extractTrajectory(taskId, agentId) {
|
|
4625
4348
|
const client = getClient();
|
|
4626
|
-
const
|
|
4627
|
-
sql: `
|
|
4628
|
-
|
|
4629
|
-
|
|
4349
|
+
const result = await client.execute({
|
|
4350
|
+
sql: `SELECT tool_name, raw_text
|
|
4351
|
+
FROM memories
|
|
4352
|
+
WHERE task_id = ? AND agent_id = ?
|
|
4353
|
+
ORDER BY timestamp ASC`,
|
|
4354
|
+
args: [taskId, agentId]
|
|
4630
4355
|
});
|
|
4631
|
-
if (
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4356
|
+
if (result.rows.length === 0) return [];
|
|
4357
|
+
const rawTools = result.rows.map((r) => {
|
|
4358
|
+
const toolName = String(r.tool_name);
|
|
4359
|
+
if (toolName === "Bash") {
|
|
4360
|
+
const text = String(r.raw_text);
|
|
4361
|
+
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
4362
|
+
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
4363
|
+
}
|
|
4364
|
+
return toolName;
|
|
4365
|
+
});
|
|
4366
|
+
const signature = [];
|
|
4367
|
+
for (const tool of rawTools) {
|
|
4368
|
+
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
4369
|
+
signature.push(tool);
|
|
4645
4370
|
}
|
|
4646
4371
|
}
|
|
4372
|
+
return signature;
|
|
4647
4373
|
}
|
|
4648
|
-
|
|
4374
|
+
function hashSignature(signature) {
|
|
4375
|
+
return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
4376
|
+
}
|
|
4377
|
+
async function storeTrajectory(opts) {
|
|
4649
4378
|
const client = getClient();
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4379
|
+
const id = crypto7.randomUUID();
|
|
4380
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4381
|
+
const signatureHash = hashSignature(opts.signature);
|
|
4382
|
+
await client.execute({
|
|
4383
|
+
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
4384
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4385
|
+
args: [
|
|
4386
|
+
id,
|
|
4387
|
+
opts.taskId,
|
|
4388
|
+
opts.agentId,
|
|
4389
|
+
opts.projectName,
|
|
4390
|
+
opts.taskTitle,
|
|
4391
|
+
JSON.stringify(opts.signature),
|
|
4392
|
+
signatureHash,
|
|
4393
|
+
opts.signature.length,
|
|
4394
|
+
now
|
|
4395
|
+
]
|
|
4656
4396
|
});
|
|
4657
|
-
|
|
4658
|
-
const nr = nextResult.rows[0];
|
|
4659
|
-
return {
|
|
4660
|
-
title: String(nr.title),
|
|
4661
|
-
priority: String(nr.priority),
|
|
4662
|
-
taskFile: String(nr.task_file)
|
|
4663
|
-
};
|
|
4664
|
-
}
|
|
4665
|
-
return void 0;
|
|
4397
|
+
return id;
|
|
4666
4398
|
}
|
|
4667
|
-
async function
|
|
4399
|
+
async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
|
|
4668
4400
|
const client = getClient();
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4401
|
+
const hash = hashSignature(signature);
|
|
4402
|
+
const result = await client.execute({
|
|
4403
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
4404
|
+
FROM trajectories
|
|
4405
|
+
WHERE signature_hash = ?
|
|
4406
|
+
ORDER BY created_at DESC
|
|
4407
|
+
LIMIT 20`,
|
|
4408
|
+
args: [hash]
|
|
4673
4409
|
});
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4410
|
+
const mapRow = (r) => ({
|
|
4411
|
+
id: String(r.id),
|
|
4412
|
+
taskId: String(r.task_id),
|
|
4413
|
+
agentId: String(r.agent_id),
|
|
4414
|
+
projectName: String(r.project_name),
|
|
4415
|
+
taskTitle: String(r.task_title),
|
|
4416
|
+
signature: JSON.parse(String(r.signature)),
|
|
4417
|
+
signatureHash: String(r.signature_hash),
|
|
4418
|
+
toolCount: Number(r.tool_count),
|
|
4419
|
+
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
4420
|
+
createdAt: String(r.created_at)
|
|
4421
|
+
});
|
|
4422
|
+
const matches = result.rows.map(mapRow);
|
|
4423
|
+
if (matches.length >= threshold) return matches;
|
|
4424
|
+
const nearResult = await client.execute({
|
|
4425
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
4426
|
+
FROM trajectories
|
|
4427
|
+
WHERE tool_count BETWEEN ? AND ?
|
|
4428
|
+
AND signature_hash != ?
|
|
4429
|
+
ORDER BY created_at DESC
|
|
4430
|
+
LIMIT 50`,
|
|
4431
|
+
args: [
|
|
4432
|
+
Math.max(1, signature.length - 3),
|
|
4433
|
+
signature.length + 3,
|
|
4434
|
+
hash
|
|
4435
|
+
]
|
|
4436
|
+
});
|
|
4437
|
+
for (const r of nearResult.rows) {
|
|
4438
|
+
const candidateSig = JSON.parse(String(r.signature));
|
|
4439
|
+
if (editDistance(signature, candidateSig) <= 2) {
|
|
4440
|
+
matches.push(mapRow(r));
|
|
4691
4441
|
}
|
|
4692
4442
|
}
|
|
4443
|
+
return matches;
|
|
4693
4444
|
}
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
init_notifications();
|
|
4699
|
-
}
|
|
4700
|
-
});
|
|
4701
|
-
|
|
4702
|
-
// src/lib/project-name.ts
|
|
4703
|
-
import { execSync as execSync6 } from "child_process";
|
|
4704
|
-
import path16 from "path";
|
|
4705
|
-
function getProjectName(cwd) {
|
|
4706
|
-
const dir = cwd ?? process.cwd();
|
|
4707
|
-
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
4708
|
-
try {
|
|
4709
|
-
let repoRoot;
|
|
4710
|
-
try {
|
|
4711
|
-
const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
|
|
4712
|
-
cwd: dir,
|
|
4713
|
-
encoding: "utf8",
|
|
4714
|
-
timeout: 2e3,
|
|
4715
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4716
|
-
}).trim();
|
|
4717
|
-
repoRoot = path16.dirname(gitCommonDir);
|
|
4718
|
-
} catch {
|
|
4719
|
-
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
4720
|
-
cwd: dir,
|
|
4721
|
-
encoding: "utf8",
|
|
4722
|
-
timeout: 2e3,
|
|
4723
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4724
|
-
}).trim();
|
|
4725
|
-
}
|
|
4726
|
-
_cached2 = path16.basename(repoRoot);
|
|
4727
|
-
_cachedCwd = dir;
|
|
4728
|
-
return _cached2;
|
|
4729
|
-
} catch {
|
|
4730
|
-
_cached2 = path16.basename(dir);
|
|
4731
|
-
_cachedCwd = dir;
|
|
4732
|
-
return _cached2;
|
|
4445
|
+
async function captureTrajectory(opts) {
|
|
4446
|
+
const signature = await extractTrajectory(opts.taskId, opts.agentId);
|
|
4447
|
+
if (signature.length < 3) {
|
|
4448
|
+
return { trajectoryId: "", similarCount: 0, similar: [] };
|
|
4733
4449
|
}
|
|
4450
|
+
const trajectoryId = await storeTrajectory({
|
|
4451
|
+
taskId: opts.taskId,
|
|
4452
|
+
agentId: opts.agentId,
|
|
4453
|
+
projectName: opts.projectName,
|
|
4454
|
+
taskTitle: opts.taskTitle,
|
|
4455
|
+
signature
|
|
4456
|
+
});
|
|
4457
|
+
const similar = await findSimilarTrajectories(
|
|
4458
|
+
signature,
|
|
4459
|
+
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
4460
|
+
);
|
|
4461
|
+
return { trajectoryId, similarCount: similar.length, similar };
|
|
4734
4462
|
}
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
"
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
}
|
|
4742
|
-
});
|
|
4463
|
+
function buildExtractionPrompt(trajectories) {
|
|
4464
|
+
const items = trajectories.map((t, i) => {
|
|
4465
|
+
const sig = t.signature.join(" \u2192 ");
|
|
4466
|
+
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
4467
|
+
Signature: ${sig}`;
|
|
4468
|
+
}).join("\n\n");
|
|
4469
|
+
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
4743
4470
|
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
return parts[parts.length - 1] ?? null;
|
|
4471
|
+
${items}
|
|
4472
|
+
|
|
4473
|
+
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
4474
|
+
|
|
4475
|
+
SKILL: {name \u2014 short, descriptive}
|
|
4476
|
+
TRIGGER: {when to use this \u2014 one sentence}
|
|
4477
|
+
STEPS:
|
|
4478
|
+
1. ...
|
|
4479
|
+
2. ...
|
|
4480
|
+
PITFALLS: {common mistakes to avoid}
|
|
4481
|
+
|
|
4482
|
+
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
4757
4483
|
}
|
|
4758
|
-
function
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4484
|
+
async function extractSkill(trajectories, model) {
|
|
4485
|
+
if (trajectories.length === 0) return null;
|
|
4486
|
+
const config2 = await loadConfig();
|
|
4487
|
+
const skillModel = model ?? config2.skillModel;
|
|
4488
|
+
const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
|
|
4489
|
+
const client = new Anthropic4();
|
|
4490
|
+
const prompt = buildExtractionPrompt(trajectories);
|
|
4491
|
+
const response = await client.messages.create({
|
|
4492
|
+
model: skillModel,
|
|
4493
|
+
max_tokens: 500,
|
|
4494
|
+
messages: [{ role: "user", content: prompt }]
|
|
4495
|
+
});
|
|
4496
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
4497
|
+
const skillText = textBlock?.text;
|
|
4498
|
+
if (!skillText) return null;
|
|
4499
|
+
const agentId = trajectories[0].agentId;
|
|
4500
|
+
const projectName = trajectories[0].projectName;
|
|
4501
|
+
const skillId = await storeBehavior({
|
|
4502
|
+
agentId,
|
|
4503
|
+
content: skillText,
|
|
4504
|
+
domain: "skill",
|
|
4505
|
+
projectName
|
|
4506
|
+
});
|
|
4507
|
+
const dbClient = getClient();
|
|
4508
|
+
for (const t of trajectories) {
|
|
4509
|
+
await dbClient.execute({
|
|
4510
|
+
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
4511
|
+
args: [skillId, t.id]
|
|
4512
|
+
});
|
|
4763
4513
|
}
|
|
4764
|
-
|
|
4514
|
+
process.stderr.write(
|
|
4515
|
+
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
4516
|
+
`
|
|
4517
|
+
);
|
|
4518
|
+
return skillId;
|
|
4765
4519
|
}
|
|
4766
|
-
function
|
|
4520
|
+
async function captureAndLearn(opts) {
|
|
4767
4521
|
try {
|
|
4768
|
-
const
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4522
|
+
const config2 = await loadConfig();
|
|
4523
|
+
if (!config2.skillLearning) return;
|
|
4524
|
+
const { trajectoryId, similarCount, similar } = await captureTrajectory({
|
|
4525
|
+
...opts,
|
|
4526
|
+
skillThreshold: config2.skillThreshold
|
|
4527
|
+
});
|
|
4528
|
+
if (!trajectoryId) return;
|
|
4529
|
+
if (similarCount >= config2.skillThreshold) {
|
|
4530
|
+
const unprocessed = similar.filter((t) => !t.skillId);
|
|
4531
|
+
if (unprocessed.length >= config2.skillThreshold) {
|
|
4532
|
+
extractSkill(unprocessed, config2.skillModel).catch((err) => {
|
|
4533
|
+
process.stderr.write(
|
|
4534
|
+
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
4535
|
+
`
|
|
4536
|
+
);
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4780
4539
|
}
|
|
4540
|
+
} catch (err) {
|
|
4781
4541
|
process.stderr.write(
|
|
4782
|
-
`[
|
|
4542
|
+
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
4783
4543
|
`
|
|
4784
4544
|
);
|
|
4785
|
-
return {
|
|
4786
|
-
allowed: true,
|
|
4787
|
-
// v1: warn-only, don't block
|
|
4788
|
-
reason: "cross_session_granted",
|
|
4789
|
-
currentProject,
|
|
4790
|
-
targetProject,
|
|
4791
|
-
targetSession: findSessionForProject(targetProject)?.windowName
|
|
4792
|
-
};
|
|
4793
|
-
} catch {
|
|
4794
|
-
return { allowed: true, reason: "no_session" };
|
|
4795
4545
|
}
|
|
4796
4546
|
}
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
const
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4547
|
+
async function sweepTrajectories(threshold, model) {
|
|
4548
|
+
const config2 = await loadConfig();
|
|
4549
|
+
if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
4550
|
+
const t = threshold ?? config2.skillThreshold;
|
|
4551
|
+
const client = getClient();
|
|
4552
|
+
const result = await client.execute({
|
|
4553
|
+
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
4554
|
+
FROM trajectories
|
|
4555
|
+
WHERE skill_id IS NULL
|
|
4556
|
+
GROUP BY signature_hash
|
|
4557
|
+
HAVING cnt >= ?
|
|
4558
|
+
ORDER BY cnt DESC
|
|
4559
|
+
LIMIT 10`,
|
|
4560
|
+
args: [t]
|
|
4561
|
+
});
|
|
4562
|
+
let clustersProcessed = 0;
|
|
4563
|
+
let skillsExtracted = 0;
|
|
4564
|
+
for (const row of result.rows) {
|
|
4565
|
+
const hash = String(row.signature_hash);
|
|
4566
|
+
const trajResult = await client.execute({
|
|
4567
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
|
|
4568
|
+
FROM trajectories
|
|
4569
|
+
WHERE signature_hash = ? AND skill_id IS NULL
|
|
4570
|
+
ORDER BY created_at DESC
|
|
4571
|
+
LIMIT 10`,
|
|
4572
|
+
args: [hash]
|
|
4573
|
+
});
|
|
4574
|
+
const trajectories = trajResult.rows.map((r) => ({
|
|
4575
|
+
id: String(r.id),
|
|
4576
|
+
taskId: String(r.task_id),
|
|
4577
|
+
agentId: String(r.agent_id),
|
|
4578
|
+
projectName: String(r.project_name),
|
|
4579
|
+
taskTitle: String(r.task_title),
|
|
4580
|
+
signature: JSON.parse(String(r.signature)),
|
|
4581
|
+
signatureHash: String(r.signature_hash),
|
|
4582
|
+
toolCount: Number(r.tool_count),
|
|
4583
|
+
skillId: null,
|
|
4584
|
+
createdAt: String(r.created_at)
|
|
4585
|
+
}));
|
|
4586
|
+
if (trajectories.length >= t) {
|
|
4587
|
+
clustersProcessed++;
|
|
4588
|
+
const skillId = await extractSkill(trajectories, model ?? config2.skillModel);
|
|
4589
|
+
if (skillId) skillsExtracted++;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
return { clustersProcessed, skillsExtracted };
|
|
4593
|
+
}
|
|
4594
|
+
function editDistance(a, b) {
|
|
4595
|
+
const m = a.length;
|
|
4596
|
+
const n = b.length;
|
|
4597
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
4598
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
4599
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
4600
|
+
for (let i = 1; i <= m; i++) {
|
|
4601
|
+
for (let j = 1; j <= n; j++) {
|
|
4602
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4603
|
+
dp[i][j] = Math.min(
|
|
4604
|
+
dp[i - 1][j] + 1,
|
|
4605
|
+
dp[i][j - 1] + 1,
|
|
4606
|
+
dp[i - 1][j - 1] + cost
|
|
4607
|
+
);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
return dp[m][n];
|
|
4611
|
+
}
|
|
4612
|
+
var DEFAULT_SKILL_THRESHOLD;
|
|
4613
|
+
var init_skill_learning = __esm({
|
|
4614
|
+
"src/lib/skill-learning.ts"() {
|
|
4615
|
+
"use strict";
|
|
4616
|
+
init_database();
|
|
4617
|
+
init_behaviors();
|
|
4618
|
+
init_config();
|
|
4619
|
+
DEFAULT_SKILL_THRESHOLD = 3;
|
|
4620
|
+
}
|
|
4621
|
+
});
|
|
4622
|
+
|
|
4623
|
+
// src/lib/tasks.ts
|
|
4624
|
+
var tasks_exports = {};
|
|
4625
|
+
__export(tasks_exports, {
|
|
4626
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
4627
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
4628
|
+
countPendingReviews: () => countPendingReviews,
|
|
4629
|
+
createTask: () => createTask,
|
|
4630
|
+
createTaskCore: () => createTaskCore,
|
|
4631
|
+
deleteTask: () => deleteTask,
|
|
4632
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
4633
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
4634
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
4635
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
4636
|
+
listPendingReviews: () => listPendingReviews,
|
|
4637
|
+
listTasks: () => listTasks,
|
|
4638
|
+
resolveTask: () => resolveTask,
|
|
4639
|
+
slugify: () => slugify,
|
|
4640
|
+
updateTask: () => updateTask,
|
|
4641
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
4642
|
+
writeCheckpoint: () => writeCheckpoint
|
|
4643
|
+
});
|
|
4644
|
+
import path16 from "path";
|
|
4645
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4 } from "fs";
|
|
4646
|
+
async function createTask(input) {
|
|
4647
|
+
const result = await createTaskCore(input);
|
|
4648
|
+
if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
4649
|
+
dispatchTaskToEmployee({
|
|
4650
|
+
assignedTo: input.assignedTo,
|
|
4651
|
+
title: input.title,
|
|
4652
|
+
priority: input.priority,
|
|
4653
|
+
taskFile: result.taskFile,
|
|
4654
|
+
initialStatus: result.status,
|
|
4655
|
+
projectName: input.projectName
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
return result;
|
|
4659
|
+
}
|
|
4660
|
+
async function updateTask(input) {
|
|
4661
|
+
const { row, taskFile, now, taskId } = await updateTaskStatus(input);
|
|
4662
|
+
try {
|
|
4663
|
+
const agent = String(row.assigned_to);
|
|
4664
|
+
const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
|
|
4665
|
+
const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
|
|
4666
|
+
if (input.status === "in_progress") {
|
|
4667
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
4668
|
+
writeFileSync4(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
4669
|
+
} else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
|
|
4670
|
+
try {
|
|
4671
|
+
unlinkSync4(cachePath);
|
|
4672
|
+
} catch {
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
} catch {
|
|
4676
|
+
}
|
|
4677
|
+
if (input.status === "done") {
|
|
4678
|
+
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
4679
|
+
}
|
|
4680
|
+
if (input.status === "done" || input.status === "cancelled") {
|
|
4681
|
+
try {
|
|
4682
|
+
const client = getClient();
|
|
4683
|
+
const taskTitle = String(row.title);
|
|
4684
|
+
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
4685
|
+
await client.execute({
|
|
4686
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
4687
|
+
WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
|
|
4688
|
+
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
4833
4689
|
});
|
|
4834
|
-
|
|
4690
|
+
} catch {
|
|
4691
|
+
}
|
|
4692
|
+
try {
|
|
4693
|
+
const client = getClient();
|
|
4694
|
+
const cascaded = await client.execute({
|
|
4695
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
4696
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
4697
|
+
args: [now, taskId]
|
|
4698
|
+
});
|
|
4699
|
+
if (cascaded.rowsAffected > 0) {
|
|
4835
4700
|
process.stderr.write(
|
|
4836
|
-
`[
|
|
4701
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
4837
4702
|
`
|
|
4838
4703
|
);
|
|
4839
|
-
return { dispatched: "session_missing" };
|
|
4840
4704
|
}
|
|
4841
|
-
|
|
4705
|
+
} catch {
|
|
4842
4706
|
}
|
|
4843
|
-
} catch {
|
|
4844
|
-
return { dispatched: "session_missing" };
|
|
4845
4707
|
}
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4708
|
+
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
4709
|
+
if (isTerminal) {
|
|
4710
|
+
const isExe = String(row.assigned_to) === "exe";
|
|
4711
|
+
if (!isExe) {
|
|
4712
|
+
notifyTaskDone();
|
|
4713
|
+
}
|
|
4714
|
+
await markTaskNotificationsRead(taskFile);
|
|
4715
|
+
if (input.status === "done") {
|
|
4716
|
+
try {
|
|
4717
|
+
await cascadeUnblock(taskId, input.baseDir, now);
|
|
4718
|
+
} catch {
|
|
4719
|
+
}
|
|
4720
|
+
orgBus.emit({
|
|
4721
|
+
type: "task_completed",
|
|
4722
|
+
taskId,
|
|
4723
|
+
employee: String(row.assigned_to),
|
|
4724
|
+
result: input.result ?? "",
|
|
4725
|
+
timestamp: now
|
|
4726
|
+
});
|
|
4727
|
+
if (row.parent_task_id) {
|
|
4728
|
+
try {
|
|
4729
|
+
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
4730
|
+
} catch {
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4852
4734
|
}
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4735
|
+
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
4736
|
+
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
4737
|
+
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
4738
|
+
taskId,
|
|
4739
|
+
agentId: String(row.assigned_to),
|
|
4740
|
+
projectName: String(row.project_name),
|
|
4741
|
+
taskTitle: String(row.title)
|
|
4742
|
+
})
|
|
4743
|
+
).catch((err) => {
|
|
4744
|
+
process.stderr.write(
|
|
4745
|
+
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
4746
|
+
`
|
|
4747
|
+
);
|
|
4748
|
+
});
|
|
4858
4749
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
init_notifications();
|
|
4866
|
-
init_transport();
|
|
4867
|
-
init_employees();
|
|
4750
|
+
let nextTask;
|
|
4751
|
+
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
4752
|
+
try {
|
|
4753
|
+
nextTask = await findNextTask(String(row.assigned_to));
|
|
4754
|
+
} catch {
|
|
4755
|
+
}
|
|
4868
4756
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4757
|
+
return {
|
|
4758
|
+
id: String(row.id),
|
|
4759
|
+
title: String(row.title),
|
|
4760
|
+
assignedTo: String(row.assigned_to),
|
|
4761
|
+
assignedBy: String(row.assigned_by),
|
|
4762
|
+
projectName: String(row.project_name),
|
|
4763
|
+
priority: String(row.priority),
|
|
4764
|
+
status: input.status,
|
|
4765
|
+
taskFile,
|
|
4766
|
+
createdAt: String(row.created_at),
|
|
4767
|
+
updatedAt: now,
|
|
4768
|
+
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
4769
|
+
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
4770
|
+
tokensUsed: Number(row.tokens_used ?? 0),
|
|
4771
|
+
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
4772
|
+
nextTask
|
|
4773
|
+
};
|
|
4774
|
+
}
|
|
4775
|
+
async function deleteTask(taskId, baseDir) {
|
|
4874
4776
|
const client = getClient();
|
|
4875
|
-
const
|
|
4876
|
-
const
|
|
4777
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
4778
|
+
const reviewer = assignedBy || "exe";
|
|
4779
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
4780
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
4877
4781
|
await client.execute({
|
|
4878
|
-
sql:
|
|
4879
|
-
|
|
4880
|
-
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
4782
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
4783
|
+
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
4881
4784
|
});
|
|
4882
|
-
|
|
4785
|
+
await markAsReadByTaskFile(taskFile);
|
|
4786
|
+
await markAsReadByTaskFile(reviewFile);
|
|
4883
4787
|
}
|
|
4884
|
-
var
|
|
4885
|
-
"src/lib/
|
|
4788
|
+
var init_tasks = __esm({
|
|
4789
|
+
"src/lib/tasks.ts"() {
|
|
4886
4790
|
"use strict";
|
|
4887
4791
|
init_database();
|
|
4792
|
+
init_config();
|
|
4793
|
+
init_notifications();
|
|
4794
|
+
init_state_bus();
|
|
4795
|
+
init_tasks_crud();
|
|
4796
|
+
init_tasks_review();
|
|
4797
|
+
init_tasks_crud();
|
|
4798
|
+
init_tasks_chain();
|
|
4799
|
+
init_tasks_review();
|
|
4800
|
+
init_tasks_notify();
|
|
4888
4801
|
}
|
|
4889
4802
|
});
|
|
4890
4803
|
|
|
4891
|
-
// src/lib/
|
|
4892
|
-
var
|
|
4893
|
-
__export(
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4804
|
+
// src/lib/capacity-monitor.ts
|
|
4805
|
+
var capacity_monitor_exports = {};
|
|
4806
|
+
__export(capacity_monitor_exports, {
|
|
4807
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
4808
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
4809
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
4810
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
4811
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
4812
|
+
extractContextPercent: () => extractContextPercent,
|
|
4813
|
+
isAtCapacity: () => isAtCapacity,
|
|
4814
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
4815
|
+
pollCapacityDead: () => pollCapacityDead
|
|
4903
4816
|
});
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
if (
|
|
4927
|
-
|
|
4817
|
+
function resumeTaskTitle(agentId) {
|
|
4818
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
4819
|
+
}
|
|
4820
|
+
function buildResumeContext(agentId, openTasks) {
|
|
4821
|
+
const taskList = openTasks.map(
|
|
4822
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
4823
|
+
).join("\n");
|
|
4824
|
+
return [
|
|
4825
|
+
"## Context",
|
|
4826
|
+
"",
|
|
4827
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
4828
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
4829
|
+
"",
|
|
4830
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
4831
|
+
"",
|
|
4832
|
+
taskList,
|
|
4833
|
+
"",
|
|
4834
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
4835
|
+
].join("\n");
|
|
4836
|
+
}
|
|
4837
|
+
function filterPaneContent(paneOutput) {
|
|
4838
|
+
return paneOutput.split("\n").filter((line) => {
|
|
4839
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
4840
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
4841
|
+
if (line.includes(marker)) return false;
|
|
4842
|
+
}
|
|
4843
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
4844
|
+
if (re.test(line)) return false;
|
|
4928
4845
|
}
|
|
4846
|
+
return true;
|
|
4847
|
+
}).join("\n");
|
|
4848
|
+
}
|
|
4849
|
+
function extractContextPercent(paneOutput) {
|
|
4850
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
4851
|
+
if (!match) return null;
|
|
4852
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
4853
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4854
|
+
}
|
|
4855
|
+
function isAtCapacity(paneOutput) {
|
|
4856
|
+
const filtered = filterPaneContent(paneOutput);
|
|
4857
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
4858
|
+
}
|
|
4859
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
4860
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
4861
|
+
if (pendingSince === void 0) {
|
|
4862
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4863
|
+
return false;
|
|
4929
4864
|
}
|
|
4930
|
-
|
|
4865
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
4866
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4867
|
+
return false;
|
|
4868
|
+
}
|
|
4869
|
+
_pendingCapacityKill.delete(agentId);
|
|
4870
|
+
return true;
|
|
4931
4871
|
}
|
|
4932
|
-
function
|
|
4933
|
-
|
|
4872
|
+
function _resetPendingCapacityKills() {
|
|
4873
|
+
_pendingCapacityKill.clear();
|
|
4934
4874
|
}
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
const id = crypto7.randomUUID();
|
|
4938
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4939
|
-
const signatureHash = hashSignature(opts.signature);
|
|
4940
|
-
await client.execute({
|
|
4941
|
-
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
4942
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4943
|
-
args: [
|
|
4944
|
-
id,
|
|
4945
|
-
opts.taskId,
|
|
4946
|
-
opts.agentId,
|
|
4947
|
-
opts.projectName,
|
|
4948
|
-
opts.taskTitle,
|
|
4949
|
-
JSON.stringify(opts.signature),
|
|
4950
|
-
signatureHash,
|
|
4951
|
-
opts.signature.length,
|
|
4952
|
-
now
|
|
4953
|
-
]
|
|
4954
|
-
});
|
|
4955
|
-
return id;
|
|
4875
|
+
function _resetLastRelaunchCache() {
|
|
4876
|
+
_lastRelaunch.clear();
|
|
4956
4877
|
}
|
|
4957
|
-
async function
|
|
4878
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
4958
4879
|
const client = getClient();
|
|
4959
|
-
const hash = hashSignature(signature);
|
|
4960
4880
|
const result = await client.execute({
|
|
4961
|
-
sql: `SELECT
|
|
4962
|
-
FROM
|
|
4963
|
-
WHERE
|
|
4964
|
-
|
|
4965
|
-
LIMIT 20`,
|
|
4966
|
-
args: [hash]
|
|
4967
|
-
});
|
|
4968
|
-
const mapRow = (r) => ({
|
|
4969
|
-
id: String(r.id),
|
|
4970
|
-
taskId: String(r.task_id),
|
|
4971
|
-
agentId: String(r.agent_id),
|
|
4972
|
-
projectName: String(r.project_name),
|
|
4973
|
-
taskTitle: String(r.task_title),
|
|
4974
|
-
signature: JSON.parse(String(r.signature)),
|
|
4975
|
-
signatureHash: String(r.signature_hash),
|
|
4976
|
-
toolCount: Number(r.tool_count),
|
|
4977
|
-
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
4978
|
-
createdAt: String(r.created_at)
|
|
4881
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
4882
|
+
FROM tasks
|
|
4883
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
4884
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
4979
4885
|
});
|
|
4980
|
-
const
|
|
4981
|
-
if (
|
|
4982
|
-
const
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4886
|
+
const raw = result.rows[0]?.last_created_at;
|
|
4887
|
+
if (raw === null || raw === void 0) return null;
|
|
4888
|
+
const parsed = Date.parse(String(raw));
|
|
4889
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4890
|
+
}
|
|
4891
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
4892
|
+
const cached = _lastRelaunch.get(agentId);
|
|
4893
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
4894
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
4895
|
+
if (persisted === null) return false;
|
|
4896
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
4897
|
+
_lastRelaunch.set(agentId, persisted);
|
|
4898
|
+
return true;
|
|
4899
|
+
}
|
|
4900
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
4901
|
+
const client = getClient();
|
|
4902
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4903
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
4904
|
+
const existing = await client.execute({
|
|
4905
|
+
sql: `SELECT id FROM tasks
|
|
4906
|
+
WHERE assigned_to = ?
|
|
4907
|
+
AND title LIKE ?
|
|
4908
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
4987
4909
|
ORDER BY created_at DESC
|
|
4988
|
-
LIMIT
|
|
4989
|
-
args: [
|
|
4990
|
-
Math.max(1, signature.length - 3),
|
|
4991
|
-
signature.length + 3,
|
|
4992
|
-
hash
|
|
4993
|
-
]
|
|
4910
|
+
LIMIT 1`,
|
|
4911
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
4994
4912
|
});
|
|
4995
|
-
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
}
|
|
5003
|
-
async function captureTrajectory(opts) {
|
|
5004
|
-
const signature = await extractTrajectory(opts.taskId, opts.agentId);
|
|
5005
|
-
if (signature.length < 3) {
|
|
5006
|
-
return { trajectoryId: "", similarCount: 0, similar: [] };
|
|
4913
|
+
if (existing.rows.length > 0) {
|
|
4914
|
+
const taskId = String(existing.rows[0].id);
|
|
4915
|
+
await client.execute({
|
|
4916
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
4917
|
+
args: [context, now, taskId]
|
|
4918
|
+
});
|
|
4919
|
+
return { created: false, taskId };
|
|
5007
4920
|
}
|
|
5008
|
-
const
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
4921
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
4922
|
+
const task = await createTask2({
|
|
4923
|
+
title: resumeTaskTitle(agentId),
|
|
4924
|
+
assignedTo: agentId,
|
|
4925
|
+
assignedBy: "system",
|
|
4926
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
4927
|
+
priority: "p0",
|
|
4928
|
+
context,
|
|
4929
|
+
baseDir: projectDir
|
|
5014
4930
|
});
|
|
5015
|
-
|
|
5016
|
-
signature,
|
|
5017
|
-
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
5018
|
-
);
|
|
5019
|
-
return { trajectoryId, similarCount: similar.length, similar };
|
|
5020
|
-
}
|
|
5021
|
-
function buildExtractionPrompt(trajectories) {
|
|
5022
|
-
const items = trajectories.map((t, i) => {
|
|
5023
|
-
const sig = t.signature.join(" \u2192 ");
|
|
5024
|
-
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
5025
|
-
Signature: ${sig}`;
|
|
5026
|
-
}).join("\n\n");
|
|
5027
|
-
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
5028
|
-
|
|
5029
|
-
${items}
|
|
5030
|
-
|
|
5031
|
-
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
5032
|
-
|
|
5033
|
-
SKILL: {name \u2014 short, descriptive}
|
|
5034
|
-
TRIGGER: {when to use this \u2014 one sentence}
|
|
5035
|
-
STEPS:
|
|
5036
|
-
1. ...
|
|
5037
|
-
2. ...
|
|
5038
|
-
PITFALLS: {common mistakes to avoid}
|
|
5039
|
-
|
|
5040
|
-
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
4931
|
+
return { created: true, taskId: task.id };
|
|
5041
4932
|
}
|
|
5042
|
-
async function
|
|
5043
|
-
|
|
5044
|
-
const
|
|
5045
|
-
const
|
|
5046
|
-
|
|
5047
|
-
const client = new Anthropic4();
|
|
5048
|
-
const prompt = buildExtractionPrompt(trajectories);
|
|
5049
|
-
const response = await client.messages.create({
|
|
5050
|
-
model: skillModel,
|
|
5051
|
-
max_tokens: 500,
|
|
5052
|
-
messages: [{ role: "user", content: prompt }]
|
|
5053
|
-
});
|
|
5054
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
5055
|
-
const skillText = textBlock?.text;
|
|
5056
|
-
if (!skillText) return null;
|
|
5057
|
-
const agentId = trajectories[0].agentId;
|
|
5058
|
-
const projectName = trajectories[0].projectName;
|
|
5059
|
-
const skillId = await storeBehavior({
|
|
5060
|
-
agentId,
|
|
5061
|
-
content: skillText,
|
|
5062
|
-
domain: "skill",
|
|
5063
|
-
projectName
|
|
5064
|
-
});
|
|
5065
|
-
const dbClient = getClient();
|
|
5066
|
-
for (const t of trajectories) {
|
|
5067
|
-
await dbClient.execute({
|
|
5068
|
-
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
5069
|
-
args: [skillId, t.id]
|
|
5070
|
-
});
|
|
5071
|
-
}
|
|
5072
|
-
process.stderr.write(
|
|
5073
|
-
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
5074
|
-
`
|
|
4933
|
+
async function pollCapacityDead() {
|
|
4934
|
+
const transport = getTransport();
|
|
4935
|
+
const relaunched = [];
|
|
4936
|
+
const registered = listSessions().filter(
|
|
4937
|
+
(s) => s.agentId !== "exe"
|
|
5075
4938
|
);
|
|
5076
|
-
return
|
|
5077
|
-
|
|
5078
|
-
async function captureAndLearn(opts) {
|
|
4939
|
+
if (registered.length === 0) return [];
|
|
4940
|
+
let liveSessions;
|
|
5079
4941
|
try {
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
}
|
|
5086
|
-
if (!
|
|
5087
|
-
if (
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
4942
|
+
liveSessions = transport.listSessions();
|
|
4943
|
+
} catch {
|
|
4944
|
+
return [];
|
|
4945
|
+
}
|
|
4946
|
+
for (const entry of registered) {
|
|
4947
|
+
const { windowName, agentId, projectDir } = entry;
|
|
4948
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
4949
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
4950
|
+
let pane;
|
|
4951
|
+
try {
|
|
4952
|
+
pane = transport.capturePane(windowName, 15);
|
|
4953
|
+
} catch {
|
|
4954
|
+
continue;
|
|
4955
|
+
}
|
|
4956
|
+
if (!isAtCapacity(pane)) continue;
|
|
4957
|
+
const ctxPct = extractContextPercent(pane);
|
|
4958
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
4959
|
+
process.stderr.write(
|
|
4960
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
5093
4961
|
`
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
4962
|
+
);
|
|
4963
|
+
continue;
|
|
4964
|
+
}
|
|
4965
|
+
if (!confirmCapacityKill(agentId)) {
|
|
4966
|
+
process.stderr.write(
|
|
4967
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
4968
|
+
`
|
|
4969
|
+
);
|
|
4970
|
+
continue;
|
|
4971
|
+
}
|
|
4972
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
4973
|
+
if (!verify.atCapacity) {
|
|
4974
|
+
process.stderr.write(
|
|
4975
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
4976
|
+
`
|
|
4977
|
+
);
|
|
4978
|
+
void recordSessionKill({
|
|
4979
|
+
sessionName: windowName,
|
|
4980
|
+
agentId,
|
|
4981
|
+
reason: "capacity_false_positive_blocked"
|
|
4982
|
+
});
|
|
4983
|
+
continue;
|
|
5097
4984
|
}
|
|
5098
|
-
} catch (err) {
|
|
5099
4985
|
process.stderr.write(
|
|
5100
|
-
`[
|
|
4986
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
5101
4987
|
`
|
|
5102
4988
|
);
|
|
4989
|
+
try {
|
|
4990
|
+
transport.kill(windowName);
|
|
4991
|
+
void recordSessionKill({
|
|
4992
|
+
sessionName: windowName,
|
|
4993
|
+
agentId,
|
|
4994
|
+
reason: "capacity"
|
|
4995
|
+
});
|
|
4996
|
+
const client = getClient();
|
|
4997
|
+
const openTasks = await client.execute({
|
|
4998
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
4999
|
+
FROM tasks
|
|
5000
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
5001
|
+
ORDER BY
|
|
5002
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
5003
|
+
created_at ASC
|
|
5004
|
+
LIMIT 10`,
|
|
5005
|
+
args: [agentId]
|
|
5006
|
+
});
|
|
5007
|
+
if (openTasks.rows.length === 0) {
|
|
5008
|
+
process.stderr.write(
|
|
5009
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
5010
|
+
`
|
|
5011
|
+
);
|
|
5012
|
+
continue;
|
|
5013
|
+
}
|
|
5014
|
+
const { created } = await createOrRefreshResumeTask(
|
|
5015
|
+
agentId,
|
|
5016
|
+
projectDir,
|
|
5017
|
+
openTasks.rows
|
|
5018
|
+
);
|
|
5019
|
+
if (created) {
|
|
5020
|
+
await writeNotification({
|
|
5021
|
+
agentId: "system",
|
|
5022
|
+
agentRole: "daemon",
|
|
5023
|
+
event: "capacity_relaunch",
|
|
5024
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
5025
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
5029
|
+
if (created) relaunched.push(agentId);
|
|
5030
|
+
} catch (err) {
|
|
5031
|
+
process.stderr.write(
|
|
5032
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
5033
|
+
`
|
|
5034
|
+
);
|
|
5035
|
+
}
|
|
5103
5036
|
}
|
|
5037
|
+
return relaunched;
|
|
5104
5038
|
}
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5039
|
+
var CAPACITY_PATTERNS, CONTENT_LINE_PREFIX, CONTENT_LINE_MARKERS, SOURCE_CODE_MARKERS, RELAUNCH_COOLDOWN_MS, _lastRelaunch, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS, _pendingCapacityKill, CC_CONTEXT_BAR_RE, CTX_FLOOR_PERCENT;
|
|
5040
|
+
var init_capacity_monitor = __esm({
|
|
5041
|
+
"src/lib/capacity-monitor.ts"() {
|
|
5042
|
+
"use strict";
|
|
5043
|
+
init_session_registry();
|
|
5044
|
+
init_transport();
|
|
5045
|
+
init_notifications();
|
|
5046
|
+
init_database();
|
|
5047
|
+
init_session_kill_telemetry();
|
|
5048
|
+
init_tmux_routing();
|
|
5049
|
+
CAPACITY_PATTERNS = [
|
|
5050
|
+
/conversation is too long/i,
|
|
5051
|
+
/maximum context length/i,
|
|
5052
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
5053
|
+
/reached.*(?:token|context).*limit/i
|
|
5054
|
+
];
|
|
5055
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
5056
|
+
CONTENT_LINE_MARKERS = [
|
|
5057
|
+
"RESUME:",
|
|
5058
|
+
"intercom",
|
|
5059
|
+
"capacity-monitor",
|
|
5060
|
+
"CAPACITY_PATTERNS",
|
|
5061
|
+
"isAtCapacity",
|
|
5062
|
+
"CONTENT_LINE_MARKERS",
|
|
5063
|
+
"pollCapacityDead",
|
|
5064
|
+
"confirmCapacityKill",
|
|
5065
|
+
"session_kills",
|
|
5066
|
+
"capacity-monitor.test"
|
|
5067
|
+
];
|
|
5068
|
+
SOURCE_CODE_MARKERS = [
|
|
5069
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
5070
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
5071
|
+
];
|
|
5072
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
5073
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
5074
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
5075
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
5076
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
5077
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
5078
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
5079
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
5080
|
+
CTX_FLOOR_PERCENT = 50;
|
|
5081
|
+
}
|
|
5082
|
+
});
|
|
5083
|
+
|
|
5084
|
+
// src/lib/tmux-routing.ts
|
|
5085
|
+
var tmux_routing_exports = {};
|
|
5086
|
+
__export(tmux_routing_exports, {
|
|
5087
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
5088
|
+
employeeSessionName: () => employeeSessionName,
|
|
5089
|
+
ensureEmployee: () => ensureEmployee,
|
|
5090
|
+
extractRootExe: () => extractRootExe,
|
|
5091
|
+
findFreeInstance: () => findFreeInstance,
|
|
5092
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
5093
|
+
getMySession: () => getMySession,
|
|
5094
|
+
getParentExe: () => getParentExe,
|
|
5095
|
+
getSessionState: () => getSessionState,
|
|
5096
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
5097
|
+
isExeSession: () => isExeSession,
|
|
5098
|
+
isSessionBusy: () => isSessionBusy,
|
|
5099
|
+
notifyParentExe: () => notifyParentExe,
|
|
5100
|
+
parseParentExe: () => parseParentExe,
|
|
5101
|
+
registerParentExe: () => registerParentExe,
|
|
5102
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
5103
|
+
resolveExeSession: () => resolveExeSession,
|
|
5104
|
+
sendIntercom: () => sendIntercom,
|
|
5105
|
+
spawnEmployee: () => spawnEmployee,
|
|
5106
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
5107
|
+
});
|
|
5108
|
+
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
5109
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
|
|
5110
|
+
import path17 from "path";
|
|
5111
|
+
import os7 from "os";
|
|
5112
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5113
|
+
import { unlinkSync as unlinkSync5 } from "fs";
|
|
5114
|
+
function spawnLockPath(sessionName) {
|
|
5115
|
+
return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
5116
|
+
}
|
|
5117
|
+
function isProcessAlive(pid) {
|
|
5118
|
+
try {
|
|
5119
|
+
process.kill(pid, 0);
|
|
5120
|
+
return true;
|
|
5121
|
+
} catch {
|
|
5122
|
+
return false;
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
function acquireSpawnLock2(sessionName) {
|
|
5126
|
+
if (!existsSync13(SPAWN_LOCK_DIR)) {
|
|
5127
|
+
mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
|
|
5128
|
+
}
|
|
5129
|
+
const lockFile = spawnLockPath(sessionName);
|
|
5130
|
+
if (existsSync13(lockFile)) {
|
|
5131
|
+
try {
|
|
5132
|
+
const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
|
|
5133
|
+
const age = Date.now() - lock.timestamp;
|
|
5134
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
5135
|
+
return false;
|
|
5136
|
+
}
|
|
5137
|
+
} catch {
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
5141
|
+
return true;
|
|
5142
|
+
}
|
|
5143
|
+
function releaseSpawnLock2(sessionName) {
|
|
5144
|
+
try {
|
|
5145
|
+
unlinkSync5(spawnLockPath(sessionName));
|
|
5146
|
+
} catch {
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
function resolveBehaviorsExporterScript() {
|
|
5150
|
+
try {
|
|
5151
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
5152
|
+
const scriptPath = path17.join(
|
|
5153
|
+
path17.dirname(thisFile),
|
|
5154
|
+
"..",
|
|
5155
|
+
"bin",
|
|
5156
|
+
"exe-export-behaviors.js"
|
|
5157
|
+
);
|
|
5158
|
+
return existsSync13(scriptPath) ? scriptPath : null;
|
|
5159
|
+
} catch {
|
|
5160
|
+
return null;
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
5164
|
+
const script = resolveBehaviorsExporterScript();
|
|
5165
|
+
if (!script) return null;
|
|
5166
|
+
try {
|
|
5167
|
+
const output = execFileSync2(
|
|
5168
|
+
process.execPath,
|
|
5169
|
+
[script, agentId, projectName, sessionKey],
|
|
5170
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
5171
|
+
).trim();
|
|
5172
|
+
return output.length > 0 ? output : null;
|
|
5173
|
+
} catch (err) {
|
|
5174
|
+
process.stderr.write(
|
|
5175
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
5176
|
+
`
|
|
5177
|
+
);
|
|
5178
|
+
return null;
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
function getMySession() {
|
|
5182
|
+
return getTransport().getMySession();
|
|
5183
|
+
}
|
|
5184
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
5185
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
5186
|
+
const root = extractRootExe(exeSession);
|
|
5187
|
+
if (root) {
|
|
5188
|
+
process.stderr.write(
|
|
5189
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
5190
|
+
`
|
|
5191
|
+
);
|
|
5192
|
+
exeSession = root;
|
|
5193
|
+
} else {
|
|
5194
|
+
throw new Error(
|
|
5195
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
5196
|
+
);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
5200
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
5201
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
5202
|
+
throw new Error(
|
|
5203
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
return name;
|
|
5207
|
+
}
|
|
5208
|
+
function parseParentExe(sessionName, agentId) {
|
|
5209
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5210
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
5211
|
+
const match = sessionName.match(regex);
|
|
5212
|
+
return match?.[1] ?? null;
|
|
5213
|
+
}
|
|
5214
|
+
function extractRootExe(name) {
|
|
5215
|
+
const match = name.match(/(exe\d+)$/);
|
|
5216
|
+
return match?.[1] ?? null;
|
|
5217
|
+
}
|
|
5218
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
5219
|
+
if (!existsSync13(SESSION_CACHE)) {
|
|
5220
|
+
mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
5221
|
+
}
|
|
5222
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
5223
|
+
const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
5224
|
+
writeFileSync5(filePath, JSON.stringify({
|
|
5225
|
+
parentExe: rootExe,
|
|
5226
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
5227
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5228
|
+
}));
|
|
5229
|
+
}
|
|
5230
|
+
function getParentExe(sessionKey) {
|
|
5231
|
+
try {
|
|
5232
|
+
const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
5233
|
+
return data.parentExe || null;
|
|
5234
|
+
} catch {
|
|
5235
|
+
return null;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
function getDispatchedBy(sessionKey) {
|
|
5239
|
+
try {
|
|
5240
|
+
const data = JSON.parse(readFileSync11(
|
|
5241
|
+
path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
5242
|
+
"utf8"
|
|
5243
|
+
));
|
|
5244
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
5245
|
+
} catch {
|
|
5246
|
+
return null;
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
function resolveExeSession() {
|
|
5250
|
+
const mySession = getMySession();
|
|
5251
|
+
if (!mySession) return null;
|
|
5252
|
+
try {
|
|
5253
|
+
const key = getSessionKey();
|
|
5254
|
+
const parentExe = getParentExe(key);
|
|
5255
|
+
if (parentExe) {
|
|
5256
|
+
return extractRootExe(parentExe) ?? parentExe;
|
|
5257
|
+
}
|
|
5258
|
+
} catch {
|
|
5259
|
+
}
|
|
5260
|
+
return extractRootExe(mySession) ?? mySession;
|
|
5261
|
+
}
|
|
5262
|
+
function isEmployeeAlive(sessionName) {
|
|
5263
|
+
return getTransport().isAlive(sessionName);
|
|
5264
|
+
}
|
|
5265
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
5266
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
5267
|
+
if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
|
|
5268
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
5269
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
5270
|
+
if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
|
|
5271
|
+
}
|
|
5272
|
+
return null;
|
|
5273
|
+
}
|
|
5274
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
5275
|
+
const transport = getTransport();
|
|
5276
|
+
if (!transport.isAlive(sessionName)) {
|
|
5277
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
5278
|
+
}
|
|
5279
|
+
let pane;
|
|
5280
|
+
try {
|
|
5281
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
5282
|
+
} catch (err) {
|
|
5283
|
+
return {
|
|
5284
|
+
atCapacity: false,
|
|
5285
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
5289
|
+
if (!isAtCapacity2(pane)) {
|
|
5290
|
+
return {
|
|
5291
|
+
atCapacity: false,
|
|
5292
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
5293
|
+
};
|
|
5294
|
+
}
|
|
5295
|
+
return {
|
|
5296
|
+
atCapacity: true,
|
|
5297
|
+
reason: "capacity banner matched in recent pane output"
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
function readDebounceState() {
|
|
5301
|
+
try {
|
|
5302
|
+
if (!existsSync13(DEBOUNCE_FILE)) return {};
|
|
5303
|
+
return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
|
|
5304
|
+
} catch {
|
|
5305
|
+
return {};
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
function writeDebounceState(state) {
|
|
5309
|
+
try {
|
|
5310
|
+
if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
|
|
5311
|
+
writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
|
|
5312
|
+
} catch {
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
function isDebounced(targetSession) {
|
|
5316
|
+
const state = readDebounceState();
|
|
5317
|
+
const lastSent = state[targetSession] ?? 0;
|
|
5318
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
5319
|
+
}
|
|
5320
|
+
function recordDebounce(targetSession) {
|
|
5321
|
+
const state = readDebounceState();
|
|
5322
|
+
state[targetSession] = Date.now();
|
|
5323
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
5324
|
+
for (const key of Object.keys(state)) {
|
|
5325
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
5326
|
+
}
|
|
5327
|
+
writeDebounceState(state);
|
|
5328
|
+
}
|
|
5329
|
+
function logIntercom(msg) {
|
|
5330
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
5331
|
+
`;
|
|
5332
|
+
process.stderr.write(`[intercom] ${msg}
|
|
5333
|
+
`);
|
|
5334
|
+
try {
|
|
5335
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
5336
|
+
} catch {
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
function getSessionState(sessionName) {
|
|
5340
|
+
const transport = getTransport();
|
|
5341
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
5342
|
+
try {
|
|
5343
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
5344
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
5345
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
5346
|
+
return "no_claude";
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
if (/Running…/.test(pane)) return "tool";
|
|
5350
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
5351
|
+
return "idle";
|
|
5352
|
+
} catch {
|
|
5353
|
+
return "offline";
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
function isSessionBusy(sessionName) {
|
|
5357
|
+
const state = getSessionState(sessionName);
|
|
5358
|
+
return state === "thinking" || state === "tool";
|
|
5359
|
+
}
|
|
5360
|
+
function isExeSession(sessionName) {
|
|
5361
|
+
return /^exe\d*$/.test(sessionName);
|
|
5362
|
+
}
|
|
5363
|
+
function sendIntercom(targetSession) {
|
|
5364
|
+
const transport = getTransport();
|
|
5365
|
+
if (isExeSession(targetSession)) {
|
|
5366
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
5367
|
+
return "skipped_exe";
|
|
5368
|
+
}
|
|
5369
|
+
if (isDebounced(targetSession)) {
|
|
5370
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
5371
|
+
return "debounced";
|
|
5372
|
+
}
|
|
5373
|
+
try {
|
|
5374
|
+
const sessions = transport.listSessions();
|
|
5375
|
+
if (!sessions.includes(targetSession)) {
|
|
5376
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
5377
|
+
return "failed";
|
|
5378
|
+
}
|
|
5379
|
+
const sessionState = getSessionState(targetSession);
|
|
5380
|
+
if (sessionState === "no_claude") {
|
|
5381
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
5382
|
+
recordDebounce(targetSession);
|
|
5383
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
5384
|
+
return "queued";
|
|
5385
|
+
}
|
|
5386
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
5387
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
5388
|
+
recordDebounce(targetSession);
|
|
5389
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
5390
|
+
return "queued";
|
|
5391
|
+
}
|
|
5392
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
5393
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
5394
|
+
transport.sendKeys(targetSession, "q");
|
|
5395
|
+
}
|
|
5396
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
5397
|
+
recordDebounce(targetSession);
|
|
5398
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
5399
|
+
return "delivered";
|
|
5400
|
+
} catch {
|
|
5401
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
5402
|
+
return "failed";
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
function notifyParentExe(sessionKey) {
|
|
5406
|
+
const target = getDispatchedBy(sessionKey);
|
|
5407
|
+
if (!target) {
|
|
5408
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
5409
|
+
`);
|
|
5410
|
+
return false;
|
|
5411
|
+
}
|
|
5412
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
5413
|
+
`);
|
|
5414
|
+
const result = sendIntercom(target);
|
|
5415
|
+
if (result === "failed") {
|
|
5416
|
+
const rootExe = resolveExeSession();
|
|
5417
|
+
if (rootExe && rootExe !== target) {
|
|
5418
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
5419
|
+
`);
|
|
5420
|
+
const fallback = sendIntercom(rootExe);
|
|
5421
|
+
return fallback !== "failed";
|
|
5422
|
+
}
|
|
5423
|
+
return false;
|
|
5424
|
+
}
|
|
5425
|
+
return true;
|
|
5426
|
+
}
|
|
5427
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
5428
|
+
if (employeeName === "exe") {
|
|
5429
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
5430
|
+
}
|
|
5431
|
+
try {
|
|
5432
|
+
assertEmployeeLimitSync();
|
|
5433
|
+
} catch (err) {
|
|
5434
|
+
if (err instanceof PlanLimitError) {
|
|
5435
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
5439
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
5440
|
+
return {
|
|
5441
|
+
status: "failed",
|
|
5442
|
+
sessionName: "",
|
|
5443
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
5447
|
+
const root = extractRootExe(exeSession);
|
|
5448
|
+
if (root) {
|
|
5449
|
+
process.stderr.write(
|
|
5450
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
5451
|
+
`
|
|
5452
|
+
);
|
|
5453
|
+
exeSession = root;
|
|
5454
|
+
} else {
|
|
5455
|
+
return {
|
|
5456
|
+
status: "failed",
|
|
5457
|
+
sessionName: "",
|
|
5458
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
5459
|
+
};
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
let effectiveInstance = opts?.instance;
|
|
5463
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
5464
|
+
const free = findFreeInstance(
|
|
5465
|
+
employeeName,
|
|
5466
|
+
exeSession,
|
|
5467
|
+
opts.maxAutoInstances ?? 10
|
|
5468
|
+
);
|
|
5469
|
+
if (free === null) {
|
|
5470
|
+
return {
|
|
5471
|
+
status: "failed",
|
|
5472
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
5473
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
5477
|
+
}
|
|
5478
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
5479
|
+
if (isEmployeeAlive(sessionName)) {
|
|
5480
|
+
const result2 = sendIntercom(sessionName);
|
|
5481
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
5482
|
+
return { status: "intercom_sent", sessionName };
|
|
5483
|
+
}
|
|
5484
|
+
if (result2 === "delivered") {
|
|
5485
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
5486
|
+
}
|
|
5487
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
5488
|
+
}
|
|
5489
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
5490
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
5491
|
+
if (result.error) {
|
|
5492
|
+
return { status: "failed", sessionName, error: result.error };
|
|
5493
|
+
}
|
|
5494
|
+
return { status: "spawned", sessionName };
|
|
5495
|
+
}
|
|
5496
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
5497
|
+
const transport = getTransport();
|
|
5498
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
5499
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
5500
|
+
const logDir = path17.join(os7.homedir(), ".exe-os", "session-logs");
|
|
5501
|
+
const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
5502
|
+
if (!existsSync13(logDir)) {
|
|
5503
|
+
mkdirSync7(logDir, { recursive: true });
|
|
5504
|
+
}
|
|
5505
|
+
transport.kill(sessionName);
|
|
5506
|
+
let cleanupSuffix = "";
|
|
5507
|
+
try {
|
|
5508
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
5509
|
+
const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
5510
|
+
if (existsSync13(cleanupScript)) {
|
|
5511
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
5512
|
+
}
|
|
5513
|
+
} catch {
|
|
5514
|
+
}
|
|
5515
|
+
try {
|
|
5516
|
+
const claudeJsonPath = path17.join(os7.homedir(), ".claude.json");
|
|
5517
|
+
let claudeJson = {};
|
|
5518
|
+
try {
|
|
5519
|
+
claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
|
|
5520
|
+
} catch {
|
|
5521
|
+
}
|
|
5522
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
5523
|
+
const projects = claudeJson.projects;
|
|
5524
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
5525
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
5526
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
5527
|
+
writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
5528
|
+
} catch {
|
|
5529
|
+
}
|
|
5530
|
+
try {
|
|
5531
|
+
const settingsDir = path17.join(os7.homedir(), ".claude", "projects");
|
|
5532
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
5533
|
+
const projSettingsDir = path17.join(settingsDir, normalizedKey);
|
|
5534
|
+
const settingsPath = path17.join(projSettingsDir, "settings.json");
|
|
5535
|
+
let settings = {};
|
|
5536
|
+
try {
|
|
5537
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
5538
|
+
} catch {
|
|
5539
|
+
}
|
|
5540
|
+
const perms = settings.permissions ?? {};
|
|
5541
|
+
const allow = perms.allow ?? [];
|
|
5542
|
+
const toolNames = [
|
|
5543
|
+
"recall_my_memory",
|
|
5544
|
+
"store_memory",
|
|
5545
|
+
"create_task",
|
|
5546
|
+
"update_task",
|
|
5547
|
+
"list_tasks",
|
|
5548
|
+
"get_task",
|
|
5549
|
+
"ask_team_memory",
|
|
5550
|
+
"store_behavior",
|
|
5551
|
+
"get_identity",
|
|
5552
|
+
"send_message"
|
|
5553
|
+
];
|
|
5554
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
5555
|
+
let changed = false;
|
|
5556
|
+
for (const tool of requiredTools) {
|
|
5557
|
+
if (!allow.includes(tool)) {
|
|
5558
|
+
allow.push(tool);
|
|
5559
|
+
changed = true;
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
if (changed) {
|
|
5563
|
+
perms.allow = allow;
|
|
5564
|
+
settings.permissions = perms;
|
|
5565
|
+
mkdirSync7(projSettingsDir, { recursive: true });
|
|
5566
|
+
writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
5567
|
+
}
|
|
5568
|
+
} catch {
|
|
5569
|
+
}
|
|
5570
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
5571
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
5572
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
5573
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
5574
|
+
let identityFlag = "";
|
|
5575
|
+
let behaviorsFlag = "";
|
|
5576
|
+
let legacyFallbackWarned = false;
|
|
5577
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
5578
|
+
const identityPath = path17.join(
|
|
5579
|
+
os7.homedir(),
|
|
5580
|
+
".exe-os",
|
|
5581
|
+
"identity",
|
|
5582
|
+
`${employeeName}.md`
|
|
5583
|
+
);
|
|
5584
|
+
_resetCcAgentSupportCache();
|
|
5585
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
5586
|
+
if (hasAgentFlag) {
|
|
5587
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
5588
|
+
} else if (existsSync13(identityPath)) {
|
|
5589
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
5590
|
+
legacyFallbackWarned = true;
|
|
5591
|
+
}
|
|
5592
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
5593
|
+
employeeName,
|
|
5594
|
+
path17.basename(spawnCwd),
|
|
5595
|
+
sessionName
|
|
5596
|
+
);
|
|
5597
|
+
if (behaviorsFile) {
|
|
5598
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
if (legacyFallbackWarned) {
|
|
5602
|
+
process.stderr.write(
|
|
5603
|
+
`[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
|
|
5604
|
+
`
|
|
5605
|
+
);
|
|
5606
|
+
}
|
|
5607
|
+
let sessionContextFlag = "";
|
|
5608
|
+
try {
|
|
5609
|
+
const ctxDir = path17.join(os7.homedir(), ".exe-os", "session-cache");
|
|
5610
|
+
mkdirSync7(ctxDir, { recursive: true });
|
|
5611
|
+
const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
|
|
5612
|
+
const ctxContent = [
|
|
5613
|
+
`## Session Context`,
|
|
5614
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
5615
|
+
`Your parent exe session is ${exeSession}.`,
|
|
5616
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
5617
|
+
].join("\n");
|
|
5618
|
+
writeFileSync5(ctxFile, ctxContent);
|
|
5619
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
5620
|
+
} catch {
|
|
5621
|
+
}
|
|
5622
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
5623
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
5624
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
5625
|
+
if (cfg?.apiKeyEnv) {
|
|
5626
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
5627
|
+
if (keyVal) {
|
|
5628
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
let spawnCommand;
|
|
5633
|
+
if (useExeAgent) {
|
|
5634
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
5635
|
+
} else if (useBinSymlink) {
|
|
5636
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
5637
|
+
process.stderr.write(
|
|
5638
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
5639
|
+
`
|
|
5640
|
+
);
|
|
5641
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
5642
|
+
} else {
|
|
5643
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
5644
|
+
}
|
|
5645
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
5646
|
+
cwd: spawnCwd,
|
|
5647
|
+
command: spawnCommand
|
|
5119
5648
|
});
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
id: String(r.id),
|
|
5134
|
-
taskId: String(r.task_id),
|
|
5135
|
-
agentId: String(r.agent_id),
|
|
5136
|
-
projectName: String(r.project_name),
|
|
5137
|
-
taskTitle: String(r.task_title),
|
|
5138
|
-
signature: JSON.parse(String(r.signature)),
|
|
5139
|
-
signatureHash: String(r.signature_hash),
|
|
5140
|
-
toolCount: Number(r.tool_count),
|
|
5141
|
-
skillId: null,
|
|
5142
|
-
createdAt: String(r.created_at)
|
|
5649
|
+
if (spawnResult.error) {
|
|
5650
|
+
releaseSpawnLock2(sessionName);
|
|
5651
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
5652
|
+
}
|
|
5653
|
+
transport.pipeLog(sessionName, logFile);
|
|
5654
|
+
try {
|
|
5655
|
+
const mySession = getMySession();
|
|
5656
|
+
const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
5657
|
+
writeFileSync5(dispatchInfo, JSON.stringify({
|
|
5658
|
+
dispatchedBy: mySession,
|
|
5659
|
+
rootExe: exeSession,
|
|
5660
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
5661
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5143
5662
|
}));
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5663
|
+
} catch {
|
|
5664
|
+
}
|
|
5665
|
+
let booted = false;
|
|
5666
|
+
for (let i = 0; i < 30; i++) {
|
|
5667
|
+
try {
|
|
5668
|
+
execSync6("sleep 0.5");
|
|
5669
|
+
} catch {
|
|
5670
|
+
}
|
|
5671
|
+
try {
|
|
5672
|
+
const pane = transport.capturePane(sessionName);
|
|
5673
|
+
if (useExeAgent) {
|
|
5674
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
5675
|
+
booted = true;
|
|
5676
|
+
break;
|
|
5677
|
+
}
|
|
5678
|
+
} else {
|
|
5679
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
5680
|
+
booted = true;
|
|
5681
|
+
break;
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
} catch {
|
|
5148
5685
|
}
|
|
5149
5686
|
}
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
for (let i = 1; i <= m; i++) {
|
|
5159
|
-
for (let j = 1; j <= n; j++) {
|
|
5160
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
5161
|
-
dp[i][j] = Math.min(
|
|
5162
|
-
dp[i - 1][j] + 1,
|
|
5163
|
-
dp[i][j - 1] + 1,
|
|
5164
|
-
dp[i - 1][j - 1] + cost
|
|
5165
|
-
);
|
|
5687
|
+
if (!booted) {
|
|
5688
|
+
releaseSpawnLock2(sessionName);
|
|
5689
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
5690
|
+
}
|
|
5691
|
+
if (!useExeAgent) {
|
|
5692
|
+
try {
|
|
5693
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
5694
|
+
} catch {
|
|
5166
5695
|
}
|
|
5167
5696
|
}
|
|
5168
|
-
|
|
5697
|
+
registerSession({
|
|
5698
|
+
windowName: sessionName,
|
|
5699
|
+
agentId: employeeName,
|
|
5700
|
+
projectDir: spawnCwd,
|
|
5701
|
+
parentExe: exeSession,
|
|
5702
|
+
pid: 0,
|
|
5703
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5704
|
+
});
|
|
5705
|
+
releaseSpawnLock2(sessionName);
|
|
5706
|
+
return { sessionName };
|
|
5169
5707
|
}
|
|
5170
|
-
var
|
|
5171
|
-
var
|
|
5172
|
-
"src/lib/
|
|
5708
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
5709
|
+
var init_tmux_routing = __esm({
|
|
5710
|
+
"src/lib/tmux-routing.ts"() {
|
|
5173
5711
|
"use strict";
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5712
|
+
init_session_registry();
|
|
5713
|
+
init_session_key();
|
|
5714
|
+
init_transport();
|
|
5715
|
+
init_cc_agent_support();
|
|
5716
|
+
init_mcp_prefix();
|
|
5717
|
+
init_provider_table();
|
|
5718
|
+
init_intercom_queue();
|
|
5719
|
+
init_plan_limits();
|
|
5720
|
+
SPAWN_LOCK_DIR = path17.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
5721
|
+
SESSION_CACHE = path17.join(os7.homedir(), ".exe-os", "session-cache");
|
|
5722
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
5723
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
5724
|
+
VERIFY_PANE_LINES = 200;
|
|
5725
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
5726
|
+
INTERCOM_LOG2 = path17.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
5727
|
+
DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
|
|
5728
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
5729
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
5178
5730
|
}
|
|
5179
5731
|
});
|
|
5180
5732
|
|
|
5181
|
-
// src/lib/
|
|
5182
|
-
var
|
|
5183
|
-
__export(
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
updateTask: () => updateTask,
|
|
5198
|
-
updateTaskStatus: () => updateTaskStatus,
|
|
5199
|
-
writeCheckpoint: () => writeCheckpoint
|
|
5733
|
+
// src/lib/messaging.ts
|
|
5734
|
+
var messaging_exports = {};
|
|
5735
|
+
__export(messaging_exports, {
|
|
5736
|
+
deliverLocalMessage: () => deliverLocalMessage,
|
|
5737
|
+
getFailedMessages: () => getFailedMessages,
|
|
5738
|
+
getMessageStatus: () => getMessageStatus,
|
|
5739
|
+
getPendingMessages: () => getPendingMessages,
|
|
5740
|
+
getReadMessages: () => getReadMessages,
|
|
5741
|
+
getUnacknowledgedMessages: () => getUnacknowledgedMessages,
|
|
5742
|
+
markAcknowledged: () => markAcknowledged,
|
|
5743
|
+
markFailed: () => markFailed,
|
|
5744
|
+
markProcessed: () => markProcessed,
|
|
5745
|
+
markRead: () => markRead,
|
|
5746
|
+
retryPendingMessages: () => retryPendingMessages,
|
|
5747
|
+
sendMessage: () => sendMessage,
|
|
5748
|
+
setWsClientSend: () => setWsClientSend
|
|
5200
5749
|
});
|
|
5201
|
-
import
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5750
|
+
import crypto8 from "crypto";
|
|
5751
|
+
function generateUlid() {
|
|
5752
|
+
const timestamp = Date.now().toString(36).padStart(10, "0");
|
|
5753
|
+
const random = crypto8.randomBytes(10).toString("hex").slice(0, 16);
|
|
5754
|
+
return (timestamp + random).toUpperCase();
|
|
5755
|
+
}
|
|
5756
|
+
function rowToMessage(row) {
|
|
5757
|
+
return {
|
|
5758
|
+
id: row.id,
|
|
5759
|
+
fromAgent: row.from_agent,
|
|
5760
|
+
fromDevice: row.from_device,
|
|
5761
|
+
targetAgent: row.target_agent,
|
|
5762
|
+
targetProject: row.target_project ?? null,
|
|
5763
|
+
targetDevice: row.target_device,
|
|
5764
|
+
content: row.content,
|
|
5765
|
+
priority: row.priority ?? "normal",
|
|
5766
|
+
status: row.status ?? "pending",
|
|
5767
|
+
serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
|
|
5768
|
+
retryCount: Number(row.retry_count ?? 0),
|
|
5769
|
+
createdAt: row.created_at,
|
|
5770
|
+
deliveredAt: row.delivered_at ?? null,
|
|
5771
|
+
processedAt: row.processed_at ?? null,
|
|
5772
|
+
failedAt: row.failed_at ?? null,
|
|
5773
|
+
failureReason: row.failure_reason ?? null
|
|
5774
|
+
};
|
|
5775
|
+
}
|
|
5776
|
+
async function sendMessage(input) {
|
|
5777
|
+
const client = getClient();
|
|
5778
|
+
const id = generateUlid();
|
|
5779
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5780
|
+
const targetDevice = input.targetDevice ?? "local";
|
|
5781
|
+
await client.execute({
|
|
5782
|
+
sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
|
|
5783
|
+
VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
5784
|
+
args: [
|
|
5785
|
+
id,
|
|
5786
|
+
input.fromAgent,
|
|
5787
|
+
input.targetAgent,
|
|
5788
|
+
input.targetProject ?? null,
|
|
5789
|
+
targetDevice,
|
|
5790
|
+
input.content,
|
|
5791
|
+
input.priority ?? "normal",
|
|
5792
|
+
now
|
|
5793
|
+
]
|
|
5794
|
+
});
|
|
5795
|
+
try {
|
|
5796
|
+
if (targetDevice !== "local") {
|
|
5797
|
+
await deliverCrossMachineMessage(id, targetDevice);
|
|
5798
|
+
} else {
|
|
5799
|
+
await deliverLocalMessage(id);
|
|
5800
|
+
}
|
|
5801
|
+
} catch {
|
|
5802
|
+
}
|
|
5803
|
+
const result = await client.execute({
|
|
5804
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
5805
|
+
args: [id]
|
|
5806
|
+
});
|
|
5807
|
+
return rowToMessage(result.rows[0]);
|
|
5808
|
+
}
|
|
5809
|
+
function setWsClientSend(fn) {
|
|
5810
|
+
_wsClientSend = fn;
|
|
5811
|
+
}
|
|
5812
|
+
async function deliverCrossMachineMessage(messageId, targetDevice) {
|
|
5813
|
+
const client = getClient();
|
|
5814
|
+
const result = await client.execute({
|
|
5815
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
5816
|
+
args: [messageId]
|
|
5817
|
+
});
|
|
5818
|
+
if (result.rows.length === 0) return false;
|
|
5819
|
+
const msg = rowToMessage(result.rows[0]);
|
|
5820
|
+
if (msg.status !== "pending") return false;
|
|
5821
|
+
if (!_wsClientSend) {
|
|
5822
|
+
return false;
|
|
5823
|
+
}
|
|
5824
|
+
const payload = JSON.stringify({
|
|
5825
|
+
id: msg.id,
|
|
5826
|
+
fromAgent: msg.fromAgent,
|
|
5827
|
+
targetAgent: msg.targetAgent,
|
|
5828
|
+
targetProject: msg.targetProject,
|
|
5829
|
+
content: msg.content,
|
|
5830
|
+
priority: msg.priority,
|
|
5831
|
+
createdAt: msg.createdAt
|
|
5832
|
+
});
|
|
5833
|
+
const sent = _wsClientSend(targetDevice, payload);
|
|
5834
|
+
if (sent) {
|
|
5835
|
+
await client.execute({
|
|
5836
|
+
sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
|
|
5837
|
+
args: [messageId]
|
|
5213
5838
|
});
|
|
5839
|
+
return true;
|
|
5214
5840
|
}
|
|
5215
|
-
return
|
|
5841
|
+
return false;
|
|
5216
5842
|
}
|
|
5217
|
-
async function
|
|
5218
|
-
const
|
|
5843
|
+
async function deliverLocalMessage(messageId) {
|
|
5844
|
+
const client = getClient();
|
|
5845
|
+
const result = await client.execute({
|
|
5846
|
+
sql: "SELECT * FROM messages WHERE id = ?",
|
|
5847
|
+
args: [messageId]
|
|
5848
|
+
});
|
|
5849
|
+
if (result.rows.length === 0) return false;
|
|
5850
|
+
const msg = rowToMessage(result.rows[0]);
|
|
5851
|
+
if (msg.status !== "pending") return false;
|
|
5852
|
+
const targetAgent = msg.targetAgent;
|
|
5853
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5219
5854
|
try {
|
|
5220
|
-
const
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
try {
|
|
5228
|
-
unlinkSync5(cachePath);
|
|
5229
|
-
} catch {
|
|
5230
|
-
}
|
|
5855
|
+
const exeSession = resolveExeSession();
|
|
5856
|
+
if (!exeSession) {
|
|
5857
|
+
throw new Error("No exe session found");
|
|
5858
|
+
}
|
|
5859
|
+
const ensureResult = ensureEmployee(targetAgent, exeSession, process.cwd());
|
|
5860
|
+
if (ensureResult.status === "failed") {
|
|
5861
|
+
throw new Error(ensureResult.error ?? "ensureEmployee failed");
|
|
5231
5862
|
}
|
|
5863
|
+
await client.execute({
|
|
5864
|
+
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
5865
|
+
args: [now, messageId]
|
|
5866
|
+
});
|
|
5867
|
+
return true;
|
|
5232
5868
|
} catch {
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
5238
|
-
try {
|
|
5239
|
-
const client = getClient();
|
|
5240
|
-
const taskTitle = String(row.title);
|
|
5241
|
-
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
5869
|
+
const newRetryCount = msg.retryCount + 1;
|
|
5870
|
+
if (newRetryCount >= MAX_RETRIES2) {
|
|
5871
|
+
await markFailed(messageId, "session unavailable after 10 retries");
|
|
5872
|
+
} else {
|
|
5242
5873
|
await client.execute({
|
|
5243
|
-
sql:
|
|
5244
|
-
|
|
5245
|
-
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
5874
|
+
sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
|
|
5875
|
+
args: [newRetryCount, messageId]
|
|
5246
5876
|
});
|
|
5247
|
-
} catch {
|
|
5248
|
-
}
|
|
5249
|
-
}
|
|
5250
|
-
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
5251
|
-
if (isTerminal) {
|
|
5252
|
-
const isExe = String(row.assigned_to) === "exe";
|
|
5253
|
-
if (!isExe) {
|
|
5254
|
-
notifyTaskDone();
|
|
5255
|
-
}
|
|
5256
|
-
await markTaskNotificationsRead(taskFile);
|
|
5257
|
-
if (input.status === "done") {
|
|
5258
|
-
try {
|
|
5259
|
-
await cascadeUnblock(taskId, input.baseDir, now);
|
|
5260
|
-
} catch {
|
|
5261
|
-
}
|
|
5262
|
-
if (row.parent_task_id) {
|
|
5263
|
-
try {
|
|
5264
|
-
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
5265
|
-
} catch {
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
}
|
|
5269
|
-
}
|
|
5270
|
-
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
5271
|
-
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
5272
|
-
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
5273
|
-
taskId,
|
|
5274
|
-
agentId: String(row.assigned_to),
|
|
5275
|
-
projectName: String(row.project_name),
|
|
5276
|
-
taskTitle: String(row.title)
|
|
5277
|
-
})
|
|
5278
|
-
).catch((err) => {
|
|
5279
|
-
process.stderr.write(
|
|
5280
|
-
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
5281
|
-
`
|
|
5282
|
-
);
|
|
5283
|
-
});
|
|
5284
|
-
}
|
|
5285
|
-
let nextTask;
|
|
5286
|
-
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
5287
|
-
try {
|
|
5288
|
-
nextTask = await findNextTask(String(row.assigned_to));
|
|
5289
|
-
} catch {
|
|
5290
5877
|
}
|
|
5878
|
+
return false;
|
|
5291
5879
|
}
|
|
5292
|
-
return {
|
|
5293
|
-
id: String(row.id),
|
|
5294
|
-
title: String(row.title),
|
|
5295
|
-
assignedTo: String(row.assigned_to),
|
|
5296
|
-
assignedBy: String(row.assigned_by),
|
|
5297
|
-
projectName: String(row.project_name),
|
|
5298
|
-
priority: String(row.priority),
|
|
5299
|
-
status: input.status,
|
|
5300
|
-
taskFile,
|
|
5301
|
-
createdAt: String(row.created_at),
|
|
5302
|
-
updatedAt: now,
|
|
5303
|
-
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
5304
|
-
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
5305
|
-
tokensUsed: Number(row.tokens_used ?? 0),
|
|
5306
|
-
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
5307
|
-
nextTask
|
|
5308
|
-
};
|
|
5309
5880
|
}
|
|
5310
|
-
async function
|
|
5881
|
+
async function getPendingMessages(targetAgent) {
|
|
5882
|
+
const client = getClient();
|
|
5883
|
+
const result = await client.execute({
|
|
5884
|
+
sql: `SELECT * FROM messages
|
|
5885
|
+
WHERE target_agent = ? AND status IN ('pending', 'delivered')
|
|
5886
|
+
ORDER BY id`,
|
|
5887
|
+
args: [targetAgent]
|
|
5888
|
+
});
|
|
5889
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
5890
|
+
}
|
|
5891
|
+
async function markRead(messageId) {
|
|
5311
5892
|
const client = getClient();
|
|
5312
|
-
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
5313
|
-
const reviewer = assignedBy || "exe";
|
|
5314
|
-
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
5315
|
-
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
5316
5893
|
await client.execute({
|
|
5317
|
-
sql: "
|
|
5318
|
-
args: [
|
|
5894
|
+
sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
|
|
5895
|
+
args: [messageId]
|
|
5319
5896
|
});
|
|
5320
|
-
await markAsReadByTaskFile(taskFile);
|
|
5321
|
-
await markAsReadByTaskFile(reviewFile);
|
|
5322
5897
|
}
|
|
5323
|
-
|
|
5324
|
-
|
|
5898
|
+
async function markAcknowledged(messageId) {
|
|
5899
|
+
const client = getClient();
|
|
5900
|
+
await client.execute({
|
|
5901
|
+
sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
|
|
5902
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
async function markProcessed(messageId) {
|
|
5906
|
+
const client = getClient();
|
|
5907
|
+
await client.execute({
|
|
5908
|
+
sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
|
|
5909
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
async function getMessageStatus(messageId) {
|
|
5913
|
+
const client = getClient();
|
|
5914
|
+
const result = await client.execute({
|
|
5915
|
+
sql: "SELECT status FROM messages WHERE id = ?",
|
|
5916
|
+
args: [messageId]
|
|
5917
|
+
});
|
|
5918
|
+
return result.rows[0]?.status ?? null;
|
|
5919
|
+
}
|
|
5920
|
+
async function getUnacknowledgedMessages(targetAgent) {
|
|
5921
|
+
const client = getClient();
|
|
5922
|
+
const result = await client.execute({
|
|
5923
|
+
sql: `SELECT * FROM messages
|
|
5924
|
+
WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
|
|
5925
|
+
ORDER BY id`,
|
|
5926
|
+
args: [targetAgent]
|
|
5927
|
+
});
|
|
5928
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
5929
|
+
}
|
|
5930
|
+
async function getReadMessages(targetAgent) {
|
|
5931
|
+
const client = getClient();
|
|
5932
|
+
const result = await client.execute({
|
|
5933
|
+
sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
|
|
5934
|
+
args: [targetAgent]
|
|
5935
|
+
});
|
|
5936
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
5937
|
+
}
|
|
5938
|
+
async function markFailed(messageId, reason) {
|
|
5939
|
+
const client = getClient();
|
|
5940
|
+
await client.execute({
|
|
5941
|
+
sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
|
|
5942
|
+
args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
|
|
5943
|
+
});
|
|
5944
|
+
}
|
|
5945
|
+
async function getFailedMessages() {
|
|
5946
|
+
const client = getClient();
|
|
5947
|
+
const result = await client.execute({
|
|
5948
|
+
sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
|
|
5949
|
+
args: []
|
|
5950
|
+
});
|
|
5951
|
+
return result.rows.map((row) => rowToMessage(row));
|
|
5952
|
+
}
|
|
5953
|
+
async function retryPendingMessages() {
|
|
5954
|
+
const client = getClient();
|
|
5955
|
+
const result = await client.execute({
|
|
5956
|
+
sql: `SELECT * FROM messages
|
|
5957
|
+
WHERE status = 'pending' AND retry_count < ?
|
|
5958
|
+
ORDER BY id`,
|
|
5959
|
+
args: [MAX_RETRIES2]
|
|
5960
|
+
});
|
|
5961
|
+
let delivered = 0;
|
|
5962
|
+
for (const row of result.rows) {
|
|
5963
|
+
const msg = rowToMessage(row);
|
|
5964
|
+
try {
|
|
5965
|
+
const success = await deliverLocalMessage(msg.id);
|
|
5966
|
+
if (success) delivered++;
|
|
5967
|
+
} catch {
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
return delivered;
|
|
5971
|
+
}
|
|
5972
|
+
var MAX_RETRIES2, _wsClientSend;
|
|
5973
|
+
var init_messaging = __esm({
|
|
5974
|
+
"src/lib/messaging.ts"() {
|
|
5325
5975
|
"use strict";
|
|
5326
5976
|
init_database();
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
init_tasks_review();
|
|
5331
|
-
init_tasks_crud();
|
|
5332
|
-
init_tasks_chain();
|
|
5333
|
-
init_tasks_review();
|
|
5334
|
-
init_tasks_notify();
|
|
5977
|
+
init_tmux_routing();
|
|
5978
|
+
MAX_RETRIES2 = 10;
|
|
5979
|
+
_wsClientSend = null;
|
|
5335
5980
|
}
|
|
5336
5981
|
});
|
|
5337
5982
|
|
|
5983
|
+
// src/gateway/gateway.ts
|
|
5984
|
+
init_state_bus();
|
|
5985
|
+
|
|
5338
5986
|
// src/gateway/router.ts
|
|
5339
5987
|
function matchesPlatform(msgPlatform, matchPlatform) {
|
|
5340
5988
|
if (!matchPlatform) return true;
|
|
@@ -5771,6 +6419,13 @@ var Gateway = class {
|
|
|
5771
6419
|
console.log(
|
|
5772
6420
|
`[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
|
|
5773
6421
|
);
|
|
6422
|
+
orgBus.emit({
|
|
6423
|
+
type: "gateway_message",
|
|
6424
|
+
platform: msg.platform,
|
|
6425
|
+
senderId: msg.senderId,
|
|
6426
|
+
botId: route.employee,
|
|
6427
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6428
|
+
});
|
|
5774
6429
|
const bot = this.botRegistry.get(route.employee);
|
|
5775
6430
|
if (!bot) {
|
|
5776
6431
|
console.error(`[gateway] No bot registered for target: ${route.employee}`);
|
|
@@ -6564,7 +7219,7 @@ var AnthropicProvider = class {
|
|
|
6564
7219
|
|
|
6565
7220
|
// src/gateway/providers/openai-compat.ts
|
|
6566
7221
|
import OpenAI from "openai";
|
|
6567
|
-
import { randomUUID as
|
|
7222
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6568
7223
|
var OpenAICompatProvider = class {
|
|
6569
7224
|
name;
|
|
6570
7225
|
client;
|
|
@@ -6677,7 +7332,7 @@ var OpenAICompatProvider = class {
|
|
|
6677
7332
|
}
|
|
6678
7333
|
content.push({
|
|
6679
7334
|
type: "tool_use",
|
|
6680
|
-
id: call.id ??
|
|
7335
|
+
id: call.id ?? randomUUID3(),
|
|
6681
7336
|
name: fn.name,
|
|
6682
7337
|
input
|
|
6683
7338
|
});
|
|
@@ -6699,7 +7354,7 @@ var OpenAICompatProvider = class {
|
|
|
6699
7354
|
};
|
|
6700
7355
|
|
|
6701
7356
|
// src/gateway/providers/ollama.ts
|
|
6702
|
-
import { randomUUID as
|
|
7357
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6703
7358
|
var OllamaProvider = class {
|
|
6704
7359
|
name;
|
|
6705
7360
|
host;
|
|
@@ -6768,7 +7423,7 @@ var OllamaProvider = class {
|
|
|
6768
7423
|
for (const call of data.message.tool_calls) {
|
|
6769
7424
|
content.push({
|
|
6770
7425
|
type: "tool_use",
|
|
6771
|
-
id:
|
|
7426
|
+
id: randomUUID4(),
|
|
6772
7427
|
name: call.function.name,
|
|
6773
7428
|
input: call.function.arguments
|
|
6774
7429
|
});
|
|
@@ -6790,7 +7445,7 @@ var OllamaProvider = class {
|
|
|
6790
7445
|
};
|
|
6791
7446
|
|
|
6792
7447
|
// src/gateway/adapters/whatsapp.ts
|
|
6793
|
-
import { randomUUID as
|
|
7448
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6794
7449
|
import { homedir } from "os";
|
|
6795
7450
|
import { join } from "path";
|
|
6796
7451
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -6972,7 +7627,7 @@ var WhatsAppAdapter = class {
|
|
|
6972
7627
|
const location = this.extractLocation(msg.message);
|
|
6973
7628
|
const dataCategory = location ? "location" : "message";
|
|
6974
7629
|
return {
|
|
6975
|
-
messageId: msg.key.id ??
|
|
7630
|
+
messageId: msg.key.id ?? randomUUID5(),
|
|
6976
7631
|
platform: "whatsapp",
|
|
6977
7632
|
senderId,
|
|
6978
7633
|
senderName: msg.pushName ?? void 0,
|
|
@@ -7017,7 +7672,7 @@ var WhatsAppAdapter = class {
|
|
|
7017
7672
|
}
|
|
7018
7673
|
const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
|
|
7019
7674
|
return {
|
|
7020
|
-
messageId:
|
|
7675
|
+
messageId: randomUUID5(),
|
|
7021
7676
|
platform: "whatsapp",
|
|
7022
7677
|
senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
|
|
7023
7678
|
channelId: remoteJid,
|
|
@@ -7040,7 +7695,7 @@ var WhatsAppAdapter = class {
|
|
|
7040
7695
|
const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
|
|
7041
7696
|
const name = contact.name ?? contact.notify ?? phone;
|
|
7042
7697
|
return {
|
|
7043
|
-
messageId:
|
|
7698
|
+
messageId: randomUUID5(),
|
|
7044
7699
|
platform: "whatsapp",
|
|
7045
7700
|
senderId: phone,
|
|
7046
7701
|
senderName: name,
|
|
@@ -7064,7 +7719,7 @@ var WhatsAppAdapter = class {
|
|
|
7064
7719
|
const participants = (group.participants ?? []).map((p) => p.id ?? p);
|
|
7065
7720
|
const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
|
|
7066
7721
|
return {
|
|
7067
|
-
messageId:
|
|
7722
|
+
messageId: randomUUID5(),
|
|
7068
7723
|
platform: "whatsapp",
|
|
7069
7724
|
senderId: groupId,
|
|
7070
7725
|
channelId: groupId,
|
|
@@ -7089,7 +7744,7 @@ var WhatsAppAdapter = class {
|
|
|
7089
7744
|
if (!reactionData) return null;
|
|
7090
7745
|
const remoteJid = key.remoteJid ?? "";
|
|
7091
7746
|
return {
|
|
7092
|
-
messageId:
|
|
7747
|
+
messageId: randomUUID5(),
|
|
7093
7748
|
platform: "whatsapp",
|
|
7094
7749
|
senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
|
|
7095
7750
|
channelId: remoteJid,
|
|
@@ -7111,7 +7766,7 @@ var WhatsAppAdapter = class {
|
|
|
7111
7766
|
if (!chatId) return null;
|
|
7112
7767
|
const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
|
|
7113
7768
|
return {
|
|
7114
|
-
messageId:
|
|
7769
|
+
messageId: randomUUID5(),
|
|
7115
7770
|
platform: "whatsapp",
|
|
7116
7771
|
senderId: caller,
|
|
7117
7772
|
channelId: chatId,
|
|
@@ -7154,7 +7809,7 @@ var WhatsAppAdapter = class {
|
|
|
7154
7809
|
};
|
|
7155
7810
|
|
|
7156
7811
|
// src/gateway/adapters/signal.ts
|
|
7157
|
-
import { randomUUID as
|
|
7812
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7158
7813
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
7159
7814
|
var SignalAdapter = class {
|
|
7160
7815
|
platform = "signal";
|
|
@@ -7239,7 +7894,7 @@ var SignalAdapter = class {
|
|
|
7239
7894
|
}
|
|
7240
7895
|
}
|
|
7241
7896
|
async rpcRequest(method, params) {
|
|
7242
|
-
const id =
|
|
7897
|
+
const id = randomUUID6();
|
|
7243
7898
|
const res = await fetch(`${this.baseUrl}/api/v1/rpc`, {
|
|
7244
7899
|
method: "POST",
|
|
7245
7900
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7329,7 +7984,7 @@ ${val}` : val;
|
|
|
7329
7984
|
if (envelope.reactionMessage) {
|
|
7330
7985
|
const rm = envelope.reactionMessage;
|
|
7331
7986
|
const normalized2 = {
|
|
7332
|
-
messageId:
|
|
7987
|
+
messageId: randomUUID6(),
|
|
7333
7988
|
platform: "signal",
|
|
7334
7989
|
senderId,
|
|
7335
7990
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7357,7 +8012,7 @@ ${val}` : val;
|
|
|
7357
8012
|
const rcpt = envelope.receiptMessage;
|
|
7358
8013
|
for (const ts of rcpt.timestamps) {
|
|
7359
8014
|
const normalized2 = {
|
|
7360
|
-
messageId:
|
|
8015
|
+
messageId: randomUUID6(),
|
|
7361
8016
|
platform: "signal",
|
|
7362
8017
|
senderId,
|
|
7363
8018
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7387,7 +8042,7 @@ ${val}` : val;
|
|
|
7387
8042
|
const dm2 = em.dataMessage;
|
|
7388
8043
|
const isGroup2 = !!dm2.groupInfo?.groupId;
|
|
7389
8044
|
const normalized2 = {
|
|
7390
|
-
messageId: String(dm2.timestamp ??
|
|
8045
|
+
messageId: String(dm2.timestamp ?? randomUUID6()),
|
|
7391
8046
|
platform: "signal",
|
|
7392
8047
|
senderId,
|
|
7393
8048
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7413,7 +8068,7 @@ ${val}` : val;
|
|
|
7413
8068
|
const dm = envelope.dataMessage;
|
|
7414
8069
|
const isGroup = !!dm.groupInfo?.groupId;
|
|
7415
8070
|
const normalized = {
|
|
7416
|
-
messageId: String(dm.timestamp ??
|
|
8071
|
+
messageId: String(dm.timestamp ?? randomUUID6()),
|
|
7417
8072
|
platform: "signal",
|
|
7418
8073
|
senderId,
|
|
7419
8074
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7454,7 +8109,7 @@ ${val}` : val;
|
|
|
7454
8109
|
if (!phone) continue;
|
|
7455
8110
|
const name = contact.name ?? contact.profileName ?? phone;
|
|
7456
8111
|
const normalized = {
|
|
7457
|
-
messageId:
|
|
8112
|
+
messageId: randomUUID6(),
|
|
7458
8113
|
platform: "signal",
|
|
7459
8114
|
senderId: phone,
|
|
7460
8115
|
senderName: name,
|
|
@@ -7483,7 +8138,7 @@ ${val}` : val;
|
|
|
7483
8138
|
if (!Array.isArray(groups)) return;
|
|
7484
8139
|
for (const group of groups) {
|
|
7485
8140
|
const normalized = {
|
|
7486
|
-
messageId:
|
|
8141
|
+
messageId: randomUUID6(),
|
|
7487
8142
|
platform: "signal",
|
|
7488
8143
|
senderId: `group:${group.id}`,
|
|
7489
8144
|
channelId: `group:${group.id}`,
|
|
@@ -7518,7 +8173,7 @@ ${val}` : val;
|
|
|
7518
8173
|
};
|
|
7519
8174
|
|
|
7520
8175
|
// src/gateway/adapters/webchat.ts
|
|
7521
|
-
import { randomUUID as
|
|
8176
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
7522
8177
|
import { createServer } from "http";
|
|
7523
8178
|
var WebChatAdapter = class {
|
|
7524
8179
|
platform = "webchat";
|
|
@@ -7614,7 +8269,7 @@ var WebChatAdapter = class {
|
|
|
7614
8269
|
res.end(JSON.stringify({ error: "No message text" }));
|
|
7615
8270
|
return;
|
|
7616
8271
|
}
|
|
7617
|
-
const requestId =
|
|
8272
|
+
const requestId = randomUUID7();
|
|
7618
8273
|
const sessionId = parsed.sessionId ?? this.extractSessionId(req);
|
|
7619
8274
|
const normalized = {
|
|
7620
8275
|
messageId: requestId,
|
|
@@ -7657,7 +8312,7 @@ var WebChatAdapter = class {
|
|
|
7657
8312
|
extractSessionId(req) {
|
|
7658
8313
|
const cookies = req.headers.cookie ?? "";
|
|
7659
8314
|
const match = cookies.match(/exe_session=([^;]+)/);
|
|
7660
|
-
return match?.[1] ?? `anon-${
|
|
8315
|
+
return match?.[1] ?? `anon-${randomUUID7().slice(0, 8)}`;
|
|
7661
8316
|
}
|
|
7662
8317
|
};
|
|
7663
8318
|
|
|
@@ -7939,7 +8594,7 @@ var DiscordAdapter = class {
|
|
|
7939
8594
|
};
|
|
7940
8595
|
|
|
7941
8596
|
// src/gateway/adapters/slack.ts
|
|
7942
|
-
import { randomUUID as
|
|
8597
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7943
8598
|
var SlackAdapter = class {
|
|
7944
8599
|
platform = "slack";
|
|
7945
8600
|
webClient = null;
|
|
@@ -7977,7 +8632,7 @@ var SlackAdapter = class {
|
|
|
7977
8632
|
if (event.subtype) return;
|
|
7978
8633
|
const isGroup = event.channel_type !== "im";
|
|
7979
8634
|
const normalized = {
|
|
7980
|
-
messageId: event.client_msg_id ?? event.ts ??
|
|
8635
|
+
messageId: event.client_msg_id ?? event.ts ?? randomUUID8(),
|
|
7981
8636
|
platform: "slack",
|
|
7982
8637
|
senderId: event.user ?? "",
|
|
7983
8638
|
channelId: event.channel ?? "",
|
|
@@ -8039,7 +8694,7 @@ var SlackAdapter = class {
|
|
|
8039
8694
|
if (!event.text) return;
|
|
8040
8695
|
const isGroup = event.channel_type !== "im";
|
|
8041
8696
|
const normalized = {
|
|
8042
|
-
messageId: event.ts ??
|
|
8697
|
+
messageId: event.ts ?? randomUUID8(),
|
|
8043
8698
|
platform: "slack",
|
|
8044
8699
|
senderId: event.user ?? "",
|
|
8045
8700
|
senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
|
|
@@ -8424,7 +9079,7 @@ var FailoverExhaustedError = class extends Error {
|
|
|
8424
9079
|
};
|
|
8425
9080
|
|
|
8426
9081
|
// src/gateway/session-store.ts
|
|
8427
|
-
import { randomUUID as
|
|
9082
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
8428
9083
|
var DEFAULT_CONFIG3 = {
|
|
8429
9084
|
idleTimeoutMs: 30 * 6e4,
|
|
8430
9085
|
maxMessages: 100
|
|
@@ -8451,7 +9106,7 @@ var SessionStore = class {
|
|
|
8451
9106
|
existing.status = "closed";
|
|
8452
9107
|
}
|
|
8453
9108
|
const session = {
|
|
8454
|
-
sessionId:
|
|
9109
|
+
sessionId: randomUUID9(),
|
|
8455
9110
|
customerId,
|
|
8456
9111
|
botId,
|
|
8457
9112
|
platform,
|
|
@@ -8848,7 +9503,7 @@ function formatAlert(alert) {
|
|
|
8848
9503
|
}
|
|
8849
9504
|
|
|
8850
9505
|
// src/gateway/customer-store.ts
|
|
8851
|
-
import { randomUUID as
|
|
9506
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
8852
9507
|
var CustomerStore = class {
|
|
8853
9508
|
customers = /* @__PURE__ */ new Map();
|
|
8854
9509
|
identities = /* @__PURE__ */ new Map();
|
|
@@ -8867,7 +9522,7 @@ var CustomerStore = class {
|
|
|
8867
9522
|
return customer2;
|
|
8868
9523
|
}
|
|
8869
9524
|
const customer = {
|
|
8870
|
-
id:
|
|
9525
|
+
id: randomUUID10(),
|
|
8871
9526
|
firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8872
9527
|
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8873
9528
|
interactionCount: 1
|
|
@@ -8927,7 +9582,7 @@ async function ensureCRMContact(info) {
|
|
|
8927
9582
|
|
|
8928
9583
|
// src/automation/trigger-engine.ts
|
|
8929
9584
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
|
|
8930
|
-
import { randomUUID as
|
|
9585
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
8931
9586
|
import path18 from "path";
|
|
8932
9587
|
import os8 from "os";
|
|
8933
9588
|
var TRIGGERS_PATH = path18.join(os8.homedir(), ".exe-os", "triggers.json");
|