@askexenow/exe-os 0.8.83 → 0.8.85
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 +746 -595
- package/dist/bin/backfill-responses.js +745 -594
- package/dist/bin/backfill-vectors.js +312 -226
- package/dist/bin/cleanup-stale-review-tasks.js +97 -2
- package/dist/bin/cli.js +14350 -12518
- package/dist/bin/exe-agent.js +97 -88
- package/dist/bin/exe-assign.js +1003 -854
- package/dist/bin/exe-boot.js +1257 -320
- package/dist/bin/exe-call.js +10 -0
- package/dist/bin/exe-cloud.js +29 -6
- package/dist/bin/exe-dispatch.js +210 -34
- package/dist/bin/exe-doctor.js +403 -6
- package/dist/bin/exe-export-behaviors.js +175 -72
- package/dist/bin/exe-forget.js +97 -2
- package/dist/bin/exe-gateway.js +550 -171
- package/dist/bin/exe-healthcheck.js +1 -0
- package/dist/bin/exe-heartbeat.js +100 -5
- package/dist/bin/exe-kill.js +175 -72
- package/dist/bin/exe-launch-agent.js +189 -76
- package/dist/bin/exe-link.js +902 -80
- package/dist/bin/exe-new-employee.js +38 -8
- package/dist/bin/exe-pending-messages.js +96 -2
- package/dist/bin/exe-pending-notifications.js +97 -2
- package/dist/bin/exe-pending-reviews.js +98 -3
- package/dist/bin/exe-rename.js +564 -23
- package/dist/bin/exe-review.js +231 -73
- package/dist/bin/exe-search.js +989 -226
- package/dist/bin/exe-session-cleanup.js +4806 -1665
- package/dist/bin/exe-settings.js +20 -5
- package/dist/bin/exe-status.js +97 -2
- package/dist/bin/exe-team.js +97 -2
- package/dist/bin/git-sweep.js +899 -207
- package/dist/bin/graph-backfill.js +175 -72
- package/dist/bin/graph-export.js +175 -72
- package/dist/bin/install.js +38 -7
- package/dist/bin/list-providers.js +1 -0
- package/dist/bin/scan-tasks.js +904 -211
- package/dist/bin/setup.js +867 -268
- package/dist/bin/shard-migrate.js +175 -72
- package/dist/bin/update.js +1 -0
- package/dist/bin/wiki-sync.js +175 -72
- package/dist/gateway/index.js +548 -166
- package/dist/hooks/bug-report-worker.js +208 -23
- package/dist/hooks/commit-complete.js +897 -205
- package/dist/hooks/error-recall.js +988 -226
- package/dist/hooks/ingest-worker.js +1638 -1194
- package/dist/hooks/ingest.js +3 -0
- package/dist/hooks/instructions-loaded.js +707 -97
- package/dist/hooks/notification.js +699 -89
- package/dist/hooks/post-compact.js +714 -104
- package/dist/hooks/pre-compact.js +897 -205
- package/dist/hooks/pre-tool-use.js +742 -123
- package/dist/hooks/prompt-ingest-worker.js +242 -101
- package/dist/hooks/prompt-submit.js +995 -233
- package/dist/hooks/response-ingest-worker.js +242 -101
- package/dist/hooks/session-end.js +3941 -400
- package/dist/hooks/session-start.js +1001 -226
- package/dist/hooks/stop.js +725 -115
- package/dist/hooks/subagent-stop.js +714 -104
- package/dist/hooks/summary-worker.js +1964 -1330
- package/dist/index.js +1651 -1053
- package/dist/lib/cloud-sync.js +907 -86
- package/dist/lib/consolidation.js +2 -1
- package/dist/lib/database.js +642 -87
- package/dist/lib/db-daemon-client.js +503 -0
- package/dist/lib/device-registry.js +547 -7
- package/dist/lib/embedder.js +14 -28
- package/dist/lib/employee-templates.js +84 -74
- package/dist/lib/employees.js +9 -0
- package/dist/lib/exe-daemon-client.js +16 -29
- package/dist/lib/exe-daemon.js +1955 -922
- package/dist/lib/hybrid-search.js +988 -226
- package/dist/lib/identity.js +87 -67
- package/dist/lib/keychain.js +9 -1
- package/dist/lib/messaging.js +8 -1
- package/dist/lib/reminders.js +91 -74
- package/dist/lib/schedules.js +96 -2
- package/dist/lib/skill-learning.js +103 -85
- package/dist/lib/store.js +234 -73
- package/dist/lib/tasks.js +111 -22
- package/dist/lib/tmux-routing.js +120 -31
- package/dist/lib/token-spend.js +273 -0
- package/dist/lib/ws-client.js +11 -0
- package/dist/mcp/server.js +5222 -475
- package/dist/mcp/tools/complete-reminder.js +94 -77
- package/dist/mcp/tools/create-reminder.js +94 -77
- package/dist/mcp/tools/create-task.js +120 -22
- package/dist/mcp/tools/deactivate-behavior.js +95 -77
- package/dist/mcp/tools/list-reminders.js +94 -77
- package/dist/mcp/tools/list-tasks.js +31 -1
- package/dist/mcp/tools/send-message.js +8 -1
- package/dist/mcp/tools/update-task.js +39 -10
- package/dist/runtime/index.js +911 -219
- package/dist/tui/App.js +997 -295
- package/package.json +6 -1
|
@@ -273,7 +273,13 @@ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
|
273
273
|
function getEmployee(employees, name) {
|
|
274
274
|
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
275
275
|
}
|
|
276
|
-
|
|
276
|
+
function isMultiInstance(agentName, employees) {
|
|
277
|
+
const roster = employees ?? loadEmployeesSync();
|
|
278
|
+
const emp = getEmployee(roster, agentName);
|
|
279
|
+
if (!emp) return false;
|
|
280
|
+
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
281
|
+
}
|
|
282
|
+
var EMPLOYEES_PATH, DEFAULT_COORDINATOR_TEMPLATE_NAME, COORDINATOR_ROLE, MULTI_INSTANCE_ROLES;
|
|
277
283
|
var init_employees = __esm({
|
|
278
284
|
"src/lib/employees.ts"() {
|
|
279
285
|
"use strict";
|
|
@@ -281,12 +287,36 @@ var init_employees = __esm({
|
|
|
281
287
|
EMPLOYEES_PATH = path2.join(EXE_AI_DIR, "exe-employees.json");
|
|
282
288
|
DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
|
|
283
289
|
COORDINATOR_ROLE = "COO";
|
|
290
|
+
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
284
291
|
}
|
|
285
292
|
});
|
|
286
293
|
|
|
287
294
|
// src/lib/session-registry.ts
|
|
295
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
288
296
|
import path4 from "path";
|
|
289
297
|
import os3 from "os";
|
|
298
|
+
function registerSession(entry) {
|
|
299
|
+
const dir = path4.dirname(REGISTRY_PATH);
|
|
300
|
+
if (!existsSync3(dir)) {
|
|
301
|
+
mkdirSync2(dir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
const sessions = listSessions();
|
|
304
|
+
const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
|
|
305
|
+
if (idx >= 0) {
|
|
306
|
+
sessions[idx] = entry;
|
|
307
|
+
} else {
|
|
308
|
+
sessions.push(entry);
|
|
309
|
+
}
|
|
310
|
+
writeFileSync3(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
|
|
311
|
+
}
|
|
312
|
+
function listSessions() {
|
|
313
|
+
try {
|
|
314
|
+
const raw = readFileSync4(REGISTRY_PATH, "utf8");
|
|
315
|
+
return JSON.parse(raw);
|
|
316
|
+
} catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
290
320
|
var REGISTRY_PATH;
|
|
291
321
|
var init_session_registry = __esm({
|
|
292
322
|
"src/lib/session-registry.ts"() {
|
|
@@ -404,13 +434,40 @@ var init_transport = __esm({
|
|
|
404
434
|
|
|
405
435
|
// src/lib/cc-agent-support.ts
|
|
406
436
|
import { execSync as execSync4 } from "child_process";
|
|
437
|
+
function _resetCcAgentSupportCache() {
|
|
438
|
+
_cachedSupport = null;
|
|
439
|
+
}
|
|
440
|
+
function claudeSupportsAgentFlag() {
|
|
441
|
+
if (_cachedSupport !== null) return _cachedSupport;
|
|
442
|
+
try {
|
|
443
|
+
const helpOutput = execSync4("claude --help 2>&1", {
|
|
444
|
+
encoding: "utf-8",
|
|
445
|
+
timeout: 5e3
|
|
446
|
+
});
|
|
447
|
+
_cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
|
|
448
|
+
} catch {
|
|
449
|
+
_cachedSupport = false;
|
|
450
|
+
}
|
|
451
|
+
return _cachedSupport;
|
|
452
|
+
}
|
|
453
|
+
var _cachedSupport;
|
|
407
454
|
var init_cc_agent_support = __esm({
|
|
408
455
|
"src/lib/cc-agent-support.ts"() {
|
|
409
456
|
"use strict";
|
|
457
|
+
_cachedSupport = null;
|
|
410
458
|
}
|
|
411
459
|
});
|
|
412
460
|
|
|
413
461
|
// src/lib/mcp-prefix.ts
|
|
462
|
+
function expandDualPrefixTools(shortNames) {
|
|
463
|
+
const out = [];
|
|
464
|
+
for (const name of shortNames) {
|
|
465
|
+
for (const prefix of MCP_TOOL_PREFIXES) {
|
|
466
|
+
out.push(prefix + name);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
414
471
|
var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
|
|
415
472
|
var init_mcp_prefix = __esm({
|
|
416
473
|
"src/lib/mcp-prefix.ts"() {
|
|
@@ -425,16 +482,68 @@ var init_mcp_prefix = __esm({
|
|
|
425
482
|
});
|
|
426
483
|
|
|
427
484
|
// src/lib/provider-table.ts
|
|
485
|
+
function detectActiveProvider(env = process.env) {
|
|
486
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
487
|
+
if (!baseUrl) return DEFAULT_PROVIDER;
|
|
488
|
+
for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
|
|
489
|
+
if (cfg.baseUrl === baseUrl) return name;
|
|
490
|
+
}
|
|
491
|
+
return DEFAULT_PROVIDER;
|
|
492
|
+
}
|
|
493
|
+
var PROVIDER_TABLE, DEFAULT_PROVIDER;
|
|
428
494
|
var init_provider_table = __esm({
|
|
429
495
|
"src/lib/provider-table.ts"() {
|
|
430
496
|
"use strict";
|
|
497
|
+
PROVIDER_TABLE = {
|
|
498
|
+
opencode: {
|
|
499
|
+
baseUrl: "https://opencode.ai/zen/go",
|
|
500
|
+
apiKeyEnv: "OPENCODE_API_KEY",
|
|
501
|
+
defaultModel: "minimax-m2.7"
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
DEFAULT_PROVIDER = "default";
|
|
431
505
|
}
|
|
432
506
|
});
|
|
433
507
|
|
|
434
508
|
// src/lib/intercom-queue.ts
|
|
435
|
-
import { readFileSync as
|
|
509
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
436
510
|
import path5 from "path";
|
|
437
511
|
import os4 from "os";
|
|
512
|
+
function ensureDir() {
|
|
513
|
+
const dir = path5.dirname(QUEUE_PATH);
|
|
514
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
515
|
+
}
|
|
516
|
+
function readQueue() {
|
|
517
|
+
try {
|
|
518
|
+
if (!existsSync4(QUEUE_PATH)) return [];
|
|
519
|
+
return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
|
|
520
|
+
} catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function writeQueue(queue) {
|
|
525
|
+
ensureDir();
|
|
526
|
+
const tmp = `${QUEUE_PATH}.tmp`;
|
|
527
|
+
writeFileSync4(tmp, JSON.stringify(queue, null, 2));
|
|
528
|
+
renameSync3(tmp, QUEUE_PATH);
|
|
529
|
+
}
|
|
530
|
+
function queueIntercom(targetSession, reason) {
|
|
531
|
+
const queue = readQueue();
|
|
532
|
+
const existing = queue.find((q) => q.targetSession === targetSession);
|
|
533
|
+
if (existing) {
|
|
534
|
+
existing.attempts++;
|
|
535
|
+
existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
536
|
+
existing.reason = reason;
|
|
537
|
+
} else {
|
|
538
|
+
queue.push({
|
|
539
|
+
targetSession,
|
|
540
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
541
|
+
attempts: 0,
|
|
542
|
+
reason
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
writeQueue(queue);
|
|
546
|
+
}
|
|
438
547
|
var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
439
548
|
var init_intercom_queue = __esm({
|
|
440
549
|
"src/lib/intercom-queue.ts"() {
|
|
@@ -500,6 +609,443 @@ var init_db_retry = __esm({
|
|
|
500
609
|
}
|
|
501
610
|
});
|
|
502
611
|
|
|
612
|
+
// src/lib/exe-daemon-client.ts
|
|
613
|
+
import net from "net";
|
|
614
|
+
import { spawn } from "child_process";
|
|
615
|
+
import { randomUUID } from "crypto";
|
|
616
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync3, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
|
|
617
|
+
import path6 from "path";
|
|
618
|
+
import { fileURLToPath } from "url";
|
|
619
|
+
function handleData(chunk) {
|
|
620
|
+
_buffer += chunk.toString();
|
|
621
|
+
if (_buffer.length > MAX_BUFFER) {
|
|
622
|
+
_buffer = "";
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
let newlineIdx;
|
|
626
|
+
while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
|
|
627
|
+
const line = _buffer.slice(0, newlineIdx).trim();
|
|
628
|
+
_buffer = _buffer.slice(newlineIdx + 1);
|
|
629
|
+
if (!line) continue;
|
|
630
|
+
try {
|
|
631
|
+
const response = JSON.parse(line);
|
|
632
|
+
const id = response.id;
|
|
633
|
+
if (!id) continue;
|
|
634
|
+
const entry = _pending.get(id);
|
|
635
|
+
if (entry) {
|
|
636
|
+
clearTimeout(entry.timer);
|
|
637
|
+
_pending.delete(id);
|
|
638
|
+
entry.resolve(response);
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function cleanupStaleFiles() {
|
|
645
|
+
if (existsSync5(PID_PATH)) {
|
|
646
|
+
try {
|
|
647
|
+
const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
|
|
648
|
+
if (pid > 0) {
|
|
649
|
+
try {
|
|
650
|
+
process.kill(pid, 0);
|
|
651
|
+
return;
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
unlinkSync3(PID_PATH);
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
unlinkSync3(SOCKET_PATH);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function findPackageRoot() {
|
|
668
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
669
|
+
const { root } = path6.parse(dir);
|
|
670
|
+
while (dir !== root) {
|
|
671
|
+
if (existsSync5(path6.join(dir, "package.json"))) return dir;
|
|
672
|
+
dir = path6.dirname(dir);
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
function spawnDaemon() {
|
|
677
|
+
const pkgRoot = findPackageRoot();
|
|
678
|
+
if (!pkgRoot) {
|
|
679
|
+
process.stderr.write("[exed-client] WARN: cannot find package root\n");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
|
|
683
|
+
if (!existsSync5(daemonPath)) {
|
|
684
|
+
process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
|
|
685
|
+
`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const resolvedPath = daemonPath;
|
|
689
|
+
process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
|
|
690
|
+
`);
|
|
691
|
+
const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
|
|
692
|
+
let stderrFd = "ignore";
|
|
693
|
+
try {
|
|
694
|
+
stderrFd = openSync(logPath, "a");
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
const child = spawn(process.execPath, [resolvedPath], {
|
|
698
|
+
detached: true,
|
|
699
|
+
stdio: ["ignore", "ignore", stderrFd],
|
|
700
|
+
env: {
|
|
701
|
+
...process.env,
|
|
702
|
+
TMUX: void 0,
|
|
703
|
+
// Daemon is global — must not inherit session scope
|
|
704
|
+
TMUX_PANE: void 0,
|
|
705
|
+
// Prevents resolveExeSession() from scoping to one session
|
|
706
|
+
EXE_DAEMON_SOCK: SOCKET_PATH,
|
|
707
|
+
EXE_DAEMON_PID: PID_PATH
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
child.unref();
|
|
711
|
+
if (typeof stderrFd === "number") {
|
|
712
|
+
try {
|
|
713
|
+
closeSync(stderrFd);
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function acquireSpawnLock() {
|
|
719
|
+
try {
|
|
720
|
+
const fd = openSync(SPAWN_LOCK_PATH, "wx");
|
|
721
|
+
closeSync(fd);
|
|
722
|
+
return true;
|
|
723
|
+
} catch {
|
|
724
|
+
try {
|
|
725
|
+
const stat = statSync(SPAWN_LOCK_PATH);
|
|
726
|
+
if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
|
|
727
|
+
try {
|
|
728
|
+
unlinkSync3(SPAWN_LOCK_PATH);
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const fd = openSync(SPAWN_LOCK_PATH, "wx");
|
|
733
|
+
closeSync(fd);
|
|
734
|
+
return true;
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function releaseSpawnLock() {
|
|
744
|
+
try {
|
|
745
|
+
unlinkSync3(SPAWN_LOCK_PATH);
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function connectToSocket() {
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
if (_socket && _connected) {
|
|
752
|
+
resolve(true);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const socket = net.createConnection({ path: SOCKET_PATH });
|
|
756
|
+
const connectTimeout = setTimeout(() => {
|
|
757
|
+
socket.destroy();
|
|
758
|
+
resolve(false);
|
|
759
|
+
}, 2e3);
|
|
760
|
+
socket.on("connect", () => {
|
|
761
|
+
clearTimeout(connectTimeout);
|
|
762
|
+
_socket = socket;
|
|
763
|
+
_connected = true;
|
|
764
|
+
_buffer = "";
|
|
765
|
+
socket.on("data", handleData);
|
|
766
|
+
socket.on("close", () => {
|
|
767
|
+
_connected = false;
|
|
768
|
+
_socket = null;
|
|
769
|
+
for (const [id, entry] of _pending) {
|
|
770
|
+
clearTimeout(entry.timer);
|
|
771
|
+
_pending.delete(id);
|
|
772
|
+
entry.resolve({ error: "Connection closed" });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
socket.on("error", () => {
|
|
776
|
+
_connected = false;
|
|
777
|
+
_socket = null;
|
|
778
|
+
});
|
|
779
|
+
resolve(true);
|
|
780
|
+
});
|
|
781
|
+
socket.on("error", () => {
|
|
782
|
+
clearTimeout(connectTimeout);
|
|
783
|
+
resolve(false);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async function connectEmbedDaemon() {
|
|
788
|
+
if (_socket && _connected) return true;
|
|
789
|
+
if (await connectToSocket()) return true;
|
|
790
|
+
if (acquireSpawnLock()) {
|
|
791
|
+
try {
|
|
792
|
+
cleanupStaleFiles();
|
|
793
|
+
spawnDaemon();
|
|
794
|
+
} finally {
|
|
795
|
+
releaseSpawnLock();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const start = Date.now();
|
|
799
|
+
let delay2 = 100;
|
|
800
|
+
while (Date.now() - start < CONNECT_TIMEOUT_MS) {
|
|
801
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
802
|
+
if (await connectToSocket()) return true;
|
|
803
|
+
delay2 = Math.min(delay2 * 2, 3e3);
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
808
|
+
return new Promise((resolve) => {
|
|
809
|
+
if (!_socket || !_connected) {
|
|
810
|
+
resolve({ error: "Not connected" });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const id = randomUUID();
|
|
814
|
+
const timer = setTimeout(() => {
|
|
815
|
+
_pending.delete(id);
|
|
816
|
+
resolve({ error: "Request timeout" });
|
|
817
|
+
}, timeoutMs);
|
|
818
|
+
_pending.set(id, { resolve, timer });
|
|
819
|
+
try {
|
|
820
|
+
_socket.write(JSON.stringify({ id, ...payload }) + "\n");
|
|
821
|
+
} catch {
|
|
822
|
+
clearTimeout(timer);
|
|
823
|
+
_pending.delete(id);
|
|
824
|
+
resolve({ error: "Write failed" });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function isClientConnected() {
|
|
829
|
+
return _connected;
|
|
830
|
+
}
|
|
831
|
+
var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
|
|
832
|
+
var init_exe_daemon_client = __esm({
|
|
833
|
+
"src/lib/exe-daemon-client.ts"() {
|
|
834
|
+
"use strict";
|
|
835
|
+
init_config();
|
|
836
|
+
SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
|
|
837
|
+
PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
|
|
838
|
+
SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
|
|
839
|
+
SPAWN_LOCK_STALE_MS = 3e4;
|
|
840
|
+
CONNECT_TIMEOUT_MS = 15e3;
|
|
841
|
+
REQUEST_TIMEOUT_MS = 3e4;
|
|
842
|
+
_socket = null;
|
|
843
|
+
_connected = false;
|
|
844
|
+
_buffer = "";
|
|
845
|
+
_pending = /* @__PURE__ */ new Map();
|
|
846
|
+
MAX_BUFFER = 1e7;
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// src/lib/daemon-protocol.ts
|
|
851
|
+
function serializeValue(v) {
|
|
852
|
+
if (v === null || v === void 0) return null;
|
|
853
|
+
if (typeof v === "bigint") return Number(v);
|
|
854
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
855
|
+
if (v instanceof Uint8Array) {
|
|
856
|
+
return { __blob: Buffer.from(v).toString("base64") };
|
|
857
|
+
}
|
|
858
|
+
if (ArrayBuffer.isView(v)) {
|
|
859
|
+
return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
|
|
860
|
+
}
|
|
861
|
+
if (v instanceof ArrayBuffer) {
|
|
862
|
+
return { __blob: Buffer.from(v).toString("base64") };
|
|
863
|
+
}
|
|
864
|
+
if (typeof v === "string" || typeof v === "number") return v;
|
|
865
|
+
return String(v);
|
|
866
|
+
}
|
|
867
|
+
function deserializeValue(v) {
|
|
868
|
+
if (v === null) return null;
|
|
869
|
+
if (typeof v === "object" && v !== null && "__blob" in v) {
|
|
870
|
+
const buf = Buffer.from(v.__blob, "base64");
|
|
871
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
872
|
+
}
|
|
873
|
+
return v;
|
|
874
|
+
}
|
|
875
|
+
function deserializeResultSet(srs) {
|
|
876
|
+
const rows = srs.rows.map((obj) => {
|
|
877
|
+
const values = srs.columns.map(
|
|
878
|
+
(col) => deserializeValue(obj[col] ?? null)
|
|
879
|
+
);
|
|
880
|
+
const row = values;
|
|
881
|
+
for (let i = 0; i < srs.columns.length; i++) {
|
|
882
|
+
const col = srs.columns[i];
|
|
883
|
+
if (col !== void 0) {
|
|
884
|
+
row[col] = values[i] ?? null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
Object.defineProperty(row, "length", {
|
|
888
|
+
value: values.length,
|
|
889
|
+
enumerable: false
|
|
890
|
+
});
|
|
891
|
+
return row;
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
columns: srs.columns,
|
|
895
|
+
columnTypes: srs.columnTypes ?? [],
|
|
896
|
+
rows,
|
|
897
|
+
rowsAffected: srs.rowsAffected,
|
|
898
|
+
lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
|
|
899
|
+
toJSON: () => ({
|
|
900
|
+
columns: srs.columns,
|
|
901
|
+
columnTypes: srs.columnTypes ?? [],
|
|
902
|
+
rows: srs.rows,
|
|
903
|
+
rowsAffected: srs.rowsAffected,
|
|
904
|
+
lastInsertRowid: srs.lastInsertRowid
|
|
905
|
+
})
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
var init_daemon_protocol = __esm({
|
|
909
|
+
"src/lib/daemon-protocol.ts"() {
|
|
910
|
+
"use strict";
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// src/lib/db-daemon-client.ts
|
|
915
|
+
var db_daemon_client_exports = {};
|
|
916
|
+
__export(db_daemon_client_exports, {
|
|
917
|
+
createDaemonDbClient: () => createDaemonDbClient,
|
|
918
|
+
initDaemonDbClient: () => initDaemonDbClient
|
|
919
|
+
});
|
|
920
|
+
function normalizeStatement(stmt) {
|
|
921
|
+
if (typeof stmt === "string") {
|
|
922
|
+
return { sql: stmt, args: [] };
|
|
923
|
+
}
|
|
924
|
+
const sql = stmt.sql;
|
|
925
|
+
let args = [];
|
|
926
|
+
if (Array.isArray(stmt.args)) {
|
|
927
|
+
args = stmt.args.map((v) => serializeValue(v));
|
|
928
|
+
} else if (stmt.args && typeof stmt.args === "object") {
|
|
929
|
+
const named = {};
|
|
930
|
+
for (const [key, val] of Object.entries(stmt.args)) {
|
|
931
|
+
named[key] = serializeValue(val);
|
|
932
|
+
}
|
|
933
|
+
return { sql, args: named };
|
|
934
|
+
}
|
|
935
|
+
return { sql, args };
|
|
936
|
+
}
|
|
937
|
+
function createDaemonDbClient(fallbackClient) {
|
|
938
|
+
let _useDaemon = false;
|
|
939
|
+
const client = {
|
|
940
|
+
async execute(stmt) {
|
|
941
|
+
if (!_useDaemon || !isClientConnected()) {
|
|
942
|
+
return fallbackClient.execute(stmt);
|
|
943
|
+
}
|
|
944
|
+
const { sql, args } = normalizeStatement(stmt);
|
|
945
|
+
const response = await sendDaemonRequest({
|
|
946
|
+
type: "db-execute",
|
|
947
|
+
sql,
|
|
948
|
+
args
|
|
949
|
+
});
|
|
950
|
+
if (response.error) {
|
|
951
|
+
const errMsg = String(response.error);
|
|
952
|
+
if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
|
|
953
|
+
process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
|
|
954
|
+
`);
|
|
955
|
+
return fallbackClient.execute(stmt);
|
|
956
|
+
}
|
|
957
|
+
throw new Error(errMsg);
|
|
958
|
+
}
|
|
959
|
+
if (response.db) {
|
|
960
|
+
return deserializeResultSet(response.db);
|
|
961
|
+
}
|
|
962
|
+
process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
|
|
963
|
+
return fallbackClient.execute(stmt);
|
|
964
|
+
},
|
|
965
|
+
async batch(stmts, mode) {
|
|
966
|
+
if (!_useDaemon || !isClientConnected()) {
|
|
967
|
+
return fallbackClient.batch(stmts, mode);
|
|
968
|
+
}
|
|
969
|
+
const statements = stmts.map(normalizeStatement);
|
|
970
|
+
const response = await sendDaemonRequest({
|
|
971
|
+
type: "db-batch",
|
|
972
|
+
statements,
|
|
973
|
+
mode: mode ?? "deferred"
|
|
974
|
+
});
|
|
975
|
+
if (response.error) {
|
|
976
|
+
const errMsg = String(response.error);
|
|
977
|
+
if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
|
|
978
|
+
process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
|
|
979
|
+
`);
|
|
980
|
+
return fallbackClient.batch(stmts, mode);
|
|
981
|
+
}
|
|
982
|
+
throw new Error(errMsg);
|
|
983
|
+
}
|
|
984
|
+
const batchResults = response["db-batch"];
|
|
985
|
+
if (batchResults) {
|
|
986
|
+
return batchResults.map(deserializeResultSet);
|
|
987
|
+
}
|
|
988
|
+
process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
|
|
989
|
+
return fallbackClient.batch(stmts, mode);
|
|
990
|
+
},
|
|
991
|
+
// Transaction support — delegate to fallback (transactions need direct connection)
|
|
992
|
+
async transaction(mode) {
|
|
993
|
+
return fallbackClient.transaction(mode);
|
|
994
|
+
},
|
|
995
|
+
// executeMultiple — delegate to fallback (used only for schema migrations)
|
|
996
|
+
async executeMultiple(sql) {
|
|
997
|
+
return fallbackClient.executeMultiple(sql);
|
|
998
|
+
},
|
|
999
|
+
// migrate — delegate to fallback
|
|
1000
|
+
async migrate(stmts) {
|
|
1001
|
+
return fallbackClient.migrate(stmts);
|
|
1002
|
+
},
|
|
1003
|
+
// Sync mode — delegate to fallback
|
|
1004
|
+
sync() {
|
|
1005
|
+
return fallbackClient.sync();
|
|
1006
|
+
},
|
|
1007
|
+
close() {
|
|
1008
|
+
_useDaemon = false;
|
|
1009
|
+
},
|
|
1010
|
+
get closed() {
|
|
1011
|
+
return fallbackClient.closed;
|
|
1012
|
+
},
|
|
1013
|
+
get protocol() {
|
|
1014
|
+
return fallbackClient.protocol;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
...client,
|
|
1019
|
+
/** Enable daemon routing (call after confirming daemon is connected) */
|
|
1020
|
+
_enableDaemon() {
|
|
1021
|
+
_useDaemon = true;
|
|
1022
|
+
},
|
|
1023
|
+
/** Check if daemon routing is active */
|
|
1024
|
+
_isDaemonActive() {
|
|
1025
|
+
return _useDaemon && isClientConnected();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async function initDaemonDbClient(fallbackClient) {
|
|
1030
|
+
if (process.env.EXE_IS_DAEMON === "1") return null;
|
|
1031
|
+
const connected = await connectEmbedDaemon();
|
|
1032
|
+
if (!connected) {
|
|
1033
|
+
process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
const client = createDaemonDbClient(fallbackClient);
|
|
1037
|
+
client._enableDaemon();
|
|
1038
|
+
process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
|
|
1039
|
+
return client;
|
|
1040
|
+
}
|
|
1041
|
+
var init_db_daemon_client = __esm({
|
|
1042
|
+
"src/lib/db-daemon-client.ts"() {
|
|
1043
|
+
"use strict";
|
|
1044
|
+
init_exe_daemon_client();
|
|
1045
|
+
init_daemon_protocol();
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
503
1049
|
// src/lib/database.ts
|
|
504
1050
|
var database_exports = {};
|
|
505
1051
|
__export(database_exports, {
|
|
@@ -508,6 +1054,7 @@ __export(database_exports, {
|
|
|
508
1054
|
ensureSchema: () => ensureSchema,
|
|
509
1055
|
getClient: () => getClient,
|
|
510
1056
|
getRawClient: () => getRawClient,
|
|
1057
|
+
initDaemonClient: () => initDaemonClient,
|
|
511
1058
|
initDatabase: () => initDatabase,
|
|
512
1059
|
initTurso: () => initTurso,
|
|
513
1060
|
isInitialized: () => isInitialized
|
|
@@ -535,8 +1082,27 @@ function getClient() {
|
|
|
535
1082
|
if (!_resilientClient) {
|
|
536
1083
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
537
1084
|
}
|
|
1085
|
+
if (process.env.EXE_IS_DAEMON === "1") {
|
|
1086
|
+
return _resilientClient;
|
|
1087
|
+
}
|
|
1088
|
+
if (_daemonClient && _daemonClient._isDaemonActive()) {
|
|
1089
|
+
return _daemonClient;
|
|
1090
|
+
}
|
|
538
1091
|
return _resilientClient;
|
|
539
1092
|
}
|
|
1093
|
+
async function initDaemonClient() {
|
|
1094
|
+
if (process.env.EXE_IS_DAEMON === "1") return;
|
|
1095
|
+
if (!_resilientClient) return;
|
|
1096
|
+
try {
|
|
1097
|
+
const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
|
|
1098
|
+
_daemonClient = await initDaemonDbClient2(_resilientClient);
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
process.stderr.write(
|
|
1101
|
+
`[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
|
|
1102
|
+
`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
540
1106
|
function getRawClient() {
|
|
541
1107
|
if (!_client) {
|
|
542
1108
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
@@ -1023,6 +1589,12 @@ async function ensureSchema() {
|
|
|
1023
1589
|
} catch {
|
|
1024
1590
|
}
|
|
1025
1591
|
}
|
|
1592
|
+
try {
|
|
1593
|
+
await client.execute(
|
|
1594
|
+
`CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
|
|
1595
|
+
);
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1026
1598
|
await client.executeMultiple(`
|
|
1027
1599
|
CREATE TABLE IF NOT EXISTS entities (
|
|
1028
1600
|
id TEXT PRIMARY KEY,
|
|
@@ -1075,7 +1647,30 @@ async function ensureSchema() {
|
|
|
1075
1647
|
entity_id TEXT NOT NULL,
|
|
1076
1648
|
PRIMARY KEY (hyperedge_id, entity_id)
|
|
1077
1649
|
);
|
|
1650
|
+
|
|
1651
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
1652
|
+
name,
|
|
1653
|
+
content=entities,
|
|
1654
|
+
content_rowid=rowid
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
|
|
1658
|
+
INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
|
|
1659
|
+
END;
|
|
1660
|
+
|
|
1661
|
+
CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
|
|
1662
|
+
INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
|
|
1663
|
+
END;
|
|
1664
|
+
|
|
1665
|
+
CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
|
|
1666
|
+
INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
|
|
1667
|
+
INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
|
|
1668
|
+
END;
|
|
1078
1669
|
`);
|
|
1670
|
+
try {
|
|
1671
|
+
await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
|
|
1672
|
+
} catch {
|
|
1673
|
+
}
|
|
1079
1674
|
await client.executeMultiple(`
|
|
1080
1675
|
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
1081
1676
|
alias TEXT NOT NULL PRIMARY KEY,
|
|
@@ -1256,6 +1851,33 @@ async function ensureSchema() {
|
|
|
1256
1851
|
CREATE INDEX IF NOT EXISTS idx_conversations_channel
|
|
1257
1852
|
ON conversations(channel_id);
|
|
1258
1853
|
`);
|
|
1854
|
+
await client.executeMultiple(`
|
|
1855
|
+
CREATE TABLE IF NOT EXISTS session_agent_map (
|
|
1856
|
+
session_uuid TEXT PRIMARY KEY,
|
|
1857
|
+
agent_id TEXT NOT NULL,
|
|
1858
|
+
session_name TEXT,
|
|
1859
|
+
task_id TEXT,
|
|
1860
|
+
project_name TEXT,
|
|
1861
|
+
started_at TEXT NOT NULL
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
|
|
1865
|
+
ON session_agent_map(agent_id);
|
|
1866
|
+
`);
|
|
1867
|
+
try {
|
|
1868
|
+
const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
|
|
1869
|
+
if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
|
|
1870
|
+
await client.execute({
|
|
1871
|
+
sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
|
|
1872
|
+
SELECT session_id, agent_id, '', MIN(timestamp)
|
|
1873
|
+
FROM memories
|
|
1874
|
+
WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
|
|
1875
|
+
GROUP BY session_id, agent_id`,
|
|
1876
|
+
args: []
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
} catch {
|
|
1880
|
+
}
|
|
1259
1881
|
try {
|
|
1260
1882
|
await client.execute({
|
|
1261
1883
|
sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
|
|
@@ -1389,15 +2011,41 @@ async function ensureSchema() {
|
|
|
1389
2011
|
});
|
|
1390
2012
|
} catch {
|
|
1391
2013
|
}
|
|
2014
|
+
for (const col of [
|
|
2015
|
+
"ALTER TABLE memories ADD COLUMN intent TEXT",
|
|
2016
|
+
"ALTER TABLE memories ADD COLUMN outcome TEXT",
|
|
2017
|
+
"ALTER TABLE memories ADD COLUMN domain TEXT",
|
|
2018
|
+
"ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
|
|
2019
|
+
"ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
|
|
2020
|
+
"ALTER TABLE memories ADD COLUMN chain_position TEXT",
|
|
2021
|
+
"ALTER TABLE memories ADD COLUMN review_status TEXT",
|
|
2022
|
+
"ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
|
|
2023
|
+
"ALTER TABLE memories ADD COLUMN file_paths TEXT",
|
|
2024
|
+
"ALTER TABLE memories ADD COLUMN commit_hash TEXT",
|
|
2025
|
+
"ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
|
|
2026
|
+
"ALTER TABLE memories ADD COLUMN token_cost REAL",
|
|
2027
|
+
"ALTER TABLE memories ADD COLUMN audience TEXT",
|
|
2028
|
+
"ALTER TABLE memories ADD COLUMN language_type TEXT",
|
|
2029
|
+
"ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
|
|
2030
|
+
]) {
|
|
2031
|
+
try {
|
|
2032
|
+
await client.execute(col);
|
|
2033
|
+
} catch {
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1392
2036
|
}
|
|
1393
2037
|
async function disposeDatabase() {
|
|
2038
|
+
if (_daemonClient) {
|
|
2039
|
+
_daemonClient.close();
|
|
2040
|
+
_daemonClient = null;
|
|
2041
|
+
}
|
|
1394
2042
|
if (_client) {
|
|
1395
2043
|
_client.close();
|
|
1396
2044
|
_client = null;
|
|
1397
2045
|
_resilientClient = null;
|
|
1398
2046
|
}
|
|
1399
2047
|
}
|
|
1400
|
-
var _client, _resilientClient, initTurso, disposeTurso;
|
|
2048
|
+
var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
|
|
1401
2049
|
var init_database = __esm({
|
|
1402
2050
|
"src/lib/database.ts"() {
|
|
1403
2051
|
"use strict";
|
|
@@ -1405,31 +2053,96 @@ var init_database = __esm({
|
|
|
1405
2053
|
init_employees();
|
|
1406
2054
|
_client = null;
|
|
1407
2055
|
_resilientClient = null;
|
|
2056
|
+
_daemonClient = null;
|
|
1408
2057
|
initTurso = initDatabase;
|
|
1409
2058
|
disposeTurso = disposeDatabase;
|
|
1410
2059
|
}
|
|
1411
2060
|
});
|
|
1412
2061
|
|
|
1413
2062
|
// src/lib/license.ts
|
|
1414
|
-
import { readFileSync as
|
|
1415
|
-
import { randomUUID } from "crypto";
|
|
1416
|
-
import
|
|
2063
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
2064
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2065
|
+
import path7 from "path";
|
|
1417
2066
|
import { jwtVerify, importSPKI } from "jose";
|
|
1418
|
-
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
|
|
2067
|
+
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
1419
2068
|
var init_license = __esm({
|
|
1420
2069
|
"src/lib/license.ts"() {
|
|
1421
2070
|
"use strict";
|
|
1422
2071
|
init_config();
|
|
1423
|
-
LICENSE_PATH =
|
|
1424
|
-
CACHE_PATH =
|
|
1425
|
-
DEVICE_ID_PATH =
|
|
2072
|
+
LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
|
|
2073
|
+
CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
2074
|
+
DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
|
|
2075
|
+
PLAN_LIMITS = {
|
|
2076
|
+
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
2077
|
+
pro: { devices: 3, employees: 5, memories: 1e5 },
|
|
2078
|
+
team: { devices: 10, employees: 20, memories: 1e6 },
|
|
2079
|
+
agency: { devices: 50, employees: 100, memories: 1e7 },
|
|
2080
|
+
enterprise: { devices: -1, employees: -1, memories: -1 }
|
|
2081
|
+
};
|
|
1426
2082
|
}
|
|
1427
2083
|
});
|
|
1428
2084
|
|
|
1429
2085
|
// src/lib/plan-limits.ts
|
|
1430
|
-
import { readFileSync as
|
|
1431
|
-
import
|
|
1432
|
-
|
|
2086
|
+
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
2087
|
+
import path8 from "path";
|
|
2088
|
+
function getLicenseSync() {
|
|
2089
|
+
try {
|
|
2090
|
+
if (!existsSync7(CACHE_PATH2)) return freeLicense();
|
|
2091
|
+
const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
|
|
2092
|
+
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
2093
|
+
const parts = raw.token.split(".");
|
|
2094
|
+
if (parts.length !== 3) return freeLicense();
|
|
2095
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
2096
|
+
const plan = payload.plan ?? "free";
|
|
2097
|
+
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
2098
|
+
return {
|
|
2099
|
+
valid: true,
|
|
2100
|
+
plan,
|
|
2101
|
+
email: payload.sub ?? "",
|
|
2102
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
|
|
2103
|
+
deviceLimit: limits.devices,
|
|
2104
|
+
employeeLimit: limits.employees,
|
|
2105
|
+
memoryLimit: limits.memories
|
|
2106
|
+
};
|
|
2107
|
+
} catch {
|
|
2108
|
+
return freeLicense();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
function freeLicense() {
|
|
2112
|
+
const limits = PLAN_LIMITS.free;
|
|
2113
|
+
return {
|
|
2114
|
+
valid: true,
|
|
2115
|
+
plan: "free",
|
|
2116
|
+
email: "",
|
|
2117
|
+
expiresAt: null,
|
|
2118
|
+
deviceLimit: limits.devices,
|
|
2119
|
+
employeeLimit: limits.employees,
|
|
2120
|
+
memoryLimit: limits.memories
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function assertEmployeeLimitSync(rosterPath) {
|
|
2124
|
+
const license = getLicenseSync();
|
|
2125
|
+
if (license.employeeLimit < 0) return;
|
|
2126
|
+
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
2127
|
+
let count = 0;
|
|
2128
|
+
try {
|
|
2129
|
+
if (existsSync7(filePath)) {
|
|
2130
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
2131
|
+
const employees = JSON.parse(raw);
|
|
2132
|
+
count = Array.isArray(employees) ? employees.length : 0;
|
|
2133
|
+
}
|
|
2134
|
+
} catch {
|
|
2135
|
+
throw new PlanLimitError(
|
|
2136
|
+
`Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
if (count >= license.employeeLimit) {
|
|
2140
|
+
throw new PlanLimitError(
|
|
2141
|
+
`Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
var PlanLimitError, CACHE_PATH2;
|
|
1433
2146
|
var init_plan_limits = __esm({
|
|
1434
2147
|
"src/lib/plan-limits.ts"() {
|
|
1435
2148
|
"use strict";
|
|
@@ -1437,32 +2150,2359 @@ var init_plan_limits = __esm({
|
|
|
1437
2150
|
init_employees();
|
|
1438
2151
|
init_license();
|
|
1439
2152
|
init_config();
|
|
1440
|
-
|
|
2153
|
+
PlanLimitError = class extends Error {
|
|
2154
|
+
constructor(message) {
|
|
2155
|
+
super(message);
|
|
2156
|
+
this.name = "PlanLimitError";
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
|
|
1441
2160
|
}
|
|
1442
2161
|
});
|
|
1443
2162
|
|
|
1444
|
-
// src/lib/
|
|
1445
|
-
|
|
1446
|
-
|
|
2163
|
+
// src/lib/notifications.ts
|
|
2164
|
+
var notifications_exports = {};
|
|
2165
|
+
__export(notifications_exports, {
|
|
2166
|
+
cleanupOldNotifications: () => cleanupOldNotifications,
|
|
2167
|
+
formatNotifications: () => formatNotifications,
|
|
2168
|
+
markAsRead: () => markAsRead,
|
|
2169
|
+
markAsReadByTaskFile: () => markAsReadByTaskFile,
|
|
2170
|
+
markDoneTaskNotificationsAsRead: () => markDoneTaskNotificationsAsRead,
|
|
2171
|
+
migrateJsonNotifications: () => migrateJsonNotifications,
|
|
2172
|
+
readUnreadNotifications: () => readUnreadNotifications,
|
|
2173
|
+
writeNotification: () => writeNotification
|
|
2174
|
+
});
|
|
2175
|
+
import crypto from "crypto";
|
|
2176
|
+
import path9 from "path";
|
|
1447
2177
|
import os5 from "os";
|
|
1448
|
-
import {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2178
|
+
import {
|
|
2179
|
+
readFileSync as readFileSync9,
|
|
2180
|
+
readdirSync as readdirSync2,
|
|
2181
|
+
unlinkSync as unlinkSync4,
|
|
2182
|
+
existsSync as existsSync8,
|
|
2183
|
+
rmdirSync
|
|
2184
|
+
} from "fs";
|
|
2185
|
+
async function writeNotification(notification) {
|
|
2186
|
+
try {
|
|
2187
|
+
const client = getClient();
|
|
2188
|
+
const id = crypto.randomUUID();
|
|
2189
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2190
|
+
await client.execute({
|
|
2191
|
+
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
2192
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
2193
|
+
args: [
|
|
2194
|
+
id,
|
|
2195
|
+
notification.agentId,
|
|
2196
|
+
notification.agentRole,
|
|
2197
|
+
notification.event,
|
|
2198
|
+
notification.project,
|
|
2199
|
+
notification.summary,
|
|
2200
|
+
notification.taskFile ?? null,
|
|
2201
|
+
now
|
|
2202
|
+
]
|
|
2203
|
+
});
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
2206
|
+
`);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
async function readUnreadNotifications(agentFilter) {
|
|
2210
|
+
try {
|
|
2211
|
+
const client = getClient();
|
|
2212
|
+
const conditions = ["read = 0"];
|
|
2213
|
+
const args = [];
|
|
2214
|
+
if (agentFilter) {
|
|
2215
|
+
conditions.push("agent_id = ?");
|
|
2216
|
+
args.push(agentFilter);
|
|
2217
|
+
}
|
|
2218
|
+
const result = await client.execute({
|
|
2219
|
+
sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
|
|
2220
|
+
FROM notifications
|
|
2221
|
+
WHERE ${conditions.join(" AND ")}
|
|
2222
|
+
ORDER BY created_at ASC`,
|
|
2223
|
+
args
|
|
2224
|
+
});
|
|
2225
|
+
return result.rows.map((r) => ({
|
|
2226
|
+
id: String(r.id),
|
|
2227
|
+
agentId: String(r.agent_id),
|
|
2228
|
+
agentRole: String(r.agent_role),
|
|
2229
|
+
event: String(r.event),
|
|
2230
|
+
project: String(r.project),
|
|
2231
|
+
summary: String(r.summary),
|
|
2232
|
+
taskFile: r.task_file ? String(r.task_file) : void 0,
|
|
2233
|
+
timestamp: String(r.created_at),
|
|
2234
|
+
read: false
|
|
2235
|
+
}));
|
|
2236
|
+
} catch {
|
|
2237
|
+
return [];
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
async function markAsRead(ids) {
|
|
2241
|
+
if (ids.length === 0) return;
|
|
2242
|
+
try {
|
|
2243
|
+
const client = getClient();
|
|
2244
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
2245
|
+
await client.execute({
|
|
2246
|
+
sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})`,
|
|
2247
|
+
args: ids
|
|
2248
|
+
});
|
|
2249
|
+
} catch {
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function markAsReadByTaskFile(taskFile) {
|
|
2253
|
+
try {
|
|
2254
|
+
const client = getClient();
|
|
2255
|
+
await client.execute({
|
|
2256
|
+
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
2257
|
+
args: [taskFile]
|
|
2258
|
+
});
|
|
2259
|
+
} catch {
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
async function cleanupOldNotifications(daysOld = CLEANUP_DAYS) {
|
|
2263
|
+
try {
|
|
2264
|
+
const client = getClient();
|
|
2265
|
+
const cutoff = new Date(
|
|
2266
|
+
Date.now() - daysOld * 24 * 60 * 60 * 1e3
|
|
2267
|
+
).toISOString();
|
|
2268
|
+
const result = await client.execute({
|
|
2269
|
+
sql: "DELETE FROM notifications WHERE created_at < ?",
|
|
2270
|
+
args: [cutoff]
|
|
2271
|
+
});
|
|
2272
|
+
return result.rowsAffected;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return 0;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
async function markDoneTaskNotificationsAsRead() {
|
|
2278
|
+
try {
|
|
2279
|
+
const client = getClient();
|
|
2280
|
+
const result = await client.execute({
|
|
2281
|
+
sql: `UPDATE notifications SET read = 1
|
|
2282
|
+
WHERE read = 0
|
|
2283
|
+
AND task_file IS NOT NULL
|
|
2284
|
+
AND task_file IN (
|
|
2285
|
+
SELECT task_file FROM tasks WHERE status = 'done'
|
|
2286
|
+
)`,
|
|
2287
|
+
args: []
|
|
2288
|
+
});
|
|
2289
|
+
return result.rowsAffected;
|
|
2290
|
+
} catch {
|
|
2291
|
+
return 0;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function formatNotifications(notifications) {
|
|
2295
|
+
if (notifications.length === 0) return "";
|
|
2296
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2297
|
+
for (const n of notifications) {
|
|
2298
|
+
const key = `${n.agentId}|${n.agentRole}`;
|
|
2299
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
2300
|
+
grouped.get(key).push(n);
|
|
2301
|
+
}
|
|
2302
|
+
const lines = [];
|
|
2303
|
+
lines.push(`## Notifications (${notifications.length} unread)
|
|
2304
|
+
`);
|
|
2305
|
+
for (const [key, items] of grouped) {
|
|
2306
|
+
const [agentId, agentRole] = key.split("|");
|
|
2307
|
+
lines.push(`**${agentId}** (${agentRole}):`);
|
|
2308
|
+
for (const item of items) {
|
|
2309
|
+
const ago = formatTimeAgo(item.timestamp);
|
|
2310
|
+
const icon = eventIcon(item.event);
|
|
2311
|
+
lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
|
|
2312
|
+
}
|
|
2313
|
+
lines.push("");
|
|
2314
|
+
}
|
|
2315
|
+
return lines.join("\n");
|
|
2316
|
+
}
|
|
2317
|
+
async function migrateJsonNotifications() {
|
|
2318
|
+
const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path9.join(os5.homedir(), ".exe-os");
|
|
2319
|
+
const notifDir = path9.join(base, "notifications");
|
|
2320
|
+
if (!existsSync8(notifDir)) return 0;
|
|
2321
|
+
let migrated = 0;
|
|
2322
|
+
try {
|
|
2323
|
+
const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
|
|
2324
|
+
if (files.length === 0) return 0;
|
|
2325
|
+
const client = getClient();
|
|
2326
|
+
for (const file of files) {
|
|
2327
|
+
try {
|
|
2328
|
+
const filePath = path9.join(notifDir, file);
|
|
2329
|
+
const data = JSON.parse(readFileSync9(filePath, "utf8"));
|
|
2330
|
+
await client.execute({
|
|
2331
|
+
sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
2332
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2333
|
+
args: [
|
|
2334
|
+
crypto.randomUUID(),
|
|
2335
|
+
data.agentId ?? "unknown",
|
|
2336
|
+
data.agentRole ?? "unknown",
|
|
2337
|
+
data.event ?? "session_summary",
|
|
2338
|
+
data.project ?? "unknown",
|
|
2339
|
+
data.summary ?? "",
|
|
2340
|
+
data.taskFile ?? null,
|
|
2341
|
+
data.read ? 1 : 0,
|
|
2342
|
+
data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2343
|
+
]
|
|
2344
|
+
});
|
|
2345
|
+
unlinkSync4(filePath);
|
|
2346
|
+
migrated++;
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
try {
|
|
2351
|
+
const remaining = readdirSync2(notifDir);
|
|
2352
|
+
if (remaining.length === 0) {
|
|
2353
|
+
rmdirSync(notifDir);
|
|
2354
|
+
}
|
|
2355
|
+
} catch {
|
|
2356
|
+
}
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
return migrated;
|
|
2360
|
+
}
|
|
2361
|
+
function eventIcon(event) {
|
|
2362
|
+
switch (event) {
|
|
2363
|
+
case "task_complete":
|
|
2364
|
+
return "Completed:";
|
|
2365
|
+
case "task_needs_fix":
|
|
2366
|
+
return "Needs fix:";
|
|
2367
|
+
case "session_summary":
|
|
2368
|
+
return "Session:";
|
|
2369
|
+
case "error_spike":
|
|
2370
|
+
return "Errors:";
|
|
2371
|
+
case "orphan_task":
|
|
2372
|
+
return "Orphan:";
|
|
2373
|
+
case "subtasks_complete":
|
|
2374
|
+
return "Subtasks done:";
|
|
2375
|
+
case "capacity_relaunch":
|
|
2376
|
+
return "Relaunched:";
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
function formatTimeAgo(timestamp) {
|
|
2380
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
2381
|
+
const mins = Math.floor(diffMs / 6e4);
|
|
2382
|
+
if (mins < 1) return "just now";
|
|
2383
|
+
if (mins < 60) return `${mins}m ago`;
|
|
2384
|
+
const hours = Math.floor(mins / 60);
|
|
2385
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2386
|
+
const days = Math.floor(hours / 24);
|
|
2387
|
+
return `${days}d ago`;
|
|
2388
|
+
}
|
|
2389
|
+
var CLEANUP_DAYS;
|
|
2390
|
+
var init_notifications = __esm({
|
|
2391
|
+
"src/lib/notifications.ts"() {
|
|
2392
|
+
"use strict";
|
|
2393
|
+
init_database();
|
|
2394
|
+
CLEANUP_DAYS = 7;
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
// src/lib/session-kill-telemetry.ts
|
|
2399
|
+
import crypto2 from "crypto";
|
|
2400
|
+
async function recordSessionKill(input2) {
|
|
2401
|
+
try {
|
|
2402
|
+
const client = getClient();
|
|
2403
|
+
await client.execute({
|
|
2404
|
+
sql: `INSERT INTO session_kills
|
|
2405
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
2406
|
+
ticks_idle, estimated_tokens_saved)
|
|
2407
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2408
|
+
args: [
|
|
2409
|
+
crypto2.randomUUID(),
|
|
2410
|
+
input2.sessionName,
|
|
2411
|
+
input2.agentId,
|
|
2412
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2413
|
+
input2.reason,
|
|
2414
|
+
input2.ticksIdle ?? null,
|
|
2415
|
+
input2.estimatedTokensSaved ?? null
|
|
2416
|
+
]
|
|
2417
|
+
});
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
process.stderr.write(
|
|
2420
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
2421
|
+
`
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
var init_session_kill_telemetry = __esm({
|
|
2426
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
2427
|
+
"use strict";
|
|
2428
|
+
init_database();
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
// src/lib/state-bus.ts
|
|
2433
|
+
var StateBus, orgBus;
|
|
2434
|
+
var init_state_bus = __esm({
|
|
2435
|
+
"src/lib/state-bus.ts"() {
|
|
2436
|
+
"use strict";
|
|
2437
|
+
StateBus = class {
|
|
2438
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2439
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
2440
|
+
/** Emit an event to all subscribers */
|
|
2441
|
+
emit(event) {
|
|
2442
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
2443
|
+
if (typeHandlers) {
|
|
2444
|
+
for (const handler of typeHandlers) {
|
|
2445
|
+
try {
|
|
2446
|
+
handler(event);
|
|
2447
|
+
} catch {
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
for (const handler of this.globalHandlers) {
|
|
2452
|
+
try {
|
|
2453
|
+
handler(event);
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
/** Subscribe to a specific event type */
|
|
2459
|
+
on(type, handler) {
|
|
2460
|
+
if (!this.handlers.has(type)) {
|
|
2461
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
2462
|
+
}
|
|
2463
|
+
this.handlers.get(type).add(handler);
|
|
2464
|
+
}
|
|
2465
|
+
/** Subscribe to ALL events */
|
|
2466
|
+
onAny(handler) {
|
|
2467
|
+
this.globalHandlers.add(handler);
|
|
2468
|
+
}
|
|
2469
|
+
/** Unsubscribe from a specific event type */
|
|
2470
|
+
off(type, handler) {
|
|
2471
|
+
this.handlers.get(type)?.delete(handler);
|
|
2472
|
+
}
|
|
2473
|
+
/** Unsubscribe from ALL events */
|
|
2474
|
+
offAny(handler) {
|
|
2475
|
+
this.globalHandlers.delete(handler);
|
|
2476
|
+
}
|
|
2477
|
+
/** Remove all listeners */
|
|
2478
|
+
clear() {
|
|
2479
|
+
this.handlers.clear();
|
|
2480
|
+
this.globalHandlers.clear();
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
orgBus = new StateBus();
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
// src/lib/tasks-crud.ts
|
|
2488
|
+
var tasks_crud_exports = {};
|
|
2489
|
+
__export(tasks_crud_exports, {
|
|
2490
|
+
TASK_ALREADY_CLAIMED_PREFIX: () => TASK_ALREADY_CLAIMED_PREFIX,
|
|
2491
|
+
checkStaleCompletion: () => checkStaleCompletion,
|
|
2492
|
+
createTaskCore: () => createTaskCore,
|
|
2493
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
2494
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
2495
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
2496
|
+
extractParentFromContext: () => extractParentFromContext,
|
|
2497
|
+
isTmuxSessionAlive: () => isTmuxSessionAlive,
|
|
2498
|
+
listTasks: () => listTasks,
|
|
2499
|
+
resolveTask: () => resolveTask,
|
|
2500
|
+
slugify: () => slugify,
|
|
2501
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
2502
|
+
writeCheckpoint: () => writeCheckpoint
|
|
2503
|
+
});
|
|
2504
|
+
import crypto3 from "crypto";
|
|
2505
|
+
import path10 from "path";
|
|
2506
|
+
import os6 from "os";
|
|
2507
|
+
import { execSync as execSync5 } from "child_process";
|
|
2508
|
+
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
2509
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
2510
|
+
async function writeCheckpoint(input2) {
|
|
2511
|
+
const client = getClient();
|
|
2512
|
+
const row = await resolveTask(client, input2.taskId);
|
|
2513
|
+
const taskId = String(row.id);
|
|
2514
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2515
|
+
const blockedByIds = [];
|
|
2516
|
+
if (row.blocked_by) {
|
|
2517
|
+
blockedByIds.push(String(row.blocked_by));
|
|
2518
|
+
}
|
|
2519
|
+
const checkpoint = {
|
|
2520
|
+
step: input2.step,
|
|
2521
|
+
context_summary: input2.contextSummary,
|
|
2522
|
+
files_touched: input2.filesTouched ?? [],
|
|
2523
|
+
blocked_by_ids: blockedByIds,
|
|
2524
|
+
last_checkpoint_at: now
|
|
2525
|
+
};
|
|
2526
|
+
const result = await client.execute({
|
|
2527
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
2528
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
2529
|
+
});
|
|
2530
|
+
if (result.rowsAffected === 0) {
|
|
2531
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
2532
|
+
}
|
|
2533
|
+
const countResult = await client.execute({
|
|
2534
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
2535
|
+
args: [taskId]
|
|
2536
|
+
});
|
|
2537
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
2538
|
+
return { checkpointCount };
|
|
2539
|
+
}
|
|
2540
|
+
function extractParentFromContext(contextBody) {
|
|
2541
|
+
if (!contextBody) return null;
|
|
2542
|
+
const match = contextBody.match(
|
|
2543
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
2544
|
+
);
|
|
2545
|
+
return match ? match[1].toLowerCase() : null;
|
|
2546
|
+
}
|
|
2547
|
+
function slugify(title) {
|
|
2548
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2549
|
+
}
|
|
2550
|
+
function buildKeywordIndex() {
|
|
2551
|
+
const idx = /* @__PURE__ */ new Map();
|
|
2552
|
+
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
2553
|
+
for (const kw of keywords) {
|
|
2554
|
+
const existing = idx.get(kw) ?? [];
|
|
2555
|
+
existing.push(role);
|
|
2556
|
+
idx.set(kw, existing);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return idx;
|
|
2560
|
+
}
|
|
2561
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
2562
|
+
const employees = loadEmployeesSync();
|
|
2563
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
2564
|
+
if (!employee) return void 0;
|
|
2565
|
+
const assigneeRole = employee.role;
|
|
2566
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
2567
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
2568
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
2569
|
+
if (text.includes(keyword)) {
|
|
2570
|
+
for (const role of roles) matchedRoles.add(role);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (matchedRoles.size === 0) return void 0;
|
|
2574
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
2575
|
+
if (assigneeRole === "COO") return void 0;
|
|
2576
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
2577
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
2578
|
+
}
|
|
2579
|
+
async function resolveTask(client, identifier, scopeSession) {
|
|
2580
|
+
const scope = sessionScopeFilter(scopeSession);
|
|
2581
|
+
let result = await client.execute({
|
|
2582
|
+
sql: `SELECT * FROM tasks WHERE id = ?${scope.sql}`,
|
|
2583
|
+
args: [identifier, ...scope.args]
|
|
2584
|
+
});
|
|
2585
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2586
|
+
result = await client.execute({
|
|
2587
|
+
sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
|
|
2588
|
+
args: [`%${identifier}%`, ...scope.args]
|
|
2589
|
+
});
|
|
2590
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2591
|
+
if (result.rows.length > 1) {
|
|
2592
|
+
const exact = result.rows.filter(
|
|
2593
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
2594
|
+
);
|
|
2595
|
+
if (exact.length === 1) return exact[0];
|
|
2596
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
2597
|
+
const active = candidates.filter(
|
|
2598
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2599
|
+
);
|
|
2600
|
+
if (active.length === 1) return active[0];
|
|
2601
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2602
|
+
throw new Error(
|
|
2603
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
result = await client.execute({
|
|
2607
|
+
sql: `SELECT * FROM tasks WHERE title LIKE ?${scope.sql}`,
|
|
2608
|
+
args: [`%${identifier}%`, ...scope.args]
|
|
2609
|
+
});
|
|
2610
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
2611
|
+
if (result.rows.length > 1) {
|
|
2612
|
+
const active = result.rows.filter(
|
|
2613
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
2614
|
+
);
|
|
2615
|
+
if (active.length === 1) return active[0];
|
|
2616
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
2617
|
+
throw new Error(
|
|
2618
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
2622
|
+
}
|
|
2623
|
+
async function createTaskCore(input2) {
|
|
2624
|
+
const client = getClient();
|
|
2625
|
+
const id = crypto3.randomUUID();
|
|
2626
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2627
|
+
const slug = slugify(input2.title);
|
|
2628
|
+
let earlySessionScope = null;
|
|
2629
|
+
try {
|
|
2630
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
2631
|
+
earlySessionScope = resolveExeSession2();
|
|
2632
|
+
} catch {
|
|
2633
|
+
}
|
|
2634
|
+
const scope = earlySessionScope ?? "default";
|
|
2635
|
+
const taskFile = input2.taskFile ?? `tasks/${scope}/${input2.assignedTo}/${slug}.md`;
|
|
2636
|
+
let blockedById = null;
|
|
2637
|
+
const initialStatus = input2.blockedBy ? "blocked" : "open";
|
|
2638
|
+
if (input2.blockedBy) {
|
|
2639
|
+
const blocker = await resolveTask(client, input2.blockedBy);
|
|
2640
|
+
blockedById = String(blocker.id);
|
|
2641
|
+
}
|
|
2642
|
+
let parentTaskId = null;
|
|
2643
|
+
let parentRef = input2.parentTaskId;
|
|
2644
|
+
if (!parentRef) {
|
|
2645
|
+
const extracted = extractParentFromContext(input2.context);
|
|
2646
|
+
if (extracted) {
|
|
2647
|
+
parentRef = extracted;
|
|
2648
|
+
process.stderr.write(
|
|
2649
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
if (parentRef) {
|
|
2654
|
+
try {
|
|
2655
|
+
const parent = await resolveTask(client, parentRef);
|
|
2656
|
+
parentTaskId = String(parent.id);
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
if (!input2.parentTaskId) {
|
|
2659
|
+
throw new Error(
|
|
2660
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
throw err;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
let warning;
|
|
2667
|
+
const dupScope = sessionScopeFilter();
|
|
2668
|
+
const dupCheck = await client.execute({
|
|
2669
|
+
sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
|
|
2670
|
+
args: [input2.title, input2.assignedTo, ...dupScope.args]
|
|
2671
|
+
});
|
|
2672
|
+
if (dupCheck.rows.length > 0) {
|
|
2673
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
2674
|
+
}
|
|
2675
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
2676
|
+
const laneWarning = checkLaneAffinity(input2.title, input2.context, input2.assignedTo);
|
|
2677
|
+
if (laneWarning) {
|
|
2678
|
+
warning = warning ? `${warning}
|
|
2679
|
+
${laneWarning}` : laneWarning;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
if (input2.baseDir) {
|
|
2683
|
+
try {
|
|
2684
|
+
await mkdir3(path10.join(input2.baseDir, "exe", "output"), { recursive: true });
|
|
2685
|
+
await mkdir3(path10.join(input2.baseDir, "exe", "research"), { recursive: true });
|
|
2686
|
+
await ensureArchitectureDoc(input2.baseDir, input2.projectName);
|
|
2687
|
+
await ensureGitignoreExe(input2.baseDir);
|
|
2688
|
+
} catch {
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
const complexity = input2.complexity ?? "standard";
|
|
2692
|
+
const sessionScope = earlySessionScope;
|
|
2693
|
+
await client.execute({
|
|
2694
|
+
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)
|
|
2695
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2696
|
+
args: [
|
|
2697
|
+
id,
|
|
2698
|
+
input2.title,
|
|
2699
|
+
input2.assignedTo,
|
|
2700
|
+
input2.assignedBy,
|
|
2701
|
+
input2.projectName,
|
|
2702
|
+
input2.priority,
|
|
2703
|
+
initialStatus,
|
|
2704
|
+
taskFile,
|
|
2705
|
+
blockedById,
|
|
2706
|
+
parentTaskId,
|
|
2707
|
+
input2.reviewer ?? null,
|
|
2708
|
+
input2.context,
|
|
2709
|
+
complexity,
|
|
2710
|
+
input2.budgetTokens ?? null,
|
|
2711
|
+
input2.budgetFallbackModel ?? null,
|
|
2712
|
+
0,
|
|
2713
|
+
null,
|
|
2714
|
+
sessionScope,
|
|
2715
|
+
now,
|
|
2716
|
+
now
|
|
2717
|
+
]
|
|
2718
|
+
});
|
|
2719
|
+
if (input2.baseDir) {
|
|
2720
|
+
try {
|
|
2721
|
+
const EXE_OS_DIR = path10.join(os6.homedir(), ".exe-os");
|
|
2722
|
+
const mdPath = path10.join(EXE_OS_DIR, taskFile);
|
|
2723
|
+
const mdDir = path10.dirname(mdPath);
|
|
2724
|
+
if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2725
|
+
const reviewer = input2.reviewer ?? input2.assignedBy;
|
|
2726
|
+
const mdContent = `# ${input2.title}
|
|
2727
|
+
|
|
2728
|
+
**ID:** ${id}
|
|
2729
|
+
**Status:** ${initialStatus}
|
|
2730
|
+
**Priority:** ${input2.priority}
|
|
2731
|
+
**Assigned by:** ${input2.assignedBy}
|
|
2732
|
+
**Assigned to:** ${input2.assignedTo}
|
|
2733
|
+
**Project:** ${input2.projectName}
|
|
2734
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
2735
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
2736
|
+
**Reviewer:** ${reviewer}
|
|
2737
|
+
|
|
2738
|
+
## Context
|
|
2739
|
+
|
|
2740
|
+
${input2.context}
|
|
2741
|
+
|
|
2742
|
+
## MANDATORY: When done
|
|
2743
|
+
|
|
2744
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
2745
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
2746
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
2747
|
+
`;
|
|
2748
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
2749
|
+
} catch {
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
id,
|
|
2754
|
+
title: input2.title,
|
|
2755
|
+
assignedTo: input2.assignedTo,
|
|
2756
|
+
assignedBy: input2.assignedBy,
|
|
2757
|
+
projectName: input2.projectName,
|
|
2758
|
+
priority: input2.priority,
|
|
2759
|
+
status: initialStatus,
|
|
2760
|
+
taskFile,
|
|
2761
|
+
createdAt: now,
|
|
2762
|
+
updatedAt: now,
|
|
2763
|
+
warning,
|
|
2764
|
+
budgetTokens: input2.budgetTokens ?? null,
|
|
2765
|
+
budgetFallbackModel: input2.budgetFallbackModel ?? null,
|
|
2766
|
+
tokensUsed: 0,
|
|
2767
|
+
tokensWarnedAt: null
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
async function listTasks(input2) {
|
|
2771
|
+
const client = getClient();
|
|
2772
|
+
const conditions = [];
|
|
2773
|
+
const args = [];
|
|
2774
|
+
if (input2.assignedTo) {
|
|
2775
|
+
conditions.push("assigned_to = ?");
|
|
2776
|
+
args.push(input2.assignedTo);
|
|
2777
|
+
}
|
|
2778
|
+
if (input2.status) {
|
|
2779
|
+
conditions.push("status = ?");
|
|
2780
|
+
args.push(input2.status);
|
|
2781
|
+
} else {
|
|
2782
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
2783
|
+
}
|
|
2784
|
+
if (input2.projectName) {
|
|
2785
|
+
conditions.push("project_name = ?");
|
|
2786
|
+
args.push(input2.projectName);
|
|
2787
|
+
}
|
|
2788
|
+
if (input2.priority) {
|
|
2789
|
+
conditions.push("priority = ?");
|
|
2790
|
+
args.push(input2.priority);
|
|
2791
|
+
}
|
|
2792
|
+
const scope = sessionScopeFilter();
|
|
2793
|
+
if (scope.sql) {
|
|
2794
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
2795
|
+
args.push(...scope.args);
|
|
2796
|
+
}
|
|
2797
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2798
|
+
const result = await client.execute({
|
|
2799
|
+
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`,
|
|
2800
|
+
args
|
|
2801
|
+
});
|
|
2802
|
+
return result.rows.map((r) => ({
|
|
2803
|
+
id: String(r.id),
|
|
2804
|
+
title: String(r.title),
|
|
2805
|
+
assignedTo: String(r.assigned_to),
|
|
2806
|
+
assignedBy: String(r.assigned_by),
|
|
2807
|
+
projectName: String(r.project_name),
|
|
2808
|
+
priority: String(r.priority),
|
|
2809
|
+
status: String(r.status),
|
|
2810
|
+
taskFile: String(r.task_file),
|
|
2811
|
+
createdAt: String(r.created_at),
|
|
2812
|
+
updatedAt: String(r.updated_at),
|
|
2813
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
2814
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
2815
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
2816
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
2817
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
2818
|
+
}));
|
|
2819
|
+
}
|
|
2820
|
+
function isTmuxSessionAlive(identifier) {
|
|
2821
|
+
if (!identifier || identifier === "unknown") return true;
|
|
2822
|
+
try {
|
|
2823
|
+
if (identifier.startsWith("%")) {
|
|
2824
|
+
const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
|
|
2825
|
+
timeout: 2e3,
|
|
2826
|
+
encoding: "utf8",
|
|
2827
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2828
|
+
});
|
|
2829
|
+
return output.split("\n").some((l) => l.trim() === identifier);
|
|
2830
|
+
} else {
|
|
2831
|
+
execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
|
|
2832
|
+
timeout: 2e3,
|
|
2833
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2834
|
+
});
|
|
2835
|
+
return true;
|
|
2836
|
+
}
|
|
2837
|
+
} catch {
|
|
2838
|
+
if (identifier.startsWith("%")) return true;
|
|
2839
|
+
try {
|
|
2840
|
+
execSync5("tmux list-sessions", {
|
|
2841
|
+
timeout: 2e3,
|
|
2842
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2843
|
+
});
|
|
2844
|
+
return false;
|
|
2845
|
+
} catch {
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
2851
|
+
if (!taskContext) return null;
|
|
2852
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
2853
|
+
try {
|
|
2854
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
2855
|
+
const branch = execSync5(
|
|
2856
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
2857
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
2858
|
+
).trim();
|
|
2859
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
2860
|
+
const commitCount = execSync5(
|
|
2861
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
2862
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2863
|
+
).trim();
|
|
2864
|
+
const count = parseInt(commitCount, 10);
|
|
2865
|
+
if (count === 0) {
|
|
2866
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
2867
|
+
}
|
|
2868
|
+
return null;
|
|
2869
|
+
} catch {
|
|
2870
|
+
return null;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
async function updateTaskStatus(input2) {
|
|
2874
|
+
const client = getClient();
|
|
2875
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2876
|
+
const row = await resolveTask(client, input2.taskId);
|
|
2877
|
+
const taskId = String(row.id);
|
|
2878
|
+
const taskFile = String(row.task_file);
|
|
2879
|
+
if (input2.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
2880
|
+
process.stderr.write(
|
|
2881
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
2882
|
+
`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
if (input2.status === "done") {
|
|
2886
|
+
const existingRow = await client.execute({
|
|
2887
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
2888
|
+
args: [taskId]
|
|
2889
|
+
});
|
|
2890
|
+
if (existingRow.rows.length > 0) {
|
|
2891
|
+
const ctx = existingRow.rows[0];
|
|
2892
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
2893
|
+
if (warning) {
|
|
2894
|
+
input2.result = input2.result ? `\u26A0\uFE0F ${warning}
|
|
2895
|
+
|
|
2896
|
+
${input2.result}` : `\u26A0\uFE0F ${warning}`;
|
|
2897
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
2898
|
+
`);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (input2.status === "in_progress") {
|
|
2903
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
2904
|
+
const claim = await client.execute({
|
|
2905
|
+
sql: `UPDATE tasks
|
|
2906
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
2907
|
+
WHERE id = ? AND status = 'open'`,
|
|
2908
|
+
args: [tmuxSession, now, taskId]
|
|
2909
|
+
});
|
|
2910
|
+
if (claim.rowsAffected === 0) {
|
|
2911
|
+
const current = await client.execute({
|
|
2912
|
+
sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
|
|
2913
|
+
args: [taskId]
|
|
2914
|
+
});
|
|
2915
|
+
const cur = current.rows[0];
|
|
2916
|
+
const curStatus = cur?.status ?? "unknown";
|
|
2917
|
+
const claimedBySession = cur?.assigned_tmux ?? "";
|
|
2918
|
+
const assignedBy = cur?.assigned_by ?? "";
|
|
2919
|
+
if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
|
|
2920
|
+
process.stderr.write(
|
|
2921
|
+
`[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
|
|
2922
|
+
`
|
|
2923
|
+
);
|
|
2924
|
+
await client.execute({
|
|
2925
|
+
sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, updated_at = ? WHERE id = ?",
|
|
2926
|
+
args: [now, taskId]
|
|
2927
|
+
});
|
|
2928
|
+
const retried = await client.execute({
|
|
2929
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ? AND status = 'open'`,
|
|
2930
|
+
args: [tmuxSession, now, taskId]
|
|
2931
|
+
});
|
|
2932
|
+
if (retried.rowsAffected > 0) {
|
|
2933
|
+
try {
|
|
2934
|
+
await writeCheckpoint({
|
|
2935
|
+
taskId,
|
|
2936
|
+
step: "reclaimed_dead_session",
|
|
2937
|
+
contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
|
|
2938
|
+
});
|
|
2939
|
+
} catch {
|
|
2940
|
+
}
|
|
2941
|
+
return { row, taskFile, now, taskId };
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
if (curStatus === "in_progress" && input2.callerAgentId && (input2.callerAgentId === assignedBy || isCoordinatorName(input2.callerAgentId))) {
|
|
2945
|
+
process.stderr.write(
|
|
2946
|
+
`[tasks] Assigner override: ${input2.callerAgentId} reclaiming ${taskId}
|
|
2947
|
+
`
|
|
2948
|
+
);
|
|
2949
|
+
await client.execute({
|
|
2950
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ? WHERE id = ?`,
|
|
2951
|
+
args: [tmuxSession, now, taskId]
|
|
2952
|
+
});
|
|
2953
|
+
try {
|
|
2954
|
+
await writeCheckpoint({
|
|
2955
|
+
taskId,
|
|
2956
|
+
step: "assigner_override",
|
|
2957
|
+
contextSummary: `Task force-reclaimed by assigner ${input2.callerAgentId}.`
|
|
2958
|
+
});
|
|
2959
|
+
} catch {
|
|
2960
|
+
}
|
|
2961
|
+
return { row, taskFile, now, taskId };
|
|
2962
|
+
}
|
|
2963
|
+
const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
|
|
2964
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
|
|
2965
|
+
}
|
|
2966
|
+
try {
|
|
2967
|
+
await writeCheckpoint({
|
|
2968
|
+
taskId,
|
|
2969
|
+
step: "claimed",
|
|
2970
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
2971
|
+
});
|
|
2972
|
+
} catch {
|
|
2973
|
+
}
|
|
2974
|
+
return { row, taskFile, now, taskId };
|
|
2975
|
+
}
|
|
2976
|
+
if (input2.result) {
|
|
2977
|
+
await client.execute({
|
|
2978
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
2979
|
+
args: [input2.status, input2.result, now, taskId]
|
|
2980
|
+
});
|
|
2981
|
+
} else {
|
|
2982
|
+
await client.execute({
|
|
2983
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
2984
|
+
args: [input2.status, now, taskId]
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
try {
|
|
2988
|
+
await writeCheckpoint({
|
|
2989
|
+
taskId,
|
|
2990
|
+
step: `status_transition:${input2.status}`,
|
|
2991
|
+
contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
|
|
2992
|
+
});
|
|
2993
|
+
} catch {
|
|
2994
|
+
}
|
|
2995
|
+
return { row, taskFile, now, taskId };
|
|
2996
|
+
}
|
|
2997
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
2998
|
+
const client = getClient();
|
|
2999
|
+
const row = await resolveTask(client, taskId);
|
|
3000
|
+
const id = String(row.id);
|
|
3001
|
+
const taskFile = String(row.task_file);
|
|
3002
|
+
const assignedTo = String(row.assigned_to);
|
|
3003
|
+
const assignedBy = String(row.assigned_by);
|
|
3004
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
3005
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
3006
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
3007
|
+
}
|
|
3008
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
3009
|
+
const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
3010
|
+
try {
|
|
3011
|
+
if (existsSync9(archPath)) return;
|
|
3012
|
+
const template = [
|
|
3013
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
3014
|
+
"",
|
|
3015
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
3016
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
3017
|
+
"",
|
|
3018
|
+
"## Overview",
|
|
3019
|
+
"",
|
|
3020
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
3021
|
+
"",
|
|
3022
|
+
"## Key Components",
|
|
3023
|
+
"",
|
|
3024
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
3025
|
+
"",
|
|
3026
|
+
"## Data Flow",
|
|
3027
|
+
"",
|
|
3028
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
3029
|
+
"",
|
|
3030
|
+
"## Invariants",
|
|
3031
|
+
"",
|
|
3032
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
3033
|
+
"",
|
|
3034
|
+
"## Dependencies",
|
|
3035
|
+
"",
|
|
3036
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
3037
|
+
""
|
|
3038
|
+
].join("\n");
|
|
3039
|
+
await writeFile3(archPath, template, "utf-8");
|
|
3040
|
+
} catch {
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
async function ensureGitignoreExe(baseDir) {
|
|
3044
|
+
const gitignorePath = path10.join(baseDir, ".gitignore");
|
|
3045
|
+
try {
|
|
3046
|
+
if (existsSync9(gitignorePath)) {
|
|
3047
|
+
const content = readFileSync10(gitignorePath, "utf-8");
|
|
3048
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
3049
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
3050
|
+
} else {
|
|
3051
|
+
await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
3052
|
+
}
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
3057
|
+
var init_tasks_crud = __esm({
|
|
3058
|
+
"src/lib/tasks-crud.ts"() {
|
|
3059
|
+
"use strict";
|
|
3060
|
+
init_database();
|
|
3061
|
+
init_task_scope();
|
|
3062
|
+
init_employees();
|
|
3063
|
+
LANE_KEYWORDS = {
|
|
3064
|
+
CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
|
|
3065
|
+
CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
|
|
3066
|
+
"Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
|
|
3067
|
+
"Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
|
|
3068
|
+
"Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
|
|
3069
|
+
"AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
|
|
3070
|
+
};
|
|
3071
|
+
KEYWORD_INDEX = buildKeywordIndex();
|
|
3072
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
3073
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
|
|
3077
|
+
// src/lib/tasks-review.ts
|
|
3078
|
+
import path11 from "path";
|
|
3079
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
|
|
3080
|
+
async function countPendingReviews(sessionScope) {
|
|
3081
|
+
const client = getClient();
|
|
3082
|
+
if (sessionScope) {
|
|
3083
|
+
const result2 = await client.execute({
|
|
3084
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND (session_scope = ? OR session_scope IS NULL)",
|
|
3085
|
+
args: [sessionScope]
|
|
3086
|
+
});
|
|
3087
|
+
return Number(result2.rows[0]?.cnt) || 0;
|
|
3088
|
+
}
|
|
3089
|
+
const result = await client.execute({
|
|
3090
|
+
sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
|
|
3091
|
+
args: []
|
|
3092
|
+
});
|
|
3093
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3094
|
+
}
|
|
3095
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
3096
|
+
const client = getClient();
|
|
3097
|
+
if (sessionScope) {
|
|
3098
|
+
const result2 = await client.execute({
|
|
3099
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3100
|
+
WHERE status = 'needs_review' AND updated_at > ?
|
|
3101
|
+
AND session_scope = ?`,
|
|
3102
|
+
args: [sinceIso, sessionScope]
|
|
3103
|
+
});
|
|
3104
|
+
return Number(result2.rows[0]?.cnt) || 0;
|
|
3105
|
+
}
|
|
3106
|
+
const result = await client.execute({
|
|
3107
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3108
|
+
WHERE status = 'needs_review' AND updated_at > ?`,
|
|
3109
|
+
args: [sinceIso]
|
|
3110
|
+
});
|
|
3111
|
+
return Number(result.rows[0]?.cnt) || 0;
|
|
3112
|
+
}
|
|
3113
|
+
async function listPendingReviews(limit, sessionScope) {
|
|
3114
|
+
const client = getClient();
|
|
3115
|
+
if (sessionScope) {
|
|
3116
|
+
const result2 = await client.execute({
|
|
3117
|
+
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
3118
|
+
WHERE status = 'needs_review'
|
|
3119
|
+
AND session_scope = ?
|
|
3120
|
+
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
3121
|
+
args: [sessionScope, limit]
|
|
3122
|
+
});
|
|
3123
|
+
return result2.rows;
|
|
3124
|
+
}
|
|
3125
|
+
const result = await client.execute({
|
|
3126
|
+
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
3127
|
+
WHERE status = 'needs_review'
|
|
3128
|
+
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
3129
|
+
args: [limit]
|
|
3130
|
+
});
|
|
3131
|
+
return result.rows;
|
|
3132
|
+
}
|
|
3133
|
+
async function cleanupOrphanedReviews() {
|
|
3134
|
+
const client = getClient();
|
|
3135
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3136
|
+
const r1 = await client.execute({
|
|
3137
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
3138
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
3139
|
+
AND assigned_by = 'system'
|
|
3140
|
+
AND title LIKE 'Review:%'
|
|
3141
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
3142
|
+
args: [now]
|
|
3143
|
+
});
|
|
3144
|
+
const r1b = await client.execute({
|
|
3145
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
3146
|
+
WHERE status IN ('open', 'needs_review')
|
|
3147
|
+
AND title LIKE 'Review:%completed%'
|
|
3148
|
+
AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
|
|
3149
|
+
args: [now]
|
|
3150
|
+
});
|
|
3151
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
3152
|
+
const r2 = await client.execute({
|
|
3153
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3154
|
+
WHERE status = 'needs_review'
|
|
3155
|
+
AND result IS NOT NULL
|
|
3156
|
+
AND updated_at < ?`,
|
|
3157
|
+
args: [now, staleThreshold]
|
|
3158
|
+
});
|
|
3159
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
3160
|
+
if (total > 0) {
|
|
3161
|
+
process.stderr.write(
|
|
3162
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
3163
|
+
`
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
return total;
|
|
3167
|
+
}
|
|
3168
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
3169
|
+
const roleLower = role.toLowerCase();
|
|
3170
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
3171
|
+
return {
|
|
3172
|
+
lens: "Code Quality (Engineer)",
|
|
3173
|
+
checklist: [
|
|
3174
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
3175
|
+
"2. Is the code clean \u2014 no dead code, no TODOs left?",
|
|
3176
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
3177
|
+
"4. Any regressions in the test suite?"
|
|
3178
|
+
]
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
3182
|
+
return {
|
|
3183
|
+
lens: "Architecture (CTO)",
|
|
3184
|
+
checklist: [
|
|
3185
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
3186
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
3187
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
3188
|
+
"4. Security implications? Any new attack surface?",
|
|
3189
|
+
"5. Does it scale? Performance considerations?",
|
|
3190
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
3191
|
+
]
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
3195
|
+
return {
|
|
3196
|
+
lens: "Strategic (COO)",
|
|
3197
|
+
checklist: [
|
|
3198
|
+
"1. Does this serve the project mission?",
|
|
3199
|
+
"2. Is this the right work at the right time?",
|
|
3200
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
3201
|
+
"4. Any cross-project implications?"
|
|
3202
|
+
]
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
return {
|
|
3206
|
+
lens: "General",
|
|
3207
|
+
checklist: [
|
|
3208
|
+
"1. Read the original task's acceptance criteria",
|
|
3209
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
3210
|
+
"3. Verify code changes match requirements",
|
|
3211
|
+
"4. Check if tests were added/updated",
|
|
3212
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
3213
|
+
]
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
3217
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
3218
|
+
try {
|
|
3219
|
+
const client = getClient();
|
|
3220
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3221
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
3222
|
+
if (parentId) {
|
|
3223
|
+
const result = await client.execute({
|
|
3224
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
3225
|
+
args: [now, parentId]
|
|
3226
|
+
});
|
|
3227
|
+
if (result.rowsAffected > 0) {
|
|
3228
|
+
process.stderr.write(
|
|
3229
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
3230
|
+
`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
} else {
|
|
3234
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
3235
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
3236
|
+
const parts = reviewPrefix.split("-");
|
|
3237
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
3238
|
+
const agent = parts[1];
|
|
3239
|
+
const slug = parts.slice(2).join("-");
|
|
3240
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
3241
|
+
const result = await client.execute({
|
|
3242
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
3243
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
3244
|
+
});
|
|
3245
|
+
if (result.rowsAffected > 0) {
|
|
3246
|
+
process.stderr.write(
|
|
3247
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
3248
|
+
`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
} catch (err) {
|
|
3254
|
+
process.stderr.write(
|
|
3255
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
3256
|
+
`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
try {
|
|
3260
|
+
const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
|
|
3261
|
+
if (existsSync10(cacheDir)) {
|
|
3262
|
+
for (const f of readdirSync3(cacheDir)) {
|
|
3263
|
+
if (f.startsWith("review-notified-")) {
|
|
3264
|
+
unlinkSync5(path11.join(cacheDir, f));
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
} catch {
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
var init_tasks_review = __esm({
|
|
3272
|
+
"src/lib/tasks-review.ts"() {
|
|
3273
|
+
"use strict";
|
|
3274
|
+
init_database();
|
|
3275
|
+
init_config();
|
|
3276
|
+
init_employees();
|
|
3277
|
+
init_notifications();
|
|
3278
|
+
init_tmux_routing();
|
|
3279
|
+
init_session_key();
|
|
3280
|
+
init_state_bus();
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
|
|
3284
|
+
// src/lib/tasks-chain.ts
|
|
3285
|
+
import path12 from "path";
|
|
3286
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
3287
|
+
async function cascadeUnblock(taskId, baseDir, now) {
|
|
3288
|
+
const client = getClient();
|
|
3289
|
+
const unblocked = await client.execute({
|
|
3290
|
+
sql: `UPDATE tasks SET status = 'open', blocked_by = NULL, updated_at = ?
|
|
3291
|
+
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
3292
|
+
args: [now, taskId]
|
|
3293
|
+
});
|
|
3294
|
+
if (baseDir && unblocked.rowsAffected > 0) {
|
|
3295
|
+
const ubScope = sessionScopeFilter();
|
|
3296
|
+
const unblockedRows = await client.execute({
|
|
3297
|
+
sql: `SELECT task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?${ubScope.sql}`,
|
|
3298
|
+
args: [now, ...ubScope.args]
|
|
3299
|
+
});
|
|
3300
|
+
for (const ur of unblockedRows.rows) {
|
|
3301
|
+
try {
|
|
3302
|
+
const ubFile = path12.join(baseDir, String(ur.task_file));
|
|
3303
|
+
let ubContent = await readFile3(ubFile, "utf-8");
|
|
3304
|
+
ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
|
|
3305
|
+
ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
|
|
3306
|
+
await writeFile4(ubFile, ubContent, "utf-8");
|
|
3307
|
+
} catch {
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
async function findNextTask(assignedTo) {
|
|
3313
|
+
const client = getClient();
|
|
3314
|
+
const ntScope = sessionScopeFilter();
|
|
3315
|
+
const nextResult = await client.execute({
|
|
3316
|
+
sql: `SELECT title, task_file, priority FROM tasks
|
|
3317
|
+
WHERE assigned_to = ? AND status = 'open'${ntScope.sql}
|
|
3318
|
+
ORDER BY priority ASC, created_at ASC
|
|
3319
|
+
LIMIT 1`,
|
|
3320
|
+
args: [assignedTo, ...ntScope.args]
|
|
3321
|
+
});
|
|
3322
|
+
if (nextResult.rows.length === 1) {
|
|
3323
|
+
const nr = nextResult.rows[0];
|
|
3324
|
+
return {
|
|
3325
|
+
title: String(nr.title),
|
|
3326
|
+
priority: String(nr.priority),
|
|
3327
|
+
taskFile: String(nr.task_file)
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
return void 0;
|
|
3331
|
+
}
|
|
3332
|
+
async function checkSubtaskCompletion(parentTaskId, projectName) {
|
|
3333
|
+
const client = getClient();
|
|
3334
|
+
const scScope = sessionScopeFilter();
|
|
3335
|
+
const remaining = await client.execute({
|
|
3336
|
+
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
3337
|
+
WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
|
|
3338
|
+
args: [parentTaskId, ...scScope.args]
|
|
3339
|
+
});
|
|
3340
|
+
const cnt = Number(remaining.rows[0]?.cnt ?? 1);
|
|
3341
|
+
if (cnt === 0) {
|
|
3342
|
+
const parentRow = await client.execute({
|
|
3343
|
+
sql: `SELECT assigned_to, title, task_file, project_name FROM tasks WHERE id = ?`,
|
|
3344
|
+
args: [parentTaskId]
|
|
3345
|
+
});
|
|
3346
|
+
if (parentRow.rows.length === 1) {
|
|
3347
|
+
const pr = parentRow.rows[0];
|
|
3348
|
+
const parentProject = pr.project_name == null ? projectName : String(pr.project_name);
|
|
3349
|
+
await writeNotification({
|
|
3350
|
+
agentId: String(pr.assigned_to),
|
|
3351
|
+
agentRole: "system",
|
|
3352
|
+
event: "subtasks_complete",
|
|
3353
|
+
project: parentProject,
|
|
3354
|
+
summary: `All subtasks complete for "${String(pr.title)}" \u2014 ready for rollup review`,
|
|
3355
|
+
taskFile: String(pr.task_file)
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
var init_tasks_chain = __esm({
|
|
3361
|
+
"src/lib/tasks-chain.ts"() {
|
|
3362
|
+
"use strict";
|
|
3363
|
+
init_database();
|
|
3364
|
+
init_notifications();
|
|
3365
|
+
init_task_scope();
|
|
3366
|
+
}
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3369
|
+
// src/lib/project-name.ts
|
|
3370
|
+
import { execSync as execSync6 } from "child_process";
|
|
3371
|
+
import path13 from "path";
|
|
3372
|
+
function getProjectName(cwd) {
|
|
3373
|
+
const dir = cwd ?? process.cwd();
|
|
3374
|
+
if (_cached2 && _cachedCwd === dir) return _cached2;
|
|
3375
|
+
try {
|
|
3376
|
+
let repoRoot;
|
|
3377
|
+
try {
|
|
3378
|
+
const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
|
|
3379
|
+
cwd: dir,
|
|
3380
|
+
encoding: "utf8",
|
|
3381
|
+
timeout: 2e3,
|
|
3382
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3383
|
+
}).trim();
|
|
3384
|
+
repoRoot = path13.dirname(gitCommonDir);
|
|
3385
|
+
} catch {
|
|
3386
|
+
repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
3387
|
+
cwd: dir,
|
|
3388
|
+
encoding: "utf8",
|
|
3389
|
+
timeout: 2e3,
|
|
3390
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3391
|
+
}).trim();
|
|
3392
|
+
}
|
|
3393
|
+
_cached2 = path13.basename(repoRoot);
|
|
3394
|
+
_cachedCwd = dir;
|
|
3395
|
+
return _cached2;
|
|
3396
|
+
} catch {
|
|
3397
|
+
_cached2 = path13.basename(dir);
|
|
3398
|
+
_cachedCwd = dir;
|
|
3399
|
+
return _cached2;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
var _cached2, _cachedCwd;
|
|
3403
|
+
var init_project_name = __esm({
|
|
3404
|
+
"src/lib/project-name.ts"() {
|
|
3405
|
+
"use strict";
|
|
3406
|
+
_cached2 = null;
|
|
3407
|
+
_cachedCwd = null;
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
|
|
3411
|
+
// src/lib/session-scope.ts
|
|
3412
|
+
var session_scope_exports = {};
|
|
3413
|
+
__export(session_scope_exports, {
|
|
3414
|
+
assertSessionScope: () => assertSessionScope,
|
|
3415
|
+
findSessionForProject: () => findSessionForProject,
|
|
3416
|
+
getSessionProject: () => getSessionProject
|
|
3417
|
+
});
|
|
3418
|
+
function getSessionProject(sessionName) {
|
|
3419
|
+
const sessions = listSessions();
|
|
3420
|
+
const entry = sessions.find((s) => s.windowName === sessionName);
|
|
3421
|
+
if (!entry) return null;
|
|
3422
|
+
const parts = entry.projectDir.split("/").filter(Boolean);
|
|
3423
|
+
return parts[parts.length - 1] ?? null;
|
|
3424
|
+
}
|
|
3425
|
+
function findSessionForProject(projectName) {
|
|
3426
|
+
const sessions = listSessions();
|
|
3427
|
+
for (const s of sessions) {
|
|
3428
|
+
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
3429
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
3430
|
+
}
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
function assertSessionScope(actionType, targetProject) {
|
|
3434
|
+
try {
|
|
3435
|
+
const currentProject = getProjectName();
|
|
3436
|
+
const exeSession = resolveExeSession();
|
|
3437
|
+
if (!exeSession) {
|
|
3438
|
+
return { allowed: true, reason: "no_session" };
|
|
3439
|
+
}
|
|
3440
|
+
if (currentProject === targetProject) {
|
|
3441
|
+
return {
|
|
3442
|
+
allowed: true,
|
|
3443
|
+
reason: "same_session",
|
|
3444
|
+
currentProject,
|
|
3445
|
+
targetProject
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
process.stderr.write(
|
|
3449
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
3450
|
+
`
|
|
3451
|
+
);
|
|
3452
|
+
return {
|
|
3453
|
+
allowed: false,
|
|
3454
|
+
reason: "cross_session_denied",
|
|
3455
|
+
currentProject,
|
|
3456
|
+
targetProject,
|
|
3457
|
+
targetSession: findSessionForProject(targetProject)?.windowName
|
|
3458
|
+
};
|
|
3459
|
+
} catch {
|
|
3460
|
+
return { allowed: true, reason: "no_session" };
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
var init_session_scope = __esm({
|
|
3464
|
+
"src/lib/session-scope.ts"() {
|
|
3465
|
+
"use strict";
|
|
3466
|
+
init_session_registry();
|
|
3467
|
+
init_project_name();
|
|
3468
|
+
init_tmux_routing();
|
|
3469
|
+
init_employees();
|
|
3470
|
+
}
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3473
|
+
// src/lib/tasks-notify.ts
|
|
3474
|
+
async function dispatchTaskToEmployee(input2) {
|
|
3475
|
+
if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
|
|
3476
|
+
let crossProject = false;
|
|
3477
|
+
if (input2.projectName) {
|
|
3478
|
+
try {
|
|
3479
|
+
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
3480
|
+
const check = assertSessionScope2("dispatch_task", input2.projectName);
|
|
3481
|
+
if (check.reason === "cross_session_denied") {
|
|
3482
|
+
crossProject = true;
|
|
3483
|
+
return { dispatched: "skipped", crossProject: true };
|
|
3484
|
+
}
|
|
3485
|
+
} catch {
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
try {
|
|
3489
|
+
const transport = getTransport();
|
|
3490
|
+
const exeSession = resolveExeSession();
|
|
3491
|
+
if (!exeSession) return { dispatched: "session_missing" };
|
|
3492
|
+
const sessionName = employeeSessionName(input2.assignedTo, exeSession);
|
|
3493
|
+
if (transport.isAlive(sessionName)) {
|
|
3494
|
+
const result = sendIntercom(sessionName);
|
|
3495
|
+
const dispatched = result === "acknowledged" || result === "debounced" || result === "queued" ? "verified" : result === "delivered" ? "sent_unverified" : "session_dead";
|
|
3496
|
+
return { dispatched, session: sessionName, crossProject };
|
|
3497
|
+
} else {
|
|
3498
|
+
const projectDir = input2.projectDir ?? process.cwd();
|
|
3499
|
+
const result = ensureEmployee(input2.assignedTo, exeSession, projectDir, {
|
|
3500
|
+
autoInstance: isMultiInstance(input2.assignedTo)
|
|
3501
|
+
});
|
|
3502
|
+
if (result.status === "failed") {
|
|
3503
|
+
process.stderr.write(
|
|
3504
|
+
`[dispatch] Failed to spawn ${input2.assignedTo}: ${result.error}
|
|
3505
|
+
`
|
|
3506
|
+
);
|
|
3507
|
+
return { dispatched: "session_missing" };
|
|
3508
|
+
}
|
|
3509
|
+
return { dispatched: "spawned", session: result.sessionName, crossProject };
|
|
3510
|
+
}
|
|
3511
|
+
} catch {
|
|
3512
|
+
return { dispatched: "session_missing" };
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
function notifyTaskDone() {
|
|
3516
|
+
try {
|
|
3517
|
+
const key = getSessionKey();
|
|
3518
|
+
if (key && !process.env.VITEST) notifyParentExe(key);
|
|
3519
|
+
} catch {
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
async function markTaskNotificationsRead(taskFile) {
|
|
3523
|
+
try {
|
|
3524
|
+
await markAsReadByTaskFile(taskFile);
|
|
3525
|
+
} catch {
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
var init_tasks_notify = __esm({
|
|
3529
|
+
"src/lib/tasks-notify.ts"() {
|
|
3530
|
+
"use strict";
|
|
3531
|
+
init_tmux_routing();
|
|
3532
|
+
init_session_key();
|
|
3533
|
+
init_notifications();
|
|
3534
|
+
init_transport();
|
|
3535
|
+
init_employees();
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
// src/lib/behaviors.ts
|
|
3540
|
+
import crypto4 from "crypto";
|
|
3541
|
+
async function storeBehavior(opts) {
|
|
3542
|
+
const client = getClient();
|
|
3543
|
+
const id = crypto4.randomUUID();
|
|
3544
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3545
|
+
await client.execute({
|
|
3546
|
+
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
3547
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
3548
|
+
args: [id, opts.agentId, opts.projectName ?? null, opts.domain ?? null, opts.priority ?? "p1", opts.content, now, now]
|
|
3549
|
+
});
|
|
3550
|
+
return id;
|
|
3551
|
+
}
|
|
3552
|
+
var init_behaviors = __esm({
|
|
3553
|
+
"src/lib/behaviors.ts"() {
|
|
3554
|
+
"use strict";
|
|
3555
|
+
init_database();
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
// src/lib/skill-learning.ts
|
|
3560
|
+
var skill_learning_exports = {};
|
|
3561
|
+
__export(skill_learning_exports, {
|
|
3562
|
+
captureAndLearn: () => captureAndLearn,
|
|
3563
|
+
captureTrajectory: () => captureTrajectory,
|
|
3564
|
+
editDistance: () => editDistance,
|
|
3565
|
+
extractSkill: () => extractSkill,
|
|
3566
|
+
extractTrajectory: () => extractTrajectory,
|
|
3567
|
+
findSimilarTrajectories: () => findSimilarTrajectories,
|
|
3568
|
+
hashSignature: () => hashSignature,
|
|
3569
|
+
storeTrajectory: () => storeTrajectory,
|
|
3570
|
+
sweepTrajectories: () => sweepTrajectories
|
|
3571
|
+
});
|
|
3572
|
+
import crypto5 from "crypto";
|
|
3573
|
+
async function extractTrajectory(taskId, agentId) {
|
|
3574
|
+
const client = getClient();
|
|
3575
|
+
const result = await client.execute({
|
|
3576
|
+
sql: `SELECT tool_name, raw_text
|
|
3577
|
+
FROM memories
|
|
3578
|
+
WHERE task_id = ? AND agent_id = ?
|
|
3579
|
+
ORDER BY timestamp ASC`,
|
|
3580
|
+
args: [taskId, agentId]
|
|
3581
|
+
});
|
|
3582
|
+
if (result.rows.length === 0) return [];
|
|
3583
|
+
const rawTools = result.rows.map((r) => {
|
|
3584
|
+
const toolName = String(r.tool_name);
|
|
3585
|
+
if (toolName === "Bash") {
|
|
3586
|
+
const text = String(r.raw_text);
|
|
3587
|
+
const cmdMatch = text.match(/(?:command|Command).*?[:\s]+"?(\w+)/);
|
|
3588
|
+
return cmdMatch ? `Bash:${cmdMatch[1]}` : "Bash";
|
|
3589
|
+
}
|
|
3590
|
+
return toolName;
|
|
3591
|
+
});
|
|
3592
|
+
const signature = [];
|
|
3593
|
+
for (const tool of rawTools) {
|
|
3594
|
+
if (signature.length === 0 || signature[signature.length - 1] !== tool) {
|
|
3595
|
+
signature.push(tool);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return signature;
|
|
3599
|
+
}
|
|
3600
|
+
function hashSignature(signature) {
|
|
3601
|
+
return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
3602
|
+
}
|
|
3603
|
+
async function storeTrajectory(opts) {
|
|
3604
|
+
const client = getClient();
|
|
3605
|
+
const id = crypto5.randomUUID();
|
|
3606
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3607
|
+
const signatureHash = hashSignature(opts.signature);
|
|
3608
|
+
await client.execute({
|
|
3609
|
+
sql: `INSERT INTO trajectories (id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at)
|
|
3610
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3611
|
+
args: [
|
|
3612
|
+
id,
|
|
3613
|
+
opts.taskId,
|
|
3614
|
+
opts.agentId,
|
|
3615
|
+
opts.projectName,
|
|
3616
|
+
opts.taskTitle,
|
|
3617
|
+
JSON.stringify(opts.signature),
|
|
3618
|
+
signatureHash,
|
|
3619
|
+
opts.signature.length,
|
|
3620
|
+
now
|
|
3621
|
+
]
|
|
3622
|
+
});
|
|
3623
|
+
return id;
|
|
3624
|
+
}
|
|
3625
|
+
async function findSimilarTrajectories(signature, threshold = DEFAULT_SKILL_THRESHOLD) {
|
|
3626
|
+
const client = getClient();
|
|
3627
|
+
const hash = hashSignature(signature);
|
|
3628
|
+
const result = await client.execute({
|
|
3629
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
3630
|
+
FROM trajectories
|
|
3631
|
+
WHERE signature_hash = ?
|
|
3632
|
+
ORDER BY created_at DESC
|
|
3633
|
+
LIMIT 20`,
|
|
3634
|
+
args: [hash]
|
|
3635
|
+
});
|
|
3636
|
+
const mapRow = (r) => ({
|
|
3637
|
+
id: String(r.id),
|
|
3638
|
+
taskId: String(r.task_id),
|
|
3639
|
+
agentId: String(r.agent_id),
|
|
3640
|
+
projectName: String(r.project_name),
|
|
3641
|
+
taskTitle: String(r.task_title),
|
|
3642
|
+
signature: JSON.parse(String(r.signature)),
|
|
3643
|
+
signatureHash: String(r.signature_hash),
|
|
3644
|
+
toolCount: Number(r.tool_count),
|
|
3645
|
+
skillId: r.skill_id ? String(r.skill_id) : null,
|
|
3646
|
+
createdAt: String(r.created_at)
|
|
3647
|
+
});
|
|
3648
|
+
const matches = result.rows.map(mapRow);
|
|
3649
|
+
if (matches.length >= threshold) return matches;
|
|
3650
|
+
const nearResult = await client.execute({
|
|
3651
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, skill_id, created_at
|
|
3652
|
+
FROM trajectories
|
|
3653
|
+
WHERE tool_count BETWEEN ? AND ?
|
|
3654
|
+
AND signature_hash != ?
|
|
3655
|
+
ORDER BY created_at DESC
|
|
3656
|
+
LIMIT 50`,
|
|
3657
|
+
args: [
|
|
3658
|
+
Math.max(1, signature.length - 3),
|
|
3659
|
+
signature.length + 3,
|
|
3660
|
+
hash
|
|
3661
|
+
]
|
|
3662
|
+
});
|
|
3663
|
+
for (const r of nearResult.rows) {
|
|
3664
|
+
const candidateSig = JSON.parse(String(r.signature));
|
|
3665
|
+
if (editDistance(signature, candidateSig) <= 2) {
|
|
3666
|
+
matches.push(mapRow(r));
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
return matches;
|
|
3670
|
+
}
|
|
3671
|
+
async function captureTrajectory(opts) {
|
|
3672
|
+
const signature = await extractTrajectory(opts.taskId, opts.agentId);
|
|
3673
|
+
if (signature.length < 3) {
|
|
3674
|
+
return { trajectoryId: "", similarCount: 0, similar: [] };
|
|
3675
|
+
}
|
|
3676
|
+
const trajectoryId = await storeTrajectory({
|
|
3677
|
+
taskId: opts.taskId,
|
|
3678
|
+
agentId: opts.agentId,
|
|
3679
|
+
projectName: opts.projectName,
|
|
3680
|
+
taskTitle: opts.taskTitle,
|
|
3681
|
+
signature
|
|
3682
|
+
});
|
|
3683
|
+
const similar = await findSimilarTrajectories(
|
|
3684
|
+
signature,
|
|
3685
|
+
opts.skillThreshold ?? DEFAULT_SKILL_THRESHOLD
|
|
3686
|
+
);
|
|
3687
|
+
return { trajectoryId, similarCount: similar.length, similar };
|
|
3688
|
+
}
|
|
3689
|
+
function buildExtractionPrompt(trajectories) {
|
|
3690
|
+
const items = trajectories.map((t, i) => {
|
|
3691
|
+
const sig = t.signature.join(" \u2192 ");
|
|
3692
|
+
return `Task ${i + 1}: "${t.taskTitle}" (${t.agentId}, ${t.projectName}) \u2014 ${t.toolCount} tool calls
|
|
3693
|
+
Signature: ${sig}`;
|
|
3694
|
+
}).join("\n\n");
|
|
3695
|
+
return `You are analyzing ${trajectories.length} completed tasks that followed similar procedures:
|
|
3696
|
+
|
|
3697
|
+
${items}
|
|
3698
|
+
|
|
3699
|
+
Extract the reusable procedure. Format your response EXACTLY like this:
|
|
3700
|
+
|
|
3701
|
+
SKILL: {name \u2014 short, descriptive}
|
|
3702
|
+
TRIGGER: {when to use this \u2014 one sentence}
|
|
3703
|
+
STEPS:
|
|
3704
|
+
1. ...
|
|
3705
|
+
2. ...
|
|
3706
|
+
PITFALLS: {common mistakes to avoid}
|
|
3707
|
+
|
|
3708
|
+
Be specific and actionable. Include tool names, file patterns, and concrete commands where applicable.`;
|
|
3709
|
+
}
|
|
3710
|
+
async function extractSkill(trajectories, model) {
|
|
3711
|
+
if (trajectories.length === 0) return null;
|
|
3712
|
+
const config = await loadConfig();
|
|
3713
|
+
const skillModel = model ?? config.skillModel;
|
|
3714
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
3715
|
+
const client = new Anthropic();
|
|
3716
|
+
const prompt = buildExtractionPrompt(trajectories);
|
|
3717
|
+
const response = await client.messages.create({
|
|
3718
|
+
model: skillModel,
|
|
3719
|
+
max_tokens: 500,
|
|
3720
|
+
messages: [{ role: "user", content: prompt }]
|
|
3721
|
+
});
|
|
3722
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
3723
|
+
const skillText = textBlock?.text;
|
|
3724
|
+
if (!skillText) return null;
|
|
3725
|
+
const agentId = trajectories[0].agentId;
|
|
3726
|
+
const projectName = trajectories[0].projectName;
|
|
3727
|
+
const skillId = await storeBehavior({
|
|
3728
|
+
agentId,
|
|
3729
|
+
content: skillText,
|
|
3730
|
+
domain: "skill",
|
|
3731
|
+
projectName
|
|
3732
|
+
});
|
|
3733
|
+
const dbClient = getClient();
|
|
3734
|
+
for (const t of trajectories) {
|
|
3735
|
+
await dbClient.execute({
|
|
3736
|
+
sql: "UPDATE trajectories SET skill_id = ? WHERE id = ?",
|
|
3737
|
+
args: [skillId, t.id]
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
process.stderr.write(
|
|
3741
|
+
`[skill-learning] Skill extracted from ${trajectories.length} trajectories \u2192 behavior ${skillId}
|
|
3742
|
+
`
|
|
3743
|
+
);
|
|
3744
|
+
return skillId;
|
|
3745
|
+
}
|
|
3746
|
+
async function captureAndLearn(opts) {
|
|
3747
|
+
try {
|
|
3748
|
+
const config = await loadConfig();
|
|
3749
|
+
if (!config.skillLearning) return;
|
|
3750
|
+
const { trajectoryId, similarCount, similar } = await captureTrajectory({
|
|
3751
|
+
...opts,
|
|
3752
|
+
skillThreshold: config.skillThreshold
|
|
3753
|
+
});
|
|
3754
|
+
if (!trajectoryId) return;
|
|
3755
|
+
if (similarCount >= config.skillThreshold) {
|
|
3756
|
+
const unprocessed = similar.filter((t) => !t.skillId);
|
|
3757
|
+
if (unprocessed.length >= config.skillThreshold) {
|
|
3758
|
+
extractSkill(unprocessed, config.skillModel).catch((err) => {
|
|
3759
|
+
process.stderr.write(
|
|
3760
|
+
`[skill-learning] Extraction failed: ${err instanceof Error ? err.message : String(err)}
|
|
3761
|
+
`
|
|
3762
|
+
);
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
process.stderr.write(
|
|
3768
|
+
`[skill-learning] captureAndLearn failed: ${err instanceof Error ? err.message : String(err)}
|
|
3769
|
+
`
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
async function sweepTrajectories(threshold, model) {
|
|
3774
|
+
const config = await loadConfig();
|
|
3775
|
+
if (!config.skillLearning) return { clustersProcessed: 0, skillsExtracted: 0 };
|
|
3776
|
+
const t = threshold ?? config.skillThreshold;
|
|
3777
|
+
const client = getClient();
|
|
3778
|
+
const result = await client.execute({
|
|
3779
|
+
sql: `SELECT signature_hash, COUNT(*) as cnt
|
|
3780
|
+
FROM trajectories
|
|
3781
|
+
WHERE skill_id IS NULL
|
|
3782
|
+
GROUP BY signature_hash
|
|
3783
|
+
HAVING cnt >= ?
|
|
3784
|
+
ORDER BY cnt DESC
|
|
3785
|
+
LIMIT 10`,
|
|
3786
|
+
args: [t]
|
|
3787
|
+
});
|
|
3788
|
+
let clustersProcessed = 0;
|
|
3789
|
+
let skillsExtracted = 0;
|
|
3790
|
+
for (const row of result.rows) {
|
|
3791
|
+
const hash = String(row.signature_hash);
|
|
3792
|
+
const trajResult = await client.execute({
|
|
3793
|
+
sql: `SELECT id, task_id, agent_id, project_name, task_title, signature, signature_hash, tool_count, created_at
|
|
3794
|
+
FROM trajectories
|
|
3795
|
+
WHERE signature_hash = ? AND skill_id IS NULL
|
|
3796
|
+
ORDER BY created_at DESC
|
|
3797
|
+
LIMIT 10`,
|
|
3798
|
+
args: [hash]
|
|
3799
|
+
});
|
|
3800
|
+
const trajectories = trajResult.rows.map((r) => ({
|
|
3801
|
+
id: String(r.id),
|
|
3802
|
+
taskId: String(r.task_id),
|
|
3803
|
+
agentId: String(r.agent_id),
|
|
3804
|
+
projectName: String(r.project_name),
|
|
3805
|
+
taskTitle: String(r.task_title),
|
|
3806
|
+
signature: JSON.parse(String(r.signature)),
|
|
3807
|
+
signatureHash: String(r.signature_hash),
|
|
3808
|
+
toolCount: Number(r.tool_count),
|
|
3809
|
+
skillId: null,
|
|
3810
|
+
createdAt: String(r.created_at)
|
|
3811
|
+
}));
|
|
3812
|
+
if (trajectories.length >= t) {
|
|
3813
|
+
clustersProcessed++;
|
|
3814
|
+
const skillId = await extractSkill(trajectories, model ?? config.skillModel);
|
|
3815
|
+
if (skillId) skillsExtracted++;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
return { clustersProcessed, skillsExtracted };
|
|
3819
|
+
}
|
|
3820
|
+
function editDistance(a, b) {
|
|
3821
|
+
const m = a.length;
|
|
3822
|
+
const n = b.length;
|
|
3823
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
3824
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
3825
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
3826
|
+
for (let i = 1; i <= m; i++) {
|
|
3827
|
+
for (let j = 1; j <= n; j++) {
|
|
3828
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3829
|
+
dp[i][j] = Math.min(
|
|
3830
|
+
dp[i - 1][j] + 1,
|
|
3831
|
+
dp[i][j - 1] + 1,
|
|
3832
|
+
dp[i - 1][j - 1] + cost
|
|
3833
|
+
);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return dp[m][n];
|
|
3837
|
+
}
|
|
3838
|
+
var DEFAULT_SKILL_THRESHOLD;
|
|
3839
|
+
var init_skill_learning = __esm({
|
|
3840
|
+
"src/lib/skill-learning.ts"() {
|
|
3841
|
+
"use strict";
|
|
3842
|
+
init_database();
|
|
3843
|
+
init_behaviors();
|
|
3844
|
+
init_config();
|
|
3845
|
+
DEFAULT_SKILL_THRESHOLD = 3;
|
|
3846
|
+
}
|
|
3847
|
+
});
|
|
3848
|
+
|
|
3849
|
+
// src/lib/tasks.ts
|
|
3850
|
+
var tasks_exports = {};
|
|
3851
|
+
__export(tasks_exports, {
|
|
3852
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
3853
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
3854
|
+
countPendingReviews: () => countPendingReviews,
|
|
3855
|
+
createTask: () => createTask,
|
|
3856
|
+
createTaskCore: () => createTaskCore,
|
|
3857
|
+
deleteTask: () => deleteTask,
|
|
3858
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
3859
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
3860
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
3861
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
3862
|
+
listPendingReviews: () => listPendingReviews,
|
|
3863
|
+
listTasks: () => listTasks,
|
|
3864
|
+
resolveTask: () => resolveTask,
|
|
3865
|
+
slugify: () => slugify,
|
|
3866
|
+
updateTask: () => updateTask,
|
|
3867
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
3868
|
+
writeCheckpoint: () => writeCheckpoint
|
|
3869
|
+
});
|
|
3870
|
+
import path14 from "path";
|
|
3871
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync6 } from "fs";
|
|
3872
|
+
async function createTask(input2) {
|
|
3873
|
+
const result = await createTaskCore(input2);
|
|
3874
|
+
if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
|
|
3875
|
+
dispatchTaskToEmployee({
|
|
3876
|
+
assignedTo: input2.assignedTo,
|
|
3877
|
+
title: input2.title,
|
|
3878
|
+
priority: input2.priority,
|
|
3879
|
+
taskFile: result.taskFile,
|
|
3880
|
+
initialStatus: result.status,
|
|
3881
|
+
projectName: input2.projectName
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
return result;
|
|
3885
|
+
}
|
|
3886
|
+
async function updateTask(input2) {
|
|
3887
|
+
const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
|
|
3888
|
+
try {
|
|
3889
|
+
const agent = String(row.assigned_to);
|
|
3890
|
+
const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
|
|
3891
|
+
const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
|
|
3892
|
+
if (input2.status === "in_progress") {
|
|
3893
|
+
mkdirSync5(cacheDir, { recursive: true });
|
|
3894
|
+
writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
|
|
3895
|
+
} else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
|
|
3896
|
+
try {
|
|
3897
|
+
unlinkSync6(cachePath);
|
|
3898
|
+
} catch {
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
} catch {
|
|
3902
|
+
}
|
|
3903
|
+
if (input2.status === "done") {
|
|
3904
|
+
await cleanupReviewFile(row, taskFile, input2.baseDir);
|
|
3905
|
+
}
|
|
3906
|
+
if (input2.status === "done" || input2.status === "cancelled") {
|
|
3907
|
+
try {
|
|
3908
|
+
const client = getClient();
|
|
3909
|
+
const taskTitle = String(row.title);
|
|
3910
|
+
const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
3911
|
+
await client.execute({
|
|
3912
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
3913
|
+
WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
|
|
3914
|
+
args: [now, `%left '${escaped}' as in\\_progress%`]
|
|
3915
|
+
});
|
|
3916
|
+
} catch {
|
|
3917
|
+
}
|
|
3918
|
+
const assignedAgent = String(row.assigned_to);
|
|
3919
|
+
if (!isCoordinatorName(assignedAgent)) {
|
|
3920
|
+
try {
|
|
3921
|
+
const draftClient = getClient();
|
|
3922
|
+
if (input2.status === "done") {
|
|
3923
|
+
await draftClient.execute({
|
|
3924
|
+
sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
|
|
3925
|
+
args: [assignedAgent]
|
|
3926
|
+
});
|
|
3927
|
+
} else if (input2.status === "cancelled") {
|
|
3928
|
+
await draftClient.execute({
|
|
3929
|
+
sql: `DELETE FROM memories WHERE agent_id = ? AND draft = 1`,
|
|
3930
|
+
args: [assignedAgent]
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
} catch {
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
try {
|
|
3937
|
+
const client = getClient();
|
|
3938
|
+
const cascaded = await client.execute({
|
|
3939
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
3940
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
3941
|
+
args: [now, taskId]
|
|
3942
|
+
});
|
|
3943
|
+
if (cascaded.rowsAffected > 0) {
|
|
3944
|
+
process.stderr.write(
|
|
3945
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
3946
|
+
`
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
} catch {
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
const isTerminal = input2.status === "done" || input2.status === "needs_review";
|
|
3953
|
+
if (isTerminal) {
|
|
3954
|
+
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
3955
|
+
if (!isCoordinator) {
|
|
3956
|
+
notifyTaskDone();
|
|
3957
|
+
}
|
|
3958
|
+
await markTaskNotificationsRead(taskFile);
|
|
3959
|
+
if (input2.status === "done") {
|
|
3960
|
+
try {
|
|
3961
|
+
await cascadeUnblock(taskId, input2.baseDir, now);
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
orgBus.emit({
|
|
3965
|
+
type: "task_completed",
|
|
3966
|
+
taskId,
|
|
3967
|
+
employee: String(row.assigned_to),
|
|
3968
|
+
result: input2.result ?? "",
|
|
3969
|
+
timestamp: now
|
|
3970
|
+
});
|
|
3971
|
+
if (row.parent_task_id) {
|
|
3972
|
+
try {
|
|
3973
|
+
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
3974
|
+
} catch {
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3980
|
+
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
3981
|
+
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
3982
|
+
taskId,
|
|
3983
|
+
agentId: String(row.assigned_to),
|
|
3984
|
+
projectName: String(row.project_name),
|
|
3985
|
+
taskTitle: String(row.title)
|
|
3986
|
+
})
|
|
3987
|
+
).catch((err) => {
|
|
3988
|
+
process.stderr.write(
|
|
3989
|
+
`[updateTask] skill learning failed: ${err instanceof Error ? err.message : String(err)}
|
|
3990
|
+
`
|
|
3991
|
+
);
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
let nextTask;
|
|
3995
|
+
if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
|
|
3996
|
+
try {
|
|
3997
|
+
nextTask = await findNextTask(String(row.assigned_to));
|
|
3998
|
+
} catch {
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
return {
|
|
4002
|
+
id: String(row.id),
|
|
4003
|
+
title: String(row.title),
|
|
4004
|
+
assignedTo: String(row.assigned_to),
|
|
4005
|
+
assignedBy: String(row.assigned_by),
|
|
4006
|
+
projectName: String(row.project_name),
|
|
4007
|
+
priority: String(row.priority),
|
|
4008
|
+
status: input2.status,
|
|
4009
|
+
taskFile,
|
|
4010
|
+
createdAt: String(row.created_at),
|
|
4011
|
+
updatedAt: now,
|
|
4012
|
+
budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
|
|
4013
|
+
budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
|
|
4014
|
+
tokensUsed: Number(row.tokens_used ?? 0),
|
|
4015
|
+
tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
|
|
4016
|
+
nextTask
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
async function deleteTask(taskId, baseDir) {
|
|
4020
|
+
const client = getClient();
|
|
4021
|
+
const { taskFile, assignedTo, assignedBy, taskSlug } = await deleteTaskCore(taskId, baseDir);
|
|
4022
|
+
const coordinatorName = getCoordinatorName();
|
|
4023
|
+
const reviewer = assignedBy || coordinatorName;
|
|
4024
|
+
const reviewSlug = `review-${assignedTo}-${taskSlug}`;
|
|
4025
|
+
const reviewFile = `exe/${reviewer}/${reviewSlug}.md`;
|
|
4026
|
+
const legacyReviewFile = `exe/${coordinatorName}/${reviewSlug}.md`;
|
|
4027
|
+
await client.execute({
|
|
4028
|
+
sql: "DELETE FROM tasks WHERE task_file = ? OR task_file = ? OR task_file = ?",
|
|
4029
|
+
args: [reviewFile, legacyReviewFile, `exe/exe/${reviewSlug}.md`]
|
|
4030
|
+
});
|
|
4031
|
+
await markAsReadByTaskFile(taskFile);
|
|
4032
|
+
await markAsReadByTaskFile(reviewFile);
|
|
4033
|
+
}
|
|
4034
|
+
var init_tasks = __esm({
|
|
4035
|
+
"src/lib/tasks.ts"() {
|
|
4036
|
+
"use strict";
|
|
4037
|
+
init_database();
|
|
4038
|
+
init_config();
|
|
4039
|
+
init_notifications();
|
|
4040
|
+
init_state_bus();
|
|
4041
|
+
init_employees();
|
|
4042
|
+
init_tasks_crud();
|
|
4043
|
+
init_tasks_review();
|
|
4044
|
+
init_tasks_crud();
|
|
4045
|
+
init_tasks_chain();
|
|
4046
|
+
init_tasks_review();
|
|
4047
|
+
init_tasks_notify();
|
|
4048
|
+
}
|
|
4049
|
+
});
|
|
4050
|
+
|
|
4051
|
+
// src/lib/capacity-monitor.ts
|
|
4052
|
+
var capacity_monitor_exports = {};
|
|
4053
|
+
__export(capacity_monitor_exports, {
|
|
4054
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
4055
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
4056
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
4057
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
4058
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
4059
|
+
extractContextPercent: () => extractContextPercent,
|
|
4060
|
+
isAtCapacity: () => isAtCapacity,
|
|
4061
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
4062
|
+
pollCapacityDead: () => pollCapacityDead
|
|
4063
|
+
});
|
|
4064
|
+
function resumeTaskTitle(agentId) {
|
|
4065
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
4066
|
+
}
|
|
4067
|
+
function buildResumeContext(agentId, openTasks) {
|
|
4068
|
+
const taskList = openTasks.map(
|
|
4069
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
4070
|
+
).join("\n");
|
|
4071
|
+
return [
|
|
4072
|
+
"## Context",
|
|
4073
|
+
"",
|
|
4074
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
4075
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
4076
|
+
"",
|
|
4077
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
4078
|
+
"",
|
|
4079
|
+
taskList,
|
|
4080
|
+
"",
|
|
4081
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
4082
|
+
].join("\n");
|
|
4083
|
+
}
|
|
4084
|
+
function filterPaneContent(paneOutput) {
|
|
4085
|
+
return paneOutput.split("\n").filter((line) => {
|
|
4086
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
4087
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
4088
|
+
if (line.includes(marker)) return false;
|
|
4089
|
+
}
|
|
4090
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
4091
|
+
if (re.test(line)) return false;
|
|
4092
|
+
}
|
|
4093
|
+
return true;
|
|
4094
|
+
}).join("\n");
|
|
4095
|
+
}
|
|
4096
|
+
function extractContextPercent(paneOutput) {
|
|
4097
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
4098
|
+
if (!match) return null;
|
|
4099
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
4100
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4101
|
+
}
|
|
4102
|
+
function isAtCapacity(paneOutput) {
|
|
4103
|
+
const filtered = filterPaneContent(paneOutput);
|
|
4104
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
4105
|
+
}
|
|
4106
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
4107
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
4108
|
+
if (pendingSince === void 0) {
|
|
4109
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
4113
|
+
_pendingCapacityKill.set(agentId, now);
|
|
4114
|
+
return false;
|
|
4115
|
+
}
|
|
4116
|
+
_pendingCapacityKill.delete(agentId);
|
|
4117
|
+
return true;
|
|
4118
|
+
}
|
|
4119
|
+
function _resetPendingCapacityKills() {
|
|
4120
|
+
_pendingCapacityKill.clear();
|
|
4121
|
+
}
|
|
4122
|
+
function _resetLastRelaunchCache() {
|
|
4123
|
+
_lastRelaunch.clear();
|
|
4124
|
+
}
|
|
4125
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
4126
|
+
const client = getClient();
|
|
4127
|
+
const cmScope = sessionScopeFilter(null);
|
|
4128
|
+
const result = await client.execute({
|
|
4129
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
4130
|
+
FROM tasks
|
|
4131
|
+
WHERE assigned_to = ? AND title LIKE ?${cmScope.sql}`,
|
|
4132
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`, ...cmScope.args]
|
|
4133
|
+
});
|
|
4134
|
+
const raw = result.rows[0]?.last_created_at;
|
|
4135
|
+
if (raw === null || raw === void 0) return null;
|
|
4136
|
+
const parsed = Date.parse(String(raw));
|
|
4137
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4138
|
+
}
|
|
4139
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
4140
|
+
const cached = _lastRelaunch.get(agentId);
|
|
4141
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
4142
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
4143
|
+
if (persisted === null) return false;
|
|
4144
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
4145
|
+
_lastRelaunch.set(agentId, persisted);
|
|
4146
|
+
return true;
|
|
4147
|
+
}
|
|
4148
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
4149
|
+
const client = getClient();
|
|
4150
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4151
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
4152
|
+
const rdScope = sessionScopeFilter(null);
|
|
4153
|
+
const existing = await client.execute({
|
|
4154
|
+
sql: `SELECT id FROM tasks
|
|
4155
|
+
WHERE assigned_to = ?
|
|
4156
|
+
AND title LIKE ?
|
|
4157
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})${rdScope.sql}
|
|
4158
|
+
ORDER BY created_at DESC
|
|
4159
|
+
LIMIT 1`,
|
|
4160
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES, ...rdScope.args]
|
|
4161
|
+
});
|
|
4162
|
+
if (existing.rows.length > 0) {
|
|
4163
|
+
const taskId = String(existing.rows[0].id);
|
|
4164
|
+
await client.execute({
|
|
4165
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
4166
|
+
args: [context, now, taskId]
|
|
4167
|
+
});
|
|
4168
|
+
return { created: false, taskId };
|
|
4169
|
+
}
|
|
4170
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
4171
|
+
const task = await createTask2({
|
|
4172
|
+
title: resumeTaskTitle(agentId),
|
|
4173
|
+
assignedTo: agentId,
|
|
4174
|
+
assignedBy: "system",
|
|
4175
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
4176
|
+
priority: "p0",
|
|
4177
|
+
context,
|
|
4178
|
+
baseDir: projectDir
|
|
4179
|
+
});
|
|
4180
|
+
return { created: true, taskId: task.id };
|
|
4181
|
+
}
|
|
4182
|
+
async function pollCapacityDead() {
|
|
4183
|
+
const transport = getTransport();
|
|
4184
|
+
const relaunched = [];
|
|
4185
|
+
const registered = listSessions().filter(
|
|
4186
|
+
(s) => s.agentId !== "exe" && !isCoordinatorName(s.agentId)
|
|
4187
|
+
);
|
|
4188
|
+
if (registered.length === 0) return [];
|
|
4189
|
+
let liveSessions;
|
|
4190
|
+
try {
|
|
4191
|
+
liveSessions = transport.listSessions();
|
|
4192
|
+
} catch {
|
|
4193
|
+
return [];
|
|
4194
|
+
}
|
|
4195
|
+
for (const entry of registered) {
|
|
4196
|
+
const { windowName, agentId, projectDir } = entry;
|
|
4197
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
4198
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
4199
|
+
let pane;
|
|
4200
|
+
try {
|
|
4201
|
+
pane = transport.capturePane(windowName, 15);
|
|
4202
|
+
} catch {
|
|
4203
|
+
continue;
|
|
4204
|
+
}
|
|
4205
|
+
if (!isAtCapacity(pane)) continue;
|
|
4206
|
+
const ctxPct = extractContextPercent(pane);
|
|
4207
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
4208
|
+
process.stderr.write(
|
|
4209
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
4210
|
+
`
|
|
4211
|
+
);
|
|
4212
|
+
continue;
|
|
4213
|
+
}
|
|
4214
|
+
if (!confirmCapacityKill(agentId)) {
|
|
4215
|
+
process.stderr.write(
|
|
4216
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
4217
|
+
`
|
|
4218
|
+
);
|
|
4219
|
+
continue;
|
|
4220
|
+
}
|
|
4221
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
4222
|
+
if (!verify.atCapacity) {
|
|
4223
|
+
process.stderr.write(
|
|
4224
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
4225
|
+
`
|
|
4226
|
+
);
|
|
4227
|
+
void recordSessionKill({
|
|
4228
|
+
sessionName: windowName,
|
|
4229
|
+
agentId,
|
|
4230
|
+
reason: "capacity_false_positive_blocked"
|
|
4231
|
+
});
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
process.stderr.write(
|
|
4235
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
4236
|
+
`
|
|
4237
|
+
);
|
|
4238
|
+
try {
|
|
4239
|
+
transport.kill(windowName);
|
|
4240
|
+
void recordSessionKill({
|
|
4241
|
+
sessionName: windowName,
|
|
4242
|
+
agentId,
|
|
4243
|
+
reason: "capacity"
|
|
4244
|
+
});
|
|
4245
|
+
const client = getClient();
|
|
4246
|
+
const rlScope = sessionScopeFilter(null);
|
|
4247
|
+
const openTasks = await client.execute({
|
|
4248
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
4249
|
+
FROM tasks
|
|
4250
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')${rlScope.sql}
|
|
4251
|
+
ORDER BY
|
|
4252
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
4253
|
+
created_at ASC
|
|
4254
|
+
LIMIT 10`,
|
|
4255
|
+
args: [agentId, ...rlScope.args]
|
|
4256
|
+
});
|
|
4257
|
+
if (openTasks.rows.length === 0) {
|
|
4258
|
+
process.stderr.write(
|
|
4259
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
4260
|
+
`
|
|
4261
|
+
);
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
const { created } = await createOrRefreshResumeTask(
|
|
4265
|
+
agentId,
|
|
4266
|
+
projectDir,
|
|
4267
|
+
openTasks.rows
|
|
4268
|
+
);
|
|
4269
|
+
if (created) {
|
|
4270
|
+
await writeNotification({
|
|
4271
|
+
agentId: "system",
|
|
4272
|
+
agentRole: "daemon",
|
|
4273
|
+
event: "capacity_relaunch",
|
|
4274
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
4275
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
4279
|
+
if (created) relaunched.push(agentId);
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
process.stderr.write(
|
|
4282
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4283
|
+
`
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return relaunched;
|
|
4288
|
+
}
|
|
4289
|
+
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;
|
|
4290
|
+
var init_capacity_monitor = __esm({
|
|
4291
|
+
"src/lib/capacity-monitor.ts"() {
|
|
4292
|
+
"use strict";
|
|
4293
|
+
init_session_registry();
|
|
4294
|
+
init_transport();
|
|
4295
|
+
init_notifications();
|
|
4296
|
+
init_database();
|
|
4297
|
+
init_session_kill_telemetry();
|
|
4298
|
+
init_tmux_routing();
|
|
4299
|
+
init_task_scope();
|
|
4300
|
+
init_employees();
|
|
4301
|
+
CAPACITY_PATTERNS = [
|
|
4302
|
+
/conversation is too long/i,
|
|
4303
|
+
/maximum context length/i,
|
|
4304
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
4305
|
+
/reached.*(?:token|context).*limit/i
|
|
4306
|
+
];
|
|
4307
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
4308
|
+
CONTENT_LINE_MARKERS = [
|
|
4309
|
+
"RESUME:",
|
|
4310
|
+
"intercom",
|
|
4311
|
+
"capacity-monitor",
|
|
4312
|
+
"CAPACITY_PATTERNS",
|
|
4313
|
+
"isAtCapacity",
|
|
4314
|
+
"CONTENT_LINE_MARKERS",
|
|
4315
|
+
"pollCapacityDead",
|
|
4316
|
+
"confirmCapacityKill",
|
|
4317
|
+
"session_kills",
|
|
4318
|
+
"capacity-monitor.test"
|
|
4319
|
+
];
|
|
4320
|
+
SOURCE_CODE_MARKERS = [
|
|
4321
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
4322
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
4323
|
+
];
|
|
4324
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
4325
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
4326
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
4327
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
4328
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
4329
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
4330
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
4331
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
4332
|
+
CTX_FLOOR_PERCENT = 50;
|
|
4333
|
+
}
|
|
4334
|
+
});
|
|
4335
|
+
|
|
4336
|
+
// src/lib/tmux-routing.ts
|
|
4337
|
+
var tmux_routing_exports = {};
|
|
4338
|
+
__export(tmux_routing_exports, {
|
|
4339
|
+
acquireSpawnLock: () => acquireSpawnLock2,
|
|
4340
|
+
employeeSessionName: () => employeeSessionName,
|
|
4341
|
+
ensureEmployee: () => ensureEmployee,
|
|
4342
|
+
extractRootExe: () => extractRootExe,
|
|
4343
|
+
findFreeInstance: () => findFreeInstance,
|
|
4344
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
4345
|
+
getMySession: () => getMySession,
|
|
4346
|
+
getParentExe: () => getParentExe,
|
|
4347
|
+
getSessionState: () => getSessionState,
|
|
4348
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
4349
|
+
isExeSession: () => isExeSession,
|
|
4350
|
+
isSessionBusy: () => isSessionBusy,
|
|
4351
|
+
notifyParentExe: () => notifyParentExe,
|
|
4352
|
+
parseParentExe: () => parseParentExe,
|
|
4353
|
+
registerParentExe: () => registerParentExe,
|
|
4354
|
+
releaseSpawnLock: () => releaseSpawnLock2,
|
|
4355
|
+
resolveExeSession: () => resolveExeSession,
|
|
4356
|
+
sendIntercom: () => sendIntercom,
|
|
4357
|
+
spawnEmployee: () => spawnEmployee,
|
|
4358
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
4359
|
+
});
|
|
4360
|
+
import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
|
|
4361
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync } from "fs";
|
|
4362
|
+
import path15 from "path";
|
|
4363
|
+
import os7 from "os";
|
|
4364
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4365
|
+
import { unlinkSync as unlinkSync7 } from "fs";
|
|
4366
|
+
function spawnLockPath(sessionName) {
|
|
4367
|
+
return path15.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
4368
|
+
}
|
|
4369
|
+
function isProcessAlive(pid) {
|
|
4370
|
+
try {
|
|
4371
|
+
process.kill(pid, 0);
|
|
4372
|
+
return true;
|
|
4373
|
+
} catch {
|
|
4374
|
+
return false;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
function acquireSpawnLock2(sessionName) {
|
|
4378
|
+
if (!existsSync11(SPAWN_LOCK_DIR)) {
|
|
4379
|
+
mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
|
|
4380
|
+
}
|
|
4381
|
+
const lockFile = spawnLockPath(sessionName);
|
|
4382
|
+
if (existsSync11(lockFile)) {
|
|
4383
|
+
try {
|
|
4384
|
+
const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
|
|
4385
|
+
const age = Date.now() - lock.timestamp;
|
|
4386
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
4387
|
+
return false;
|
|
4388
|
+
}
|
|
4389
|
+
} catch {
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
4393
|
+
return true;
|
|
4394
|
+
}
|
|
4395
|
+
function releaseSpawnLock2(sessionName) {
|
|
4396
|
+
try {
|
|
4397
|
+
unlinkSync7(spawnLockPath(sessionName));
|
|
4398
|
+
} catch {
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
function resolveBehaviorsExporterScript() {
|
|
4402
|
+
try {
|
|
4403
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
4404
|
+
const scriptPath = path15.join(
|
|
4405
|
+
path15.dirname(thisFile),
|
|
4406
|
+
"..",
|
|
4407
|
+
"bin",
|
|
4408
|
+
"exe-export-behaviors.js"
|
|
4409
|
+
);
|
|
4410
|
+
return existsSync11(scriptPath) ? scriptPath : null;
|
|
4411
|
+
} catch {
|
|
4412
|
+
return null;
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
4416
|
+
const script = resolveBehaviorsExporterScript();
|
|
4417
|
+
if (!script) return null;
|
|
4418
|
+
try {
|
|
4419
|
+
const output = execFileSync2(
|
|
4420
|
+
process.execPath,
|
|
4421
|
+
[script, agentId, projectName, sessionKey],
|
|
4422
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
4423
|
+
).trim();
|
|
4424
|
+
return output.length > 0 ? output : null;
|
|
4425
|
+
} catch (err) {
|
|
4426
|
+
process.stderr.write(
|
|
4427
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
4428
|
+
`
|
|
4429
|
+
);
|
|
4430
|
+
return null;
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
function getMySession() {
|
|
4434
|
+
return getTransport().getMySession();
|
|
4435
|
+
}
|
|
4436
|
+
function isRootSession(name) {
|
|
4437
|
+
return name.length > 0 && !name.includes("-");
|
|
4438
|
+
}
|
|
4439
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
4440
|
+
if (!isRootSession(exeSession)) {
|
|
4441
|
+
const root = extractRootExe(exeSession);
|
|
4442
|
+
if (root) {
|
|
4443
|
+
process.stderr.write(
|
|
4444
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
|
|
4445
|
+
`
|
|
4446
|
+
);
|
|
4447
|
+
exeSession = root;
|
|
4448
|
+
} else {
|
|
4449
|
+
throw new Error(
|
|
4450
|
+
`Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
4451
|
+
);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
4455
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
4456
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
4457
|
+
throw new Error(
|
|
4458
|
+
`Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
|
|
4459
|
+
);
|
|
4460
|
+
}
|
|
4461
|
+
return name;
|
|
4462
|
+
}
|
|
4463
|
+
function parseParentExe(sessionName, agentId) {
|
|
4464
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4465
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
4466
|
+
const match = sessionName.match(regex);
|
|
4467
|
+
return match?.[1] ?? null;
|
|
4468
|
+
}
|
|
4469
|
+
function extractRootExe(name) {
|
|
4470
|
+
if (!name) return null;
|
|
4471
|
+
if (!name.includes("-")) return name;
|
|
1455
4472
|
const parts = name.split("-").filter(Boolean);
|
|
1456
4473
|
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
1457
4474
|
}
|
|
4475
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
4476
|
+
if (!existsSync11(SESSION_CACHE)) {
|
|
4477
|
+
mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
4478
|
+
}
|
|
4479
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
4480
|
+
const filePath = path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
4481
|
+
writeFileSync7(filePath, JSON.stringify({
|
|
4482
|
+
parentExe: rootExe,
|
|
4483
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
4484
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4485
|
+
}));
|
|
4486
|
+
}
|
|
1458
4487
|
function getParentExe(sessionKey) {
|
|
1459
4488
|
try {
|
|
1460
|
-
const data = JSON.parse(
|
|
4489
|
+
const data = JSON.parse(readFileSync11(path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1461
4490
|
return data.parentExe || null;
|
|
1462
4491
|
} catch {
|
|
1463
4492
|
return null;
|
|
1464
4493
|
}
|
|
1465
4494
|
}
|
|
4495
|
+
function getDispatchedBy(sessionKey) {
|
|
4496
|
+
try {
|
|
4497
|
+
const data = JSON.parse(readFileSync11(
|
|
4498
|
+
path15.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
4499
|
+
"utf8"
|
|
4500
|
+
));
|
|
4501
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
4502
|
+
} catch {
|
|
4503
|
+
return null;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
1466
4506
|
function resolveExeSession() {
|
|
1467
4507
|
const mySession = getMySession();
|
|
1468
4508
|
if (!mySession) return null;
|
|
@@ -1476,7 +4516,455 @@ function resolveExeSession() {
|
|
|
1476
4516
|
}
|
|
1477
4517
|
return extractRootExe(mySession) ?? mySession;
|
|
1478
4518
|
}
|
|
1479
|
-
|
|
4519
|
+
function isEmployeeAlive(sessionName) {
|
|
4520
|
+
return getTransport().isAlive(sessionName);
|
|
4521
|
+
}
|
|
4522
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
4523
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
4524
|
+
if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
|
|
4525
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
4526
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
4527
|
+
if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
|
|
4528
|
+
}
|
|
4529
|
+
return null;
|
|
4530
|
+
}
|
|
4531
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
4532
|
+
const transport = getTransport();
|
|
4533
|
+
if (!transport.isAlive(sessionName)) {
|
|
4534
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
4535
|
+
}
|
|
4536
|
+
let pane;
|
|
4537
|
+
try {
|
|
4538
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
4539
|
+
} catch (err) {
|
|
4540
|
+
return {
|
|
4541
|
+
atCapacity: false,
|
|
4542
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
4546
|
+
if (!isAtCapacity2(pane)) {
|
|
4547
|
+
return {
|
|
4548
|
+
atCapacity: false,
|
|
4549
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
4550
|
+
};
|
|
4551
|
+
}
|
|
4552
|
+
return {
|
|
4553
|
+
atCapacity: true,
|
|
4554
|
+
reason: "capacity banner matched in recent pane output"
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
function readDebounceState() {
|
|
4558
|
+
try {
|
|
4559
|
+
if (!existsSync11(DEBOUNCE_FILE)) return {};
|
|
4560
|
+
return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
|
|
4561
|
+
} catch {
|
|
4562
|
+
return {};
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
function writeDebounceState(state) {
|
|
4566
|
+
try {
|
|
4567
|
+
if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
|
|
4568
|
+
writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
function isDebounced(targetSession) {
|
|
4573
|
+
const state = readDebounceState();
|
|
4574
|
+
const lastSent = state[targetSession] ?? 0;
|
|
4575
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
4576
|
+
}
|
|
4577
|
+
function recordDebounce(targetSession) {
|
|
4578
|
+
const state = readDebounceState();
|
|
4579
|
+
state[targetSession] = Date.now();
|
|
4580
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
4581
|
+
for (const key of Object.keys(state)) {
|
|
4582
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
4583
|
+
}
|
|
4584
|
+
writeDebounceState(state);
|
|
4585
|
+
}
|
|
4586
|
+
function logIntercom(msg) {
|
|
4587
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
4588
|
+
`;
|
|
4589
|
+
process.stderr.write(`[intercom] ${msg}
|
|
4590
|
+
`);
|
|
4591
|
+
try {
|
|
4592
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
4593
|
+
} catch {
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
function getSessionState(sessionName) {
|
|
4597
|
+
const transport = getTransport();
|
|
4598
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
4599
|
+
try {
|
|
4600
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
4601
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
4602
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
4603
|
+
return "no_claude";
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
if (/Running…/.test(pane)) return "tool";
|
|
4607
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
4608
|
+
return "idle";
|
|
4609
|
+
} catch {
|
|
4610
|
+
return "offline";
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
function isSessionBusy(sessionName) {
|
|
4614
|
+
const state = getSessionState(sessionName);
|
|
4615
|
+
return state === "thinking" || state === "tool";
|
|
4616
|
+
}
|
|
4617
|
+
function isExeSession(sessionName) {
|
|
4618
|
+
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
4619
|
+
const coordinatorName = getCoordinatorName();
|
|
4620
|
+
return matchesBaseWithInstance(coordinatorName) || matchesBaseWithInstance("exe");
|
|
4621
|
+
}
|
|
4622
|
+
function sendIntercom(targetSession) {
|
|
4623
|
+
const transport = getTransport();
|
|
4624
|
+
if (isExeSession(targetSession)) {
|
|
4625
|
+
logIntercom(`SKIP_COORDINATOR \u2192 ${targetSession} (coordinator sessions use prompt-submit hook)`);
|
|
4626
|
+
return "skipped_exe";
|
|
4627
|
+
}
|
|
4628
|
+
if (isDebounced(targetSession)) {
|
|
4629
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
4630
|
+
return "debounced";
|
|
4631
|
+
}
|
|
4632
|
+
try {
|
|
4633
|
+
const sessions = transport.listSessions();
|
|
4634
|
+
if (!sessions.includes(targetSession)) {
|
|
4635
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
4636
|
+
return "failed";
|
|
4637
|
+
}
|
|
4638
|
+
const sessionState = getSessionState(targetSession);
|
|
4639
|
+
if (sessionState === "no_claude") {
|
|
4640
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
4641
|
+
recordDebounce(targetSession);
|
|
4642
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
4643
|
+
return "queued";
|
|
4644
|
+
}
|
|
4645
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
4646
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
4647
|
+
recordDebounce(targetSession);
|
|
4648
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
4649
|
+
return "queued";
|
|
4650
|
+
}
|
|
4651
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
4652
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
4653
|
+
transport.sendKeys(targetSession, "q");
|
|
4654
|
+
}
|
|
4655
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
4656
|
+
recordDebounce(targetSession);
|
|
4657
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
4658
|
+
return "delivered";
|
|
4659
|
+
} catch {
|
|
4660
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
4661
|
+
return "failed";
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
function notifyParentExe(sessionKey) {
|
|
4665
|
+
const target = getDispatchedBy(sessionKey);
|
|
4666
|
+
if (!target) {
|
|
4667
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
4668
|
+
`);
|
|
4669
|
+
return false;
|
|
4670
|
+
}
|
|
4671
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
4672
|
+
`);
|
|
4673
|
+
const result = sendIntercom(target);
|
|
4674
|
+
if (result === "failed") {
|
|
4675
|
+
const rootExe = resolveExeSession();
|
|
4676
|
+
if (rootExe && rootExe !== target) {
|
|
4677
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
4678
|
+
`);
|
|
4679
|
+
const fallback = sendIntercom(rootExe);
|
|
4680
|
+
return fallback !== "failed";
|
|
4681
|
+
}
|
|
4682
|
+
return false;
|
|
4683
|
+
}
|
|
4684
|
+
return true;
|
|
4685
|
+
}
|
|
4686
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4687
|
+
if (isCoordinatorName(employeeName)) {
|
|
4688
|
+
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
4689
|
+
}
|
|
4690
|
+
try {
|
|
4691
|
+
assertEmployeeLimitSync();
|
|
4692
|
+
} catch (err) {
|
|
4693
|
+
if (err instanceof PlanLimitError) {
|
|
4694
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
if (employeeName.includes("-")) {
|
|
4698
|
+
const bare = employeeName.split("-")[0].replace(/\d+$/, "");
|
|
4699
|
+
return {
|
|
4700
|
+
status: "failed",
|
|
4701
|
+
sessionName: "",
|
|
4702
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
if (!isRootSession(exeSession)) {
|
|
4706
|
+
const root = extractRootExe(exeSession);
|
|
4707
|
+
if (root) {
|
|
4708
|
+
process.stderr.write(
|
|
4709
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
|
|
4710
|
+
`
|
|
4711
|
+
);
|
|
4712
|
+
exeSession = root;
|
|
4713
|
+
} else {
|
|
4714
|
+
return {
|
|
4715
|
+
status: "failed",
|
|
4716
|
+
sessionName: "",
|
|
4717
|
+
error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
let effectiveInstance = opts?.instance;
|
|
4722
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
4723
|
+
const free = findFreeInstance(
|
|
4724
|
+
employeeName,
|
|
4725
|
+
exeSession,
|
|
4726
|
+
opts.maxAutoInstances ?? 10
|
|
4727
|
+
);
|
|
4728
|
+
if (free === null) {
|
|
4729
|
+
return {
|
|
4730
|
+
status: "failed",
|
|
4731
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
4732
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
4736
|
+
}
|
|
4737
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
4738
|
+
if (isEmployeeAlive(sessionName)) {
|
|
4739
|
+
const result2 = sendIntercom(sessionName);
|
|
4740
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
4741
|
+
return { status: "intercom_sent", sessionName };
|
|
4742
|
+
}
|
|
4743
|
+
if (result2 === "delivered") {
|
|
4744
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
4745
|
+
}
|
|
4746
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
4747
|
+
}
|
|
4748
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
4749
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
4750
|
+
if (result.error) {
|
|
4751
|
+
return { status: "failed", sessionName, error: result.error };
|
|
4752
|
+
}
|
|
4753
|
+
return { status: "spawned", sessionName };
|
|
4754
|
+
}
|
|
4755
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
4756
|
+
const transport = getTransport();
|
|
4757
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
4758
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
4759
|
+
const logDir = path15.join(os7.homedir(), ".exe-os", "session-logs");
|
|
4760
|
+
const logFile = path15.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
4761
|
+
if (!existsSync11(logDir)) {
|
|
4762
|
+
mkdirSync6(logDir, { recursive: true });
|
|
4763
|
+
}
|
|
4764
|
+
transport.kill(sessionName);
|
|
4765
|
+
let cleanupSuffix = "";
|
|
4766
|
+
try {
|
|
4767
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
4768
|
+
const cleanupScript = path15.join(path15.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
4769
|
+
if (existsSync11(cleanupScript)) {
|
|
4770
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
4771
|
+
}
|
|
4772
|
+
} catch {
|
|
4773
|
+
}
|
|
4774
|
+
try {
|
|
4775
|
+
const claudeJsonPath = path15.join(os7.homedir(), ".claude.json");
|
|
4776
|
+
let claudeJson = {};
|
|
4777
|
+
try {
|
|
4778
|
+
claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
|
|
4779
|
+
} catch {
|
|
4780
|
+
}
|
|
4781
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
4782
|
+
const projects = claudeJson.projects;
|
|
4783
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
4784
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
4785
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
4786
|
+
writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
4787
|
+
} catch {
|
|
4788
|
+
}
|
|
4789
|
+
try {
|
|
4790
|
+
const settingsDir = path15.join(os7.homedir(), ".claude", "projects");
|
|
4791
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
4792
|
+
const projSettingsDir = path15.join(settingsDir, normalizedKey);
|
|
4793
|
+
const settingsPath = path15.join(projSettingsDir, "settings.json");
|
|
4794
|
+
let settings = {};
|
|
4795
|
+
try {
|
|
4796
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
4797
|
+
} catch {
|
|
4798
|
+
}
|
|
4799
|
+
const perms = settings.permissions ?? {};
|
|
4800
|
+
const allow = perms.allow ?? [];
|
|
4801
|
+
const toolNames = [
|
|
4802
|
+
"recall_my_memory",
|
|
4803
|
+
"store_memory",
|
|
4804
|
+
"create_task",
|
|
4805
|
+
"update_task",
|
|
4806
|
+
"list_tasks",
|
|
4807
|
+
"get_task",
|
|
4808
|
+
"ask_team_memory",
|
|
4809
|
+
"store_behavior",
|
|
4810
|
+
"get_identity",
|
|
4811
|
+
"send_message"
|
|
4812
|
+
];
|
|
4813
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
4814
|
+
let changed = false;
|
|
4815
|
+
for (const tool of requiredTools) {
|
|
4816
|
+
if (!allow.includes(tool)) {
|
|
4817
|
+
allow.push(tool);
|
|
4818
|
+
changed = true;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
if (changed) {
|
|
4822
|
+
perms.allow = allow;
|
|
4823
|
+
settings.permissions = perms;
|
|
4824
|
+
mkdirSync6(projSettingsDir, { recursive: true });
|
|
4825
|
+
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
4826
|
+
}
|
|
4827
|
+
} catch {
|
|
4828
|
+
}
|
|
4829
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
4830
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
4831
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
4832
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
4833
|
+
let identityFlag = "";
|
|
4834
|
+
let behaviorsFlag = "";
|
|
4835
|
+
let legacyFallbackWarned = false;
|
|
4836
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
4837
|
+
const identityPath = path15.join(
|
|
4838
|
+
os7.homedir(),
|
|
4839
|
+
".exe-os",
|
|
4840
|
+
"identity",
|
|
4841
|
+
`${employeeName}.md`
|
|
4842
|
+
);
|
|
4843
|
+
_resetCcAgentSupportCache();
|
|
4844
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
4845
|
+
if (hasAgentFlag) {
|
|
4846
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
4847
|
+
} else if (existsSync11(identityPath)) {
|
|
4848
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
4849
|
+
legacyFallbackWarned = true;
|
|
4850
|
+
}
|
|
4851
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
4852
|
+
employeeName,
|
|
4853
|
+
path15.basename(spawnCwd),
|
|
4854
|
+
sessionName
|
|
4855
|
+
);
|
|
4856
|
+
if (behaviorsFile) {
|
|
4857
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
if (legacyFallbackWarned) {
|
|
4861
|
+
process.stderr.write(
|
|
4862
|
+
`[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.
|
|
4863
|
+
`
|
|
4864
|
+
);
|
|
4865
|
+
}
|
|
4866
|
+
let sessionContextFlag = "";
|
|
4867
|
+
try {
|
|
4868
|
+
const ctxDir = path15.join(os7.homedir(), ".exe-os", "session-cache");
|
|
4869
|
+
mkdirSync6(ctxDir, { recursive: true });
|
|
4870
|
+
const ctxFile = path15.join(ctxDir, `session-context-${sessionName}.md`);
|
|
4871
|
+
const ctxContent = [
|
|
4872
|
+
`## Session Context`,
|
|
4873
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
4874
|
+
`Your parent coordinator session is ${exeSession}.`,
|
|
4875
|
+
`Your employees (if any) use the -${exeSession} suffix.`
|
|
4876
|
+
].join("\n");
|
|
4877
|
+
writeFileSync7(ctxFile, ctxContent);
|
|
4878
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
4879
|
+
} catch {
|
|
4880
|
+
}
|
|
4881
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
4882
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
4883
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
4884
|
+
if (cfg?.apiKeyEnv) {
|
|
4885
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
4886
|
+
if (keyVal) {
|
|
4887
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
let spawnCommand;
|
|
4892
|
+
if (useExeAgent) {
|
|
4893
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
4894
|
+
} else if (useBinSymlink) {
|
|
4895
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
4896
|
+
process.stderr.write(
|
|
4897
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
4898
|
+
`
|
|
4899
|
+
);
|
|
4900
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
4901
|
+
} else {
|
|
4902
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
4903
|
+
}
|
|
4904
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
4905
|
+
cwd: spawnCwd,
|
|
4906
|
+
command: spawnCommand
|
|
4907
|
+
});
|
|
4908
|
+
if (spawnResult.error) {
|
|
4909
|
+
releaseSpawnLock2(sessionName);
|
|
4910
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
4911
|
+
}
|
|
4912
|
+
transport.pipeLog(sessionName, logFile);
|
|
4913
|
+
try {
|
|
4914
|
+
const mySession = getMySession();
|
|
4915
|
+
const dispatchInfo = path15.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
4916
|
+
writeFileSync7(dispatchInfo, JSON.stringify({
|
|
4917
|
+
dispatchedBy: mySession,
|
|
4918
|
+
rootExe: exeSession,
|
|
4919
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
4920
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4921
|
+
}));
|
|
4922
|
+
} catch {
|
|
4923
|
+
}
|
|
4924
|
+
let booted = false;
|
|
4925
|
+
for (let i = 0; i < 30; i++) {
|
|
4926
|
+
try {
|
|
4927
|
+
execSync7("sleep 0.5");
|
|
4928
|
+
} catch {
|
|
4929
|
+
}
|
|
4930
|
+
try {
|
|
4931
|
+
const pane = transport.capturePane(sessionName);
|
|
4932
|
+
if (useExeAgent) {
|
|
4933
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
4934
|
+
booted = true;
|
|
4935
|
+
break;
|
|
4936
|
+
}
|
|
4937
|
+
} else {
|
|
4938
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
4939
|
+
booted = true;
|
|
4940
|
+
break;
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
} catch {
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
if (!booted) {
|
|
4947
|
+
releaseSpawnLock2(sessionName);
|
|
4948
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
4949
|
+
}
|
|
4950
|
+
if (!useExeAgent) {
|
|
4951
|
+
try {
|
|
4952
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
4953
|
+
} catch {
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
registerSession({
|
|
4957
|
+
windowName: sessionName,
|
|
4958
|
+
agentId: employeeName,
|
|
4959
|
+
projectDir: spawnCwd,
|
|
4960
|
+
parentExe: exeSession,
|
|
4961
|
+
pid: 0,
|
|
4962
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4963
|
+
});
|
|
4964
|
+
releaseSpawnLock2(sessionName);
|
|
4965
|
+
return { sessionName };
|
|
4966
|
+
}
|
|
4967
|
+
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;
|
|
1480
4968
|
var init_tmux_routing = __esm({
|
|
1481
4969
|
"src/lib/tmux-routing.ts"() {
|
|
1482
4970
|
"use strict";
|
|
@@ -1489,11 +4977,16 @@ var init_tmux_routing = __esm({
|
|
|
1489
4977
|
init_intercom_queue();
|
|
1490
4978
|
init_plan_limits();
|
|
1491
4979
|
init_employees();
|
|
1492
|
-
SPAWN_LOCK_DIR =
|
|
1493
|
-
SESSION_CACHE =
|
|
1494
|
-
|
|
1495
|
-
|
|
4980
|
+
SPAWN_LOCK_DIR = path15.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
4981
|
+
SESSION_CACHE = path15.join(os7.homedir(), ".exe-os", "session-cache");
|
|
4982
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
4983
|
+
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
4984
|
+
VERIFY_PANE_LINES = 200;
|
|
4985
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
4986
|
+
INTERCOM_LOG2 = path15.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
4987
|
+
DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1496
4988
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
4989
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
1497
4990
|
}
|
|
1498
4991
|
});
|
|
1499
4992
|
|
|
@@ -1531,15 +5024,15 @@ var init_memory = __esm({
|
|
|
1531
5024
|
});
|
|
1532
5025
|
|
|
1533
5026
|
// src/lib/keychain.ts
|
|
1534
|
-
import { readFile as
|
|
1535
|
-
import { existsSync as
|
|
1536
|
-
import
|
|
1537
|
-
import
|
|
5027
|
+
import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
|
|
5028
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5029
|
+
import path16 from "path";
|
|
5030
|
+
import os8 from "os";
|
|
1538
5031
|
function getKeyDir() {
|
|
1539
|
-
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ??
|
|
5032
|
+
return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path16.join(os8.homedir(), ".exe-os");
|
|
1540
5033
|
}
|
|
1541
5034
|
function getKeyPath() {
|
|
1542
|
-
return
|
|
5035
|
+
return path16.join(getKeyDir(), "master.key");
|
|
1543
5036
|
}
|
|
1544
5037
|
async function tryKeytar() {
|
|
1545
5038
|
try {
|
|
@@ -1560,77 +5053,30 @@ async function getMasterKey() {
|
|
|
1560
5053
|
}
|
|
1561
5054
|
}
|
|
1562
5055
|
const keyPath = getKeyPath();
|
|
1563
|
-
if (!
|
|
5056
|
+
if (!existsSync12(keyPath)) {
|
|
5057
|
+
process.stderr.write(
|
|
5058
|
+
`[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
|
|
5059
|
+
`
|
|
5060
|
+
);
|
|
1564
5061
|
return null;
|
|
1565
5062
|
}
|
|
1566
5063
|
try {
|
|
1567
|
-
const content = await
|
|
5064
|
+
const content = await readFile4(keyPath, "utf-8");
|
|
1568
5065
|
return Buffer.from(content.trim(), "base64");
|
|
1569
|
-
} catch {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
var StateBus, orgBus;
|
|
1584
|
-
var init_state_bus = __esm({
|
|
1585
|
-
"src/lib/state-bus.ts"() {
|
|
1586
|
-
"use strict";
|
|
1587
|
-
StateBus = class {
|
|
1588
|
-
handlers = /* @__PURE__ */ new Map();
|
|
1589
|
-
globalHandlers = /* @__PURE__ */ new Set();
|
|
1590
|
-
/** Emit an event to all subscribers */
|
|
1591
|
-
emit(event) {
|
|
1592
|
-
const typeHandlers = this.handlers.get(event.type);
|
|
1593
|
-
if (typeHandlers) {
|
|
1594
|
-
for (const handler of typeHandlers) {
|
|
1595
|
-
try {
|
|
1596
|
-
handler(event);
|
|
1597
|
-
} catch {
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
for (const handler of this.globalHandlers) {
|
|
1602
|
-
try {
|
|
1603
|
-
handler(event);
|
|
1604
|
-
} catch {
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
/** Subscribe to a specific event type */
|
|
1609
|
-
on(type, handler) {
|
|
1610
|
-
if (!this.handlers.has(type)) {
|
|
1611
|
-
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
1612
|
-
}
|
|
1613
|
-
this.handlers.get(type).add(handler);
|
|
1614
|
-
}
|
|
1615
|
-
/** Subscribe to ALL events */
|
|
1616
|
-
onAny(handler) {
|
|
1617
|
-
this.globalHandlers.add(handler);
|
|
1618
|
-
}
|
|
1619
|
-
/** Unsubscribe from a specific event type */
|
|
1620
|
-
off(type, handler) {
|
|
1621
|
-
this.handlers.get(type)?.delete(handler);
|
|
1622
|
-
}
|
|
1623
|
-
/** Unsubscribe from ALL events */
|
|
1624
|
-
offAny(handler) {
|
|
1625
|
-
this.globalHandlers.delete(handler);
|
|
1626
|
-
}
|
|
1627
|
-
/** Remove all listeners */
|
|
1628
|
-
clear() {
|
|
1629
|
-
this.handlers.clear();
|
|
1630
|
-
this.globalHandlers.clear();
|
|
1631
|
-
}
|
|
1632
|
-
};
|
|
1633
|
-
orgBus = new StateBus();
|
|
5066
|
+
} catch (err) {
|
|
5067
|
+
process.stderr.write(
|
|
5068
|
+
`[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
|
|
5069
|
+
`
|
|
5070
|
+
);
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
var SERVICE, ACCOUNT;
|
|
5075
|
+
var init_keychain = __esm({
|
|
5076
|
+
"src/lib/keychain.ts"() {
|
|
5077
|
+
"use strict";
|
|
5078
|
+
SERVICE = "exe-mem";
|
|
5079
|
+
ACCOUNT = "master-key";
|
|
1634
5080
|
}
|
|
1635
5081
|
});
|
|
1636
5082
|
|
|
@@ -1647,13 +5093,13 @@ __export(shard_manager_exports, {
|
|
|
1647
5093
|
listShards: () => listShards,
|
|
1648
5094
|
shardExists: () => shardExists
|
|
1649
5095
|
});
|
|
1650
|
-
import
|
|
1651
|
-
import { existsSync as
|
|
5096
|
+
import path17 from "path";
|
|
5097
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
|
|
1652
5098
|
import { createClient as createClient2 } from "@libsql/client";
|
|
1653
5099
|
function initShardManager(encryptionKey) {
|
|
1654
5100
|
_encryptionKey = encryptionKey;
|
|
1655
|
-
if (!
|
|
1656
|
-
|
|
5101
|
+
if (!existsSync13(SHARDS_DIR)) {
|
|
5102
|
+
mkdirSync7(SHARDS_DIR, { recursive: true });
|
|
1657
5103
|
}
|
|
1658
5104
|
_shardingEnabled = true;
|
|
1659
5105
|
}
|
|
@@ -1673,7 +5119,7 @@ function getShardClient(projectName) {
|
|
|
1673
5119
|
}
|
|
1674
5120
|
const cached = _shards.get(safeName);
|
|
1675
5121
|
if (cached) return cached;
|
|
1676
|
-
const dbPath =
|
|
5122
|
+
const dbPath = path17.join(SHARDS_DIR, `${safeName}.db`);
|
|
1677
5123
|
const client = createClient2({
|
|
1678
5124
|
url: `file:${dbPath}`,
|
|
1679
5125
|
encryptionKey: _encryptionKey
|
|
@@ -1683,11 +5129,11 @@ function getShardClient(projectName) {
|
|
|
1683
5129
|
}
|
|
1684
5130
|
function shardExists(projectName) {
|
|
1685
5131
|
const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1686
|
-
return
|
|
5132
|
+
return existsSync13(path17.join(SHARDS_DIR, `${safeName}.db`));
|
|
1687
5133
|
}
|
|
1688
5134
|
function listShards() {
|
|
1689
|
-
if (!
|
|
1690
|
-
return
|
|
5135
|
+
if (!existsSync13(SHARDS_DIR)) return [];
|
|
5136
|
+
return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
|
|
1691
5137
|
}
|
|
1692
5138
|
async function ensureShardSchema(client) {
|
|
1693
5139
|
await client.execute("PRAGMA journal_mode = WAL");
|
|
@@ -1872,7 +5318,7 @@ var init_shard_manager = __esm({
|
|
|
1872
5318
|
"src/lib/shard-manager.ts"() {
|
|
1873
5319
|
"use strict";
|
|
1874
5320
|
init_config();
|
|
1875
|
-
SHARDS_DIR =
|
|
5321
|
+
SHARDS_DIR = path17.join(EXE_AI_DIR, "shards");
|
|
1876
5322
|
_shards = /* @__PURE__ */ new Map();
|
|
1877
5323
|
_encryptionKey = null;
|
|
1878
5324
|
_shardingEnabled = false;
|
|
@@ -1997,7 +5443,7 @@ __export(global_procedures_exports, {
|
|
|
1997
5443
|
loadGlobalProcedures: () => loadGlobalProcedures,
|
|
1998
5444
|
storeGlobalProcedure: () => storeGlobalProcedure
|
|
1999
5445
|
});
|
|
2000
|
-
import { randomUUID as
|
|
5446
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2001
5447
|
async function loadGlobalProcedures() {
|
|
2002
5448
|
const client = getClient();
|
|
2003
5449
|
const result = await client.execute({
|
|
@@ -2026,7 +5472,7 @@ ${sections.join("\n\n")}
|
|
|
2026
5472
|
`;
|
|
2027
5473
|
}
|
|
2028
5474
|
async function storeGlobalProcedure(input2) {
|
|
2029
|
-
const id =
|
|
5475
|
+
const id = randomUUID3();
|
|
2030
5476
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2031
5477
|
const client = getClient();
|
|
2032
5478
|
await client.execute({
|
|
@@ -2077,6 +5523,7 @@ __export(store_exports, {
|
|
|
2077
5523
|
vectorToBlob: () => vectorToBlob,
|
|
2078
5524
|
writeMemory: () => writeMemory
|
|
2079
5525
|
});
|
|
5526
|
+
import { createHash } from "crypto";
|
|
2080
5527
|
function isBusyError2(err) {
|
|
2081
5528
|
if (err instanceof Error) {
|
|
2082
5529
|
const msg = err.message.toLowerCase();
|
|
@@ -2150,12 +5597,52 @@ function classifyTier(record) {
|
|
|
2150
5597
|
if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
|
|
2151
5598
|
return 3;
|
|
2152
5599
|
}
|
|
5600
|
+
function inferFilePaths(record) {
|
|
5601
|
+
if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
|
|
5602
|
+
const firstLine = record.raw_text.split("\n")[0] ?? "";
|
|
5603
|
+
const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
|
|
5604
|
+
return match ? JSON.stringify([match[1]]) : null;
|
|
5605
|
+
}
|
|
5606
|
+
function inferCommitHash(record) {
|
|
5607
|
+
if (record.tool_name !== "Bash") return null;
|
|
5608
|
+
const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
|
|
5609
|
+
return match ? match[1] : null;
|
|
5610
|
+
}
|
|
5611
|
+
function inferLanguageType(record) {
|
|
5612
|
+
const text = record.raw_text;
|
|
5613
|
+
if (!text || text.length < 10) return null;
|
|
5614
|
+
const trimmed = text.trimStart();
|
|
5615
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
5616
|
+
if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
|
|
5617
|
+
if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
|
|
5618
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
|
|
5619
|
+
return "mixed";
|
|
5620
|
+
}
|
|
5621
|
+
function inferDomain(record) {
|
|
5622
|
+
const proj = (record.project_name ?? "").toLowerCase();
|
|
5623
|
+
if (proj.includes("marketing") || proj.includes("content")) return "marketing";
|
|
5624
|
+
if (proj.includes("crm") || proj.includes("customer")) return "customer";
|
|
5625
|
+
return null;
|
|
5626
|
+
}
|
|
2153
5627
|
async function writeMemory(record) {
|
|
2154
5628
|
if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
|
|
2155
5629
|
throw new Error(
|
|
2156
5630
|
`Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
|
|
2157
5631
|
);
|
|
2158
5632
|
}
|
|
5633
|
+
const contentHash = createHash("md5").update(record.raw_text).digest("hex");
|
|
5634
|
+
if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
|
|
5635
|
+
return;
|
|
5636
|
+
}
|
|
5637
|
+
try {
|
|
5638
|
+
const client = getClient();
|
|
5639
|
+
const existing = await client.execute({
|
|
5640
|
+
sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
|
|
5641
|
+
args: [contentHash, record.agent_id]
|
|
5642
|
+
});
|
|
5643
|
+
if (existing.rows.length > 0) return;
|
|
5644
|
+
} catch {
|
|
5645
|
+
}
|
|
2159
5646
|
const dbRow = {
|
|
2160
5647
|
id: record.id,
|
|
2161
5648
|
agent_id: record.agent_id,
|
|
@@ -2185,7 +5672,23 @@ async function writeMemory(record) {
|
|
|
2185
5672
|
supersedes_id: record.supersedes_id ?? null,
|
|
2186
5673
|
draft: record.draft ? 1 : 0,
|
|
2187
5674
|
memory_type: record.memory_type ?? "raw",
|
|
2188
|
-
trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
|
|
5675
|
+
trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
|
|
5676
|
+
content_hash: contentHash,
|
|
5677
|
+
intent: record.intent ?? null,
|
|
5678
|
+
outcome: record.outcome ?? null,
|
|
5679
|
+
domain: record.domain ?? inferDomain(record),
|
|
5680
|
+
referenced_entities: record.referenced_entities ?? null,
|
|
5681
|
+
retrieval_count: record.retrieval_count ?? 0,
|
|
5682
|
+
chain_position: record.chain_position ?? null,
|
|
5683
|
+
review_status: record.review_status ?? null,
|
|
5684
|
+
context_window_pct: record.context_window_pct ?? null,
|
|
5685
|
+
file_paths: record.file_paths ?? inferFilePaths(record),
|
|
5686
|
+
commit_hash: record.commit_hash ?? inferCommitHash(record),
|
|
5687
|
+
duration_ms: record.duration_ms ?? null,
|
|
5688
|
+
token_cost: record.token_cost ?? null,
|
|
5689
|
+
audience: record.audience ?? null,
|
|
5690
|
+
language_type: record.language_type ?? inferLanguageType(record),
|
|
5691
|
+
parent_memory_id: record.parent_memory_id ?? null
|
|
2189
5692
|
};
|
|
2190
5693
|
_pendingRecords.push(dbRow);
|
|
2191
5694
|
orgBus.emit({
|
|
@@ -2243,80 +5746,85 @@ async function flushBatch() {
|
|
|
2243
5746
|
const draft = row.draft ? 1 : 0;
|
|
2244
5747
|
const memoryType = row.memory_type ?? "raw";
|
|
2245
5748
|
const trajectory = row.trajectory ?? null;
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
5749
|
+
const contentHash = row.content_hash ?? null;
|
|
5750
|
+
const intent = row.intent ?? null;
|
|
5751
|
+
const outcome = row.outcome ?? null;
|
|
5752
|
+
const domain = row.domain ?? null;
|
|
5753
|
+
const referencedEntities = row.referenced_entities ?? null;
|
|
5754
|
+
const retrievalCount = row.retrieval_count ?? 0;
|
|
5755
|
+
const chainPosition = row.chain_position ?? null;
|
|
5756
|
+
const reviewStatus = row.review_status ?? null;
|
|
5757
|
+
const contextWindowPct = row.context_window_pct ?? null;
|
|
5758
|
+
const filePaths = row.file_paths ?? null;
|
|
5759
|
+
const commitHash = row.commit_hash ?? null;
|
|
5760
|
+
const durationMs = row.duration_ms ?? null;
|
|
5761
|
+
const tokenCost = row.token_cost ?? null;
|
|
5762
|
+
const audience = row.audience ?? null;
|
|
5763
|
+
const languageType = row.language_type ?? null;
|
|
5764
|
+
const parentMemoryId = row.parent_memory_id ?? null;
|
|
5765
|
+
const cols = `id, agent_id, agent_role, session_id, timestamp,
|
|
2256
5766
|
tool_name, project_name,
|
|
2257
5767
|
has_error, raw_text, vector, version, task_id, importance, status,
|
|
2258
5768
|
confidence, last_accessed,
|
|
2259
5769
|
workspace_id, document_id, user_id, char_offset, page_number,
|
|
2260
|
-
source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
trajectory
|
|
2319
|
-
]
|
|
5770
|
+
source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
|
|
5771
|
+
intent, outcome, domain, referenced_entities, retrieval_count,
|
|
5772
|
+
chain_position, review_status, context_window_pct, file_paths, commit_hash,
|
|
5773
|
+
duration_ms, token_cost, audience, language_type, parent_memory_id`;
|
|
5774
|
+
const metaArgs = [
|
|
5775
|
+
intent,
|
|
5776
|
+
outcome,
|
|
5777
|
+
domain,
|
|
5778
|
+
referencedEntities,
|
|
5779
|
+
retrievalCount,
|
|
5780
|
+
chainPosition,
|
|
5781
|
+
reviewStatus,
|
|
5782
|
+
contextWindowPct,
|
|
5783
|
+
filePaths,
|
|
5784
|
+
commitHash,
|
|
5785
|
+
durationMs,
|
|
5786
|
+
tokenCost,
|
|
5787
|
+
audience,
|
|
5788
|
+
languageType,
|
|
5789
|
+
parentMemoryId
|
|
5790
|
+
];
|
|
5791
|
+
const baseArgs = [
|
|
5792
|
+
row.id,
|
|
5793
|
+
row.agent_id,
|
|
5794
|
+
row.agent_role,
|
|
5795
|
+
row.session_id,
|
|
5796
|
+
row.timestamp,
|
|
5797
|
+
row.tool_name,
|
|
5798
|
+
row.project_name,
|
|
5799
|
+
row.has_error,
|
|
5800
|
+
row.raw_text
|
|
5801
|
+
];
|
|
5802
|
+
const sharedArgs = [
|
|
5803
|
+
row.version,
|
|
5804
|
+
taskId,
|
|
5805
|
+
importance,
|
|
5806
|
+
status,
|
|
5807
|
+
confidence,
|
|
5808
|
+
lastAccessed,
|
|
5809
|
+
workspaceId,
|
|
5810
|
+
documentId,
|
|
5811
|
+
userId,
|
|
5812
|
+
charOffset,
|
|
5813
|
+
pageNumber,
|
|
5814
|
+
sourcePath,
|
|
5815
|
+
sourceType,
|
|
5816
|
+
tier,
|
|
5817
|
+
supersedesId,
|
|
5818
|
+
draft,
|
|
5819
|
+
memoryType,
|
|
5820
|
+
trajectory,
|
|
5821
|
+
contentHash
|
|
5822
|
+
];
|
|
5823
|
+
return {
|
|
5824
|
+
sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
|
|
5825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
|
|
5826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5827
|
+
args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
|
|
2320
5828
|
};
|
|
2321
5829
|
};
|
|
2322
5830
|
const globalClient = getClient();
|
|
@@ -2566,238 +6074,221 @@ var init_store = __esm({
|
|
|
2566
6074
|
}
|
|
2567
6075
|
});
|
|
2568
6076
|
|
|
2569
|
-
// src/lib/
|
|
2570
|
-
var
|
|
2571
|
-
__export(
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
migrateJsonNotifications: () => migrateJsonNotifications,
|
|
2578
|
-
readUnreadNotifications: () => readUnreadNotifications,
|
|
2579
|
-
writeNotification: () => writeNotification
|
|
6077
|
+
// src/lib/git-task-sweep.ts
|
|
6078
|
+
var git_task_sweep_exports = {};
|
|
6079
|
+
__export(git_task_sweep_exports, {
|
|
6080
|
+
extractKeywords: () => extractKeywords,
|
|
6081
|
+
findBestMatch: () => findBestMatch,
|
|
6082
|
+
getRecentCommits: () => getRecentCommits,
|
|
6083
|
+
matchScore: () => matchScore,
|
|
6084
|
+
sweepTasks: () => sweepTasks
|
|
2580
6085
|
});
|
|
2581
|
-
import
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
import {
|
|
2585
|
-
readFileSync as readFileSync8,
|
|
2586
|
-
readdirSync as readdirSync3,
|
|
2587
|
-
unlinkSync as unlinkSync3,
|
|
2588
|
-
existsSync as existsSync9,
|
|
2589
|
-
rmdirSync
|
|
2590
|
-
} from "fs";
|
|
2591
|
-
async function writeNotification(notification) {
|
|
2592
|
-
try {
|
|
2593
|
-
const client = getClient();
|
|
2594
|
-
const id = crypto.randomUUID();
|
|
2595
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2596
|
-
await client.execute({
|
|
2597
|
-
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
|
|
2598
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
2599
|
-
args: [
|
|
2600
|
-
id,
|
|
2601
|
-
notification.agentId,
|
|
2602
|
-
notification.agentRole,
|
|
2603
|
-
notification.event,
|
|
2604
|
-
notification.project,
|
|
2605
|
-
notification.summary,
|
|
2606
|
-
notification.taskFile ?? null,
|
|
2607
|
-
now
|
|
2608
|
-
]
|
|
2609
|
-
});
|
|
2610
|
-
} catch (err) {
|
|
2611
|
-
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
2612
|
-
`);
|
|
2613
|
-
}
|
|
6086
|
+
import { execSync as execSync8 } from "child_process";
|
|
6087
|
+
function extractKeywords(text) {
|
|
6088
|
+
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
|
|
2614
6089
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
6090
|
+
function matchScore(task, commitMessage, changedFiles) {
|
|
6091
|
+
if (task.id.length >= 8 && commitMessage.includes(task.id)) {
|
|
6092
|
+
return EXACT_UUID_SCORE;
|
|
6093
|
+
}
|
|
6094
|
+
let score = 0;
|
|
6095
|
+
const titleWords = extractKeywords(task.title);
|
|
6096
|
+
const commitWords = new Set(extractKeywords(commitMessage));
|
|
6097
|
+
const overlap = titleWords.filter((w) => commitWords.has(w));
|
|
6098
|
+
if (overlap.length >= MIN_KEYWORD_OVERLAP) {
|
|
6099
|
+
score += TITLE_KEYWORD_SCORE;
|
|
6100
|
+
}
|
|
6101
|
+
if (task.context && changedFiles.length > 0) {
|
|
6102
|
+
const contextLower = task.context.toLowerCase();
|
|
6103
|
+
const hasFileMatch = changedFiles.some(
|
|
6104
|
+
(f) => contextLower.includes(f.toLowerCase())
|
|
6105
|
+
);
|
|
6106
|
+
if (hasFileMatch) {
|
|
6107
|
+
score += FILE_PATH_SCORE;
|
|
2623
6108
|
}
|
|
2624
|
-
const result = await client.execute({
|
|
2625
|
-
sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
|
|
2626
|
-
FROM notifications
|
|
2627
|
-
WHERE ${conditions.join(" AND ")}
|
|
2628
|
-
ORDER BY created_at ASC`,
|
|
2629
|
-
args
|
|
2630
|
-
});
|
|
2631
|
-
return result.rows.map((r) => ({
|
|
2632
|
-
id: String(r.id),
|
|
2633
|
-
agentId: String(r.agent_id),
|
|
2634
|
-
agentRole: String(r.agent_role),
|
|
2635
|
-
event: String(r.event),
|
|
2636
|
-
project: String(r.project),
|
|
2637
|
-
summary: String(r.summary),
|
|
2638
|
-
taskFile: r.task_file ? String(r.task_file) : void 0,
|
|
2639
|
-
timestamp: String(r.created_at),
|
|
2640
|
-
read: false
|
|
2641
|
-
}));
|
|
2642
|
-
} catch {
|
|
2643
|
-
return [];
|
|
2644
6109
|
}
|
|
6110
|
+
return score;
|
|
2645
6111
|
}
|
|
2646
|
-
|
|
2647
|
-
if (ids.length === 0) return;
|
|
6112
|
+
function getRecentCommits(limit = DEFAULT_COMMIT_LIMIT) {
|
|
2648
6113
|
try {
|
|
2649
|
-
const
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
6114
|
+
const SEPARATOR = "<<SEP>>";
|
|
6115
|
+
const output = execSync8(
|
|
6116
|
+
`git log --format="%h${SEPARATOR}%s${SEPARATOR}%aI" --name-only -n ${limit} -z`,
|
|
6117
|
+
{ encoding: "utf8", timeout: 1e4 }
|
|
6118
|
+
);
|
|
6119
|
+
const entries = output.split("\0").filter(Boolean);
|
|
6120
|
+
const commits = [];
|
|
6121
|
+
let current = null;
|
|
6122
|
+
for (const entry of entries) {
|
|
6123
|
+
if (entry.includes(SEPARATOR)) {
|
|
6124
|
+
const lines = entry.split("\n");
|
|
6125
|
+
const headerLine = lines[0];
|
|
6126
|
+
const parts = headerLine.split(SEPARATOR);
|
|
6127
|
+
if (parts.length >= 3) {
|
|
6128
|
+
if (current) commits.push(current);
|
|
6129
|
+
current = {
|
|
6130
|
+
hash: parts[0],
|
|
6131
|
+
message: parts[1],
|
|
6132
|
+
files: lines.slice(1).filter(Boolean),
|
|
6133
|
+
date: new Date(parts[2])
|
|
6134
|
+
};
|
|
6135
|
+
}
|
|
6136
|
+
} else if (current) {
|
|
6137
|
+
const files = entry.split("\n").filter(Boolean);
|
|
6138
|
+
current.files.push(...files);
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
if (current) commits.push(current);
|
|
6142
|
+
return commits;
|
|
2655
6143
|
} catch {
|
|
6144
|
+
return [];
|
|
2656
6145
|
}
|
|
2657
6146
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
|
|
2663
|
-
args: [taskFile]
|
|
2664
|
-
});
|
|
2665
|
-
} catch {
|
|
2666
|
-
}
|
|
6147
|
+
function isStale(updatedAt, staleMinutes) {
|
|
6148
|
+
const updated = new Date(updatedAt).getTime();
|
|
6149
|
+
const threshold = Date.now() - staleMinutes * 6e4;
|
|
6150
|
+
return updated < threshold;
|
|
2667
6151
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
sql: "DELETE FROM notifications WHERE created_at < ?",
|
|
2676
|
-
args: [cutoff]
|
|
2677
|
-
});
|
|
2678
|
-
return result.rowsAffected;
|
|
2679
|
-
} catch {
|
|
2680
|
-
return 0;
|
|
6152
|
+
function findBestMatch(task, commits) {
|
|
6153
|
+
let best = null;
|
|
6154
|
+
for (const commit of commits) {
|
|
6155
|
+
const score = matchScore(task, commit.message, commit.files);
|
|
6156
|
+
if (score >= AUTO_ESCALATE_THRESHOLD && (!best || score > best.score)) {
|
|
6157
|
+
best = { commit, score };
|
|
6158
|
+
}
|
|
2681
6159
|
}
|
|
6160
|
+
return best;
|
|
2682
6161
|
}
|
|
2683
|
-
async function
|
|
6162
|
+
async function sweepTasks(projectName, options = {}) {
|
|
6163
|
+
const commitLimit = options.commitLimit ?? DEFAULT_COMMIT_LIMIT;
|
|
6164
|
+
const staleMinutes = options.staleMinutes ?? DEFAULT_STALE_MINUTES;
|
|
6165
|
+
const dryRun = options.dryRun ?? false;
|
|
6166
|
+
const result = { escalated: [], unchanged: 0, errors: [] };
|
|
6167
|
+
const commits = getRecentCommits(commitLimit);
|
|
6168
|
+
if (commits.length === 0) {
|
|
6169
|
+
result.errors.push("No git commits found (not a git repo or empty history)");
|
|
6170
|
+
return result;
|
|
6171
|
+
}
|
|
6172
|
+
let tasks;
|
|
2684
6173
|
try {
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
args
|
|
6174
|
+
const { initStore: initStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
|
|
6175
|
+
await initStore2();
|
|
6176
|
+
const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
6177
|
+
const client = getClient2();
|
|
6178
|
+
const conditions = ["status = 'in_progress'"];
|
|
6179
|
+
const args = [];
|
|
6180
|
+
if (projectName) {
|
|
6181
|
+
conditions.push("project_name = ?");
|
|
6182
|
+
args.push(projectName);
|
|
6183
|
+
}
|
|
6184
|
+
const swScope = sessionScopeFilter();
|
|
6185
|
+
if (swScope.sql) {
|
|
6186
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
6187
|
+
args.push(...swScope.args);
|
|
6188
|
+
}
|
|
6189
|
+
const queryResult = await client.execute({
|
|
6190
|
+
sql: `SELECT id, title, assigned_to, project_name, status, updated_at, context
|
|
6191
|
+
FROM tasks WHERE ${conditions.join(" AND ")}
|
|
6192
|
+
ORDER BY updated_at ASC`,
|
|
6193
|
+
args
|
|
2694
6194
|
});
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
6195
|
+
tasks = queryResult.rows.map((r) => ({
|
|
6196
|
+
id: String(r.id),
|
|
6197
|
+
title: String(r.title),
|
|
6198
|
+
assignedTo: String(r.assigned_to),
|
|
6199
|
+
projectName: String(r.project_name),
|
|
6200
|
+
status: String(r.status),
|
|
6201
|
+
updatedAt: String(r.updated_at),
|
|
6202
|
+
context: r.context ? String(r.context) : void 0
|
|
6203
|
+
}));
|
|
6204
|
+
} catch (err) {
|
|
6205
|
+
result.errors.push(`DB query failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
6206
|
+
return result;
|
|
2698
6207
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
if (notifications.length === 0) return "";
|
|
2702
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
2703
|
-
for (const n of notifications) {
|
|
2704
|
-
const key = `${n.agentId}|${n.agentRole}`;
|
|
2705
|
-
if (!grouped.has(key)) grouped.set(key, []);
|
|
2706
|
-
grouped.get(key).push(n);
|
|
6208
|
+
if (tasks.length === 0) {
|
|
6209
|
+
return result;
|
|
2707
6210
|
}
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const [agentId, agentRole] = key.split("|");
|
|
2713
|
-
lines.push(`**${agentId}** (${agentRole}):`);
|
|
2714
|
-
for (const item of items) {
|
|
2715
|
-
const ago = formatTimeAgo(item.timestamp);
|
|
2716
|
-
const icon = eventIcon(item.event);
|
|
2717
|
-
lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
|
|
6211
|
+
for (const task of tasks) {
|
|
6212
|
+
if (!isStale(task.updatedAt, staleMinutes)) {
|
|
6213
|
+
result.unchanged++;
|
|
6214
|
+
continue;
|
|
2718
6215
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
const notifDir = path11.join(base, "notifications");
|
|
2726
|
-
if (!existsSync9(notifDir)) return 0;
|
|
2727
|
-
let migrated = 0;
|
|
2728
|
-
try {
|
|
2729
|
-
const files = readdirSync3(notifDir).filter((f) => f.endsWith(".json"));
|
|
2730
|
-
if (files.length === 0) return 0;
|
|
2731
|
-
const client = getClient();
|
|
2732
|
-
for (const file of files) {
|
|
6216
|
+
const match = findBestMatch(task, commits);
|
|
6217
|
+
if (!match) {
|
|
6218
|
+
result.unchanged++;
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
if (!dryRun) {
|
|
2733
6222
|
try {
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
args: [
|
|
2740
|
-
crypto.randomUUID(),
|
|
2741
|
-
data.agentId ?? "unknown",
|
|
2742
|
-
data.agentRole ?? "unknown",
|
|
2743
|
-
data.event ?? "session_summary",
|
|
2744
|
-
data.project ?? "unknown",
|
|
2745
|
-
data.summary ?? "",
|
|
2746
|
-
data.taskFile ?? null,
|
|
2747
|
-
data.read ? 1 : 0,
|
|
2748
|
-
data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2749
|
-
]
|
|
6223
|
+
const { updateTaskStatus: updateTaskStatus2 } = await Promise.resolve().then(() => (init_tasks_crud(), tasks_crud_exports));
|
|
6224
|
+
await updateTaskStatus2({
|
|
6225
|
+
taskId: task.id,
|
|
6226
|
+
status: "needs_review",
|
|
6227
|
+
result: `Auto-escalated by git-sweep: matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)})`
|
|
2750
6228
|
});
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
try {
|
|
2757
|
-
const remaining = readdirSync3(notifDir);
|
|
2758
|
-
if (remaining.length === 0) {
|
|
2759
|
-
rmdirSync(notifDir);
|
|
6229
|
+
} catch (err) {
|
|
6230
|
+
result.errors.push(
|
|
6231
|
+
`Failed to escalate task ${task.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
6232
|
+
);
|
|
6233
|
+
continue;
|
|
2760
6234
|
}
|
|
2761
|
-
} catch {
|
|
2762
6235
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
case "session_summary":
|
|
2774
|
-
return "Session:";
|
|
2775
|
-
case "error_spike":
|
|
2776
|
-
return "Errors:";
|
|
2777
|
-
case "orphan_task":
|
|
2778
|
-
return "Orphan:";
|
|
2779
|
-
case "subtasks_complete":
|
|
2780
|
-
return "Subtasks done:";
|
|
2781
|
-
case "capacity_relaunch":
|
|
2782
|
-
return "Relaunched:";
|
|
6236
|
+
result.escalated.push({
|
|
6237
|
+
taskId: task.id,
|
|
6238
|
+
title: task.title,
|
|
6239
|
+
matchedCommit: match.commit.hash,
|
|
6240
|
+
score: match.score
|
|
6241
|
+
});
|
|
6242
|
+
process.stderr.write(
|
|
6243
|
+
`[git-sweep] ${dryRun ? "WOULD escalate" : "Escalated"} task ${task.id} \u2192 commit ${match.commit.hash} (score: ${match.score.toFixed(2)})
|
|
6244
|
+
`
|
|
6245
|
+
);
|
|
2783
6246
|
}
|
|
6247
|
+
return result;
|
|
2784
6248
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
if (mins < 1) return "just now";
|
|
2789
|
-
if (mins < 60) return `${mins}m ago`;
|
|
2790
|
-
const hours = Math.floor(mins / 60);
|
|
2791
|
-
if (hours < 24) return `${hours}h ago`;
|
|
2792
|
-
const days = Math.floor(hours / 24);
|
|
2793
|
-
return `${days}d ago`;
|
|
2794
|
-
}
|
|
2795
|
-
var CLEANUP_DAYS;
|
|
2796
|
-
var init_notifications = __esm({
|
|
2797
|
-
"src/lib/notifications.ts"() {
|
|
6249
|
+
var DEFAULT_COMMIT_LIMIT, DEFAULT_STALE_MINUTES, AUTO_ESCALATE_THRESHOLD, EXACT_UUID_SCORE, TITLE_KEYWORD_SCORE, FILE_PATH_SCORE, MIN_KEYWORD_OVERLAP, STOP_WORDS;
|
|
6250
|
+
var init_git_task_sweep = __esm({
|
|
6251
|
+
"src/lib/git-task-sweep.ts"() {
|
|
2798
6252
|
"use strict";
|
|
2799
|
-
|
|
2800
|
-
|
|
6253
|
+
init_task_scope();
|
|
6254
|
+
DEFAULT_COMMIT_LIMIT = 50;
|
|
6255
|
+
DEFAULT_STALE_MINUTES = 10;
|
|
6256
|
+
AUTO_ESCALATE_THRESHOLD = 0.6;
|
|
6257
|
+
EXACT_UUID_SCORE = 1;
|
|
6258
|
+
TITLE_KEYWORD_SCORE = 0.5;
|
|
6259
|
+
FILE_PATH_SCORE = 0.3;
|
|
6260
|
+
MIN_KEYWORD_OVERLAP = 3;
|
|
6261
|
+
STOP_WORDS = /* @__PURE__ */ new Set([
|
|
6262
|
+
"a",
|
|
6263
|
+
"an",
|
|
6264
|
+
"the",
|
|
6265
|
+
"and",
|
|
6266
|
+
"or",
|
|
6267
|
+
"but",
|
|
6268
|
+
"in",
|
|
6269
|
+
"on",
|
|
6270
|
+
"at",
|
|
6271
|
+
"to",
|
|
6272
|
+
"for",
|
|
6273
|
+
"of",
|
|
6274
|
+
"with",
|
|
6275
|
+
"by",
|
|
6276
|
+
"from",
|
|
6277
|
+
"is",
|
|
6278
|
+
"it",
|
|
6279
|
+
"as",
|
|
6280
|
+
"be",
|
|
6281
|
+
"was",
|
|
6282
|
+
"are",
|
|
6283
|
+
"this",
|
|
6284
|
+
"that",
|
|
6285
|
+
"not",
|
|
6286
|
+
"no",
|
|
6287
|
+
"if",
|
|
6288
|
+
"so",
|
|
6289
|
+
"do",
|
|
6290
|
+
"up"
|
|
6291
|
+
]);
|
|
2801
6292
|
}
|
|
2802
6293
|
});
|
|
2803
6294
|
|
|
@@ -2938,7 +6429,7 @@ process.stdin.on("end", async () => {
|
|
|
2938
6429
|
await initStore2();
|
|
2939
6430
|
const { writeMemory: writeMemory2, flushBatch: flushBatch2 } = await Promise.resolve().then(() => (init_store(), store_exports));
|
|
2940
6431
|
const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
2941
|
-
const { randomUUID:
|
|
6432
|
+
const { randomUUID: randomUUID4 } = await import("crypto");
|
|
2942
6433
|
const client = getClient2();
|
|
2943
6434
|
const seScope = sessionScopeFilter();
|
|
2944
6435
|
const orphanResult = await client.execute({
|
|
@@ -2948,7 +6439,7 @@ process.stdin.on("end", async () => {
|
|
|
2948
6439
|
const orphanInfo = orphanResult.rows.length > 0 ? `
|
|
2949
6440
|
Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title)}" (${String(r.status)})`).join(", ")}` : "";
|
|
2950
6441
|
await writeMemory2({
|
|
2951
|
-
id:
|
|
6442
|
+
id: randomUUID4(),
|
|
2952
6443
|
agent_id: agent.agentId,
|
|
2953
6444
|
agent_role: agent.agentRole,
|
|
2954
6445
|
session_id: data.session_id,
|
|
@@ -2968,15 +6459,57 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
|
|
|
2968
6459
|
`[session-end] WARNING: ${agent.agentId} ended with ${inProgress.length} in_progress task(s): ${titles}
|
|
2969
6460
|
`
|
|
2970
6461
|
);
|
|
6462
|
+
let commits = [];
|
|
6463
|
+
try {
|
|
6464
|
+
const { getRecentCommits: getRecentCommits2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
|
|
6465
|
+
commits = getRecentCommits2(30);
|
|
6466
|
+
} catch {
|
|
6467
|
+
}
|
|
6468
|
+
const autoClosed = [];
|
|
6469
|
+
const leftInProgress = [];
|
|
2971
6470
|
for (const row of inProgress) {
|
|
6471
|
+
const title = String(row.title);
|
|
2972
6472
|
try {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
6473
|
+
if (commits.length > 0) {
|
|
6474
|
+
const { findBestMatch: findBestMatch2 } = await Promise.resolve().then(() => (init_git_task_sweep(), git_task_sweep_exports));
|
|
6475
|
+
let context;
|
|
6476
|
+
try {
|
|
6477
|
+
const ctxResult = await client.execute({
|
|
6478
|
+
sql: "SELECT id, context FROM tasks WHERE title = ? AND assigned_to = ? AND status = 'in_progress' LIMIT 1",
|
|
6479
|
+
args: [title, agent.agentId]
|
|
6480
|
+
});
|
|
6481
|
+
if (ctxResult.rows.length > 0) {
|
|
6482
|
+
context = ctxResult.rows[0].context ? String(ctxResult.rows[0].context) : void 0;
|
|
6483
|
+
}
|
|
6484
|
+
} catch {
|
|
6485
|
+
}
|
|
6486
|
+
const taskForMatch = { id: "", title, context };
|
|
6487
|
+
const match = findBestMatch2(taskForMatch, commits);
|
|
6488
|
+
if (match) {
|
|
6489
|
+
await client.execute({
|
|
6490
|
+
sql: "UPDATE tasks SET status = 'done', result = ?, updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
|
|
6491
|
+
args: [
|
|
6492
|
+
`Auto-closed: session ended but matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)}). Message: "${match.commit.message}"`,
|
|
6493
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
6494
|
+
title,
|
|
6495
|
+
agent.agentId
|
|
6496
|
+
]
|
|
6497
|
+
});
|
|
6498
|
+
autoClosed.push(`"${title}" \u2192 commit ${match.commit.hash}`);
|
|
6499
|
+
continue;
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
leftInProgress.push(`"${title}"`);
|
|
2977
6503
|
} catch {
|
|
2978
6504
|
}
|
|
2979
6505
|
}
|
|
6506
|
+
const parts = [];
|
|
6507
|
+
if (autoClosed.length > 0) {
|
|
6508
|
+
parts.push(`Auto-closed (work committed): ${autoClosed.join(", ")}`);
|
|
6509
|
+
}
|
|
6510
|
+
if (leftInProgress.length > 0) {
|
|
6511
|
+
parts.push(`Left in_progress (no matching commits, needs triage): ${leftInProgress.join(", ")}`);
|
|
6512
|
+
}
|
|
2980
6513
|
try {
|
|
2981
6514
|
const { writeNotification: writeNotification2 } = await Promise.resolve().then(() => (init_notifications(), notifications_exports));
|
|
2982
6515
|
await writeNotification2({
|
|
@@ -2984,10 +6517,18 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
|
|
|
2984
6517
|
agentRole: agent.agentRole,
|
|
2985
6518
|
event: "orphan_task",
|
|
2986
6519
|
project: process.env.EXE_PROJECT_NAME ?? "unknown",
|
|
2987
|
-
summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s)
|
|
6520
|
+
summary: `${agent.agentId} session ended with ${inProgress.length} in_progress task(s). ${parts.join(". ")}`
|
|
2988
6521
|
});
|
|
2989
6522
|
} catch {
|
|
2990
6523
|
}
|
|
6524
|
+
if (autoClosed.length > 0) {
|
|
6525
|
+
process.stderr.write(`[session-end] Auto-closed ${autoClosed.length} task(s) with matching commits
|
|
6526
|
+
`);
|
|
6527
|
+
}
|
|
6528
|
+
if (leftInProgress.length > 0) {
|
|
6529
|
+
process.stderr.write(`[session-end] Left ${leftInProgress.length} task(s) as in_progress for coordinator triage
|
|
6530
|
+
`);
|
|
6531
|
+
}
|
|
2991
6532
|
}
|
|
2992
6533
|
}
|
|
2993
6534
|
} catch {
|