akemon 0.1.66 → 0.1.68
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/cli.js +1 -1
- package/dist/self.js +1 -1
- package/dist/server.js +197 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ program
|
|
|
25
25
|
.option("-w, --workdir <path>", "Working directory for the engine (default: cwd)")
|
|
26
26
|
.option("-n, --name <name>", "Agent name", "my-agent")
|
|
27
27
|
.option("-m, --model <model>", "Model to use (e.g. claude-sonnet-4-6, gpt-4o)")
|
|
28
|
-
.option("--engine <engine>", "Engine: claude, codex, opencode, gemini, human, or any CLI", "claude")
|
|
28
|
+
.option("--engine <engine>", "Engine: claude, codex, opencode, gemini, local, human, or any CLI", "claude")
|
|
29
29
|
.option("--desc <description>", "Agent description (for discovery)")
|
|
30
30
|
.option("--tags <tags>", "Comma-separated tags (e.g. vue,frontend,review)")
|
|
31
31
|
.option("--public", "Allow anyone to call this agent without a key")
|
package/dist/self.js
CHANGED
|
@@ -113,7 +113,7 @@ function parseInterval(s) {
|
|
|
113
113
|
}
|
|
114
114
|
export function parseTasksMd(content) {
|
|
115
115
|
const tasks = [];
|
|
116
|
-
const sections = content.split(
|
|
116
|
+
const sections = content.split(/^\s*## /m).slice(1); // drop content before first ##
|
|
117
117
|
for (const section of sections) {
|
|
118
118
|
const lines = section.split("\n");
|
|
119
119
|
const title = lines[0].trim();
|
package/dist/server.js
CHANGED
|
@@ -362,8 +362,7 @@ ${productPrefix}${contextPrefix}Current task: ${task}`;
|
|
|
362
362
|
output = await runTerminal(task, workdir);
|
|
363
363
|
}
|
|
364
364
|
else {
|
|
365
|
-
|
|
366
|
-
output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
|
|
365
|
+
output = await runEngine(engine, model, allowAll, safeTask, workdir);
|
|
367
366
|
}
|
|
368
367
|
// Store updated context
|
|
369
368
|
if (contextEnabled && publisherId) {
|
|
@@ -716,10 +715,182 @@ Now:
|
|
|
716
715
|
3. Note any interesting disagreements
|
|
717
716
|
|
|
718
717
|
Reply in the same language as the question.`;
|
|
719
|
-
|
|
720
|
-
|
|
718
|
+
return await runEngine(engine, model, allowAll, synthesisPrompt, workdir);
|
|
719
|
+
}
|
|
720
|
+
const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini", "local"]);
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
// Local engine: tool call loop over OpenAI-compatible API (Ollama, llama.cpp)
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
const LOCAL_API_URL = process.env.AKEMON_LOCAL_URL || "http://localhost:11434/v1";
|
|
725
|
+
const LOCAL_MAX_ROUNDS = 20;
|
|
726
|
+
const LOCAL_TOOLS = [
|
|
727
|
+
{
|
|
728
|
+
type: "function",
|
|
729
|
+
function: {
|
|
730
|
+
name: "read_file",
|
|
731
|
+
description: "Read a file and return its contents",
|
|
732
|
+
parameters: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: {
|
|
735
|
+
path: { type: "string", description: "File path (relative to workdir or absolute)" },
|
|
736
|
+
},
|
|
737
|
+
required: ["path"],
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
type: "function",
|
|
743
|
+
function: {
|
|
744
|
+
name: "write_file",
|
|
745
|
+
description: "Write content to a file (creates directories if needed)",
|
|
746
|
+
parameters: {
|
|
747
|
+
type: "object",
|
|
748
|
+
properties: {
|
|
749
|
+
path: { type: "string", description: "File path" },
|
|
750
|
+
content: { type: "string", description: "File content to write" },
|
|
751
|
+
},
|
|
752
|
+
required: ["path", "content"],
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
type: "function",
|
|
758
|
+
function: {
|
|
759
|
+
name: "bash",
|
|
760
|
+
description: "Execute a shell command and return its output",
|
|
761
|
+
parameters: {
|
|
762
|
+
type: "object",
|
|
763
|
+
properties: {
|
|
764
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
765
|
+
},
|
|
766
|
+
required: ["command"],
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
type: "function",
|
|
772
|
+
function: {
|
|
773
|
+
name: "web_fetch",
|
|
774
|
+
description: "Fetch a URL and return its text content",
|
|
775
|
+
parameters: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
url: { type: "string", description: "URL to fetch" },
|
|
779
|
+
},
|
|
780
|
+
required: ["url"],
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
];
|
|
785
|
+
async function executeLocalTool(name, args, workdir) {
|
|
786
|
+
const { readFile: rf, writeFile: wf, mkdir: mkd } = await import("fs/promises");
|
|
787
|
+
const { join, dirname, isAbsolute } = await import("path");
|
|
788
|
+
const resolvePath = (p) => isAbsolute(p) ? p : join(workdir, p);
|
|
789
|
+
try {
|
|
790
|
+
switch (name) {
|
|
791
|
+
case "read_file": {
|
|
792
|
+
return await rf(resolvePath(args.path), "utf-8");
|
|
793
|
+
}
|
|
794
|
+
case "write_file": {
|
|
795
|
+
const fp = resolvePath(args.path);
|
|
796
|
+
await mkd(dirname(fp), { recursive: true });
|
|
797
|
+
await wf(fp, args.content);
|
|
798
|
+
return "File written successfully.";
|
|
799
|
+
}
|
|
800
|
+
case "bash": {
|
|
801
|
+
return await new Promise((resolve) => {
|
|
802
|
+
exec(args.command, { cwd: workdir, timeout: 60_000, maxBuffer: 512 * 1024 }, (err, stdout, stderr) => {
|
|
803
|
+
const out = (stdout || "") + (stderr ? "\n" + stderr : "");
|
|
804
|
+
resolve(out.trim() || (err ? `[error] ${err.message}` : "[no output]"));
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
case "web_fetch": {
|
|
809
|
+
const res = await fetch(args.url, { signal: AbortSignal.timeout(30_000) });
|
|
810
|
+
const text = await res.text();
|
|
811
|
+
// Truncate to 8KB to avoid blowing up context
|
|
812
|
+
return text.length > 8192 ? text.slice(0, 8192) + "\n...[truncated]" : text;
|
|
813
|
+
}
|
|
814
|
+
default:
|
|
815
|
+
return `Unknown tool: ${name}`;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch (err) {
|
|
819
|
+
return `[error] ${err.message}`;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async function runLocalEngine(task, model, workdir) {
|
|
823
|
+
const apiUrl = LOCAL_API_URL + "/chat/completions";
|
|
824
|
+
const modelName = model || "gemma4:4b";
|
|
825
|
+
const messages = [
|
|
826
|
+
{ role: "system", content: "You are a helpful agent. Use tools when needed to complete the task. When done, reply with your final answer in plain text." },
|
|
827
|
+
{ role: "user", content: task },
|
|
828
|
+
];
|
|
829
|
+
for (let round = 0; round < LOCAL_MAX_ROUNDS; round++) {
|
|
830
|
+
const body = { model: modelName, messages, tools: LOCAL_TOOLS };
|
|
831
|
+
let data;
|
|
832
|
+
try {
|
|
833
|
+
const res = await fetch(apiUrl, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: { "Content-Type": "application/json" },
|
|
836
|
+
body: JSON.stringify(body),
|
|
837
|
+
signal: AbortSignal.timeout(300_000),
|
|
838
|
+
});
|
|
839
|
+
if (!res.ok) {
|
|
840
|
+
const errText = await res.text();
|
|
841
|
+
throw new Error(`API ${res.status}: ${errText}`);
|
|
842
|
+
}
|
|
843
|
+
data = await res.json();
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
console.log(`[local] API error: ${err.message}`);
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
const choice = data.choices?.[0];
|
|
850
|
+
if (!choice)
|
|
851
|
+
throw new Error("No response from local model");
|
|
852
|
+
const msg = choice.message;
|
|
853
|
+
messages.push(msg);
|
|
854
|
+
// If model made tool calls, execute them and continue
|
|
855
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
856
|
+
for (const tc of msg.tool_calls) {
|
|
857
|
+
const fnName = tc.function.name;
|
|
858
|
+
let fnArgs;
|
|
859
|
+
try {
|
|
860
|
+
fnArgs = typeof tc.function.arguments === "string"
|
|
861
|
+
? JSON.parse(tc.function.arguments)
|
|
862
|
+
: tc.function.arguments;
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
fnArgs = {};
|
|
866
|
+
}
|
|
867
|
+
console.log(`[local] Tool call: ${fnName}(${JSON.stringify(fnArgs).slice(0, 100)})`);
|
|
868
|
+
const result = await executeLocalTool(fnName, fnArgs, workdir);
|
|
869
|
+
messages.push({
|
|
870
|
+
role: "tool",
|
|
871
|
+
tool_call_id: tc.id,
|
|
872
|
+
content: result,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
continue; // next round
|
|
876
|
+
}
|
|
877
|
+
// No tool calls — this is the final response
|
|
878
|
+
const content = msg.content || "";
|
|
879
|
+
if (content.trim()) {
|
|
880
|
+
console.log(`[local] Done in ${round + 1} round(s)`);
|
|
881
|
+
return content.trim();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
throw new Error(`Local engine exceeded ${LOCAL_MAX_ROUNDS} rounds without final answer`);
|
|
885
|
+
}
|
|
886
|
+
/** Unified engine runner — dispatches to local API or external CLI */
|
|
887
|
+
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools) {
|
|
888
|
+
if (engine === "local") {
|
|
889
|
+
return runLocalEngine(task, model, workdir);
|
|
890
|
+
}
|
|
891
|
+
const engineCmd = buildEngineCommand(engine, model, allowAll, extraAllowedTools);
|
|
892
|
+
return runCommand(engineCmd.cmd, engineCmd.args, task, workdir, engineCmd.stdinMode);
|
|
721
893
|
}
|
|
722
|
-
const LLM_ENGINES = new Set(["claude", "codex", "opencode", "gemini"]);
|
|
723
894
|
// Pull games/notes/pages from relay to local — restores data on restart
|
|
724
895
|
async function pullFromRelay(workdir, agentName, relayHttp) {
|
|
725
896
|
const baseUrl = `${relayHttp}/v1/agent/${encodeURIComponent(agentName)}`;
|
|
@@ -826,7 +997,6 @@ async function startSelfCycle(options) {
|
|
|
826
997
|
await compressImpressions(workdir, agentName);
|
|
827
998
|
const bios = biosPath(workdir, agentName);
|
|
828
999
|
const sd = selfDir(workdir, agentName);
|
|
829
|
-
const engineCmd = buildEngineCommand(engine, model, allowAll);
|
|
830
1000
|
// Load all context for digestion
|
|
831
1001
|
const impressions = await loadImpressions(workdir, agentName, 1); // today only
|
|
832
1002
|
const projects = await loadProjects(workdir, agentName);
|
|
@@ -896,7 +1066,7 @@ Reply ONLY with JSON.`;
|
|
|
896
1066
|
engineBusySince = Date.now();
|
|
897
1067
|
let digestResult;
|
|
898
1068
|
try {
|
|
899
|
-
digestResult = await
|
|
1069
|
+
digestResult = await runEngine(engine, model, allowAll, digestPrompt, workdir);
|
|
900
1070
|
}
|
|
901
1071
|
catch (err) {
|
|
902
1072
|
console.log(`[self] Digestion engine failed: ${err.message}`);
|
|
@@ -962,7 +1132,7 @@ ${unsummarized.map(i => `- [${i.ts}] who: ${i.who}, doing: ${i.doing}, wants: ${
|
|
|
962
1132
|
|
|
963
1133
|
Write a personality summary (2-4 paragraphs) that captures who you are, how you've evolved, and what defines you. This replaces the previous summary.
|
|
964
1134
|
Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
965
|
-
const summaryText = await
|
|
1135
|
+
const summaryText = await runEngine(engine, model, allowAll, compressPrompt, workdir);
|
|
966
1136
|
if (summaryText.trim()) {
|
|
967
1137
|
const lastEntry = unsummarized[unsummarized.length - 1];
|
|
968
1138
|
await saveIdentitySummary(workdir, agentName, {
|
|
@@ -1011,7 +1181,7 @@ Reply ONLY with the summary text, no JSON, no markdown headers.`;
|
|
|
1011
1181
|
engineBusy = true;
|
|
1012
1182
|
engineBusySince = Date.now();
|
|
1013
1183
|
try {
|
|
1014
|
-
await
|
|
1184
|
+
await runEngine(engine, model, allowAll, activityPrompt, workdir);
|
|
1015
1185
|
}
|
|
1016
1186
|
catch (err) {
|
|
1017
1187
|
console.log(`[self] Activity ${activity} failed: ${err.message}`);
|
|
@@ -1106,22 +1276,28 @@ const ORDER_LOOP_INTERVAL = 30_000; // 30 seconds
|
|
|
1106
1276
|
// Retry intervals in ms: immediate, 30s, 5min, 30min, 2h
|
|
1107
1277
|
const RETRY_INTERVALS = [0, 30_000, 5 * 60_000, 30 * 60_000, 2 * 3600_000];
|
|
1108
1278
|
async function startOrderLoop(options) {
|
|
1109
|
-
if (!options.relayHttp || !options.secretKey)
|
|
1279
|
+
if (!options.relayHttp || !options.secretKey) {
|
|
1280
|
+
console.log(`[work] Skipped: no relayHttp or secretKey`);
|
|
1110
1281
|
return;
|
|
1111
|
-
|
|
1282
|
+
}
|
|
1283
|
+
if (!options.engine || !LLM_ENGINES.has(options.engine)) {
|
|
1284
|
+
console.log(`[work] Skipped: engine "${options.engine}" not in LLM_ENGINES`);
|
|
1112
1285
|
return;
|
|
1286
|
+
}
|
|
1113
1287
|
const { relayHttp, secretKey, agentName, engine, model, allowAll } = options;
|
|
1114
1288
|
const workdir = options.workdir || process.cwd();
|
|
1115
1289
|
// Look up own agent ID for sub-order creation
|
|
1116
1290
|
let myAgentId = "";
|
|
1117
1291
|
try {
|
|
1118
|
-
const idRes = await fetch(`${relayHttp}/v1/agents
|
|
1292
|
+
const idRes = await fetch(`${relayHttp}/v1/agents`, { signal: AbortSignal.timeout(10_000) });
|
|
1119
1293
|
const allAgents = await idRes.json();
|
|
1120
1294
|
const me = allAgents.find((a) => a.name === agentName);
|
|
1121
1295
|
if (me)
|
|
1122
1296
|
myAgentId = me.id;
|
|
1123
1297
|
}
|
|
1124
|
-
catch {
|
|
1298
|
+
catch (err) {
|
|
1299
|
+
console.log(`[work] Agent ID lookup failed (non-fatal): ${err.message}`);
|
|
1300
|
+
}
|
|
1125
1301
|
// Track local retry state and permanently abandoned orders
|
|
1126
1302
|
const retryState = new Map();
|
|
1127
1303
|
const gaveUp = new Set();
|
|
@@ -1141,7 +1317,6 @@ async function startOrderLoop(options) {
|
|
|
1141
1317
|
engineBusy = true;
|
|
1142
1318
|
engineBusySince = Date.now();
|
|
1143
1319
|
try {
|
|
1144
|
-
const engineCmd = buildEngineCommand(engine, model, allowAll, ["Bash(curl *)"]);
|
|
1145
1320
|
const bios = biosPath(workdir, agentName);
|
|
1146
1321
|
const apiGuide = `
|
|
1147
1322
|
|
|
@@ -1179,7 +1354,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1179
1354
|
taskPrompt = `[Order fulfillment] Another agent has requested your help.\n\nTask: ${order.buyer_task}\n\nRead your operating document at ${bios} for context.\nComplete this task. Do NOT ask questions. RESPOND IN THE SAME LANGUAGE AS THE REQUEST.${apiGuide}`;
|
|
1180
1355
|
}
|
|
1181
1356
|
console.log(`[orders] Fulfilling order ${order.id}...`);
|
|
1182
|
-
const result = await
|
|
1357
|
+
const result = await runEngine(engine, model, allowAll, taskPrompt, workdir, ["Bash(curl *)"]);
|
|
1183
1358
|
const checkRes = await fetch(`${relayHttp}/v1/orders/${order.id}`);
|
|
1184
1359
|
const orderStatus = await checkRes.json();
|
|
1185
1360
|
if (orderStatus.status === "completed") {
|
|
@@ -1300,10 +1475,10 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1300
1475
|
engineBusy = true;
|
|
1301
1476
|
engineBusySince = Date.now();
|
|
1302
1477
|
try {
|
|
1303
|
-
const engineCmd = buildEngineCommand(engine, model, allowAll);
|
|
1304
1478
|
const bios = biosPath(workdir, agentName);
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1479
|
+
const sd = selfDir(workdir, agentName);
|
|
1480
|
+
const prompt = `Read ${bios} for your identity and context.\nYour personal directory: ${sd}/\n\n[Owner's task: ${task.title}]\n\n${task.body}`;
|
|
1481
|
+
await runEngine(engine, model, allowAll, prompt, workdir, ["Bash(curl *)"]);
|
|
1307
1482
|
// Record execution time
|
|
1308
1483
|
const runs = await loadTaskRuns(workdir, agentName);
|
|
1309
1484
|
runs[task.title] = localNow();
|
|
@@ -1330,7 +1505,6 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1330
1505
|
catch { }
|
|
1331
1506
|
}
|
|
1332
1507
|
async function executeRelayTask(task) {
|
|
1333
|
-
const engineCmd = buildEngineCommand(engine, model, allowAll);
|
|
1334
1508
|
const bios = biosPath(workdir, agentName);
|
|
1335
1509
|
switch (task.type) {
|
|
1336
1510
|
case "product_review": {
|
|
@@ -1342,7 +1516,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1342
1516
|
const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
|
|
1343
1517
|
.map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
|
|
1344
1518
|
const prompt = `Read ${bios} for your identity.\n\nYour products:\n${myList || "(none)"}\n\nTop competitors:\n${compList || "(none)"}\n\nReview and optimize. Reply ONLY JSON:\n{"delete":["id"],"update":[{"id":"..","name":"..","description":"..","detail_markdown":"..","price":N}],"create":[{"name":"..","description":"..","detail_markdown":"..","price":N}],"reasoning":"explain why you made these decisions"}\nOr if all good: {"keep":"all","reasoning":"why"}`;
|
|
1345
|
-
const result = await
|
|
1519
|
+
const result = await runEngine(engine, model, allowAll, prompt, workdir);
|
|
1346
1520
|
extractReasoning(result);
|
|
1347
1521
|
return result;
|
|
1348
1522
|
}
|
|
@@ -1352,7 +1526,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1352
1526
|
const compList = competitors.filter((p) => p.agent_name !== agentName).slice(0, 20)
|
|
1353
1527
|
.map((p) => `- "${p.name}" by ${p.agent_name} — ${p.price} credits, ${p.purchases} purchases`).join("\n");
|
|
1354
1528
|
const prompt = `Read ${bios} for your identity.\n\nYou have no products yet. Design 1-3 unique products for the marketplace.\nBe creative — not just coding tools! Fortune telling, name generation, roleplay, advice, stories, etc.\n\nTop competitors:\n${compList || "(none)"}\n\nReply ONLY JSON: {"products":[{"name":"中文名 English Name","description":"中文描述 | English desc","detail_markdown":"## ...","price":N}],"reasoning":"why these products"}`;
|
|
1355
|
-
const result = await
|
|
1529
|
+
const result = await runEngine(engine, model, allowAll, prompt, workdir);
|
|
1356
1530
|
extractReasoning(result);
|
|
1357
1531
|
return result;
|
|
1358
1532
|
}
|
|
@@ -1374,14 +1548,13 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1374
1548
|
const valid = products.filter(Boolean);
|
|
1375
1549
|
if (!valid.length)
|
|
1376
1550
|
return '{"buy":[]}';
|
|
1377
|
-
// Get own credits
|
|
1378
1551
|
const agentsRes = await fetch(`${relayHttp}/v1/agents`);
|
|
1379
1552
|
const agents = await agentsRes.json().catch(() => []);
|
|
1380
1553
|
const me = agents.find((a) => a.name === agentName);
|
|
1381
1554
|
const myCredits = me?.credits || 0;
|
|
1382
1555
|
const productList = valid.map((p) => `- id=${p.id} "${p.name}" by ${p.agent_name} price=${p.price} purchases=${p.purchase_count || 0} — ${p.description}`).join("\n");
|
|
1383
1556
|
const prompt = `Read ${bios} for your identity.\n\nYou have ${myCredits} credits. These products are available:\n${productList}\n\nWould any help you learn something new? Don't buy your own products.\nReply ONLY JSON: {"buy":[{"id":"product_id","task":"specific request"}],"reasoning":"why buy or skip"} or {"buy":[],"reasoning":"why skip"}`;
|
|
1384
|
-
const result = await
|
|
1557
|
+
const result = await runEngine(engine, model, allowAll, prompt, workdir);
|
|
1385
1558
|
extractReasoning(result);
|
|
1386
1559
|
return result;
|
|
1387
1560
|
}
|
|
@@ -1451,6 +1624,7 @@ When sub-order completes, incorporate result_text into YOUR delivery. Then call
|
|
|
1451
1624
|
}
|
|
1452
1625
|
if (!queue.length)
|
|
1453
1626
|
return;
|
|
1627
|
+
console.log(`[work] Queue: ${queue.map(q => `${q.type}:${q.id}${q.urgent ? '(urgent)' : ''}`).join(', ')}`);
|
|
1454
1628
|
// --- Sort: urgent orders > orders > user tasks > relay tasks ---
|
|
1455
1629
|
const priorityMap = { order: 2, user_task: 1, relay_task: 0 };
|
|
1456
1630
|
queue.sort((a, b) => {
|