@askexenow/exe-os 0.8.41 → 0.8.43
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 +1345 -660
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2518 -1798
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +15 -1
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +790 -633
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2500 -1877
- 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 +28 -2
- package/dist/bin/exe-new-employee.js +25 -3
- 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 +147 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +154 -3
- package/dist/bin/exe-session-cleanup.js +2466 -413
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2690 -150
- 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 +62 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2504 -1895
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2689 -149
- package/dist/hooks/error-recall.js +154 -3
- package/dist/hooks/ingest-worker.js +1439 -815
- 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 +1714 -1537
- package/dist/hooks/prompt-submit.js +2658 -1113
- package/dist/hooks/response-ingest-worker.js +170 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +154 -3
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +151 -0
- package/dist/hooks/summary-worker.js +179 -7
- package/dist/index.js +278 -100
- package/dist/lib/cloud-sync.js +28 -2
- 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/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +236 -16
- package/dist/lib/hybrid-search.js +154 -3
- package/dist/lib/license.js +15 -1
- 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 +1614 -1091
- package/dist/lib/tmux-routing.js +149 -9
- package/dist/mcp/server.js +1825 -1138
- package/dist/mcp/tools/create-task.js +2280 -828
- 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 +64 -0
- package/dist/runtime/index.js +235 -67
- package/dist/tui/App.js +1452 -644
- 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,
|
|
@@ -2128,6 +2202,71 @@ var init_shard_manager = __esm({
|
|
|
2128
2202
|
}
|
|
2129
2203
|
});
|
|
2130
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
|
+
|
|
2131
2270
|
// src/lib/store.ts
|
|
2132
2271
|
var store_exports = {};
|
|
2133
2272
|
__export(store_exports, {
|
|
@@ -2207,6 +2346,11 @@ async function initStore(options) {
|
|
|
2207
2346
|
"version-query"
|
|
2208
2347
|
);
|
|
2209
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
|
+
}
|
|
2210
2354
|
}
|
|
2211
2355
|
function classifyTier(record) {
|
|
2212
2356
|
if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
|
|
@@ -2248,6 +2392,12 @@ async function writeMemory(record) {
|
|
|
2248
2392
|
supersedes_id: record.supersedes_id ?? null
|
|
2249
2393
|
};
|
|
2250
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
|
+
});
|
|
2251
2401
|
const MAX_PENDING = 1e3;
|
|
2252
2402
|
if (_pendingRecords.length > MAX_PENDING) {
|
|
2253
2403
|
const dropped = _pendingRecords.length - MAX_PENDING;
|
|
@@ -2593,6 +2743,7 @@ var init_store = __esm({
|
|
|
2593
2743
|
init_database();
|
|
2594
2744
|
init_keychain();
|
|
2595
2745
|
init_config();
|
|
2746
|
+
init_state_bus();
|
|
2596
2747
|
INIT_MAX_RETRIES = 3;
|
|
2597
2748
|
INIT_RETRY_DELAY_MS = 1e3;
|
|
2598
2749
|
_pendingRecords = [];
|
|
@@ -3132,7 +3283,7 @@ var init_employees = __esm({
|
|
|
3132
3283
|
|
|
3133
3284
|
// src/lib/license.ts
|
|
3134
3285
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
3135
|
-
import { randomUUID as
|
|
3286
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
3136
3287
|
import path9 from "path";
|
|
3137
3288
|
import { jwtVerify, importSPKI } from "jose";
|
|
3138
3289
|
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
@@ -3231,2156 +3382,2607 @@ var init_plan_limits = __esm({
|
|
|
3231
3382
|
}
|
|
3232
3383
|
});
|
|
3233
3384
|
|
|
3234
|
-
// src/lib/
|
|
3235
|
-
import
|
|
3236
|
-
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";
|
|
3237
3387
|
import path11 from "path";
|
|
3238
3388
|
import os6 from "os";
|
|
3239
|
-
import {
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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) {
|
|
3245
3397
|
try {
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
+
`);
|
|
3266
3418
|
}
|
|
3267
|
-
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
3268
|
-
return true;
|
|
3269
3419
|
}
|
|
3270
|
-
function
|
|
3420
|
+
async function markAsReadByTaskFile(taskFile) {
|
|
3271
3421
|
try {
|
|
3272
|
-
|
|
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
|
+
});
|
|
3273
3427
|
} catch {
|
|
3274
3428
|
}
|
|
3275
3429
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
path11.dirname(thisFile),
|
|
3281
|
-
"..",
|
|
3282
|
-
"bin",
|
|
3283
|
-
"exe-export-behaviors.js"
|
|
3284
|
-
);
|
|
3285
|
-
return existsSync10(scriptPath) ? scriptPath : null;
|
|
3286
|
-
} catch {
|
|
3287
|
-
return null;
|
|
3430
|
+
var init_notifications = __esm({
|
|
3431
|
+
"src/lib/notifications.ts"() {
|
|
3432
|
+
"use strict";
|
|
3433
|
+
init_database();
|
|
3288
3434
|
}
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
// src/lib/session-kill-telemetry.ts
|
|
3438
|
+
import crypto4 from "crypto";
|
|
3439
|
+
async function recordSessionKill(input) {
|
|
3293
3440
|
try {
|
|
3294
|
-
const
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
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
|
+
});
|
|
3300
3457
|
} catch (err) {
|
|
3301
3458
|
process.stderr.write(
|
|
3302
|
-
`[
|
|
3459
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
3303
3460
|
`
|
|
3304
3461
|
);
|
|
3305
|
-
return null;
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
function getMySession() {
|
|
3309
|
-
return getTransport().getMySession();
|
|
3310
|
-
}
|
|
3311
|
-
function employeeSessionName(employee, exeSession, instance) {
|
|
3312
|
-
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
3313
|
-
return `${employee}${suffix}-${exeSession}`;
|
|
3314
|
-
}
|
|
3315
|
-
function extractRootExe(name) {
|
|
3316
|
-
const match = name.match(/(exe\d+)$/);
|
|
3317
|
-
return match?.[1] ?? null;
|
|
3318
|
-
}
|
|
3319
|
-
function getParentExe(sessionKey) {
|
|
3320
|
-
try {
|
|
3321
|
-
const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
3322
|
-
return data.parentExe || null;
|
|
3323
|
-
} catch {
|
|
3324
|
-
return null;
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
function getDispatchedBy(sessionKey) {
|
|
3328
|
-
try {
|
|
3329
|
-
const data = JSON.parse(readFileSync9(
|
|
3330
|
-
path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
3331
|
-
"utf8"
|
|
3332
|
-
));
|
|
3333
|
-
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
3334
|
-
} catch {
|
|
3335
|
-
return null;
|
|
3336
3462
|
}
|
|
3337
3463
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
const key = getSessionKey();
|
|
3343
|
-
const parentExe = getParentExe(key);
|
|
3344
|
-
if (parentExe) {
|
|
3345
|
-
return extractRootExe(parentExe) ?? parentExe;
|
|
3346
|
-
}
|
|
3347
|
-
} catch {
|
|
3464
|
+
var init_session_kill_telemetry = __esm({
|
|
3465
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
3466
|
+
"use strict";
|
|
3467
|
+
init_database();
|
|
3348
3468
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
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));
|
|
3360
3485
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
}
|
|
3368
|
-
|
|
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`);
|
|
3369
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 };
|
|
3370
3506
|
}
|
|
3371
|
-
function
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
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;
|
|
3377
3513
|
}
|
|
3378
|
-
function
|
|
3379
|
-
|
|
3380
|
-
const lastSent = state[targetSession] ?? 0;
|
|
3381
|
-
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
3514
|
+
function slugify(title) {
|
|
3515
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3382
3516
|
}
|
|
3383
|
-
function
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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
|
+
);
|
|
3389
3542
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
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
|
+
);
|
|
3400
3557
|
}
|
|
3558
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
3401
3559
|
}
|
|
3402
|
-
function
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
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
|
+
);
|
|
3410
3592
|
}
|
|
3593
|
+
throw err;
|
|
3411
3594
|
}
|
|
3412
|
-
if (/Running…/.test(pane)) return "tool";
|
|
3413
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3414
|
-
return "idle";
|
|
3415
|
-
} catch {
|
|
3416
|
-
return "offline";
|
|
3417
3595
|
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
3426
|
-
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.`;
|
|
3427
3603
|
}
|
|
3428
|
-
if (
|
|
3429
|
-
|
|
3430
|
-
|
|
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
|
+
}
|
|
3431
3612
|
}
|
|
3613
|
+
const complexity = input.complexity ?? "standard";
|
|
3614
|
+
let sessionScope = null;
|
|
3432
3615
|
try {
|
|
3433
|
-
const
|
|
3434
|
-
|
|
3435
|
-
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
3436
|
-
return "failed";
|
|
3437
|
-
}
|
|
3438
|
-
const sessionState = getSessionState(targetSession);
|
|
3439
|
-
if (sessionState === "no_claude") {
|
|
3440
|
-
queueIntercom(targetSession, "claude not running in session");
|
|
3441
|
-
recordDebounce(targetSession);
|
|
3442
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
3443
|
-
return "queued";
|
|
3444
|
-
}
|
|
3445
|
-
if (sessionState === "thinking" || sessionState === "tool") {
|
|
3446
|
-
queueIntercom(targetSession, "session busy at send time");
|
|
3447
|
-
recordDebounce(targetSession);
|
|
3448
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
3449
|
-
return "queued";
|
|
3450
|
-
}
|
|
3451
|
-
if (transport.isPaneInCopyMode(targetSession)) {
|
|
3452
|
-
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
3453
|
-
transport.sendKeys(targetSession, "q");
|
|
3454
|
-
}
|
|
3455
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
3456
|
-
recordDebounce(targetSession);
|
|
3457
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
3458
|
-
return "delivered";
|
|
3616
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
3617
|
+
sessionScope = resolveExeSession2();
|
|
3459
3618
|
} catch {
|
|
3460
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3461
|
-
return "failed";
|
|
3462
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
|
+
};
|
|
3463
3663
|
}
|
|
3464
|
-
function
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
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);
|
|
3470
3671
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
if (rootExe && rootExe !== target) {
|
|
3477
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
3478
|
-
`);
|
|
3479
|
-
const fallback = sendIntercom(rootExe);
|
|
3480
|
-
return fallback !== "failed";
|
|
3481
|
-
}
|
|
3482
|
-
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')");
|
|
3483
3677
|
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
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);
|
|
3489
3685
|
}
|
|
3490
3686
|
try {
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
if (
|
|
3494
|
-
|
|
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);
|
|
3495
3692
|
}
|
|
3693
|
+
} catch {
|
|
3496
3694
|
}
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
)
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
3520
|
-
}
|
|
3521
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3522
|
-
if (isEmployeeAlive(sessionName)) {
|
|
3523
|
-
const result2 = sendIntercom(sessionName);
|
|
3524
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3525
|
-
return { status: "intercom_sent", sessionName };
|
|
3526
|
-
}
|
|
3527
|
-
if (result2 === "delivered") {
|
|
3528
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
3529
|
-
}
|
|
3530
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3531
|
-
}
|
|
3532
|
-
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
3533
|
-
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
3534
|
-
if (result.error) {
|
|
3535
|
-
return { status: "failed", sessionName, error: result.error };
|
|
3536
|
-
}
|
|
3537
|
-
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
|
+
}));
|
|
3538
3717
|
}
|
|
3539
|
-
function
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3543
|
-
const logDir = path11.join(os6.homedir(), ".exe-os", "session-logs");
|
|
3544
|
-
const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3545
|
-
if (!existsSync10(logDir)) {
|
|
3546
|
-
mkdirSync6(logDir, { recursive: true });
|
|
3547
|
-
}
|
|
3548
|
-
transport.kill(sessionName);
|
|
3549
|
-
let cleanupSuffix = "";
|
|
3718
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
3719
|
+
if (!taskContext) return null;
|
|
3720
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
3550
3721
|
try {
|
|
3551
|
-
const
|
|
3552
|
-
const
|
|
3553
|
-
|
|
3554
|
-
|
|
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.";
|
|
3555
3735
|
}
|
|
3736
|
+
return null;
|
|
3556
3737
|
} catch {
|
|
3738
|
+
return null;
|
|
3557
3739
|
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3571
|
-
} catch {
|
|
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-")) {
|
|
3748
|
+
process.stderr.write(
|
|
3749
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
3750
|
+
`
|
|
3751
|
+
);
|
|
3572
3752
|
}
|
|
3573
|
-
|
|
3574
|
-
const
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
"store_memory",
|
|
3588
|
-
"create_task",
|
|
3589
|
-
"update_task",
|
|
3590
|
-
"list_tasks",
|
|
3591
|
-
"get_task",
|
|
3592
|
-
"ask_team_memory",
|
|
3593
|
-
"store_behavior",
|
|
3594
|
-
"get_identity",
|
|
3595
|
-
"send_message"
|
|
3596
|
-
];
|
|
3597
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3598
|
-
let changed = false;
|
|
3599
|
-
for (const tool of requiredTools) {
|
|
3600
|
-
if (!allow.includes(tool)) {
|
|
3601
|
-
allow.push(tool);
|
|
3602
|
-
changed = true;
|
|
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
|
+
`);
|
|
3603
3767
|
}
|
|
3604
3768
|
}
|
|
3605
|
-
if (changed) {
|
|
3606
|
-
perms.allow = allow;
|
|
3607
|
-
settings.permissions = perms;
|
|
3608
|
-
mkdirSync6(projSettingsDir, { recursive: true });
|
|
3609
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3610
|
-
}
|
|
3611
|
-
} catch {
|
|
3612
3769
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
identityFlag = ` --agent ${employeeName}`;
|
|
3631
|
-
} else if (existsSync10(identityPath)) {
|
|
3632
|
-
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
3633
|
-
legacyFallbackWarned = true;
|
|
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}`);
|
|
3634
3787
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3788
|
+
try {
|
|
3789
|
+
await writeCheckpoint({
|
|
3790
|
+
taskId,
|
|
3791
|
+
step: "claimed",
|
|
3792
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
3793
|
+
});
|
|
3794
|
+
} catch {
|
|
3642
3795
|
}
|
|
3796
|
+
return { row, taskFile, now, taskId };
|
|
3643
3797
|
}
|
|
3644
|
-
if (
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
);
|
|
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
|
+
});
|
|
3649
3808
|
}
|
|
3650
|
-
let sessionContextFlag = "";
|
|
3651
3809
|
try {
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
`You are running in tmux session: ${sessionName}.`,
|
|
3658
|
-
`Your parent exe session is ${exeSession}.`,
|
|
3659
|
-
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
3660
|
-
].join("\n");
|
|
3661
|
-
writeFileSync4(ctxFile, ctxContent);
|
|
3662
|
-
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
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
|
+
});
|
|
3663
3815
|
} catch {
|
|
3664
3816
|
}
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
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");
|
|
3706
3862
|
} catch {
|
|
3707
3863
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
booted = true;
|
|
3719
|
-
break;
|
|
3720
|
-
}
|
|
3721
|
-
} else {
|
|
3722
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3723
|
-
booted = true;
|
|
3724
|
-
break;
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
} catch {
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
if (!booted) {
|
|
3731
|
-
releaseSpawnLock2(sessionName);
|
|
3732
|
-
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
3733
|
-
}
|
|
3734
|
-
if (!useExeAgent) {
|
|
3735
|
-
try {
|
|
3736
|
-
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
3737
|
-
} catch {
|
|
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");
|
|
3738
3874
|
}
|
|
3875
|
+
} catch {
|
|
3739
3876
|
}
|
|
3740
|
-
registerSession({
|
|
3741
|
-
windowName: sessionName,
|
|
3742
|
-
agentId: employeeName,
|
|
3743
|
-
projectDir: spawnCwd,
|
|
3744
|
-
parentExe: exeSession,
|
|
3745
|
-
pid: 0,
|
|
3746
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3747
|
-
});
|
|
3748
|
-
releaseSpawnLock2(sessionName);
|
|
3749
|
-
return { sessionName };
|
|
3750
3877
|
}
|
|
3751
|
-
var
|
|
3752
|
-
var
|
|
3753
|
-
"src/lib/
|
|
3878
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
3879
|
+
var init_tasks_crud = __esm({
|
|
3880
|
+
"src/lib/tasks-crud.ts"() {
|
|
3754
3881
|
"use strict";
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
init_cc_agent_support();
|
|
3759
|
-
init_mcp_prefix();
|
|
3760
|
-
init_provider_table();
|
|
3761
|
-
init_intercom_queue();
|
|
3762
|
-
init_plan_limits();
|
|
3763
|
-
SPAWN_LOCK_DIR = path11.join(os6.homedir(), ".exe-os", "spawn-locks");
|
|
3764
|
-
SESSION_CACHE = path11.join(os6.homedir(), ".exe-os", "session-cache");
|
|
3765
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3766
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3767
|
-
INTERCOM_LOG2 = path11.join(os6.homedir(), ".exe-os", "intercom.log");
|
|
3768
|
-
DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3769
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3770
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
3882
|
+
init_database();
|
|
3883
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
3884
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3771
3885
|
}
|
|
3772
3886
|
});
|
|
3773
3887
|
|
|
3774
|
-
// src/lib/
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
getFailedMessages: () => getFailedMessages,
|
|
3779
|
-
getMessageStatus: () => getMessageStatus,
|
|
3780
|
-
getPendingMessages: () => getPendingMessages,
|
|
3781
|
-
getReadMessages: () => getReadMessages,
|
|
3782
|
-
getUnacknowledgedMessages: () => getUnacknowledgedMessages,
|
|
3783
|
-
markAcknowledged: () => markAcknowledged,
|
|
3784
|
-
markFailed: () => markFailed,
|
|
3785
|
-
markProcessed: () => markProcessed,
|
|
3786
|
-
markRead: () => markRead,
|
|
3787
|
-
retryPendingMessages: () => retryPendingMessages,
|
|
3788
|
-
sendMessage: () => sendMessage,
|
|
3789
|
-
setWsClientSend: () => setWsClientSend
|
|
3790
|
-
});
|
|
3791
|
-
import crypto3 from "crypto";
|
|
3792
|
-
function generateUlid() {
|
|
3793
|
-
const timestamp = Date.now().toString(36).padStart(10, "0");
|
|
3794
|
-
const random = crypto3.randomBytes(10).toString("hex").slice(0, 16);
|
|
3795
|
-
return (timestamp + random).toUpperCase();
|
|
3796
|
-
}
|
|
3797
|
-
function rowToMessage(row) {
|
|
3798
|
-
return {
|
|
3799
|
-
id: row.id,
|
|
3800
|
-
fromAgent: row.from_agent,
|
|
3801
|
-
fromDevice: row.from_device,
|
|
3802
|
-
targetAgent: row.target_agent,
|
|
3803
|
-
targetProject: row.target_project ?? null,
|
|
3804
|
-
targetDevice: row.target_device,
|
|
3805
|
-
content: row.content,
|
|
3806
|
-
priority: row.priority ?? "normal",
|
|
3807
|
-
status: row.status ?? "pending",
|
|
3808
|
-
serverSeq: row.server_seq != null ? Number(row.server_seq) : null,
|
|
3809
|
-
retryCount: Number(row.retry_count ?? 0),
|
|
3810
|
-
createdAt: row.created_at,
|
|
3811
|
-
deliveredAt: row.delivered_at ?? null,
|
|
3812
|
-
processedAt: row.processed_at ?? null,
|
|
3813
|
-
failedAt: row.failed_at ?? null,
|
|
3814
|
-
failureReason: row.failure_reason ?? null
|
|
3815
|
-
};
|
|
3816
|
-
}
|
|
3817
|
-
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() {
|
|
3818
3892
|
const client = getClient();
|
|
3819
|
-
const id = generateUlid();
|
|
3820
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3821
|
-
const targetDevice = input.targetDevice ?? "local";
|
|
3822
|
-
await client.execute({
|
|
3823
|
-
sql: `INSERT INTO messages (id, from_agent, from_device, target_agent, target_project, target_device, content, priority, status, created_at)
|
|
3824
|
-
VALUES (?, ?, 'local', ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
3825
|
-
args: [
|
|
3826
|
-
id,
|
|
3827
|
-
input.fromAgent,
|
|
3828
|
-
input.targetAgent,
|
|
3829
|
-
input.targetProject ?? null,
|
|
3830
|
-
targetDevice,
|
|
3831
|
-
input.content,
|
|
3832
|
-
input.priority ?? "normal",
|
|
3833
|
-
now
|
|
3834
|
-
]
|
|
3835
|
-
});
|
|
3836
|
-
try {
|
|
3837
|
-
if (targetDevice !== "local") {
|
|
3838
|
-
await deliverCrossMachineMessage(id, targetDevice);
|
|
3839
|
-
} else {
|
|
3840
|
-
await deliverLocalMessage(id);
|
|
3841
|
-
}
|
|
3842
|
-
} catch {
|
|
3843
|
-
}
|
|
3844
3893
|
const result = await client.execute({
|
|
3845
|
-
sql: "SELECT * FROM
|
|
3846
|
-
args: [
|
|
3894
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
3895
|
+
args: []
|
|
3847
3896
|
});
|
|
3848
|
-
return
|
|
3849
|
-
}
|
|
3850
|
-
function setWsClientSend(fn) {
|
|
3851
|
-
_wsClientSend = fn;
|
|
3897
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3852
3898
|
}
|
|
3853
|
-
async function
|
|
3899
|
+
async function countNewPendingReviewsSince(sinceIso) {
|
|
3854
3900
|
const client = getClient();
|
|
3855
3901
|
const result = await client.execute({
|
|
3856
|
-
sql:
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
if (result.rows.length === 0) return false;
|
|
3860
|
-
const msg = rowToMessage(result.rows[0]);
|
|
3861
|
-
if (msg.status !== "pending") return false;
|
|
3862
|
-
if (!_wsClientSend) {
|
|
3863
|
-
return false;
|
|
3864
|
-
}
|
|
3865
|
-
const payload = JSON.stringify({
|
|
3866
|
-
id: msg.id,
|
|
3867
|
-
fromAgent: msg.fromAgent,
|
|
3868
|
-
targetAgent: msg.targetAgent,
|
|
3869
|
-
targetProject: msg.targetProject,
|
|
3870
|
-
content: msg.content,
|
|
3871
|
-
priority: msg.priority,
|
|
3872
|
-
createdAt: msg.createdAt
|
|
3902
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3903
|
+
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
3904
|
+
args: [sinceIso]
|
|
3873
3905
|
});
|
|
3874
|
-
|
|
3875
|
-
if (sent) {
|
|
3876
|
-
await client.execute({
|
|
3877
|
-
sql: "UPDATE messages SET status = 'synced' WHERE id = ?",
|
|
3878
|
-
args: [messageId]
|
|
3879
|
-
});
|
|
3880
|
-
return true;
|
|
3881
|
-
}
|
|
3882
|
-
return false;
|
|
3906
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3883
3907
|
}
|
|
3884
|
-
async function
|
|
3908
|
+
async function listPendingReviews(limit) {
|
|
3885
3909
|
const client = getClient();
|
|
3886
3910
|
const result = await client.execute({
|
|
3887
|
-
sql:
|
|
3888
|
-
|
|
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]
|
|
3889
3915
|
});
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
const
|
|
3916
|
+
return result.rows;
|
|
3917
|
+
}
|
|
3918
|
+
async function cleanupOrphanedReviews() {
|
|
3919
|
+
const client = getClient();
|
|
3894
3920
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
throw new Error(ensureResult.error ?? "ensureEmployee failed");
|
|
3903
|
-
}
|
|
3904
|
-
await client.execute({
|
|
3905
|
-
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
3906
|
-
args: [now, messageId]
|
|
3907
|
-
});
|
|
3908
|
-
return true;
|
|
3909
|
-
} catch {
|
|
3910
|
-
const newRetryCount = msg.retryCount + 1;
|
|
3911
|
-
if (newRetryCount >= MAX_RETRIES2) {
|
|
3912
|
-
await markFailed(messageId, "session unavailable after 10 retries");
|
|
3913
|
-
} else {
|
|
3914
|
-
await client.execute({
|
|
3915
|
-
sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
|
|
3916
|
-
args: [newRetryCount, messageId]
|
|
3917
|
-
});
|
|
3918
|
-
}
|
|
3919
|
-
return false;
|
|
3920
|
-
}
|
|
3921
|
-
}
|
|
3922
|
-
async function getPendingMessages(targetAgent) {
|
|
3923
|
-
const client = getClient();
|
|
3924
|
-
const result = await client.execute({
|
|
3925
|
-
sql: `SELECT * FROM messages
|
|
3926
|
-
WHERE target_agent = ? AND status IN ('pending', 'delivered')
|
|
3927
|
-
ORDER BY id`,
|
|
3928
|
-
args: [targetAgent]
|
|
3929
|
-
});
|
|
3930
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3931
|
-
}
|
|
3932
|
-
async function markRead(messageId) {
|
|
3933
|
-
const client = getClient();
|
|
3934
|
-
await client.execute({
|
|
3935
|
-
sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
|
|
3936
|
-
args: [messageId]
|
|
3937
|
-
});
|
|
3938
|
-
}
|
|
3939
|
-
async function markAcknowledged(messageId) {
|
|
3940
|
-
const client = getClient();
|
|
3941
|
-
await client.execute({
|
|
3942
|
-
sql: "UPDATE messages SET status = 'acknowledged', processed_at = ? WHERE id = ? AND status = 'read'",
|
|
3943
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
3944
|
-
});
|
|
3945
|
-
}
|
|
3946
|
-
async function markProcessed(messageId) {
|
|
3947
|
-
const client = getClient();
|
|
3948
|
-
await client.execute({
|
|
3949
|
-
sql: "UPDATE messages SET status = 'processed', processed_at = ? WHERE id = ?",
|
|
3950
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), messageId]
|
|
3951
|
-
});
|
|
3952
|
-
}
|
|
3953
|
-
async function getMessageStatus(messageId) {
|
|
3954
|
-
const client = getClient();
|
|
3955
|
-
const result = await client.execute({
|
|
3956
|
-
sql: "SELECT status FROM messages WHERE id = ?",
|
|
3957
|
-
args: [messageId]
|
|
3958
|
-
});
|
|
3959
|
-
return result.rows[0]?.status ?? null;
|
|
3960
|
-
}
|
|
3961
|
-
async function getUnacknowledgedMessages(targetAgent) {
|
|
3962
|
-
const client = getClient();
|
|
3963
|
-
const result = await client.execute({
|
|
3964
|
-
sql: `SELECT * FROM messages
|
|
3965
|
-
WHERE target_agent = ? AND status IN ('pending', 'delivered', 'read')
|
|
3966
|
-
ORDER BY id`,
|
|
3967
|
-
args: [targetAgent]
|
|
3968
|
-
});
|
|
3969
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3970
|
-
}
|
|
3971
|
-
async function getReadMessages(targetAgent) {
|
|
3972
|
-
const client = getClient();
|
|
3973
|
-
const result = await client.execute({
|
|
3974
|
-
sql: "SELECT * FROM messages WHERE target_agent = ? AND status = 'read' ORDER BY id",
|
|
3975
|
-
args: [targetAgent]
|
|
3976
|
-
});
|
|
3977
|
-
return result.rows.map((row) => rowToMessage(row));
|
|
3978
|
-
}
|
|
3979
|
-
async function markFailed(messageId, reason) {
|
|
3980
|
-
const client = getClient();
|
|
3981
|
-
await client.execute({
|
|
3982
|
-
sql: "UPDATE messages SET status = 'failed', failed_at = ?, failure_reason = ? WHERE id = ?",
|
|
3983
|
-
args: [(/* @__PURE__ */ new Date()).toISOString(), reason, messageId]
|
|
3984
|
-
});
|
|
3985
|
-
}
|
|
3986
|
-
async function getFailedMessages() {
|
|
3987
|
-
const client = getClient();
|
|
3988
|
-
const result = await client.execute({
|
|
3989
|
-
sql: "SELECT * FROM messages WHERE status = 'failed' ORDER BY created_at DESC",
|
|
3990
|
-
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]
|
|
3991
3928
|
});
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
ORDER BY id`,
|
|
4000
|
-
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]
|
|
4001
3936
|
});
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
} catch {
|
|
4009
|
-
}
|
|
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
|
+
);
|
|
4010
3943
|
}
|
|
4011
|
-
return
|
|
3944
|
+
return total;
|
|
4012
3945
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
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
|
+
};
|
|
4021
3958
|
}
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
}
|
|
4035
|
-
|
|
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;
|
|
4036
3996
|
try {
|
|
4037
3997
|
const client = getClient();
|
|
4038
|
-
const id = crypto4.randomUUID();
|
|
4039
3998
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
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
|
+
}
|
|
4054
4031
|
} catch (err) {
|
|
4055
|
-
process.stderr.write(
|
|
4056
|
-
`)
|
|
4032
|
+
process.stderr.write(
|
|
4033
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
4034
|
+
`
|
|
4035
|
+
);
|
|
4057
4036
|
}
|
|
4058
|
-
}
|
|
4059
|
-
async function markAsReadByTaskFile(taskFile) {
|
|
4060
4037
|
try {
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
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
|
+
}
|
|
4066
4046
|
} catch {
|
|
4067
4047
|
}
|
|
4068
4048
|
}
|
|
4069
|
-
var
|
|
4070
|
-
"src/lib/
|
|
4049
|
+
var init_tasks_review = __esm({
|
|
4050
|
+
"src/lib/tasks-review.ts"() {
|
|
4071
4051
|
"use strict";
|
|
4072
4052
|
init_database();
|
|
4073
|
-
|
|
4074
|
-
|
|
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();
|
|
4060
|
+
}
|
|
4061
|
+
});
|
|
4075
4062
|
|
|
4076
|
-
// src/lib/tasks-
|
|
4077
|
-
import
|
|
4078
|
-
import
|
|
4079
|
-
|
|
4080
|
-
import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
|
|
4081
|
-
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
4082
|
-
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) {
|
|
4083
4067
|
const client = getClient();
|
|
4084
|
-
const
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
if (row.blocked_by) {
|
|
4089
|
-
blockedByIds.push(String(row.blocked_by));
|
|
4090
|
-
}
|
|
4091
|
-
const checkpoint = {
|
|
4092
|
-
step: input.step,
|
|
4093
|
-
context_summary: input.contextSummary,
|
|
4094
|
-
files_touched: input.filesTouched ?? [],
|
|
4095
|
-
blocked_by_ids: blockedByIds,
|
|
4096
|
-
last_checkpoint_at: now
|
|
4097
|
-
};
|
|
4098
|
-
const result = await client.execute({
|
|
4099
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
4100
|
-
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]
|
|
4101
4072
|
});
|
|
4102
|
-
if (
|
|
4103
|
-
|
|
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
|
+
}
|
|
4104
4088
|
}
|
|
4105
|
-
const countResult = await client.execute({
|
|
4106
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
4107
|
-
args: [taskId]
|
|
4108
|
-
});
|
|
4109
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
4110
|
-
return { checkpointCount };
|
|
4111
|
-
}
|
|
4112
|
-
function extractParentFromContext(contextBody) {
|
|
4113
|
-
if (!contextBody) return null;
|
|
4114
|
-
const match = contextBody.match(
|
|
4115
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
4116
|
-
);
|
|
4117
|
-
return match ? match[1].toLowerCase() : null;
|
|
4118
4089
|
}
|
|
4119
|
-
function
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
4128
|
-
result = await client.execute({
|
|
4129
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
4130
|
-
args: [`%${identifier}%`]
|
|
4131
|
-
});
|
|
4132
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
4133
|
-
if (result.rows.length > 1) {
|
|
4134
|
-
const exact = result.rows.filter(
|
|
4135
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
4136
|
-
);
|
|
4137
|
-
if (exact.length === 1) return exact[0];
|
|
4138
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
4139
|
-
const active = candidates.filter(
|
|
4140
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
4141
|
-
);
|
|
4142
|
-
if (active.length === 1) return active[0];
|
|
4143
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
4144
|
-
throw new Error(
|
|
4145
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
4146
|
-
);
|
|
4147
|
-
}
|
|
4148
|
-
result = await client.execute({
|
|
4149
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
4150
|
-
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]
|
|
4151
4098
|
});
|
|
4152
|
-
if (
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
throw new Error(
|
|
4160
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
4161
|
-
);
|
|
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
|
+
};
|
|
4162
4106
|
}
|
|
4163
|
-
|
|
4107
|
+
return void 0;
|
|
4164
4108
|
}
|
|
4165
|
-
async function
|
|
4109
|
+
async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
4166
4110
|
const client = getClient();
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
const
|
|
4173
|
-
if (
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
try {
|
|
4190
|
-
const parent = await resolveTask(client, parentRef);
|
|
4191
|
-
parentTaskId = String(parent.id);
|
|
4192
|
-
} catch (err) {
|
|
4193
|
-
if (!input.parentTaskId) {
|
|
4194
|
-
throw new Error(
|
|
4195
|
-
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
4196
|
-
);
|
|
4197
|
-
}
|
|
4198
|
-
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
|
+
});
|
|
4199
4133
|
}
|
|
4200
4134
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
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();
|
|
4208
4141
|
}
|
|
4209
|
-
|
|
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;
|
|
4210
4152
|
try {
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
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);
|
|
4215
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();
|
|
4216
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;
|
|
4217
4175
|
}
|
|
4218
|
-
const complexity = input.complexity ?? "standard";
|
|
4219
|
-
await client.execute({
|
|
4220
|
-
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)
|
|
4221
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4222
|
-
args: [
|
|
4223
|
-
id,
|
|
4224
|
-
input.title,
|
|
4225
|
-
input.assignedTo,
|
|
4226
|
-
input.assignedBy,
|
|
4227
|
-
input.projectName,
|
|
4228
|
-
input.priority,
|
|
4229
|
-
initialStatus,
|
|
4230
|
-
taskFile,
|
|
4231
|
-
blockedById,
|
|
4232
|
-
parentTaskId,
|
|
4233
|
-
input.reviewer ?? null,
|
|
4234
|
-
input.context,
|
|
4235
|
-
complexity,
|
|
4236
|
-
input.budgetTokens ?? null,
|
|
4237
|
-
input.budgetFallbackModel ?? null,
|
|
4238
|
-
0,
|
|
4239
|
-
null,
|
|
4240
|
-
now,
|
|
4241
|
-
now
|
|
4242
|
-
]
|
|
4243
|
-
});
|
|
4244
|
-
return {
|
|
4245
|
-
id,
|
|
4246
|
-
title: input.title,
|
|
4247
|
-
assignedTo: input.assignedTo,
|
|
4248
|
-
assignedBy: input.assignedBy,
|
|
4249
|
-
projectName: input.projectName,
|
|
4250
|
-
priority: input.priority,
|
|
4251
|
-
status: initialStatus,
|
|
4252
|
-
taskFile,
|
|
4253
|
-
createdAt: now,
|
|
4254
|
-
updatedAt: now,
|
|
4255
|
-
warning,
|
|
4256
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
4257
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
4258
|
-
tokensUsed: 0,
|
|
4259
|
-
tokensWarnedAt: null
|
|
4260
|
-
};
|
|
4261
4176
|
}
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
args.push(input.assignedTo);
|
|
4269
|
-
}
|
|
4270
|
-
if (input.status) {
|
|
4271
|
-
conditions.push("status = ?");
|
|
4272
|
-
args.push(input.status);
|
|
4273
|
-
} else {
|
|
4274
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
4275
|
-
}
|
|
4276
|
-
if (input.projectName) {
|
|
4277
|
-
conditions.push("project_name = ?");
|
|
4278
|
-
args.push(input.projectName);
|
|
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;
|
|
4279
4183
|
}
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
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;
|
|
4283
4205
|
}
|
|
4284
|
-
|
|
4285
|
-
const result = await client.execute({
|
|
4286
|
-
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`,
|
|
4287
|
-
args
|
|
4288
|
-
});
|
|
4289
|
-
return result.rows.map((r) => ({
|
|
4290
|
-
id: String(r.id),
|
|
4291
|
-
title: String(r.title),
|
|
4292
|
-
assignedTo: String(r.assigned_to),
|
|
4293
|
-
assignedBy: String(r.assigned_by),
|
|
4294
|
-
projectName: String(r.project_name),
|
|
4295
|
-
priority: String(r.priority),
|
|
4296
|
-
status: String(r.status),
|
|
4297
|
-
taskFile: String(r.task_file),
|
|
4298
|
-
createdAt: String(r.created_at),
|
|
4299
|
-
updatedAt: String(r.updated_at),
|
|
4300
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
4301
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
4302
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
4303
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
4304
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
4305
|
-
}));
|
|
4206
|
+
return null;
|
|
4306
4207
|
}
|
|
4307
|
-
function
|
|
4308
|
-
if (!taskContext) return null;
|
|
4309
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
4208
|
+
function assertSessionScope(actionType, targetProject) {
|
|
4310
4209
|
try {
|
|
4311
|
-
const
|
|
4312
|
-
const
|
|
4313
|
-
|
|
4314
|
-
{
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
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
|
+
};
|
|
4324
4222
|
}
|
|
4325
|
-
return null;
|
|
4326
|
-
} catch {
|
|
4327
|
-
return null;
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
async function updateTaskStatus(input) {
|
|
4331
|
-
const client = getClient();
|
|
4332
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4333
|
-
const row = await resolveTask(client, input.taskId);
|
|
4334
|
-
const taskId = String(row.id);
|
|
4335
|
-
const taskFile = String(row.task_file);
|
|
4336
|
-
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
4337
4223
|
process.stderr.write(
|
|
4338
|
-
`[
|
|
4224
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
4339
4225
|
`
|
|
4340
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" };
|
|
4341
4236
|
}
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
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();
|
|
4244
|
+
}
|
|
4245
|
+
});
|
|
4352
4246
|
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
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) {
|
|
4252
|
+
try {
|
|
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 };
|
|
4356
4258
|
}
|
|
4259
|
+
} catch {
|
|
4357
4260
|
}
|
|
4358
4261
|
}
|
|
4359
|
-
|
|
4360
|
-
const
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
const cur = current.rows[0];
|
|
4373
|
-
const status = cur?.status ?? "unknown";
|
|
4374
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
4375
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
4376
|
-
}
|
|
4377
|
-
try {
|
|
4378
|
-
await writeCheckpoint({
|
|
4379
|
-
taskId,
|
|
4380
|
-
step: "claimed",
|
|
4381
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
4262
|
+
try {
|
|
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)
|
|
4382
4275
|
});
|
|
4383
|
-
|
|
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 };
|
|
4384
4284
|
}
|
|
4385
|
-
|
|
4285
|
+
} catch {
|
|
4286
|
+
return { dispatched: "session_missing" };
|
|
4386
4287
|
}
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
}
|
|
4393
|
-
await client.execute({
|
|
4394
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
4395
|
-
args: [input.status, now, taskId]
|
|
4396
|
-
});
|
|
4288
|
+
}
|
|
4289
|
+
function notifyTaskDone() {
|
|
4290
|
+
try {
|
|
4291
|
+
const key = getSessionKey();
|
|
4292
|
+
if (key && !process.env.VITEST) notifyParentExe(key);
|
|
4293
|
+
} catch {
|
|
4397
4294
|
}
|
|
4295
|
+
}
|
|
4296
|
+
async function markTaskNotificationsRead(taskFile) {
|
|
4398
4297
|
try {
|
|
4399
|
-
await
|
|
4400
|
-
taskId,
|
|
4401
|
-
step: `status_transition:${input.status}`,
|
|
4402
|
-
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
4403
|
-
});
|
|
4298
|
+
await markAsReadByTaskFile(taskFile);
|
|
4404
4299
|
} catch {
|
|
4405
4300
|
}
|
|
4406
|
-
return { row, taskFile, now, taskId };
|
|
4407
4301
|
}
|
|
4408
|
-
|
|
4302
|
+
var init_tasks_notify = __esm({
|
|
4303
|
+
"src/lib/tasks-notify.ts"() {
|
|
4304
|
+
"use strict";
|
|
4305
|
+
init_tmux_routing();
|
|
4306
|
+
init_session_key();
|
|
4307
|
+
init_notifications();
|
|
4308
|
+
init_transport();
|
|
4309
|
+
init_employees();
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
|
|
4313
|
+
// src/lib/behaviors.ts
|
|
4314
|
+
import crypto6 from "crypto";
|
|
4315
|
+
async function storeBehavior(opts) {
|
|
4409
4316
|
const client = getClient();
|
|
4410
|
-
const
|
|
4411
|
-
const
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
return
|
|
4418
|
-
}
|
|
4419
|
-
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
4420
|
-
const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
4421
|
-
try {
|
|
4422
|
-
if (existsSync12(archPath)) return;
|
|
4423
|
-
const template = [
|
|
4424
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
4425
|
-
"",
|
|
4426
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
4427
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
4428
|
-
"",
|
|
4429
|
-
"## Overview",
|
|
4430
|
-
"",
|
|
4431
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
4432
|
-
"",
|
|
4433
|
-
"## Key Components",
|
|
4434
|
-
"",
|
|
4435
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
4436
|
-
"",
|
|
4437
|
-
"## Data Flow",
|
|
4438
|
-
"",
|
|
4439
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
4440
|
-
"",
|
|
4441
|
-
"## Invariants",
|
|
4442
|
-
"",
|
|
4443
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
4444
|
-
"",
|
|
4445
|
-
"## Dependencies",
|
|
4446
|
-
"",
|
|
4447
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
4448
|
-
""
|
|
4449
|
-
].join("\n");
|
|
4450
|
-
await writeFile4(archPath, template, "utf-8");
|
|
4451
|
-
} catch {
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
async function ensureGitignoreExe(baseDir) {
|
|
4455
|
-
const gitignorePath = path13.join(baseDir, ".gitignore");
|
|
4456
|
-
try {
|
|
4457
|
-
if (existsSync12(gitignorePath)) {
|
|
4458
|
-
const content = readFileSync11(gitignorePath, "utf-8");
|
|
4459
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
4460
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
4461
|
-
} else {
|
|
4462
|
-
await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
4463
|
-
}
|
|
4464
|
-
} catch {
|
|
4465
|
-
}
|
|
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]
|
|
4323
|
+
});
|
|
4324
|
+
return id;
|
|
4466
4325
|
}
|
|
4467
|
-
var
|
|
4468
|
-
|
|
4469
|
-
"src/lib/tasks-crud.ts"() {
|
|
4326
|
+
var init_behaviors = __esm({
|
|
4327
|
+
"src/lib/behaviors.ts"() {
|
|
4470
4328
|
"use strict";
|
|
4471
4329
|
init_database();
|
|
4472
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
4473
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
4474
4330
|
}
|
|
4475
4331
|
});
|
|
4476
4332
|
|
|
4477
|
-
// src/lib/
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
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) {
|
|
4481
4348
|
const client = getClient();
|
|
4482
4349
|
const result = await client.execute({
|
|
4483
|
-
sql:
|
|
4484
|
-
|
|
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]
|
|
4485
4355
|
});
|
|
4486
|
-
|
|
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);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
return signature;
|
|
4487
4373
|
}
|
|
4488
|
-
|
|
4374
|
+
function hashSignature(signature) {
|
|
4375
|
+
return crypto7.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
4376
|
+
}
|
|
4377
|
+
async function storeTrajectory(opts) {
|
|
4489
4378
|
const client = getClient();
|
|
4490
|
-
const
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
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
|
+
]
|
|
4494
4396
|
});
|
|
4495
|
-
return
|
|
4397
|
+
return id;
|
|
4496
4398
|
}
|
|
4497
|
-
async function
|
|
4399
|
+
async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
|
|
4498
4400
|
const client = getClient();
|
|
4401
|
+
const hash = hashSignature(signature);
|
|
4499
4402
|
const result = await client.execute({
|
|
4500
|
-
sql: `SELECT
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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]
|
|
4504
4409
|
});
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
args: [now]
|
|
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)
|
|
4517
4421
|
});
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
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
|
+
]
|
|
4525
4436
|
});
|
|
4526
|
-
const
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
);
|
|
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));
|
|
4441
|
+
}
|
|
4532
4442
|
}
|
|
4533
|
-
return
|
|
4443
|
+
return matches;
|
|
4534
4444
|
}
|
|
4535
|
-
function
|
|
4536
|
-
const
|
|
4537
|
-
if (
|
|
4538
|
-
return {
|
|
4539
|
-
lens: "Code Quality (Engineer)",
|
|
4540
|
-
checklist: [
|
|
4541
|
-
"1. Do all tests pass? Any new tests needed?",
|
|
4542
|
-
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
4543
|
-
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
4544
|
-
"4. Any regressions in the test suite?"
|
|
4545
|
-
]
|
|
4546
|
-
};
|
|
4547
|
-
}
|
|
4548
|
-
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
4549
|
-
return {
|
|
4550
|
-
lens: "Architecture (CTO)",
|
|
4551
|
-
checklist: [
|
|
4552
|
-
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
4553
|
-
"2. Is it backward compatible? Any breaking changes?",
|
|
4554
|
-
"3. Does it introduce technical debt? Is that debt justified?",
|
|
4555
|
-
"4. Security implications? Any new attack surface?",
|
|
4556
|
-
"5. Does it scale? Performance considerations?",
|
|
4557
|
-
"6. Coordination: does this affect other employees' work or other projects?"
|
|
4558
|
-
]
|
|
4559
|
-
};
|
|
4560
|
-
}
|
|
4561
|
-
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
4562
|
-
return {
|
|
4563
|
-
lens: "Strategic (COO)",
|
|
4564
|
-
checklist: [
|
|
4565
|
-
"1. Does this serve the project mission?",
|
|
4566
|
-
"2. Is this the right work at the right time?",
|
|
4567
|
-
"3. Does the architectural assessment make sense for the business?",
|
|
4568
|
-
"4. Any cross-project implications?"
|
|
4569
|
-
]
|
|
4570
|
-
};
|
|
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: [] };
|
|
4571
4449
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
const
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
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 };
|
|
4462
|
+
}
|
|
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:
|
|
4470
|
+
|
|
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.`;
|
|
4483
|
+
}
|
|
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
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
process.stderr.write(
|
|
4515
|
+
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
4597
4516
|
`
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4517
|
+
);
|
|
4518
|
+
return skillId;
|
|
4519
|
+
}
|
|
4520
|
+
async function captureAndLearn(opts) {
|
|
4521
|
+
try {
|
|
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) => {
|
|
4613
4533
|
process.stderr.write(
|
|
4614
|
-
`[
|
|
4534
|
+
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
4615
4535
|
`
|
|
4616
4536
|
);
|
|
4617
|
-
}
|
|
4537
|
+
});
|
|
4618
4538
|
}
|
|
4619
4539
|
}
|
|
4620
4540
|
} catch (err) {
|
|
4621
4541
|
process.stderr.write(
|
|
4622
|
-
`[
|
|
4542
|
+
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
4623
4543
|
`
|
|
4624
4544
|
);
|
|
4625
4545
|
}
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4546
|
+
}
|
|
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++;
|
|
4634
4590
|
}
|
|
4635
|
-
} catch {
|
|
4636
4591
|
}
|
|
4592
|
+
return { clustersProcessed, skillsExtracted };
|
|
4637
4593
|
}
|
|
4638
|
-
|
|
4639
|
-
|
|
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"() {
|
|
4640
4615
|
"use strict";
|
|
4641
4616
|
init_database();
|
|
4617
|
+
init_behaviors();
|
|
4642
4618
|
init_config();
|
|
4643
|
-
|
|
4644
|
-
init_notifications();
|
|
4645
|
-
init_tasks_crud();
|
|
4646
|
-
init_tmux_routing();
|
|
4647
|
-
init_session_key();
|
|
4619
|
+
DEFAULT_SKILL_THRESHOLD = 3;
|
|
4648
4620
|
}
|
|
4649
4621
|
});
|
|
4650
4622
|
|
|
4651
|
-
// src/lib/tasks
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
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
|
|
4665
4656
|
});
|
|
4666
|
-
|
|
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") {
|
|
4667
4670
|
try {
|
|
4668
|
-
|
|
4669
|
-
let ubContent = await readFile4(ubFile, "utf-8");
|
|
4670
|
-
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
4671
|
-
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
4672
|
-
await writeFile5(ubFile, ubContent, "utf-8");
|
|
4671
|
+
unlinkSync4(cachePath);
|
|
4673
4672
|
} catch {
|
|
4674
4673
|
}
|
|
4675
4674
|
}
|
|
4675
|
+
} catch {
|
|
4676
4676
|
}
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
const client = getClient();
|
|
4680
|
-
const nextResult = await client.execute({
|
|
4681
|
-
sql: `SELECT title, task_file, priority FROM tasks
|
|
4682
|
-
WHERE assigned_to = ? AND status = 'open'
|
|
4683
|
-
ORDER BY priority ASC, created_at ASC
|
|
4684
|
-
LIMIT 1`,
|
|
4685
|
-
args: [assignedTo]
|
|
4686
|
-
});
|
|
4687
|
-
if (nextResult.rows.length === 1) {
|
|
4688
|
-
const nr = nextResult.rows[0];
|
|
4689
|
-
return {
|
|
4690
|
-
title: String(nr.title),
|
|
4691
|
-
priority: String(nr.priority),
|
|
4692
|
-
taskFile: String(nr.task_file)
|
|
4693
|
-
};
|
|
4677
|
+
if (input.status === "done") {
|
|
4678
|
+
await cleanupReviewFile(row, taskFile, input.baseDir);
|
|
4694
4679
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
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%`]
|
|
4689
|
+
});
|
|
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) {
|
|
4700
|
+
process.stderr.write(
|
|
4701
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
4702
|
+
`
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
} catch {
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
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
|
|
4720
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
|
+
}
|
|
4721
4733
|
}
|
|
4722
4734
|
}
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
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
|
+
});
|
|
4729
4749
|
}
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
// src/lib/project-name.ts
|
|
4733
|
-
import { execSync as execSync6 } from "child_process";
|
|
4734
|
-
import path16 from "path";
|
|
4735
|
-
function getProjectName(cwd) {
|
|
4736
|
-
const dir = cwd ?? process.cwd();
|
|
4737
|
-
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
4738
|
-
try {
|
|
4739
|
-
let repoRoot;
|
|
4750
|
+
let nextTask;
|
|
4751
|
+
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
4740
4752
|
try {
|
|
4741
|
-
|
|
4742
|
-
cwd: dir,
|
|
4743
|
-
encoding: "utf8",
|
|
4744
|
-
timeout: 2e3,
|
|
4745
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4746
|
-
}).trim();
|
|
4747
|
-
repoRoot = path16.dirname(gitCommonDir);
|
|
4753
|
+
nextTask = await findNextTask(String(row.assigned_to));
|
|
4748
4754
|
} catch {
|
|
4749
|
-
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
4750
|
-
cwd: dir,
|
|
4751
|
-
encoding: "utf8",
|
|
4752
|
-
timeout: 2e3,
|
|
4753
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4754
|
-
}).trim();
|
|
4755
4755
|
}
|
|
4756
|
-
_cached2 = path16.basename(repoRoot);
|
|
4757
|
-
_cachedCwd = dir;
|
|
4758
|
-
return _cached2;
|
|
4759
|
-
} catch {
|
|
4760
|
-
_cached2 = path16.basename(dir);
|
|
4761
|
-
_cachedCwd = dir;
|
|
4762
|
-
return _cached2;
|
|
4763
4756
|
}
|
|
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
|
+
};
|
|
4764
4774
|
}
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4775
|
+
async function deleteTask(taskId, baseDir) {
|
|
4776
|
+
const client = getClient();
|
|
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`;
|
|
4781
|
+
await client.execute({
|
|
4782
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ?",
|
|
4783
|
+
args: [reviewFile, `exe/exe/${reviewSlug}.md`]
|
|
4784
|
+
});
|
|
4785
|
+
await markAsReadByTaskFile(taskFile);
|
|
4786
|
+
await markAsReadByTaskFile(reviewFile);
|
|
4787
|
+
}
|
|
4788
|
+
var init_tasks = __esm({
|
|
4789
|
+
"src/lib/tasks.ts"() {
|
|
4768
4790
|
"use strict";
|
|
4769
|
-
|
|
4770
|
-
|
|
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();
|
|
4771
4801
|
}
|
|
4772
4802
|
});
|
|
4773
4803
|
|
|
4774
|
-
// src/lib/
|
|
4775
|
-
var
|
|
4776
|
-
__export(
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
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
|
|
4780
4816
|
});
|
|
4781
|
-
function
|
|
4782
|
-
|
|
4783
|
-
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
4784
|
-
if (!entry) return null;
|
|
4785
|
-
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
4786
|
-
return parts[parts.length - 1] ?? null;
|
|
4817
|
+
function resumeTaskTitle(agentId) {
|
|
4818
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
4787
4819
|
}
|
|
4788
|
-
function
|
|
4789
|
-
const
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
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;
|
|
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;
|
|
4793
4864
|
}
|
|
4794
|
-
|
|
4865
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
4866
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4867
|
+
return false;
|
|
4868
|
+
}
|
|
4869
|
+
_pendingCapacityKill.delete(agentId);
|
|
4870
|
+
return true;
|
|
4795
4871
|
}
|
|
4796
|
-
function
|
|
4872
|
+
function _resetPendingCapacityKills() {
|
|
4873
|
+
_pendingCapacityKill.clear();
|
|
4874
|
+
}
|
|
4875
|
+
function _resetLastRelaunchCache() {
|
|
4876
|
+
_lastRelaunch.clear();
|
|
4877
|
+
}
|
|
4878
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
4879
|
+
const client = getClient();
|
|
4880
|
+
const result = await client.execute({
|
|
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} %`]
|
|
4885
|
+
});
|
|
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(", ")})
|
|
4909
|
+
ORDER BY created_at DESC
|
|
4910
|
+
LIMIT 1`,
|
|
4911
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
4912
|
+
});
|
|
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 };
|
|
4920
|
+
}
|
|
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
|
|
4930
|
+
});
|
|
4931
|
+
return { created: true, taskId: task.id };
|
|
4932
|
+
}
|
|
4933
|
+
async function pollCapacityDead() {
|
|
4934
|
+
const transport = getTransport();
|
|
4935
|
+
const relaunched = [];
|
|
4936
|
+
const registered = listSessions().filter(
|
|
4937
|
+
(s) => s.agentId !== "exe"
|
|
4938
|
+
);
|
|
4939
|
+
if (registered.length === 0) return [];
|
|
4940
|
+
let liveSessions;
|
|
4797
4941
|
try {
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
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;
|
|
4802
4955
|
}
|
|
4803
|
-
if (
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
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).
|
|
4961
|
+
`
|
|
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;
|
|
4810
4984
|
}
|
|
4811
4985
|
process.stderr.write(
|
|
4812
|
-
`[
|
|
4986
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
4813
4987
|
`
|
|
4814
4988
|
);
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
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
|
+
}
|
|
4825
5036
|
}
|
|
5037
|
+
return relaunched;
|
|
4826
5038
|
}
|
|
4827
|
-
var
|
|
4828
|
-
|
|
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"() {
|
|
4829
5042
|
"use strict";
|
|
4830
5043
|
init_session_registry();
|
|
4831
|
-
|
|
5044
|
+
init_transport();
|
|
5045
|
+
init_notifications();
|
|
5046
|
+
init_database();
|
|
5047
|
+
init_session_kill_telemetry();
|
|
4832
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;
|
|
4833
5081
|
}
|
|
4834
5082
|
});
|
|
4835
5083
|
|
|
4836
|
-
// src/lib/
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
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)) {
|
|
4841
5131
|
try {
|
|
4842
|
-
const
|
|
4843
|
-
const
|
|
4844
|
-
if (
|
|
4845
|
-
|
|
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;
|
|
4846
5136
|
}
|
|
4847
5137
|
} catch {
|
|
4848
5138
|
}
|
|
4849
5139
|
}
|
|
5140
|
+
writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
5141
|
+
return true;
|
|
5142
|
+
}
|
|
5143
|
+
function releaseSpawnLock2(sessionName) {
|
|
4850
5144
|
try {
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
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)}
|
|
4867
5176
|
`
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
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
|
+
);
|
|
4872
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;
|
|
4873
5234
|
} catch {
|
|
4874
|
-
return
|
|
5235
|
+
return null;
|
|
4875
5236
|
}
|
|
4876
5237
|
}
|
|
4877
|
-
function
|
|
5238
|
+
function getDispatchedBy(sessionKey) {
|
|
4878
5239
|
try {
|
|
4879
|
-
const
|
|
4880
|
-
|
|
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;
|
|
4881
5245
|
} catch {
|
|
5246
|
+
return null;
|
|
4882
5247
|
}
|
|
4883
5248
|
}
|
|
4884
|
-
|
|
5249
|
+
function resolveExeSession() {
|
|
5250
|
+
const mySession = getMySession();
|
|
5251
|
+
if (!mySession) return null;
|
|
4885
5252
|
try {
|
|
4886
|
-
|
|
5253
|
+
const key = getSessionKey();
|
|
5254
|
+
const parentExe = getParentExe(key);
|
|
5255
|
+
if (parentExe) {
|
|
5256
|
+
return extractRootExe(parentExe) ?? parentExe;
|
|
5257
|
+
}
|
|
4887
5258
|
} catch {
|
|
4888
5259
|
}
|
|
5260
|
+
return extractRootExe(mySession) ?? mySession;
|
|
4889
5261
|
}
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
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;
|
|
4898
5271
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
// src/lib/behaviors.ts
|
|
4902
|
-
import crypto6 from "crypto";
|
|
4903
|
-
async function storeBehavior(opts) {
|
|
4904
|
-
const client = getClient();
|
|
4905
|
-
const id = crypto6.randomUUID();
|
|
4906
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4907
|
-
await client.execute({
|
|
4908
|
-
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
4909
|
-
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
4910
|
-
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
4911
|
-
});
|
|
4912
|
-
return id;
|
|
5272
|
+
return null;
|
|
4913
5273
|
}
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
5274
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
5275
|
+
const transport = getTransport();
|
|
5276
|
+
if (!transport.isAlive(sessionName)) {
|
|
5277
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
4918
5278
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
extractSkill: () => extractSkill,
|
|
4928
|
-
extractTrajectory: () => extractTrajectory,
|
|
4929
|
-
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
4930
|
-
hashSignature: () => hashSignature,
|
|
4931
|
-
storeTrajectory: () => storeTrajectory,
|
|
4932
|
-
sweepTrajectories: () => sweepTrajectories
|
|
4933
|
-
});
|
|
4934
|
-
import crypto7 from "crypto";
|
|
4935
|
-
async function extractTrajectory(taskId, agentId) {
|
|
4936
|
-
const client = getClient();
|
|
4937
|
-
const result = await client.execute({
|
|
4938
|
-
sql: `SELECT tool_name, raw_text
|
|
4939
|
-
FROM memories
|
|
4940
|
-
WHERE task_id = ? AND agent_id = ?
|
|
4941
|
-
ORDER BY timestamp ASC`,
|
|
4942
|
-
args: [taskId, agentId]
|
|
4943
|
-
});
|
|
4944
|
-
if (result.rows.length === 0) return [];
|
|
4945
|
-
const rawTools = result.rows.map((r) => {
|
|
4946
|
-
const toolName = String(r.tool_name);
|
|
4947
|
-
if (toolName === "Bash") {
|
|
4948
|
-
const text = String(r.raw_text);
|
|
4949
|
-
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
4950
|
-
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
4951
|
-
}
|
|
4952
|
-
return toolName;
|
|
4953
|
-
});
|
|
4954
|
-
const signature = [];
|
|
4955
|
-
for (const tool of rawTools) {
|
|
4956
|
-
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
4957
|
-
signature.push(tool);
|
|
4958
|
-
}
|
|
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
|
+
};
|
|
4959
5287
|
}
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
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
|
+
};
|
|
4964
5299
|
}
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4973
|
-
args: [
|
|
4974
|
-
id,
|
|
4975
|
-
opts.taskId,
|
|
4976
|
-
opts.agentId,
|
|
4977
|
-
opts.projectName,
|
|
4978
|
-
opts.taskTitle,
|
|
4979
|
-
JSON.stringify(opts.signature),
|
|
4980
|
-
signatureHash,
|
|
4981
|
-
opts.signature.length,
|
|
4982
|
-
now
|
|
4983
|
-
]
|
|
4984
|
-
});
|
|
4985
|
-
return id;
|
|
5300
|
+
function readDebounceState() {
|
|
5301
|
+
try {
|
|
5302
|
+
if (!existsSync13(DEBOUNCE_FILE)) return {};
|
|
5303
|
+
return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
|
|
5304
|
+
} catch {
|
|
5305
|
+
return {};
|
|
5306
|
+
}
|
|
4986
5307
|
}
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
}
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
if (editDistance(signature, candidateSig) <= 2) {
|
|
5028
|
-
matches.push(mapRow(r));
|
|
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
|
+
}
|
|
5029
5348
|
}
|
|
5349
|
+
if (/Running…/.test(pane)) return "tool";
|
|
5350
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
5351
|
+
return "idle";
|
|
5352
|
+
} catch {
|
|
5353
|
+
return "offline";
|
|
5030
5354
|
}
|
|
5031
|
-
return matches;
|
|
5032
5355
|
}
|
|
5033
|
-
|
|
5034
|
-
const
|
|
5035
|
-
|
|
5036
|
-
|
|
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";
|
|
5037
5403
|
}
|
|
5038
|
-
const trajectoryId = await storeTrajectory({
|
|
5039
|
-
taskId: opts.taskId,
|
|
5040
|
-
agentId: opts.agentId,
|
|
5041
|
-
projectName: opts.projectName,
|
|
5042
|
-
taskTitle: opts.taskTitle,
|
|
5043
|
-
signature
|
|
5044
|
-
});
|
|
5045
|
-
const similar = await findSimilarTrajectories(
|
|
5046
|
-
signature,
|
|
5047
|
-
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
5048
|
-
);
|
|
5049
|
-
return { trajectoryId, similarCount: similar.length, similar };
|
|
5050
5404
|
}
|
|
5051
|
-
function
|
|
5052
|
-
const
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
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;
|
|
5071
5426
|
}
|
|
5072
|
-
|
|
5073
|
-
if (
|
|
5074
|
-
|
|
5075
|
-
const skillModel = model ?? config2.skillModel;
|
|
5076
|
-
const Anthropic4 = (await import("@anthropic-ai/sdk")).default;
|
|
5077
|
-
const client = new Anthropic4();
|
|
5078
|
-
const prompt = buildExtractionPrompt(trajectories);
|
|
5079
|
-
const response = await client.messages.create({
|
|
5080
|
-
model: skillModel,
|
|
5081
|
-
max_tokens: 500,
|
|
5082
|
-
messages: [{ role: "user", content: prompt }]
|
|
5083
|
-
});
|
|
5084
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
5085
|
-
const skillText = textBlock?.text;
|
|
5086
|
-
if (!skillText) return null;
|
|
5087
|
-
const agentId = trajectories[0].agentId;
|
|
5088
|
-
const projectName = trajectories[0].projectName;
|
|
5089
|
-
const skillId = await storeBehavior({
|
|
5090
|
-
agentId,
|
|
5091
|
-
content: skillText,
|
|
5092
|
-
domain: "skill",
|
|
5093
|
-
projectName
|
|
5094
|
-
});
|
|
5095
|
-
const dbClient = getClient();
|
|
5096
|
-
for (const t of trajectories) {
|
|
5097
|
-
await dbClient.execute({
|
|
5098
|
-
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
5099
|
-
args: [skillId, t.id]
|
|
5100
|
-
});
|
|
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" };
|
|
5101
5430
|
}
|
|
5102
|
-
|
|
5103
|
-
|
|
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}".
|
|
5104
5451
|
`
|
|
5105
|
-
|
|
5106
|
-
|
|
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 };
|
|
5107
5495
|
}
|
|
5108
|
-
|
|
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 = "";
|
|
5109
5507
|
try {
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
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.
|
|
5123
5604
|
`
|
|
5124
|
-
|
|
5125
|
-
|
|
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}`;
|
|
5126
5629
|
}
|
|
5127
5630
|
}
|
|
5128
|
-
}
|
|
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}`;
|
|
5129
5637
|
process.stderr.write(
|
|
5130
|
-
`[
|
|
5638
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
5131
5639
|
`
|
|
5132
5640
|
);
|
|
5641
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
5642
|
+
} else {
|
|
5643
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
5133
5644
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
if (!config2.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
5138
|
-
const t = threshold ?? config2.skillThreshold;
|
|
5139
|
-
const client = getClient();
|
|
5140
|
-
const result = await client.execute({
|
|
5141
|
-
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
5142
|
-
FROM trajectories
|
|
5143
|
-
WHERE skill_id IS NULL
|
|
5144
|
-
GROUP BY signature_hash
|
|
5145
|
-
HAVING cnt >= ?
|
|
5146
|
-
ORDER BY cnt DESC
|
|
5147
|
-
LIMIT 10`,
|
|
5148
|
-
args: [t]
|
|
5645
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
5646
|
+
cwd: spawnCwd,
|
|
5647
|
+
command: spawnCommand
|
|
5149
5648
|
});
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
id: String(r.id),
|
|
5164
|
-
taskId: String(r.task_id),
|
|
5165
|
-
agentId: String(r.agent_id),
|
|
5166
|
-
projectName: String(r.project_name),
|
|
5167
|
-
taskTitle: String(r.task_title),
|
|
5168
|
-
signature: JSON.parse(String(r.signature)),
|
|
5169
|
-
signatureHash: String(r.signature_hash),
|
|
5170
|
-
toolCount: Number(r.tool_count),
|
|
5171
|
-
skillId: null,
|
|
5172
|
-
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()
|
|
5173
5662
|
}));
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
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 {
|
|
5178
5685
|
}
|
|
5179
5686
|
}
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
for (let i = 1; i <= m; i++) {
|
|
5189
|
-
for (let j = 1; j <= n; j++) {
|
|
5190
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
5191
|
-
dp[i][j] = Math.min(
|
|
5192
|
-
dp[i - 1][j] + 1,
|
|
5193
|
-
dp[i][j - 1] + 1,
|
|
5194
|
-
dp[i - 1][j - 1] + cost
|
|
5195
|
-
);
|
|
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 {
|
|
5196
5695
|
}
|
|
5197
5696
|
}
|
|
5198
|
-
|
|
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 };
|
|
5199
5707
|
}
|
|
5200
|
-
var
|
|
5201
|
-
var
|
|
5202
|
-
"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"() {
|
|
5203
5711
|
"use strict";
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
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…/;
|
|
5208
5730
|
}
|
|
5209
5731
|
});
|
|
5210
5732
|
|
|
5211
|
-
// src/lib/
|
|
5212
|
-
var
|
|
5213
|
-
__export(
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
slugify: () => slugify,
|
|
5228
|
-
updateTask: () => updateTask,
|
|
5229
|
-
updateTaskStatus: () => updateTaskStatus,
|
|
5230
|
-
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
|
|
5231
5749
|
});
|
|
5232
|
-
import
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
const
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
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]
|
|
5244
5838
|
});
|
|
5839
|
+
return true;
|
|
5245
5840
|
}
|
|
5246
|
-
return
|
|
5841
|
+
return false;
|
|
5247
5842
|
}
|
|
5248
|
-
async function
|
|
5249
|
-
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();
|
|
5250
5854
|
try {
|
|
5251
|
-
const
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
try {
|
|
5259
|
-
unlinkSync5(cachePath);
|
|
5260
|
-
} catch {
|
|
5261
|
-
}
|
|
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");
|
|
5262
5862
|
}
|
|
5863
|
+
await client.execute({
|
|
5864
|
+
sql: "UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id = ?",
|
|
5865
|
+
args: [now, messageId]
|
|
5866
|
+
});
|
|
5867
|
+
return true;
|
|
5263
5868
|
} catch {
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
if (input.status === "done" || input.status === "cancelled") {
|
|
5269
|
-
try {
|
|
5270
|
-
const client = getClient();
|
|
5271
|
-
const taskTitle = String(row.title);
|
|
5272
|
-
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 {
|
|
5273
5873
|
await client.execute({
|
|
5274
|
-
sql:
|
|
5275
|
-
|
|
5276
|
-
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
5277
|
-
});
|
|
5278
|
-
} catch {
|
|
5279
|
-
}
|
|
5280
|
-
try {
|
|
5281
|
-
const client = getClient();
|
|
5282
|
-
const cascaded = await client.execute({
|
|
5283
|
-
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
5284
|
-
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
5285
|
-
args: [now, taskId]
|
|
5874
|
+
sql: "UPDATE messages SET retry_count = ? WHERE id = ?",
|
|
5875
|
+
args: [newRetryCount, messageId]
|
|
5286
5876
|
});
|
|
5287
|
-
if (cascaded.rowsAffected > 0) {
|
|
5288
|
-
process.stderr.write(
|
|
5289
|
-
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
5290
|
-
`
|
|
5291
|
-
);
|
|
5292
|
-
}
|
|
5293
|
-
} catch {
|
|
5294
|
-
}
|
|
5295
|
-
}
|
|
5296
|
-
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
5297
|
-
if (isTerminal) {
|
|
5298
|
-
const isExe = String(row.assigned_to) === "exe";
|
|
5299
|
-
if (!isExe) {
|
|
5300
|
-
notifyTaskDone();
|
|
5301
|
-
}
|
|
5302
|
-
await markTaskNotificationsRead(taskFile);
|
|
5303
|
-
if (input.status === "done") {
|
|
5304
|
-
try {
|
|
5305
|
-
await cascadeUnblock(taskId, input.baseDir, now);
|
|
5306
|
-
} catch {
|
|
5307
|
-
}
|
|
5308
|
-
if (row.parent_task_id) {
|
|
5309
|
-
try {
|
|
5310
|
-
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
5311
|
-
} catch {
|
|
5312
|
-
}
|
|
5313
|
-
}
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
if (input.status === "done" && String(row.assigned_to) !== "exe" && !process.env.VITEST) {
|
|
5317
|
-
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
5318
|
-
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
5319
|
-
taskId,
|
|
5320
|
-
agentId: String(row.assigned_to),
|
|
5321
|
-
projectName: String(row.project_name),
|
|
5322
|
-
taskTitle: String(row.title)
|
|
5323
|
-
})
|
|
5324
|
-
).catch((err) => {
|
|
5325
|
-
process.stderr.write(
|
|
5326
|
-
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
5327
|
-
`
|
|
5328
|
-
);
|
|
5329
|
-
});
|
|
5330
|
-
}
|
|
5331
|
-
let nextTask;
|
|
5332
|
-
if (isTerminal && String(row.assigned_to) !== "exe") {
|
|
5333
|
-
try {
|
|
5334
|
-
nextTask = await findNextTask(String(row.assigned_to));
|
|
5335
|
-
} catch {
|
|
5336
5877
|
}
|
|
5878
|
+
return false;
|
|
5337
5879
|
}
|
|
5338
|
-
return {
|
|
5339
|
-
id: String(row.id),
|
|
5340
|
-
title: String(row.title),
|
|
5341
|
-
assignedTo: String(row.assigned_to),
|
|
5342
|
-
assignedBy: String(row.assigned_by),
|
|
5343
|
-
projectName: String(row.project_name),
|
|
5344
|
-
priority: String(row.priority),
|
|
5345
|
-
status: input.status,
|
|
5346
|
-
taskFile,
|
|
5347
|
-
createdAt: String(row.created_at),
|
|
5348
|
-
updatedAt: now,
|
|
5349
|
-
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
5350
|
-
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
5351
|
-
tokensUsed: Number(row.tokens_used ?? 0),
|
|
5352
|
-
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
5353
|
-
nextTask
|
|
5354
|
-
};
|
|
5355
5880
|
}
|
|
5356
|
-
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) {
|
|
5357
5892
|
const client = getClient();
|
|
5358
|
-
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
5359
|
-
const reviewer = assignedBy || "exe";
|
|
5360
|
-
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
5361
|
-
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
5362
5893
|
await client.execute({
|
|
5363
|
-
sql: "
|
|
5364
|
-
args: [
|
|
5894
|
+
sql: "UPDATE messages SET status = 'read' WHERE id = ? AND status IN ('pending', 'delivered')",
|
|
5895
|
+
args: [messageId]
|
|
5365
5896
|
});
|
|
5366
|
-
await markAsReadByTaskFile(taskFile);
|
|
5367
|
-
await markAsReadByTaskFile(reviewFile);
|
|
5368
5897
|
}
|
|
5369
|
-
|
|
5370
|
-
|
|
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"() {
|
|
5371
5975
|
"use strict";
|
|
5372
5976
|
init_database();
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
init_tasks_review();
|
|
5377
|
-
init_tasks_crud();
|
|
5378
|
-
init_tasks_chain();
|
|
5379
|
-
init_tasks_review();
|
|
5380
|
-
init_tasks_notify();
|
|
5977
|
+
init_tmux_routing();
|
|
5978
|
+
MAX_RETRIES2 = 10;
|
|
5979
|
+
_wsClientSend = null;
|
|
5381
5980
|
}
|
|
5382
5981
|
});
|
|
5383
5982
|
|
|
5983
|
+
// src/gateway/gateway.ts
|
|
5984
|
+
init_state_bus();
|
|
5985
|
+
|
|
5384
5986
|
// src/gateway/router.ts
|
|
5385
5987
|
function matchesPlatform(msgPlatform, matchPlatform) {
|
|
5386
5988
|
if (!matchPlatform) return true;
|
|
@@ -5817,6 +6419,13 @@ var Gateway = class {
|
|
|
5817
6419
|
console.log(
|
|
5818
6420
|
`[gateway] ${msg.platform}/${msg.senderId} \u2192 ${route.employee} (${route.routeName})`
|
|
5819
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
|
+
});
|
|
5820
6429
|
const bot = this.botRegistry.get(route.employee);
|
|
5821
6430
|
if (!bot) {
|
|
5822
6431
|
console.error(`[gateway] No bot registered for target: ${route.employee}`);
|
|
@@ -6610,7 +7219,7 @@ var AnthropicProvider = class {
|
|
|
6610
7219
|
|
|
6611
7220
|
// src/gateway/providers/openai-compat.ts
|
|
6612
7221
|
import OpenAI from "openai";
|
|
6613
|
-
import { randomUUID as
|
|
7222
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6614
7223
|
var OpenAICompatProvider = class {
|
|
6615
7224
|
name;
|
|
6616
7225
|
client;
|
|
@@ -6723,7 +7332,7 @@ var OpenAICompatProvider = class {
|
|
|
6723
7332
|
}
|
|
6724
7333
|
content.push({
|
|
6725
7334
|
type: "tool_use",
|
|
6726
|
-
id: call.id ??
|
|
7335
|
+
id: call.id ?? randomUUID3(),
|
|
6727
7336
|
name: fn.name,
|
|
6728
7337
|
input
|
|
6729
7338
|
});
|
|
@@ -6745,7 +7354,7 @@ var OpenAICompatProvider = class {
|
|
|
6745
7354
|
};
|
|
6746
7355
|
|
|
6747
7356
|
// src/gateway/providers/ollama.ts
|
|
6748
|
-
import { randomUUID as
|
|
7357
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6749
7358
|
var OllamaProvider = class {
|
|
6750
7359
|
name;
|
|
6751
7360
|
host;
|
|
@@ -6814,7 +7423,7 @@ var OllamaProvider = class {
|
|
|
6814
7423
|
for (const call of data.message.tool_calls) {
|
|
6815
7424
|
content.push({
|
|
6816
7425
|
type: "tool_use",
|
|
6817
|
-
id:
|
|
7426
|
+
id: randomUUID4(),
|
|
6818
7427
|
name: call.function.name,
|
|
6819
7428
|
input: call.function.arguments
|
|
6820
7429
|
});
|
|
@@ -6836,7 +7445,7 @@ var OllamaProvider = class {
|
|
|
6836
7445
|
};
|
|
6837
7446
|
|
|
6838
7447
|
// src/gateway/adapters/whatsapp.ts
|
|
6839
|
-
import { randomUUID as
|
|
7448
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6840
7449
|
import { homedir } from "os";
|
|
6841
7450
|
import { join } from "path";
|
|
6842
7451
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -7018,7 +7627,7 @@ var WhatsAppAdapter = class {
|
|
|
7018
7627
|
const location = this.extractLocation(msg.message);
|
|
7019
7628
|
const dataCategory = location ? "location" : "message";
|
|
7020
7629
|
return {
|
|
7021
|
-
messageId: msg.key.id ??
|
|
7630
|
+
messageId: msg.key.id ?? randomUUID5(),
|
|
7022
7631
|
platform: "whatsapp",
|
|
7023
7632
|
senderId,
|
|
7024
7633
|
senderName: msg.pushName ?? void 0,
|
|
@@ -7063,7 +7672,7 @@ var WhatsAppAdapter = class {
|
|
|
7063
7672
|
}
|
|
7064
7673
|
const timestamp = receipt.readTimestamp ?? receipt.receiptTimestamp ?? Date.now() / 1e3;
|
|
7065
7674
|
return {
|
|
7066
|
-
messageId:
|
|
7675
|
+
messageId: randomUUID5(),
|
|
7067
7676
|
platform: "whatsapp",
|
|
7068
7677
|
senderId: remoteJid.replace("@s.whatsapp.net", "").replace("@g.us", ""),
|
|
7069
7678
|
channelId: remoteJid,
|
|
@@ -7086,7 +7695,7 @@ var WhatsAppAdapter = class {
|
|
|
7086
7695
|
const phone = id.replace("@s.whatsapp.net", "").replace("@g.us", "");
|
|
7087
7696
|
const name = contact.name ?? contact.notify ?? phone;
|
|
7088
7697
|
return {
|
|
7089
|
-
messageId:
|
|
7698
|
+
messageId: randomUUID5(),
|
|
7090
7699
|
platform: "whatsapp",
|
|
7091
7700
|
senderId: phone,
|
|
7092
7701
|
senderName: name,
|
|
@@ -7110,7 +7719,7 @@ var WhatsAppAdapter = class {
|
|
|
7110
7719
|
const participants = (group.participants ?? []).map((p) => p.id ?? p);
|
|
7111
7720
|
const admins = (group.participants ?? []).filter((p) => p.admin === "admin" || p.admin === "superadmin").map((p) => p.id ?? p);
|
|
7112
7721
|
return {
|
|
7113
|
-
messageId:
|
|
7722
|
+
messageId: randomUUID5(),
|
|
7114
7723
|
platform: "whatsapp",
|
|
7115
7724
|
senderId: groupId,
|
|
7116
7725
|
channelId: groupId,
|
|
@@ -7135,7 +7744,7 @@ var WhatsAppAdapter = class {
|
|
|
7135
7744
|
if (!reactionData) return null;
|
|
7136
7745
|
const remoteJid = key.remoteJid ?? "";
|
|
7137
7746
|
return {
|
|
7138
|
-
messageId:
|
|
7747
|
+
messageId: randomUUID5(),
|
|
7139
7748
|
platform: "whatsapp",
|
|
7140
7749
|
senderId: reactionData.key?.participant ?? reactionData.key?.remoteJid?.replace("@s.whatsapp.net", "") ?? "",
|
|
7141
7750
|
channelId: remoteJid,
|
|
@@ -7157,7 +7766,7 @@ var WhatsAppAdapter = class {
|
|
|
7157
7766
|
if (!chatId) return null;
|
|
7158
7767
|
const caller = call.from?.replace("@s.whatsapp.net", "") ?? "";
|
|
7159
7768
|
return {
|
|
7160
|
-
messageId:
|
|
7769
|
+
messageId: randomUUID5(),
|
|
7161
7770
|
platform: "whatsapp",
|
|
7162
7771
|
senderId: caller,
|
|
7163
7772
|
channelId: chatId,
|
|
@@ -7200,7 +7809,7 @@ var WhatsAppAdapter = class {
|
|
|
7200
7809
|
};
|
|
7201
7810
|
|
|
7202
7811
|
// src/gateway/adapters/signal.ts
|
|
7203
|
-
import { randomUUID as
|
|
7812
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7204
7813
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
7205
7814
|
var SignalAdapter = class {
|
|
7206
7815
|
platform = "signal";
|
|
@@ -7285,7 +7894,7 @@ var SignalAdapter = class {
|
|
|
7285
7894
|
}
|
|
7286
7895
|
}
|
|
7287
7896
|
async rpcRequest(method, params) {
|
|
7288
|
-
const id =
|
|
7897
|
+
const id = randomUUID6();
|
|
7289
7898
|
const res = await fetch(`${this.baseUrl}/api/v1/rpc`, {
|
|
7290
7899
|
method: "POST",
|
|
7291
7900
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7375,7 +7984,7 @@ ${val}` : val;
|
|
|
7375
7984
|
if (envelope.reactionMessage) {
|
|
7376
7985
|
const rm = envelope.reactionMessage;
|
|
7377
7986
|
const normalized2 = {
|
|
7378
|
-
messageId:
|
|
7987
|
+
messageId: randomUUID6(),
|
|
7379
7988
|
platform: "signal",
|
|
7380
7989
|
senderId,
|
|
7381
7990
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7403,7 +8012,7 @@ ${val}` : val;
|
|
|
7403
8012
|
const rcpt = envelope.receiptMessage;
|
|
7404
8013
|
for (const ts of rcpt.timestamps) {
|
|
7405
8014
|
const normalized2 = {
|
|
7406
|
-
messageId:
|
|
8015
|
+
messageId: randomUUID6(),
|
|
7407
8016
|
platform: "signal",
|
|
7408
8017
|
senderId,
|
|
7409
8018
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7433,7 +8042,7 @@ ${val}` : val;
|
|
|
7433
8042
|
const dm2 = em.dataMessage;
|
|
7434
8043
|
const isGroup2 = !!dm2.groupInfo?.groupId;
|
|
7435
8044
|
const normalized2 = {
|
|
7436
|
-
messageId: String(dm2.timestamp ??
|
|
8045
|
+
messageId: String(dm2.timestamp ?? randomUUID6()),
|
|
7437
8046
|
platform: "signal",
|
|
7438
8047
|
senderId,
|
|
7439
8048
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7459,7 +8068,7 @@ ${val}` : val;
|
|
|
7459
8068
|
const dm = envelope.dataMessage;
|
|
7460
8069
|
const isGroup = !!dm.groupInfo?.groupId;
|
|
7461
8070
|
const normalized = {
|
|
7462
|
-
messageId: String(dm.timestamp ??
|
|
8071
|
+
messageId: String(dm.timestamp ?? randomUUID6()),
|
|
7463
8072
|
platform: "signal",
|
|
7464
8073
|
senderId,
|
|
7465
8074
|
senderName: envelope.sourceName ?? void 0,
|
|
@@ -7500,7 +8109,7 @@ ${val}` : val;
|
|
|
7500
8109
|
if (!phone) continue;
|
|
7501
8110
|
const name = contact.name ?? contact.profileName ?? phone;
|
|
7502
8111
|
const normalized = {
|
|
7503
|
-
messageId:
|
|
8112
|
+
messageId: randomUUID6(),
|
|
7504
8113
|
platform: "signal",
|
|
7505
8114
|
senderId: phone,
|
|
7506
8115
|
senderName: name,
|
|
@@ -7529,7 +8138,7 @@ ${val}` : val;
|
|
|
7529
8138
|
if (!Array.isArray(groups)) return;
|
|
7530
8139
|
for (const group of groups) {
|
|
7531
8140
|
const normalized = {
|
|
7532
|
-
messageId:
|
|
8141
|
+
messageId: randomUUID6(),
|
|
7533
8142
|
platform: "signal",
|
|
7534
8143
|
senderId: `group:${group.id}`,
|
|
7535
8144
|
channelId: `group:${group.id}`,
|
|
@@ -7564,7 +8173,7 @@ ${val}` : val;
|
|
|
7564
8173
|
};
|
|
7565
8174
|
|
|
7566
8175
|
// src/gateway/adapters/webchat.ts
|
|
7567
|
-
import { randomUUID as
|
|
8176
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
7568
8177
|
import { createServer } from "http";
|
|
7569
8178
|
var WebChatAdapter = class {
|
|
7570
8179
|
platform = "webchat";
|
|
@@ -7660,7 +8269,7 @@ var WebChatAdapter = class {
|
|
|
7660
8269
|
res.end(JSON.stringify({ error: "No message text" }));
|
|
7661
8270
|
return;
|
|
7662
8271
|
}
|
|
7663
|
-
const requestId =
|
|
8272
|
+
const requestId = randomUUID7();
|
|
7664
8273
|
const sessionId = parsed.sessionId ?? this.extractSessionId(req);
|
|
7665
8274
|
const normalized = {
|
|
7666
8275
|
messageId: requestId,
|
|
@@ -7703,7 +8312,7 @@ var WebChatAdapter = class {
|
|
|
7703
8312
|
extractSessionId(req) {
|
|
7704
8313
|
const cookies = req.headers.cookie ?? "";
|
|
7705
8314
|
const match = cookies.match(/exe_session=([^;]+)/);
|
|
7706
|
-
return match?.[1] ?? `anon-${
|
|
8315
|
+
return match?.[1] ?? `anon-${randomUUID7().slice(0, 8)}`;
|
|
7707
8316
|
}
|
|
7708
8317
|
};
|
|
7709
8318
|
|
|
@@ -7985,7 +8594,7 @@ var DiscordAdapter = class {
|
|
|
7985
8594
|
};
|
|
7986
8595
|
|
|
7987
8596
|
// src/gateway/adapters/slack.ts
|
|
7988
|
-
import { randomUUID as
|
|
8597
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7989
8598
|
var SlackAdapter = class {
|
|
7990
8599
|
platform = "slack";
|
|
7991
8600
|
webClient = null;
|
|
@@ -8023,7 +8632,7 @@ var SlackAdapter = class {
|
|
|
8023
8632
|
if (event.subtype) return;
|
|
8024
8633
|
const isGroup = event.channel_type !== "im";
|
|
8025
8634
|
const normalized = {
|
|
8026
|
-
messageId: event.client_msg_id ?? event.ts ??
|
|
8635
|
+
messageId: event.client_msg_id ?? event.ts ?? randomUUID8(),
|
|
8027
8636
|
platform: "slack",
|
|
8028
8637
|
senderId: event.user ?? "",
|
|
8029
8638
|
channelId: event.channel ?? "",
|
|
@@ -8085,7 +8694,7 @@ var SlackAdapter = class {
|
|
|
8085
8694
|
if (!event.text) return;
|
|
8086
8695
|
const isGroup = event.channel_type !== "im";
|
|
8087
8696
|
const normalized = {
|
|
8088
|
-
messageId: event.ts ??
|
|
8697
|
+
messageId: event.ts ?? randomUUID8(),
|
|
8089
8698
|
platform: "slack",
|
|
8090
8699
|
senderId: event.user ?? "",
|
|
8091
8700
|
senderName: event.user_profile?.display_name ?? event.user_profile?.real_name ?? void 0,
|
|
@@ -8470,7 +9079,7 @@ var FailoverExhaustedError = class extends Error {
|
|
|
8470
9079
|
};
|
|
8471
9080
|
|
|
8472
9081
|
// src/gateway/session-store.ts
|
|
8473
|
-
import { randomUUID as
|
|
9082
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
8474
9083
|
var DEFAULT_CONFIG3 = {
|
|
8475
9084
|
idleTimeoutMs: 30 * 6e4,
|
|
8476
9085
|
maxMessages: 100
|
|
@@ -8497,7 +9106,7 @@ var SessionStore = class {
|
|
|
8497
9106
|
existing.status = "closed";
|
|
8498
9107
|
}
|
|
8499
9108
|
const session = {
|
|
8500
|
-
sessionId:
|
|
9109
|
+
sessionId: randomUUID9(),
|
|
8501
9110
|
customerId,
|
|
8502
9111
|
botId,
|
|
8503
9112
|
platform,
|
|
@@ -8894,7 +9503,7 @@ function formatAlert(alert) {
|
|
|
8894
9503
|
}
|
|
8895
9504
|
|
|
8896
9505
|
// src/gateway/customer-store.ts
|
|
8897
|
-
import { randomUUID as
|
|
9506
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
8898
9507
|
var CustomerStore = class {
|
|
8899
9508
|
customers = /* @__PURE__ */ new Map();
|
|
8900
9509
|
identities = /* @__PURE__ */ new Map();
|
|
@@ -8913,7 +9522,7 @@ var CustomerStore = class {
|
|
|
8913
9522
|
return customer2;
|
|
8914
9523
|
}
|
|
8915
9524
|
const customer = {
|
|
8916
|
-
id:
|
|
9525
|
+
id: randomUUID10(),
|
|
8917
9526
|
firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8918
9527
|
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8919
9528
|
interactionCount: 1
|
|
@@ -8973,7 +9582,7 @@ async function ensureCRMContact(info) {
|
|
|
8973
9582
|
|
|
8974
9583
|
// src/automation/trigger-engine.ts
|
|
8975
9584
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
|
|
8976
|
-
import { randomUUID as
|
|
9585
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
8977
9586
|
import path18 from "path";
|
|
8978
9587
|
import os8 from "os";
|
|
8979
9588
|
var TRIGGERS_PATH = path18.join(os8.homedir(), ".exe-os", "triggers.json");
|