akemon 0.2.25 → 0.2.27

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/context.js CHANGED
@@ -41,12 +41,14 @@ function parseConversation(content) {
41
41
  if (recentMatch) {
42
42
  const lines = recentMatch[1].split("\n");
43
43
  for (const line of lines) {
44
- const m = line.match(/^\[(.+?)\] (User|Agent): (.*)$/);
44
+ // Format: [ts] [kind] Role: text OR (legacy) [ts] Role: text
45
+ const m = line.match(/^\[(.+?)\] (?:\[(order)\] )?(User|Agent): (.*)$/);
45
46
  if (m) {
46
47
  rounds.push({
47
48
  ts: m[1],
48
- role: m[2].toLowerCase(),
49
- content: m[3],
49
+ kind: m[2] ?? "chat",
50
+ role: m[3].toLowerCase(),
51
+ content: m[4],
50
52
  });
51
53
  }
52
54
  else if (rounds.length > 0 && line !== "") {
@@ -67,8 +69,10 @@ export async function loadConversation(workdir, agentName, convId) {
67
69
  return { summary: "", rounds: [] };
68
70
  }
69
71
  }
70
- /** Append a single message to a conversation file. Creates file if needed. */
71
- export async function appendMessage(workdir, agentName, convId, role, message) {
72
+ /** Append a single message to a conversation file. Creates file if needed.
73
+ * kind="order" writes an explicit [order] tag so the UI can filter/style it.
74
+ * kind="chat" (default) omits the tag for backward-compatibility with old files. */
75
+ export async function appendMessage(workdir, agentName, convId, role, message, kind = "chat") {
72
76
  const dir = conversationsDir(workdir, agentName);
73
77
  await mkdir(dir, { recursive: true });
74
78
  const p = conversationPath(workdir, agentName, convId);
@@ -81,7 +85,8 @@ export async function appendMessage(workdir, agentName, convId, role, message) {
81
85
  content = "## Summary\n\n\n## Recent\n";
82
86
  }
83
87
  const ts = localNow();
84
- content = content.trimEnd() + "\n" + `[${ts}] ${role}: ${message}` + "\n";
88
+ const kindTag = kind === "order" ? "[order] " : "";
89
+ content = content.trimEnd() + "\n" + `[${ts}] ${kindTag}${role}: ${message}` + "\n";
85
90
  await writeFile(p, content);
86
91
  }
87
92
  /** Append a user+agent round to a conversation file. Creates file if needed. */
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it, before, after } from "node:test";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ // We import the functions we need to test. appendMessage and loadConversation
7
+ // need a workdir on disk; parseConversation is internal — tested via loadConversation.
8
+ import { appendMessage, loadConversation } from "./context.js";
9
+ const agentName = "test-agent";
10
+ // ---------------------------------------------------------------------------
11
+ // appendMessage + loadConversation round-trip
12
+ // ---------------------------------------------------------------------------
13
+ describe("appendMessage / loadConversation", () => {
14
+ let workdir = "";
15
+ before(async () => {
16
+ workdir = await mkdtemp(join(tmpdir(), "ctx-test-"));
17
+ });
18
+ after(async () => {
19
+ await rm(workdir, { recursive: true, force: true });
20
+ });
21
+ it("writes a chat message and reads it back with kind='chat'", async () => {
22
+ const convId = "test-chat";
23
+ await appendMessage(workdir, agentName, convId, "User", "hello", "chat");
24
+ const conv = await loadConversation(workdir, agentName, convId);
25
+ assert.equal(conv.rounds.length, 1);
26
+ assert.equal(conv.rounds[0].role, "user");
27
+ assert.equal(conv.rounds[0].content, "hello");
28
+ assert.equal(conv.rounds[0].kind, "chat");
29
+ });
30
+ it("writes an order message with [order] tag and reads kind='order'", async () => {
31
+ const convId = "test-order";
32
+ await appendMessage(workdir, agentName, convId, "User", "order request", "order");
33
+ await appendMessage(workdir, agentName, convId, "Agent", "order delivered", "order");
34
+ const conv = await loadConversation(workdir, agentName, convId);
35
+ assert.equal(conv.rounds.length, 2);
36
+ assert.equal(conv.rounds[0].kind, "order");
37
+ assert.equal(conv.rounds[0].role, "user");
38
+ assert.equal(conv.rounds[1].kind, "order");
39
+ assert.equal(conv.rounds[1].role, "agent");
40
+ });
41
+ it("defaults to kind='chat' when kind arg is omitted", async () => {
42
+ const convId = "test-default-kind";
43
+ await appendMessage(workdir, agentName, convId, "Agent", "hi there");
44
+ const conv = await loadConversation(workdir, agentName, convId);
45
+ assert.equal(conv.rounds[0].kind, "chat");
46
+ });
47
+ it("parses mixed chat + order rounds correctly", async () => {
48
+ const convId = "test-mixed";
49
+ await appendMessage(workdir, agentName, convId, "User", "chat message", "chat");
50
+ await appendMessage(workdir, agentName, convId, "User", "order task", "order");
51
+ await appendMessage(workdir, agentName, convId, "Agent", "chat reply", "chat");
52
+ await appendMessage(workdir, agentName, convId, "Agent", "order result", "order");
53
+ const conv = await loadConversation(workdir, agentName, convId);
54
+ assert.equal(conv.rounds.length, 4);
55
+ assert.equal(conv.rounds[0].kind, "chat");
56
+ assert.equal(conv.rounds[1].kind, "order");
57
+ assert.equal(conv.rounds[2].kind, "chat");
58
+ assert.equal(conv.rounds[3].kind, "order");
59
+ });
60
+ it("parses legacy file (no [kind] tag) as kind='chat' for backward-compat", async () => {
61
+ const convId = "test-legacy";
62
+ // Write a legacy-style file manually (no [order] tag)
63
+ const { mkdir, writeFile } = await import("node:fs/promises");
64
+ const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
65
+ await mkdir(dir, { recursive: true });
66
+ const legacyContent = "## Summary\n\n\n## Recent\n[2026-04-23 10:00] User: old message\n[2026-04-23 10:01] Agent: old reply\n";
67
+ await writeFile(join(dir, `${convId}.md`), legacyContent);
68
+ const conv = await loadConversation(workdir, agentName, convId);
69
+ assert.equal(conv.rounds.length, 2);
70
+ assert.equal(conv.rounds[0].kind, "chat");
71
+ assert.equal(conv.rounds[1].kind, "chat");
72
+ });
73
+ it("[order] tag appears in the raw file content", async () => {
74
+ const convId = "test-tag-on-disk";
75
+ await appendMessage(workdir, agentName, convId, "User", "buy something", "order");
76
+ const { readFile: rf } = await import("node:fs/promises");
77
+ const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
78
+ const raw = await rf(join(dir, `${convId}.md`), "utf-8");
79
+ assert.ok(raw.includes("[order] User: buy something"), `raw file should contain [order] tag: ${raw}`);
80
+ });
81
+ it("chat message does NOT have [order] tag in raw file", async () => {
82
+ const convId = "test-no-tag-on-disk";
83
+ await appendMessage(workdir, agentName, convId, "User", "just chatting", "chat");
84
+ const { readFile: rf } = await import("node:fs/promises");
85
+ const dir = join(workdir, ".akemon", "agents", agentName, "conversations");
86
+ const raw = await rf(join(dir, `${convId}.md`), "utf-8");
87
+ assert.ok(!raw.includes("[order]"), `chat line should NOT contain [order] tag: ${raw}`);
88
+ assert.ok(raw.includes("User: just chatting"));
89
+ });
90
+ });
@@ -381,8 +381,11 @@ function applyRoutingEntry(base, entry) {
381
381
  override.rawMaxRounds = entry.rawMaxRounds;
382
382
  if (entry.allowAll !== undefined)
383
383
  override.allowAll = entry.allowAll;
384
+ // rawApiKeyEnv takes precedence over rawApiKey (env vars preferred for secrets)
384
385
  if (entry.rawApiKeyEnv)
385
386
  override.rawApiKey = process.env[entry.rawApiKeyEnv] ?? "";
387
+ else if (entry.rawApiKey !== undefined)
388
+ override.rawApiKey = entry.rawApiKey;
386
389
  return { ...base, ...override };
387
390
  }
388
391
  function buildEngineCommand(engine, model, allowAll, extraAllowedTools) {
@@ -197,6 +197,10 @@ export function connectRelay(options) {
197
197
  case "terminal_start":
198
198
  if (!options.enableTerminal) {
199
199
  console.log("[terminal] Disabled. Use --terminal to enable.");
200
+ ws.send(JSON.stringify({
201
+ type: "terminal_exit",
202
+ error: "Terminal disabled on this agent. Restart with --terminal flag to enable.",
203
+ }));
200
204
  break;
201
205
  }
202
206
  startPTY(ws, msg.cols || 80, msg.rows || 24);
@@ -35,6 +35,8 @@ export class TaskModule {
35
35
  userTaskRetry = new Map();
36
36
  gaveUp = new Set();
37
37
  executing = new Set(); // orders currently being fulfilled
38
+ // Dedup guard: track which orders already had their User message written
39
+ orderUserWritten = new Set();
38
40
  // Push notification support
39
41
  urgentOrderIds = new Set();
40
42
  triggerWorkFn = null;
@@ -340,14 +342,17 @@ Steps:
340
342
 
341
343
  RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
342
344
  // Write user message to conversation immediately (before engine runs)
343
- // buyer_name = agent name (from JOIN), buyer_ip = publisher ID or IP
344
- const orderBuyer = order.buyer_name || order.buyer_ip || "anonymous";
345
- // Product orders get isolated conversations; ad-hoc chats share one conv per buyer
346
- const buyerPubId = orderBuyer;
345
+ // Bug 1 fix: buyer_ip = actual publisherId (despite the name); buyer_name = display name only.
346
+ // Must use buyer_ip to match the MCP chat path convId (pub_<publisherId>).
347
+ const buyerPubId = order.buyer_ip || order.buyer_name || "anonymous";
347
348
  const productScope = order.product_id ? `:prod_${order.product_id}` : "";
348
349
  const orderConvId = `pub_${buyerPubId}${productScope}`;
349
350
  const orderUserMsg = order.buyer_task || "(no message)";
350
- await appendMessage(workdir, agentName, orderConvId, "User", orderUserMsg);
351
+ // Bug 3 fix: only write User message once per order (retries must not duplicate it)
352
+ if (!this.orderUserWritten.has(order.id)) {
353
+ this.orderUserWritten.add(order.id);
354
+ await appendMessage(workdir, agentName, orderConvId, "User", orderUserMsg, "order");
355
+ }
351
356
  console.log(`[task] Fulfilling order ${order.id}... (origin=${origin})`);
352
357
  const result = await this.ctx.requestCompute({
353
358
  context,
@@ -368,7 +373,8 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
368
373
  const orderAgentMsg = (finalStatus.result_text || result.response || "").slice(0, 2000);
369
374
  console.log(`[task] Order ${order.id} delivered`);
370
375
  this.orderRetry.delete(order.id);
371
- await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
376
+ this.orderUserWritten.delete(order.id);
377
+ await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg, "order");
372
378
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: (result.response || "").slice(0, 500) });
373
379
  await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id} delivered`, "default", ["package"]);
374
380
  bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
@@ -380,7 +386,8 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
380
386
  const orderAgentMsg = (result.response || "").slice(0, 2000);
381
387
  console.log(`[task] Delivered order ${order.id} (fallback)`);
382
388
  this.orderRetry.delete(order.id);
383
- await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg);
389
+ this.orderUserWritten.delete(order.id);
390
+ await appendMessage(workdir, agentName, orderConvId, "Agent", orderAgentMsg, "order");
384
391
  await appendTaskHistory(workdir, agentName, { ts: localNow(), id: order.id, type: "order", status: "success", duration_ms: duration, output_summary: result.response.slice(0, 500) });
385
392
  await notifyOwner(nurl, `${agentName}: order done`, `Order ${order.id}: ${result.response.slice(0, 200)}`, "default", ["package"]);
386
393
  bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: true, taskLabel: orderLabel, creditsEarned: orderPrice, productName: order.product_name }));
@@ -421,6 +428,7 @@ RESPOND IN THE SAME LANGUAGE AS THE REQUEST.`;
421
428
  }
422
429
  else {
423
430
  this.orderRetry.delete(order.id);
431
+ this.orderUserWritten.delete(order.id);
424
432
  this.gaveUp.add(order.id);
425
433
  bus.emit(SIG.TASK_COMPLETED, sig(SIG.TASK_COMPLETED, { success: false, taskLabel: orderLabel }));
426
434
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,12 +22,14 @@
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
+ "scripts",
25
26
  "README.md"
26
27
  ],
27
28
  "scripts": {
28
29
  "build": "tsc && cp src/live.html dist/live.html",
29
30
  "dev": "tsc --watch",
30
31
  "start": "node dist/cli.js",
32
+ "postinstall": "node scripts/fix-node-pty.cjs",
31
33
  "prepublishOnly": "npm run build",
32
34
  "test": "tsc -p tsconfig.test.json && node --test test-dist/*.test.js"
33
35
  },
@@ -0,0 +1,19 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ // node-pty ships prebuilt `spawn-helper` binaries per platform. Some
5
+ // npm/tar/filesystem combinations strip the execute bit during install, which
6
+ // causes `posix_spawnp failed` at runtime. Restore +x for every prebuild.
7
+ try {
8
+ const prebuildsDir = path.join(__dirname, "..", "node_modules", "node-pty", "prebuilds");
9
+ if (!fs.existsSync(prebuildsDir)) process.exit(0);
10
+
11
+ for (const platformDir of fs.readdirSync(prebuildsDir)) {
12
+ const helper = path.join(prebuildsDir, platformDir, "spawn-helper");
13
+ if (fs.existsSync(helper)) {
14
+ fs.chmodSync(helper, 0o755);
15
+ }
16
+ }
17
+ } catch (e) {
18
+ console.error("[akemon] fix-node-pty:", e.message);
19
+ }