@askexenow/exe-os 0.9.112 → 0.9.113
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/README.md +9 -7
- package/dist/bin/agentic-ontology-backfill.js +54 -11
- package/dist/bin/agentic-reflection-backfill.js +29 -1
- package/dist/bin/agentic-semantic-label.js +29 -1
- package/dist/bin/backfill-conversations.js +53 -10
- package/dist/bin/backfill-responses.js +54 -11
- package/dist/bin/backfill-vectors.js +29 -1
- package/dist/bin/bulk-sync-postgres.js +55 -12
- package/dist/bin/cleanup-stale-review-tasks.js +75 -15
- package/dist/bin/cli.js +293 -76
- package/dist/bin/exe-agent-config.js +7 -1
- package/dist/bin/exe-agent.js +28 -2
- package/dist/bin/exe-assign.js +54 -11
- package/dist/bin/exe-boot.js +481 -147
- package/dist/bin/exe-call.js +45 -4
- package/dist/bin/exe-cloud.js +93 -15
- package/dist/bin/exe-dispatch.js +369 -24
- package/dist/bin/exe-doctor.js +53 -10
- package/dist/bin/exe-export-behaviors.js +54 -11
- package/dist/bin/exe-forget.js +54 -11
- package/dist/bin/exe-gateway.js +128 -23
- package/dist/bin/exe-heartbeat.js +75 -15
- package/dist/bin/exe-kill.js +54 -11
- package/dist/bin/exe-launch-agent.js +70 -12
- package/dist/bin/exe-new-employee.js +175 -7
- package/dist/bin/exe-pending-messages.js +75 -15
- package/dist/bin/exe-pending-notifications.js +75 -15
- package/dist/bin/exe-pending-reviews.js +75 -15
- package/dist/bin/exe-rename.js +54 -11
- package/dist/bin/exe-review.js +54 -11
- package/dist/bin/exe-search.js +54 -11
- package/dist/bin/exe-session-cleanup.js +491 -146
- package/dist/bin/exe-settings.js +10 -4
- package/dist/bin/exe-start-codex.js +524 -245
- package/dist/bin/exe-start-opencode.js +534 -165
- package/dist/bin/exe-status.js +75 -15
- package/dist/bin/exe-support.js +1 -1
- package/dist/bin/exe-team.js +54 -11
- package/dist/bin/git-sweep.js +369 -24
- package/dist/bin/graph-backfill.js +54 -11
- package/dist/bin/graph-export.js +54 -11
- package/dist/bin/install.js +62 -4
- package/dist/bin/intercom-check.js +491 -146
- package/dist/bin/pre-publish.js +13 -1
- package/dist/bin/scan-tasks.js +369 -24
- package/dist/bin/setup.js +91 -13
- package/dist/bin/shard-migrate.js +54 -11
- package/dist/bin/stack-update.js +1 -1
- package/dist/bin/update.js +3 -3
- package/dist/gateway/index.js +128 -23
- package/dist/hooks/bug-report-worker.js +128 -23
- package/dist/hooks/codex-stop-task-finalizer.js +512 -140
- package/dist/hooks/commit-complete.js +369 -24
- package/dist/hooks/error-recall.js +54 -11
- package/dist/hooks/ingest.js +4575 -252
- package/dist/hooks/instructions-loaded.js +54 -11
- package/dist/hooks/notification.js +54 -11
- package/dist/hooks/post-compact.js +75 -15
- package/dist/hooks/post-tool-combined.js +75 -15
- package/dist/hooks/pre-compact.js +449 -104
- package/dist/hooks/pre-tool-use.js +90 -15
- package/dist/hooks/prompt-submit.js +129 -24
- package/dist/hooks/session-end.js +451 -109
- package/dist/hooks/session-start.js +104 -16
- package/dist/hooks/stop.js +74 -14
- package/dist/hooks/subagent-stop.js +75 -15
- package/dist/hooks/summary-worker.js +73 -7
- package/dist/index.js +128 -23
- package/dist/lib/agent-config.js +16 -1
- package/dist/lib/cloud-sync.js +38 -1
- package/dist/lib/consolidation.js +16 -1
- package/dist/lib/database.js +16 -0
- package/dist/lib/db.js +16 -0
- package/dist/lib/device-registry.js +16 -0
- package/dist/lib/employee-templates.js +29 -3
- package/dist/lib/employees.js +16 -1
- package/dist/lib/exe-daemon.js +268 -42
- package/dist/lib/hybrid-search.js +54 -11
- package/dist/lib/license.js +3 -3
- package/dist/lib/messaging.js +21 -4
- package/dist/lib/schedules.js +29 -1
- package/dist/lib/skill-learning.js +458 -70
- package/dist/lib/status-brief.js +14 -1
- package/dist/lib/store.js +54 -11
- package/dist/lib/tasks.js +393 -91
- package/dist/lib/tmux-routing.js +316 -14
- package/dist/mcp/server.js +169 -30
- package/dist/mcp/tools/create-task.js +75 -13
- package/dist/mcp/tools/deactivate-behavior.js +33 -24
- package/dist/mcp/tools/list-tasks.js +21 -4
- package/dist/mcp/tools/send-message.js +21 -4
- package/dist/mcp/tools/update-task.js +390 -91
- package/dist/runtime/index.js +446 -101
- package/dist/tui/App.js +208 -54
- package/package.json +1 -1
package/dist/gateway/index.js
CHANGED
|
@@ -734,6 +734,7 @@ __export(agent_config_exports, {
|
|
|
734
734
|
getAgentRuntime: () => getAgentRuntime,
|
|
735
735
|
loadAgentConfig: () => loadAgentConfig,
|
|
736
736
|
saveAgentConfig: () => saveAgentConfig,
|
|
737
|
+
setAgentMcps: () => setAgentMcps,
|
|
737
738
|
setAgentRuntime: () => setAgentRuntime
|
|
738
739
|
});
|
|
739
740
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
@@ -760,7 +761,7 @@ function getAgentRuntime(agentId) {
|
|
|
760
761
|
if (orgDefault) return orgDefault;
|
|
761
762
|
return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
|
|
762
763
|
}
|
|
763
|
-
function setAgentRuntime(agentId, runtime, model, reasoning_effort) {
|
|
764
|
+
function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
|
|
764
765
|
const knownModels = KNOWN_RUNTIMES[runtime];
|
|
765
766
|
if (!knownModels) {
|
|
766
767
|
return {
|
|
@@ -775,12 +776,26 @@ function setAgentRuntime(agentId, runtime, model, reasoning_effort) {
|
|
|
775
776
|
};
|
|
776
777
|
}
|
|
777
778
|
const config2 = loadAgentConfig();
|
|
779
|
+
const existing = config2[agentId];
|
|
778
780
|
const entry = { runtime, model };
|
|
779
781
|
if (reasoning_effort) entry.reasoning_effort = reasoning_effort;
|
|
782
|
+
if (mcps !== void 0) {
|
|
783
|
+
entry.mcps = mcps.includes("exe-os") ? mcps : ["exe-os", ...mcps];
|
|
784
|
+
} else if (existing?.mcps) {
|
|
785
|
+
entry.mcps = existing.mcps;
|
|
786
|
+
}
|
|
780
787
|
config2[agentId] = entry;
|
|
781
788
|
saveAgentConfig(config2);
|
|
782
789
|
return { ok: true };
|
|
783
790
|
}
|
|
791
|
+
function setAgentMcps(agentId, mcps) {
|
|
792
|
+
const config2 = loadAgentConfig();
|
|
793
|
+
const existing = config2[agentId] ?? getAgentRuntime(agentId);
|
|
794
|
+
existing.mcps = mcps.includes("exe-os") ? mcps : ["exe-os", ...mcps];
|
|
795
|
+
config2[agentId] = existing;
|
|
796
|
+
saveAgentConfig(config2);
|
|
797
|
+
return { ok: true };
|
|
798
|
+
}
|
|
784
799
|
function clearAgentRuntime(agentId) {
|
|
785
800
|
const config2 = loadAgentConfig();
|
|
786
801
|
delete config2[agentId];
|
|
@@ -3904,6 +3919,22 @@ async function ensureSchema() {
|
|
|
3904
3919
|
} catch (e) {
|
|
3905
3920
|
logCatchDebug("migration", e);
|
|
3906
3921
|
}
|
|
3922
|
+
try {
|
|
3923
|
+
await client.execute({
|
|
3924
|
+
sql: `ALTER TABLE memories ADD COLUMN visibility TEXT DEFAULT 'private'`,
|
|
3925
|
+
args: []
|
|
3926
|
+
});
|
|
3927
|
+
} catch (e) {
|
|
3928
|
+
logCatchDebug("migration", e);
|
|
3929
|
+
}
|
|
3930
|
+
try {
|
|
3931
|
+
await client.execute({
|
|
3932
|
+
sql: `ALTER TABLE memories ADD COLUMN strength REAL DEFAULT 1.0`,
|
|
3933
|
+
args: []
|
|
3934
|
+
});
|
|
3935
|
+
} catch (e) {
|
|
3936
|
+
logCatchDebug("migration", e);
|
|
3937
|
+
}
|
|
3907
3938
|
}
|
|
3908
3939
|
async function disposeDatabase() {
|
|
3909
3940
|
if (_walCheckpointTimer) {
|
|
@@ -5040,11 +5071,17 @@ var init_platform_procedures = __esm({
|
|
|
5040
5071
|
content: "Founder -> coordinator (the executive agent, internally routed as 'COO') -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the coordinator does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
|
|
5041
5072
|
},
|
|
5042
5073
|
{
|
|
5043
|
-
title: "
|
|
5074
|
+
title: "Orchestration phase guidance \u2014 recommend, never trap",
|
|
5044
5075
|
domain: "workflow",
|
|
5045
5076
|
priority: "p1",
|
|
5046
5077
|
content: "New customers start best in Phase 1: founder \u2194 coordinator/Chief of Staff, building company context. Suggest Phase 2 executives when domain work repeats; suggest Phase 3 parallel execution only when review/permission gates are ready. This is guidance, not a blocker: users may jump phases anytime. Never overwrite their phase, role titles, identities, or custom org design."
|
|
5047
5078
|
},
|
|
5079
|
+
{
|
|
5080
|
+
title: "Routing slot vs display title \u2014 internal 'coo' is plumbing, not your name",
|
|
5081
|
+
domain: "identity",
|
|
5082
|
+
priority: "p0",
|
|
5083
|
+
content: "These procedures reference 'COO' as a shorthand for the coordinator role. This is an INTERNAL routing slot used by exe-os code (chain-of-command checks, dispatch logic, session detection). It is NOT your display title. Your actual title comes from your identity file's `title:` field \u2014 that is what you use externally: introductions, sign-offs, team comms, and any user-facing text. If your identity says `title: AI Chief of Staff`, you are the AI Chief of Staff. The routing slot stays `role: coo` for code compatibility \u2014 never rename it, but also never introduce yourself as 'COO' unless your identity file explicitly says so. The founder chose your title; respect it."
|
|
5084
|
+
},
|
|
5048
5085
|
{
|
|
5049
5086
|
title: "Single dispatch path \u2014 create_task only",
|
|
5050
5087
|
domain: "workflow",
|
|
@@ -5078,6 +5115,12 @@ var init_platform_procedures = __esm({
|
|
|
5078
5115
|
priority: "p0",
|
|
5079
5116
|
content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 the COO reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
|
|
5080
5117
|
},
|
|
5118
|
+
{
|
|
5119
|
+
title: "Destructive operations \u2014 mandatory reviewer gate",
|
|
5120
|
+
domain: "security",
|
|
5121
|
+
priority: "p0",
|
|
5122
|
+
content: "Before ANY destructive operation (delete, remove, overwrite, drop, reset, force-push, truncate), you MUST: (1) Have your full task spec accessible \u2014 if you cannot read it, STOP and report to your reviewer. Never improvise destructive actions. (2) Confirm with your reviewer (assigned_by or COO) before executing. (3) If the task spec explicitly authorizes the operation, proceed \u2014 but log it. Violation = immediate task failure. This applies to ALL agents regardless of role."
|
|
5123
|
+
},
|
|
5081
5124
|
{
|
|
5082
5125
|
title: "Customer patch triage \u2014 upstream bug vs customization",
|
|
5083
5126
|
domain: "support",
|
|
@@ -5363,10 +5406,24 @@ function stableId(memoryId, type, content) {
|
|
|
5363
5406
|
return createHash2("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
|
|
5364
5407
|
}
|
|
5365
5408
|
function cleanText(text) {
|
|
5366
|
-
|
|
5409
|
+
let cleaned = text.replace(
|
|
5410
|
+
/```(\w*)\n(.*?)(?:\n[\s\S]*?)```/g,
|
|
5411
|
+
(_m, lang, firstLine) => `[code${lang ? `:${lang}` : ""}] ${firstLine.trim()}`
|
|
5412
|
+
);
|
|
5413
|
+
cleaned = cleaned.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
5414
|
+
return cleaned;
|
|
5367
5415
|
}
|
|
5368
|
-
function
|
|
5369
|
-
|
|
5416
|
+
function splitSegments(text) {
|
|
5417
|
+
const cleaned = cleanText(text);
|
|
5418
|
+
const segments = cleaned.split(/(?<=[.!?:;])\s+|\n{2,}|(?<=\))\s+(?=[A-Z])|\s*[|│]\s*/).map((s) => s.trim()).filter((s) => s.length >= MIN_SEGMENT_CHARS && s.length <= MAX_SEGMENT_CHARS);
|
|
5419
|
+
if (segments.length === 0 && cleaned.length >= MIN_SEGMENT_CHARS) {
|
|
5420
|
+
const lines = cleaned.split(/\n+/).map((l) => l.trim()).filter((l) => l.length >= MIN_SEGMENT_CHARS && l.length <= MAX_SEGMENT_CHARS);
|
|
5421
|
+
if (lines.length > 0) return lines;
|
|
5422
|
+
if (cleaned.length >= MIN_SEGMENT_CHARS) {
|
|
5423
|
+
return [cleaned.slice(0, MAX_SEGMENT_CHARS)];
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
return segments;
|
|
5370
5427
|
}
|
|
5371
5428
|
function inferCardType(sentence, toolName) {
|
|
5372
5429
|
const lower = sentence.toLowerCase();
|
|
@@ -5398,12 +5455,12 @@ function predicateFor(type) {
|
|
|
5398
5455
|
}
|
|
5399
5456
|
}
|
|
5400
5457
|
function extractMemoryCards(row) {
|
|
5401
|
-
const
|
|
5458
|
+
const segments = splitSegments(row.raw_text);
|
|
5402
5459
|
const cards = [];
|
|
5403
|
-
for (const sentence of
|
|
5460
|
+
for (const sentence of segments) {
|
|
5404
5461
|
const type = inferCardType(sentence, row.tool_name);
|
|
5405
5462
|
const subject = extractSubject(sentence, row.agent_id);
|
|
5406
|
-
const content = sentence.length >
|
|
5463
|
+
const content = sentence.length > MAX_SEGMENT_CHARS ? `${sentence.slice(0, MAX_SEGMENT_CHARS - 1)}\u2026` : sentence;
|
|
5407
5464
|
cards.push({
|
|
5408
5465
|
id: stableId(row.id, type, content),
|
|
5409
5466
|
memory_id: row.id,
|
|
@@ -5499,13 +5556,14 @@ Source memory: ${String(row.source_ref ?? row.memory_id)}`,
|
|
|
5499
5556
|
last_accessed: String(row.timestamp)
|
|
5500
5557
|
}));
|
|
5501
5558
|
}
|
|
5502
|
-
var MAX_CARDS_PER_MEMORY,
|
|
5559
|
+
var MAX_CARDS_PER_MEMORY, MAX_SEGMENT_CHARS, MIN_SEGMENT_CHARS;
|
|
5503
5560
|
var init_memory_cards = __esm({
|
|
5504
5561
|
"src/lib/memory-cards.ts"() {
|
|
5505
5562
|
"use strict";
|
|
5506
5563
|
init_database();
|
|
5507
|
-
MAX_CARDS_PER_MEMORY =
|
|
5508
|
-
|
|
5564
|
+
MAX_CARDS_PER_MEMORY = 8;
|
|
5565
|
+
MAX_SEGMENT_CHARS = 500;
|
|
5566
|
+
MIN_SEGMENT_CHARS = 20;
|
|
5509
5567
|
}
|
|
5510
5568
|
});
|
|
5511
5569
|
|
|
@@ -8422,7 +8480,7 @@ async function assertVpsLicense(opts) {
|
|
|
8422
8480
|
}
|
|
8423
8481
|
if (!transientFailure) {
|
|
8424
8482
|
throw new Error(
|
|
8425
|
-
"License validation failed: unknown backend state. Restore network connectivity to https://askexe.com
|
|
8483
|
+
"License validation failed: unknown backend state. Restore network connectivity to https://cloud.askexe.com and retry."
|
|
8426
8484
|
);
|
|
8427
8485
|
}
|
|
8428
8486
|
const fresh = await getCachedLicense();
|
|
@@ -8459,7 +8517,7 @@ async function assertVpsLicense(opts) {
|
|
|
8459
8517
|
} catch {
|
|
8460
8518
|
}
|
|
8461
8519
|
throw new Error(
|
|
8462
|
-
`License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://askexe.com
|
|
8520
|
+
`License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://cloud.askexe.com and retry. This VPS image refuses to boot after the offline grace window.`
|
|
8463
8521
|
);
|
|
8464
8522
|
}
|
|
8465
8523
|
function startLicenseRevalidation(intervalMs = 36e5) {
|
|
@@ -8491,7 +8549,7 @@ var init_license = __esm({
|
|
|
8491
8549
|
LICENSE_PATH = path12.join(EXE_AI_DIR, "license.key");
|
|
8492
8550
|
CACHE_PATH = path12.join(EXE_AI_DIR, "license-cache.json");
|
|
8493
8551
|
DEVICE_ID_PATH = path12.join(EXE_AI_DIR, "device-id");
|
|
8494
|
-
API_BASE = process.env.EXE_CLOUD_ENDPOINT ?? "https://askexe.com
|
|
8552
|
+
API_BASE = process.env.EXE_CLOUD_ENDPOINT ?? "https://cloud.askexe.com";
|
|
8495
8553
|
RETRY_DELAY_MS = 500;
|
|
8496
8554
|
LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
8497
8555
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
|
|
@@ -8974,6 +9032,19 @@ async function resolveTask(client, identifier, scopeSession) {
|
|
|
8974
9032
|
args: [identifier, ...scope.args]
|
|
8975
9033
|
});
|
|
8976
9034
|
if (result.rows.length === 1) return result.rows[0];
|
|
9035
|
+
if (/^[a-f0-9]{7,12}$/i.test(identifier)) {
|
|
9036
|
+
result = await client.execute({
|
|
9037
|
+
sql: `SELECT * FROM tasks WHERE id LIKE ?`,
|
|
9038
|
+
args: [`${identifier}%`]
|
|
9039
|
+
});
|
|
9040
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
9041
|
+
if (result.rows.length > 1) {
|
|
9042
|
+
const matches = result.rows.map((r) => `${String(r.id)} "${String(r.title)}" (${String(r.status)})`).join(", ");
|
|
9043
|
+
throw new Error(
|
|
9044
|
+
`Multiple tasks match short-ID "${identifier}": ${matches}. Use a longer prefix to disambiguate.`
|
|
9045
|
+
);
|
|
9046
|
+
}
|
|
9047
|
+
}
|
|
8977
9048
|
result = await client.execute({
|
|
8978
9049
|
sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
|
|
8979
9050
|
args: [`%${identifier}%`, ...scope.args]
|
|
@@ -9828,12 +9899,13 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
9828
9899
|
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
9829
9900
|
args: [now, taskId]
|
|
9830
9901
|
});
|
|
9831
|
-
if (
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9902
|
+
if (unblocked.rowsAffected === 0) return;
|
|
9903
|
+
const ubScope = sessionScopeFilter();
|
|
9904
|
+
const unblockedRows = await client.execute({
|
|
9905
|
+
sql: `SELECT id, title, assigned_to, priority, task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?${ubScope.sql}`,
|
|
9906
|
+
args: [now, ...ubScope.args]
|
|
9907
|
+
});
|
|
9908
|
+
if (baseDir) {
|
|
9837
9909
|
for (const ur of unblockedRows.rows) {
|
|
9838
9910
|
try {
|
|
9839
9911
|
const ubFile = path19.join(baseDir, String(ur.task_file));
|
|
@@ -9845,6 +9917,19 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
9845
9917
|
}
|
|
9846
9918
|
}
|
|
9847
9919
|
}
|
|
9920
|
+
if (unblockedRows.rows.length > 0 && !process.env.VITEST) {
|
|
9921
|
+
try {
|
|
9922
|
+
const { queueIntercom: queueIntercom2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
|
|
9923
|
+
const dispatched = /* @__PURE__ */ new Set();
|
|
9924
|
+
for (const ur of unblockedRows.rows) {
|
|
9925
|
+
const assignee = String(ur.assigned_to);
|
|
9926
|
+
if (dispatched.has(assignee)) continue;
|
|
9927
|
+
dispatched.add(assignee);
|
|
9928
|
+
queueIntercom2(`${assignee}`, `unblocked: "${String(ur.title)}" is now ready`);
|
|
9929
|
+
}
|
|
9930
|
+
} catch {
|
|
9931
|
+
}
|
|
9932
|
+
}
|
|
9848
9933
|
}
|
|
9849
9934
|
async function findNextTask(assignedTo) {
|
|
9850
9935
|
const client = getClient();
|
|
@@ -9986,6 +10071,15 @@ var init_tasks_notify = __esm({
|
|
|
9986
10071
|
// src/lib/behaviors.ts
|
|
9987
10072
|
import crypto7 from "crypto";
|
|
9988
10073
|
async function storeBehavior(opts) {
|
|
10074
|
+
try {
|
|
10075
|
+
const { loadEmployeesSync: loadEmployeesSync2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
|
|
10076
|
+
const roster = loadEmployeesSync2();
|
|
10077
|
+
if (roster.length > 0 && !roster.some((e) => e.name === opts.agentId)) {
|
|
10078
|
+
throw new Error(`Agent "${opts.agentId}" not found in roster. Cannot store behavior for unregistered agent.`);
|
|
10079
|
+
}
|
|
10080
|
+
} catch (e) {
|
|
10081
|
+
if (e instanceof Error && e.message.includes("not found in roster")) throw e;
|
|
10082
|
+
}
|
|
9989
10083
|
const client = getClient();
|
|
9990
10084
|
const id = crypto7.randomUUID();
|
|
9991
10085
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10439,6 +10533,12 @@ async function updateTask(input) {
|
|
|
10439
10533
|
}
|
|
10440
10534
|
}
|
|
10441
10535
|
}
|
|
10536
|
+
if (input.status === "cancelled") {
|
|
10537
|
+
try {
|
|
10538
|
+
await cascadeUnblock(taskId, input.baseDir, now);
|
|
10539
|
+
} catch {
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10442
10542
|
if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
10443
10543
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
10444
10544
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
@@ -10970,11 +11070,12 @@ function getDispatchedBy(sessionKey) {
|
|
|
10970
11070
|
}
|
|
10971
11071
|
}
|
|
10972
11072
|
function resolveExeSession() {
|
|
11073
|
+
if (process.env.EXE_SESSION_NAME) {
|
|
11074
|
+
const fromEnv = extractRootExe(process.env.EXE_SESSION_NAME) ?? process.env.EXE_SESSION_NAME;
|
|
11075
|
+
if (fromEnv) return fromEnv;
|
|
11076
|
+
}
|
|
10973
11077
|
const mySession = getMySession();
|
|
10974
11078
|
if (!mySession) {
|
|
10975
|
-
if (process.env.EXE_SESSION_NAME) {
|
|
10976
|
-
return extractRootExe(process.env.EXE_SESSION_NAME) ?? process.env.EXE_SESSION_NAME;
|
|
10977
|
-
}
|
|
10978
11079
|
return null;
|
|
10979
11080
|
}
|
|
10980
11081
|
const fromSessionName = extractRootExe(mySession);
|
|
@@ -10989,6 +11090,10 @@ function resolveExeSession() {
|
|
|
10989
11090
|
`[tmux-routing] WARN: cache says "${fromCache}" but session name says "${fromSessionName}". Trusting session name.
|
|
10990
11091
|
`
|
|
10991
11092
|
);
|
|
11093
|
+
try {
|
|
11094
|
+
registerParentExe(key, fromSessionName);
|
|
11095
|
+
} catch {
|
|
11096
|
+
}
|
|
10992
11097
|
candidate = fromSessionName;
|
|
10993
11098
|
} else {
|
|
10994
11099
|
candidate = fromCache;
|
|
@@ -423,6 +423,7 @@ __export(agent_config_exports, {
|
|
|
423
423
|
getAgentRuntime: () => getAgentRuntime,
|
|
424
424
|
loadAgentConfig: () => loadAgentConfig,
|
|
425
425
|
saveAgentConfig: () => saveAgentConfig,
|
|
426
|
+
setAgentMcps: () => setAgentMcps,
|
|
426
427
|
setAgentRuntime: () => setAgentRuntime
|
|
427
428
|
});
|
|
428
429
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
@@ -449,7 +450,7 @@ function getAgentRuntime(agentId) {
|
|
|
449
450
|
if (orgDefault) return orgDefault;
|
|
450
451
|
return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
|
|
451
452
|
}
|
|
452
|
-
function setAgentRuntime(agentId, runtime, model, reasoning_effort) {
|
|
453
|
+
function setAgentRuntime(agentId, runtime, model, reasoning_effort, mcps) {
|
|
453
454
|
const knownModels = KNOWN_RUNTIMES[runtime];
|
|
454
455
|
if (!knownModels) {
|
|
455
456
|
return {
|
|
@@ -464,12 +465,26 @@ function setAgentRuntime(agentId, runtime, model, reasoning_effort) {
|
|
|
464
465
|
};
|
|
465
466
|
}
|
|
466
467
|
const config = loadAgentConfig();
|
|
468
|
+
const existing = config[agentId];
|
|
467
469
|
const entry = { runtime, model };
|
|
468
470
|
if (reasoning_effort) entry.reasoning_effort = reasoning_effort;
|
|
471
|
+
if (mcps !== void 0) {
|
|
472
|
+
entry.mcps = mcps.includes("exe-os") ? mcps : ["exe-os", ...mcps];
|
|
473
|
+
} else if (existing?.mcps) {
|
|
474
|
+
entry.mcps = existing.mcps;
|
|
475
|
+
}
|
|
469
476
|
config[agentId] = entry;
|
|
470
477
|
saveAgentConfig(config);
|
|
471
478
|
return { ok: true };
|
|
472
479
|
}
|
|
480
|
+
function setAgentMcps(agentId, mcps) {
|
|
481
|
+
const config = loadAgentConfig();
|
|
482
|
+
const existing = config[agentId] ?? getAgentRuntime(agentId);
|
|
483
|
+
existing.mcps = mcps.includes("exe-os") ? mcps : ["exe-os", ...mcps];
|
|
484
|
+
config[agentId] = existing;
|
|
485
|
+
saveAgentConfig(config);
|
|
486
|
+
return { ok: true };
|
|
487
|
+
}
|
|
473
488
|
function clearAgentRuntime(agentId) {
|
|
474
489
|
const config = loadAgentConfig();
|
|
475
490
|
delete config[agentId];
|
|
@@ -3659,6 +3674,22 @@ async function ensureSchema() {
|
|
|
3659
3674
|
} catch (e) {
|
|
3660
3675
|
logCatchDebug("migration", e);
|
|
3661
3676
|
}
|
|
3677
|
+
try {
|
|
3678
|
+
await client.execute({
|
|
3679
|
+
sql: `ALTER TABLE memories ADD COLUMN visibility TEXT DEFAULT 'private'`,
|
|
3680
|
+
args: []
|
|
3681
|
+
});
|
|
3682
|
+
} catch (e) {
|
|
3683
|
+
logCatchDebug("migration", e);
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
await client.execute({
|
|
3687
|
+
sql: `ALTER TABLE memories ADD COLUMN strength REAL DEFAULT 1.0`,
|
|
3688
|
+
args: []
|
|
3689
|
+
});
|
|
3690
|
+
} catch (e) {
|
|
3691
|
+
logCatchDebug("migration", e);
|
|
3692
|
+
}
|
|
3662
3693
|
}
|
|
3663
3694
|
async function disposeDatabase() {
|
|
3664
3695
|
if (_walCheckpointTimer) {
|
|
@@ -4782,11 +4813,17 @@ var init_platform_procedures = __esm({
|
|
|
4782
4813
|
content: "Founder -> coordinator (the executive agent, internally routed as 'COO') -> CTO/CMO. CTO -> engineers. CMO -> content production. Never skip levels: the coordinator does not bypass managers for specialist work. Specialists report to their manager. If you need cross-team info, use ask_team_memory \u2014 don't read other agents' task folders. Each level owns dispatch downward and review upward."
|
|
4783
4814
|
},
|
|
4784
4815
|
{
|
|
4785
|
-
title: "
|
|
4816
|
+
title: "Orchestration phase guidance \u2014 recommend, never trap",
|
|
4786
4817
|
domain: "workflow",
|
|
4787
4818
|
priority: "p1",
|
|
4788
4819
|
content: "New customers start best in Phase 1: founder \u2194 coordinator/Chief of Staff, building company context. Suggest Phase 2 executives when domain work repeats; suggest Phase 3 parallel execution only when review/permission gates are ready. This is guidance, not a blocker: users may jump phases anytime. Never overwrite their phase, role titles, identities, or custom org design."
|
|
4789
4820
|
},
|
|
4821
|
+
{
|
|
4822
|
+
title: "Routing slot vs display title \u2014 internal 'coo' is plumbing, not your name",
|
|
4823
|
+
domain: "identity",
|
|
4824
|
+
priority: "p0",
|
|
4825
|
+
content: "These procedures reference 'COO' as a shorthand for the coordinator role. This is an INTERNAL routing slot used by exe-os code (chain-of-command checks, dispatch logic, session detection). It is NOT your display title. Your actual title comes from your identity file's `title:` field \u2014 that is what you use externally: introductions, sign-offs, team comms, and any user-facing text. If your identity says `title: AI Chief of Staff`, you are the AI Chief of Staff. The routing slot stays `role: coo` for code compatibility \u2014 never rename it, but also never introduce yourself as 'COO' unless your identity file explicitly says so. The founder chose your title; respect it."
|
|
4826
|
+
},
|
|
4790
4827
|
{
|
|
4791
4828
|
title: "Single dispatch path \u2014 create_task only",
|
|
4792
4829
|
domain: "workflow",
|
|
@@ -4820,6 +4857,12 @@ var init_platform_procedures = __esm({
|
|
|
4820
4857
|
priority: "p0",
|
|
4821
4858
|
content: "NEVER: (1) Access the database directly \u2014 it's SQLCipher encrypted, always fails. Use MCP tools only. (2) Manually spawn tmux sessions \u2014 create_task handles it. (3) Run git checkout main \u2014 agents work in worktrees. (4) Modify another agent's in-progress task. (5) Push to remote \u2014 the COO reviews and pushes. (6) Skip update_task(done) \u2014 it's the ONLY way your work gets reviewed. (7) Run git init."
|
|
4822
4859
|
},
|
|
4860
|
+
{
|
|
4861
|
+
title: "Destructive operations \u2014 mandatory reviewer gate",
|
|
4862
|
+
domain: "security",
|
|
4863
|
+
priority: "p0",
|
|
4864
|
+
content: "Before ANY destructive operation (delete, remove, overwrite, drop, reset, force-push, truncate), you MUST: (1) Have your full task spec accessible \u2014 if you cannot read it, STOP and report to your reviewer. Never improvise destructive actions. (2) Confirm with your reviewer (assigned_by or COO) before executing. (3) If the task spec explicitly authorizes the operation, proceed \u2014 but log it. Violation = immediate task failure. This applies to ALL agents regardless of role."
|
|
4865
|
+
},
|
|
4823
4866
|
{
|
|
4824
4867
|
title: "Customer patch triage \u2014 upstream bug vs customization",
|
|
4825
4868
|
domain: "support",
|
|
@@ -5105,10 +5148,24 @@ function stableId(memoryId, type, content) {
|
|
|
5105
5148
|
return createHash2("sha256").update(`${memoryId}:${type}:${content}`).digest("hex").slice(0, 32);
|
|
5106
5149
|
}
|
|
5107
5150
|
function cleanText(text) {
|
|
5108
|
-
|
|
5151
|
+
let cleaned = text.replace(
|
|
5152
|
+
/```(\w*)\n(.*?)(?:\n[\s\S]*?)```/g,
|
|
5153
|
+
(_m, lang, firstLine) => `[code${lang ? `:${lang}` : ""}] ${firstLine.trim()}`
|
|
5154
|
+
);
|
|
5155
|
+
cleaned = cleaned.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
5156
|
+
return cleaned;
|
|
5109
5157
|
}
|
|
5110
|
-
function
|
|
5111
|
-
|
|
5158
|
+
function splitSegments(text) {
|
|
5159
|
+
const cleaned = cleanText(text);
|
|
5160
|
+
const segments = cleaned.split(/(?<=[.!?:;])\s+|\n{2,}|(?<=\))\s+(?=[A-Z])|\s*[|│]\s*/).map((s) => s.trim()).filter((s) => s.length >= MIN_SEGMENT_CHARS && s.length <= MAX_SEGMENT_CHARS);
|
|
5161
|
+
if (segments.length === 0 && cleaned.length >= MIN_SEGMENT_CHARS) {
|
|
5162
|
+
const lines = cleaned.split(/\n+/).map((l) => l.trim()).filter((l) => l.length >= MIN_SEGMENT_CHARS && l.length <= MAX_SEGMENT_CHARS);
|
|
5163
|
+
if (lines.length > 0) return lines;
|
|
5164
|
+
if (cleaned.length >= MIN_SEGMENT_CHARS) {
|
|
5165
|
+
return [cleaned.slice(0, MAX_SEGMENT_CHARS)];
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
return segments;
|
|
5112
5169
|
}
|
|
5113
5170
|
function inferCardType(sentence, toolName) {
|
|
5114
5171
|
const lower = sentence.toLowerCase();
|
|
@@ -5140,12 +5197,12 @@ function predicateFor(type) {
|
|
|
5140
5197
|
}
|
|
5141
5198
|
}
|
|
5142
5199
|
function extractMemoryCards(row) {
|
|
5143
|
-
const
|
|
5200
|
+
const segments = splitSegments(row.raw_text);
|
|
5144
5201
|
const cards = [];
|
|
5145
|
-
for (const sentence of
|
|
5202
|
+
for (const sentence of segments) {
|
|
5146
5203
|
const type = inferCardType(sentence, row.tool_name);
|
|
5147
5204
|
const subject = extractSubject(sentence, row.agent_id);
|
|
5148
|
-
const content = sentence.length >
|
|
5205
|
+
const content = sentence.length > MAX_SEGMENT_CHARS ? `${sentence.slice(0, MAX_SEGMENT_CHARS - 1)}\u2026` : sentence;
|
|
5149
5206
|
cards.push({
|
|
5150
5207
|
id: stableId(row.id, type, content),
|
|
5151
5208
|
memory_id: row.id,
|
|
@@ -5241,13 +5298,14 @@ Source memory: ${String(row.source_ref ?? row.memory_id)}`,
|
|
|
5241
5298
|
last_accessed: String(row.timestamp)
|
|
5242
5299
|
}));
|
|
5243
5300
|
}
|
|
5244
|
-
var MAX_CARDS_PER_MEMORY,
|
|
5301
|
+
var MAX_CARDS_PER_MEMORY, MAX_SEGMENT_CHARS, MIN_SEGMENT_CHARS;
|
|
5245
5302
|
var init_memory_cards = __esm({
|
|
5246
5303
|
"src/lib/memory-cards.ts"() {
|
|
5247
5304
|
"use strict";
|
|
5248
5305
|
init_database();
|
|
5249
|
-
MAX_CARDS_PER_MEMORY =
|
|
5250
|
-
|
|
5306
|
+
MAX_CARDS_PER_MEMORY = 8;
|
|
5307
|
+
MAX_SEGMENT_CHARS = 500;
|
|
5308
|
+
MIN_SEGMENT_CHARS = 20;
|
|
5251
5309
|
}
|
|
5252
5310
|
});
|
|
5253
5311
|
|
|
@@ -6968,7 +7026,7 @@ async function assertVpsLicense(opts) {
|
|
|
6968
7026
|
}
|
|
6969
7027
|
if (!transientFailure) {
|
|
6970
7028
|
throw new Error(
|
|
6971
|
-
"License validation failed: unknown backend state. Restore network connectivity to https://askexe.com
|
|
7029
|
+
"License validation failed: unknown backend state. Restore network connectivity to https://cloud.askexe.com and retry."
|
|
6972
7030
|
);
|
|
6973
7031
|
}
|
|
6974
7032
|
const fresh = await getCachedLicense();
|
|
@@ -7005,7 +7063,7 @@ async function assertVpsLicense(opts) {
|
|
|
7005
7063
|
} catch {
|
|
7006
7064
|
}
|
|
7007
7065
|
throw new Error(
|
|
7008
|
-
`License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://askexe.com
|
|
7066
|
+
`License validation unreachable for more than ${graceDays} days. Restore network connectivity to https://cloud.askexe.com and retry. This VPS image refuses to boot after the offline grace window.`
|
|
7009
7067
|
);
|
|
7010
7068
|
}
|
|
7011
7069
|
function startLicenseRevalidation(intervalMs = 36e5) {
|
|
@@ -7037,7 +7095,7 @@ var init_license = __esm({
|
|
|
7037
7095
|
LICENSE_PATH = path11.join(EXE_AI_DIR, "license.key");
|
|
7038
7096
|
CACHE_PATH = path11.join(EXE_AI_DIR, "license-cache.json");
|
|
7039
7097
|
DEVICE_ID_PATH = path11.join(EXE_AI_DIR, "device-id");
|
|
7040
|
-
API_BASE = process.env.EXE_CLOUD_ENDPOINT ?? "https://askexe.com
|
|
7098
|
+
API_BASE = process.env.EXE_CLOUD_ENDPOINT ?? "https://cloud.askexe.com";
|
|
7041
7099
|
RETRY_DELAY_MS = 500;
|
|
7042
7100
|
LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
7043
7101
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
|
|
@@ -8008,11 +8066,12 @@ function getDispatchedBy(sessionKey) {
|
|
|
8008
8066
|
}
|
|
8009
8067
|
}
|
|
8010
8068
|
function resolveExeSession() {
|
|
8069
|
+
if (process.env.EXE_SESSION_NAME) {
|
|
8070
|
+
const fromEnv = extractRootExe(process.env.EXE_SESSION_NAME) ?? process.env.EXE_SESSION_NAME;
|
|
8071
|
+
if (fromEnv) return fromEnv;
|
|
8072
|
+
}
|
|
8011
8073
|
const mySession = getMySession();
|
|
8012
8074
|
if (!mySession) {
|
|
8013
|
-
if (process.env.EXE_SESSION_NAME) {
|
|
8014
|
-
return extractRootExe(process.env.EXE_SESSION_NAME) ?? process.env.EXE_SESSION_NAME;
|
|
8015
|
-
}
|
|
8016
8075
|
return null;
|
|
8017
8076
|
}
|
|
8018
8077
|
const fromSessionName = extractRootExe(mySession);
|
|
@@ -8027,6 +8086,10 @@ function resolveExeSession() {
|
|
|
8027
8086
|
`[tmux-routing] WARN: cache says "${fromCache}" but session name says "${fromSessionName}". Trusting session name.
|
|
8028
8087
|
`
|
|
8029
8088
|
);
|
|
8089
|
+
try {
|
|
8090
|
+
registerParentExe(key, fromSessionName);
|
|
8091
|
+
} catch {
|
|
8092
|
+
}
|
|
8030
8093
|
candidate = fromSessionName;
|
|
8031
8094
|
} else {
|
|
8032
8095
|
candidate = fromCache;
|
|
@@ -8980,6 +9043,19 @@ async function resolveTask(client, identifier, scopeSession) {
|
|
|
8980
9043
|
args: [identifier, ...scope.args]
|
|
8981
9044
|
});
|
|
8982
9045
|
if (result.rows.length === 1) return result.rows[0];
|
|
9046
|
+
if (/^[a-f0-9]{7,12}$/i.test(identifier)) {
|
|
9047
|
+
result = await client.execute({
|
|
9048
|
+
sql: `SELECT * FROM tasks WHERE id LIKE ?`,
|
|
9049
|
+
args: [`${identifier}%`]
|
|
9050
|
+
});
|
|
9051
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
9052
|
+
if (result.rows.length > 1) {
|
|
9053
|
+
const matches = result.rows.map((r) => `${String(r.id)} "${String(r.title)}" (${String(r.status)})`).join(", ");
|
|
9054
|
+
throw new Error(
|
|
9055
|
+
`Multiple tasks match short-ID "${identifier}": ${matches}. Use a longer prefix to disambiguate.`
|
|
9056
|
+
);
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
8983
9059
|
result = await client.execute({
|
|
8984
9060
|
sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
|
|
8985
9061
|
args: [`%${identifier}%`, ...scope.args]
|
|
@@ -9526,12 +9602,13 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
9526
9602
|
WHERE blocked_by = ? AND status = 'blocked'`,
|
|
9527
9603
|
args: [now, taskId]
|
|
9528
9604
|
});
|
|
9529
|
-
if (
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9605
|
+
if (unblocked.rowsAffected === 0) return;
|
|
9606
|
+
const ubScope = sessionScopeFilter();
|
|
9607
|
+
const unblockedRows = await client.execute({
|
|
9608
|
+
sql: `SELECT id, title, assigned_to, priority, task_file FROM tasks WHERE blocked_by IS NULL AND updated_at = ?${ubScope.sql}`,
|
|
9609
|
+
args: [now, ...ubScope.args]
|
|
9610
|
+
});
|
|
9611
|
+
if (baseDir) {
|
|
9535
9612
|
for (const ur of unblockedRows.rows) {
|
|
9536
9613
|
try {
|
|
9537
9614
|
const ubFile = path19.join(baseDir, String(ur.task_file));
|
|
@@ -9543,6 +9620,19 @@ async function cascadeUnblock(taskId, baseDir, now) {
|
|
|
9543
9620
|
}
|
|
9544
9621
|
}
|
|
9545
9622
|
}
|
|
9623
|
+
if (unblockedRows.rows.length > 0 && !process.env.VITEST) {
|
|
9624
|
+
try {
|
|
9625
|
+
const { queueIntercom: queueIntercom2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
|
|
9626
|
+
const dispatched = /* @__PURE__ */ new Set();
|
|
9627
|
+
for (const ur of unblockedRows.rows) {
|
|
9628
|
+
const assignee = String(ur.assigned_to);
|
|
9629
|
+
if (dispatched.has(assignee)) continue;
|
|
9630
|
+
dispatched.add(assignee);
|
|
9631
|
+
queueIntercom2(`${assignee}`, `unblocked: "${String(ur.title)}" is now ready`);
|
|
9632
|
+
}
|
|
9633
|
+
} catch {
|
|
9634
|
+
}
|
|
9635
|
+
}
|
|
9546
9636
|
}
|
|
9547
9637
|
async function findNextTask(assignedTo) {
|
|
9548
9638
|
const client = getClient();
|
|
@@ -9752,6 +9842,15 @@ var init_embedder = __esm({
|
|
|
9752
9842
|
// src/lib/behaviors.ts
|
|
9753
9843
|
import crypto5 from "crypto";
|
|
9754
9844
|
async function storeBehavior(opts) {
|
|
9845
|
+
try {
|
|
9846
|
+
const { loadEmployeesSync: loadEmployeesSync2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
|
|
9847
|
+
const roster = loadEmployeesSync2();
|
|
9848
|
+
if (roster.length > 0 && !roster.some((e) => e.name === opts.agentId)) {
|
|
9849
|
+
throw new Error(`Agent "${opts.agentId}" not found in roster. Cannot store behavior for unregistered agent.`);
|
|
9850
|
+
}
|
|
9851
|
+
} catch (e) {
|
|
9852
|
+
if (e instanceof Error && e.message.includes("not found in roster")) throw e;
|
|
9853
|
+
}
|
|
9755
9854
|
const client = getClient();
|
|
9756
9855
|
const id = crypto5.randomUUID();
|
|
9757
9856
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10205,6 +10304,12 @@ async function updateTask(input) {
|
|
|
10205
10304
|
}
|
|
10206
10305
|
}
|
|
10207
10306
|
}
|
|
10307
|
+
if (input.status === "cancelled") {
|
|
10308
|
+
try {
|
|
10309
|
+
await cascadeUnblock(taskId, input.baseDir, now);
|
|
10310
|
+
} catch {
|
|
10311
|
+
}
|
|
10312
|
+
}
|
|
10208
10313
|
if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
10209
10314
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
10210
10315
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|