@perkos/perkos-a2a 0.12.19 → 0.12.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.
Files changed (53) hide show
  1. package/README.md +60 -0
  2. package/dist/agent.js +3 -2
  3. package/dist/agent.js.map +1 -1
  4. package/dist/bridge-agent.js +25 -56
  5. package/dist/bridge-agent.js.map +1 -1
  6. package/dist/bridge-chat-reply.d.ts +14 -1
  7. package/dist/bridge-chat-reply.d.ts.map +1 -1
  8. package/dist/bridge-chat-reply.js +71 -5
  9. package/dist/bridge-chat-reply.js.map +1 -1
  10. package/dist/gateway-health.d.ts +5 -11
  11. package/dist/gateway-health.d.ts.map +1 -1
  12. package/dist/gateway-health.js +53 -15
  13. package/dist/gateway-health.js.map +1 -1
  14. package/dist/hermes-plugin.d.ts +2 -0
  15. package/dist/hermes-plugin.d.ts.map +1 -1
  16. package/dist/hermes-plugin.js +4 -2
  17. package/dist/hermes-plugin.js.map +1 -1
  18. package/dist/index.d.ts +35 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +407 -19
  21. package/dist/index.js.map +3 -3
  22. package/dist/platform-heartbeat.d.ts +26 -0
  23. package/dist/platform-heartbeat.d.ts.map +1 -0
  24. package/dist/platform-heartbeat.js +72 -0
  25. package/dist/platform-heartbeat.js.map +1 -0
  26. package/dist/project-chat-context.d.ts +3 -0
  27. package/dist/project-chat-context.d.ts.map +1 -0
  28. package/dist/project-chat-context.js +16 -0
  29. package/dist/project-chat-context.js.map +1 -0
  30. package/dist/relay-client.d.ts +21 -2
  31. package/dist/relay-client.d.ts.map +1 -1
  32. package/dist/relay-client.js +82 -17
  33. package/dist/relay-client.js.map +1 -1
  34. package/dist/repair-cli.d.ts +3 -0
  35. package/dist/repair-cli.d.ts.map +1 -0
  36. package/dist/repair-cli.js +31 -0
  37. package/dist/repair-cli.js.map +1 -0
  38. package/dist/repair.d.ts +53 -0
  39. package/dist/repair.d.ts.map +1 -0
  40. package/dist/repair.js +357 -0
  41. package/dist/repair.js.map +1 -0
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +5 -2
  44. package/dist/server.js.map +1 -1
  45. package/dist/task-context.d.ts +18 -0
  46. package/dist/task-context.d.ts.map +1 -0
  47. package/dist/task-context.js +35 -0
  48. package/dist/task-context.js.map +1 -0
  49. package/dist/types.d.ts +20 -1
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js.map +1 -1
  52. package/openclaw.plugin.json +28 -1
  53. package/package.json +9 -5
package/dist/index.js CHANGED
@@ -26252,6 +26252,8 @@ import { randomUUID } from "crypto";
26252
26252
  var MIN_RECONNECT_MS = 1e3;
26253
26253
  var MAX_RECONNECT_MS = 6e4;
26254
26254
  var HEARTBEAT_INTERVAL_MS = 25e3;
26255
+ var ORPHAN_TTL_MS = 10 * 6e4;
26256
+ var ORPHAN_MAX = 256;
26255
26257
  var RelayClient = class {
26256
26258
  ws = null;
26257
26259
  options;
@@ -26262,6 +26264,8 @@ var RelayClient = class {
26262
26264
  connected = false;
26263
26265
  stopped = false;
26264
26266
  pendingCallbacks = /* @__PURE__ */ new Map();
26267
+ /** Responses that arrived with nobody waiting, claimable by a retry. */
26268
+ orphanResponses = /* @__PURE__ */ new Map();
26265
26269
  constructor(options) {
26266
26270
  this.options = options;
26267
26271
  this.logger = options.logger || { info: console.log, error: console.error };
@@ -26282,12 +26286,25 @@ var RelayClient = class {
26282
26286
  }
26283
26287
  this.connected = false;
26284
26288
  }
26285
- /** Send a task to another agent via the relay */
26286
- async sendTask(targetAgent, payload) {
26289
+ /**
26290
+ * Send a task to another agent via the relay.
26291
+ *
26292
+ * Pass `taskId` to retry a task that already went out: the relay dedups on
26293
+ * the id and will not run the work twice, and if the first attempt's response
26294
+ * arrived after we gave up it is claimed from the orphan buffer instead of
26295
+ * waiting again. Omit it for genuinely new work.
26296
+ */
26297
+ async sendTask(targetAgent, payload, taskId) {
26298
+ const id = taskId ?? randomUUID();
26299
+ const alreadyAnswered = this.claimOrphan(id);
26300
+ if (alreadyAnswered) {
26301
+ this.logger.info(`[perkos-a2a] Reusing buffered response for task ${id}`);
26302
+ return alreadyAnswered;
26303
+ }
26287
26304
  return this.sendAndWait({
26288
26305
  type: "task",
26289
26306
  to: targetAgent,
26290
- id: randomUUID(),
26307
+ id,
26291
26308
  from: this.options.agentName,
26292
26309
  payload,
26293
26310
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -26372,10 +26389,10 @@ var RelayClient = class {
26372
26389
  this.send(msg);
26373
26390
  }
26374
26391
  handleMessage(msg) {
26375
- const callback = this.pendingCallbacks.get(msg.id);
26376
- if (callback) {
26392
+ const pending = this.pendingCallbacks.get(msg.id);
26393
+ if (pending && (msg.type === pending.expectedType || msg.type === "error")) {
26377
26394
  this.pendingCallbacks.delete(msg.id);
26378
- callback(msg);
26395
+ pending.settle(msg);
26379
26396
  return;
26380
26397
  }
26381
26398
  switch (msg.type) {
@@ -26385,8 +26402,15 @@ var RelayClient = class {
26385
26402
  case "task":
26386
26403
  this.options.onTask(msg);
26387
26404
  break;
26405
+ case "task_ack":
26406
+ if (msg.payload?.deduplicated) {
26407
+ this.logger.info(`[perkos-a2a] Relay deduplicated task ${msg.id}; awaiting the original response`);
26408
+ }
26409
+ break;
26388
26410
  case "task_response":
26389
- this.logger.info(`[perkos-a2a] Received unmatched task response: ${msg.id}`);
26411
+ this.bufferOrphan(msg);
26412
+ this.logger.info(`[perkos-a2a] Buffered unmatched task response: ${msg.id}`);
26413
+ this.options.onOrphanTaskResponse?.(msg);
26390
26414
  break;
26391
26415
  case "discover_response":
26392
26416
  if (this.options.onDiscoverResponse) {
@@ -26407,18 +26431,39 @@ var RelayClient = class {
26407
26431
  this.ws.send(JSON.stringify(msg));
26408
26432
  }
26409
26433
  }
26434
+ /** Take a buffered response for `id`, if one arrived while nobody waited. */
26435
+ claimOrphan(id) {
26436
+ const entry = this.orphanResponses.get(id);
26437
+ if (!entry) return null;
26438
+ this.orphanResponses.delete(id);
26439
+ if (entry.expiresAt <= Date.now()) return null;
26440
+ return entry.msg;
26441
+ }
26442
+ bufferOrphan(msg) {
26443
+ const now = Date.now();
26444
+ for (const [id, entry] of this.orphanResponses) {
26445
+ if (entry.expiresAt <= now) this.orphanResponses.delete(id);
26446
+ }
26447
+ this.orphanResponses.set(msg.id, { msg, expiresAt: now + ORPHAN_TTL_MS });
26448
+ while (this.orphanResponses.size > ORPHAN_MAX) {
26449
+ this.orphanResponses.delete(this.orphanResponses.keys().next().value);
26450
+ }
26451
+ }
26410
26452
  async sendAndWait(msg, expectedType, timeoutMs) {
26411
26453
  return new Promise((resolve, reject) => {
26412
26454
  const timer = setTimeout(() => {
26413
26455
  this.pendingCallbacks.delete(msg.id);
26414
26456
  reject(new Error(`Relay request timed out after ${timeoutMs}ms`));
26415
26457
  }, timeoutMs);
26416
- this.pendingCallbacks.set(msg.id, (response) => {
26417
- clearTimeout(timer);
26418
- if (response.type === "error") {
26419
- reject(new Error(`Relay error: ${response.payload.message}`));
26420
- } else {
26421
- resolve(response);
26458
+ this.pendingCallbacks.set(msg.id, {
26459
+ expectedType,
26460
+ settle: (response) => {
26461
+ clearTimeout(timer);
26462
+ if (response.type === "error") {
26463
+ reject(new Error(`Relay error: ${response.payload.message}`));
26464
+ } else {
26465
+ resolve(response);
26466
+ }
26422
26467
  }
26423
26468
  });
26424
26469
  this.send(msg);
@@ -27034,6 +27079,7 @@ var A2AServer = class {
27034
27079
  messages: [message],
27035
27080
  artifacts: [],
27036
27081
  metadata: {
27082
+ ...message?.metadata,
27037
27083
  fromAgent: message?.metadata?.fromAgent || "unknown"
27038
27084
  },
27039
27085
  sessionKeyHint: "agent:main"
@@ -27281,9 +27327,9 @@ var A2AServer = class {
27281
27327
  const payload = msg.payload;
27282
27328
  const params = payload.params || payload;
27283
27329
  const rpcId = payload.id || msg.id;
27284
- const acceptQueued = params?.message?.metadata?.acceptQueued === true;
27285
27330
  this.handleSendMessage(params, rpcId).then(async (response) => {
27286
27331
  const task = response.result;
27332
+ const acceptQueued = task?.messages?.[0]?.metadata?.acceptQueued === true;
27287
27333
  if (task?.id && !isTerminalTaskState(task.status?.state)) {
27288
27334
  if (acceptQueued) {
27289
27335
  this.relayClient?.sendTaskResponse(
@@ -27301,7 +27347,12 @@ var A2AServer = class {
27301
27347
  return;
27302
27348
  }
27303
27349
  }
27304
- this.relayClient?.sendTaskResponse(msg, response);
27350
+ if (!acceptQueued) {
27351
+ this.relayClient?.sendTaskResponse(
27352
+ msg,
27353
+ response
27354
+ );
27355
+ }
27305
27356
  }).catch((err) => {
27306
27357
  const errMsg2 = err instanceof Error ? err.message : String(err);
27307
27358
  this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
@@ -28412,6 +28463,69 @@ function errMsg(err) {
28412
28463
  return err instanceof Error ? err.message : String(err);
28413
28464
  }
28414
28465
 
28466
+ // src/platform-heartbeat.ts
28467
+ var DEFAULT_INTERVAL_MS = 6e4;
28468
+ var MIN_INTERVAL_MS = 1e4;
28469
+ function startPlatformHeartbeat(opts) {
28470
+ const env = opts.env ?? process.env;
28471
+ const fetcher = opts.fetcher ?? fetch;
28472
+ const url = opts.url?.trim() || env.PERKOS_HEARTBEAT_URL?.trim();
28473
+ const relayKey = opts.relayKey?.trim() || env.A2A_RELAY_API_KEY?.trim();
28474
+ if (!url || !relayKey) return { stop: () => void 0 };
28475
+ const configuredInterval = Number(
28476
+ opts.intervalMs ?? env.PERKOS_HEARTBEAT_INTERVAL_MS ?? DEFAULT_INTERVAL_MS
28477
+ );
28478
+ const intervalMs = Number.isFinite(configuredInterval) ? Math.max(MIN_INTERVAL_MS, configuredInterval) : DEFAULT_INTERVAL_MS;
28479
+ const a2aRuntime = env.A2A_RUNTIME?.trim() || "hermes-api";
28480
+ const runtimeKind = opts.runtimeKind ?? (a2aRuntime === "openclaw" ? "openclaw" : a2aRuntime === "custom" ? "custom" : "hermes");
28481
+ let stopped = false;
28482
+ let firstSuccess = true;
28483
+ const report = async () => {
28484
+ if (stopped) return;
28485
+ try {
28486
+ const res = await fetcher(url, {
28487
+ method: "POST",
28488
+ headers: {
28489
+ "content-type": "application/json",
28490
+ authorization: `Bearer ${relayKey}`,
28491
+ "x-relay-key": relayKey
28492
+ },
28493
+ body: JSON.stringify({
28494
+ runtimeKind,
28495
+ version: opts.version?.trim() || env.PERKOS_A2A_VERSION?.trim() || "0.12.26",
28496
+ ts: Date.now()
28497
+ })
28498
+ });
28499
+ if (res.ok) {
28500
+ if (firstSuccess) {
28501
+ opts.logger.info(
28502
+ `[perkos-heartbeat] reported online to ${url} (status=${res.status}, every=${intervalMs}ms)`
28503
+ );
28504
+ firstSuccess = false;
28505
+ }
28506
+ } else {
28507
+ opts.logger.warn(
28508
+ `[perkos-heartbeat] ${res.status} from ${url} (continuing)`
28509
+ );
28510
+ }
28511
+ } catch (err) {
28512
+ const msg = err instanceof Error ? err.message : String(err);
28513
+ opts.logger.warn(
28514
+ `[perkos-heartbeat] POST failed to ${url}: ${msg} (continuing)`
28515
+ );
28516
+ }
28517
+ };
28518
+ void report();
28519
+ const timer = setInterval(() => void report(), intervalMs);
28520
+ timer.unref?.();
28521
+ return {
28522
+ stop: () => {
28523
+ stopped = true;
28524
+ clearInterval(timer);
28525
+ }
28526
+ };
28527
+ }
28528
+
28415
28529
  // src/types.ts
28416
28530
  function isWebhookEvent(msg) {
28417
28531
  return msg.type === "webhook_event";
@@ -28444,11 +28558,204 @@ function openclawChatSessionKey(config, convId) {
28444
28558
  const safe = convId.replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 120) || "unknown";
28445
28559
  return `${configured}:perkos-chat-${safe}`;
28446
28560
  }
28561
+ var MISSING_AGENT_HARNESS_RE = /Requested agent harness "([^"]+)" is not registered\./u;
28562
+ function missingAgentHarnessId(error) {
28563
+ const message = error instanceof Error ? error.message : String(error);
28564
+ return message.match(MISSING_AGENT_HARNESS_RE)?.[1]?.trim() || null;
28565
+ }
28566
+ function omitUnavailableAgentHarness(value, unavailableHarnessId) {
28567
+ if (Array.isArray(value)) {
28568
+ return value.map((entry) => omitUnavailableAgentHarness(entry, unavailableHarnessId));
28569
+ }
28570
+ if (!value || typeof value !== "object") return value;
28571
+ const source = value;
28572
+ const next = {};
28573
+ for (const [key, entry] of Object.entries(source)) {
28574
+ if (key === "agentRuntime" && entry && typeof entry === "object" && String(entry.id ?? "").trim() === unavailableHarnessId) {
28575
+ continue;
28576
+ }
28577
+ if ((key === "embeddedHarness" || key === "agentRuntimeOverride" || key === "agentHarnessId") && typeof entry === "string" && entry.trim() === unavailableHarnessId) {
28578
+ continue;
28579
+ }
28580
+ next[key] = omitUnavailableAgentHarness(entry, unavailableHarnessId);
28581
+ }
28582
+ return next;
28583
+ }
28584
+ function alignOpenClawChatSessionModel(entry, configuredModel) {
28585
+ const next = { ...entry };
28586
+ for (const key of [
28587
+ "providerOverride",
28588
+ "modelOverride",
28589
+ "modelOverrideSource",
28590
+ "modelOverrideFallbackOriginProvider",
28591
+ "modelOverrideFallbackOriginModel",
28592
+ "agentRuntimeOverride",
28593
+ "agentHarnessId",
28594
+ "modelProvider",
28595
+ "model",
28596
+ "fallbackNoticeSelectedModel",
28597
+ "fallbackNoticeActiveModel",
28598
+ "fallbackNoticeReason"
28599
+ ]) {
28600
+ delete next[key];
28601
+ }
28602
+ const modelRef = configuredModel?.trim();
28603
+ const separator = modelRef?.indexOf("/") ?? -1;
28604
+ if (modelRef && separator > 0 && separator < modelRef.length - 1) {
28605
+ const provider = modelRef.slice(0, separator);
28606
+ const model = modelRef.slice(separator + 1);
28607
+ next.providerOverride = provider;
28608
+ next.modelOverride = model;
28609
+ next.modelOverrideSource = "user";
28610
+ next.modelProvider = provider;
28611
+ next.model = model;
28612
+ }
28613
+ return next;
28614
+ }
28615
+ async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
28616
+ const patchSessionEntry = api.runtime?.agent?.session?.patchSessionEntry;
28617
+ if (typeof patchSessionEntry !== "function") {
28618
+ logger.info("[perkos-chat] OpenClaw session metadata API unavailable \u2014 using gateway session defaults");
28619
+ return;
28620
+ }
28621
+ try {
28622
+ await patchSessionEntry({
28623
+ sessionKey,
28624
+ fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
28625
+ preserveActivity: true,
28626
+ replaceEntry: true,
28627
+ update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
28628
+ });
28629
+ logger.info(
28630
+ config.runtime?.model ? `[perkos-chat] session ${sessionKey} pinned to ${config.runtime.model}` : `[perkos-chat] session ${sessionKey} aligned to the gateway's active default model`
28631
+ );
28632
+ } catch (err) {
28633
+ logger.error(
28634
+ `[perkos-chat] failed to align OpenClaw session model: ${err instanceof Error ? err.message : String(err)}`
28635
+ );
28636
+ }
28637
+ }
28638
+ async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
28639
+ const patchSessionEntry = api.runtime?.agent?.session?.patchSessionEntry;
28640
+ if (typeof patchSessionEntry !== "function") return;
28641
+ try {
28642
+ await patchSessionEntry({
28643
+ sessionKey,
28644
+ fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
28645
+ preserveActivity: true,
28646
+ replaceEntry: true,
28647
+ update: (entry) => ({
28648
+ ...alignOpenClawChatSessionModel(entry, config.runtime?.model),
28649
+ // OpenClaw <= 2026.5.x calls its built-in runtime "pi". Newer
28650
+ // gateways normalize the alias to their native "openclaw" harness.
28651
+ agentRuntimeOverride: "pi",
28652
+ agentHarnessId: "pi"
28653
+ })
28654
+ });
28655
+ logger.info(`[perkos-chat] session ${sessionKey} retrying with the native OpenClaw harness`);
28656
+ } catch (err) {
28657
+ logger.error(
28658
+ `[perkos-chat] failed to select native OpenClaw harness: ${err instanceof Error ? err.message : String(err)}`
28659
+ );
28660
+ }
28661
+ }
28662
+ function extractOpenClawChatReply(result) {
28663
+ const finalText = result.meta?.finalAssistantVisibleText?.trim() || result.meta?.finalAssistantRawText?.trim();
28664
+ if (finalText) return finalText;
28665
+ const payloadText = result.payloads?.filter((payload) => !payload.isError && !payload.isReasoning && !payload.isCommentary).map((payload) => payload.text?.trim()).filter((text) => Boolean(text)).join("\n\n").trim();
28666
+ return payloadText || null;
28667
+ }
28668
+ function openclawAgentId(config) {
28669
+ const sessionKey = config.runtime?.sessionKey || "agent:main";
28670
+ const parts = sessionKey.split(":");
28671
+ return parts[0] === "agent" && parts[1] ? parts[1] : "main";
28672
+ }
28673
+ async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPrompt, logger) {
28674
+ const runEmbeddedAgent = api.runtime?.agent?.runEmbeddedAgent;
28675
+ const currentConfig = api.runtime?.config?.current;
28676
+ const resolveAgentWorkspaceDir = api.runtime?.agent?.resolveAgentWorkspaceDir;
28677
+ if (typeof runEmbeddedAgent !== "function" || typeof currentConfig !== "function" || typeof resolveAgentWorkspaceDir !== "function") {
28678
+ logger.info("[perkos-chat] embedded OpenClaw runner unavailable \u2014 using gateway wake fallback");
28679
+ return null;
28680
+ }
28681
+ const agentId = openclawAgentId(config);
28682
+ const runtimeConfig = currentConfig();
28683
+ const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
28684
+ const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
28685
+ const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID6();
28686
+ const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
28687
+ const timeoutMs = Math.min(
28688
+ 75e3,
28689
+ typeof configuredTimeout === "number" && configuredTimeout > 0 ? configuredTimeout : 75e3
28690
+ );
28691
+ const run = async (configOverride, retryNative = false) => runEmbeddedAgent({
28692
+ sessionId,
28693
+ sessionKey,
28694
+ agentId,
28695
+ workspaceDir,
28696
+ config: configOverride,
28697
+ prompt,
28698
+ transcriptPrompt,
28699
+ timeoutMs,
28700
+ runId: randomUUID6(),
28701
+ trigger: "user",
28702
+ messageChannel: "perkos-chat",
28703
+ disableTools: true,
28704
+ disableMessageTool: true,
28705
+ terminalReplyExpectation: "required",
28706
+ suppressLiveStreamOutput: true,
28707
+ cleanupBundleMcpOnRunEnd: true,
28708
+ ...retryNative ? {
28709
+ // Cross-version compatibility: 2026.5.x recognizes `pi`, while
28710
+ // current gateways recognize the explicit native runtime override.
28711
+ agentHarnessId: "pi",
28712
+ agentHarnessRuntimeOverride: "openclaw",
28713
+ suppressNextUserMessagePersistence: true
28714
+ } : {}
28715
+ });
28716
+ try {
28717
+ let result;
28718
+ try {
28719
+ result = await run(runtimeConfig);
28720
+ } catch (err) {
28721
+ const unavailableHarnessId = missingAgentHarnessId(err);
28722
+ if (!unavailableHarnessId) throw err;
28723
+ logger.info(
28724
+ `[perkos-chat] configured harness ${unavailableHarnessId} is unavailable; retrying with the native OpenClaw runtime and the same gateway-selected model`
28725
+ );
28726
+ await forceNativeOpenClawChatRuntime(api, config, sessionKey, logger);
28727
+ result = await run(
28728
+ omitUnavailableAgentHarness(runtimeConfig, unavailableHarnessId),
28729
+ true
28730
+ );
28731
+ }
28732
+ const reply = extractOpenClawChatReply(result);
28733
+ if (!reply) {
28734
+ logger.error(`[perkos-chat] embedded OpenClaw run completed without reply text for ${sessionKey}`);
28735
+ return null;
28736
+ }
28737
+ logger.info(`[perkos-chat] embedded OpenClaw run completed for ${sessionKey}`);
28738
+ return reply;
28739
+ } catch (err) {
28740
+ logger.error(
28741
+ `[perkos-chat] embedded OpenClaw run failed for ${sessionKey}: ${err instanceof Error ? err.message : String(err)}`
28742
+ );
28743
+ return null;
28744
+ }
28745
+ }
28447
28746
  function parseWalletFromIdentity(identity) {
28448
28747
  if (typeof identity !== "string" || !identity.startsWith("user:")) return null;
28449
28748
  const addr = identity.slice("user:".length);
28450
28749
  return addr.startsWith("0x") ? addr.toLowerCase() : null;
28451
28750
  }
28751
+ var CHAT_CONTEXT_MESSAGE_LIMIT = 12;
28752
+ var CHAT_CONTEXT_CHAR_LIMIT = 6e3;
28753
+ function formatRecentChatContext(messages, currentMessageId) {
28754
+ const prior = messages.filter((message) => message.id !== currentMessageId).slice(-CHAT_CONTEXT_MESSAGE_LIMIT).map((message) => `${message.from}: ${message.text.trim()}`).filter((line) => line.length > 0);
28755
+ if (prior.length === 0) return "(No earlier messages in this conversation.)";
28756
+ const transcript = prior.join("\n");
28757
+ return transcript.length <= CHAT_CONTEXT_CHAR_LIMIT ? transcript : `\u2026${transcript.slice(-CHAT_CONTEXT_CHAR_LIMIT)}`;
28758
+ }
28452
28759
  async function deliverToHermes(config, message, logger) {
28453
28760
  const runtime = config.runtime || {};
28454
28761
  const baseUrl = (runtime.hermesUrl || "http://127.0.0.1:8642").replace(/\/+$/, "");
@@ -28463,7 +28770,8 @@ async function deliverToHermes(config, message, logger) {
28463
28770
  };
28464
28771
  if (token) headers.authorization = `Bearer ${token}`;
28465
28772
  const normalizedEndpoint = endpoint.replace(/^\/?/, "/");
28466
- const body = normalizedEndpoint.startsWith("/v1/chat/completions") ? { model: "hermes-agent", messages: [{ role: "user", content: message }], stream: false } : normalizedEndpoint.startsWith("/v1/runs") ? { input: message, session_id: sessionKey } : normalizedEndpoint.startsWith("/v1/responses") ? { model: "hermes-agent", input: message, store: true } : { sessionKey, message };
28773
+ const model = runtime.model?.trim();
28774
+ const body = normalizedEndpoint.startsWith("/v1/chat/completions") ? { ...model ? { model } : {}, messages: [{ role: "user", content: message }], stream: false } : normalizedEndpoint.startsWith("/v1/runs") ? { input: message, session_id: sessionKey } : normalizedEndpoint.startsWith("/v1/responses") ? { ...model ? { model } : {}, input: message, store: true } : { sessionKey, message };
28467
28775
  const response = await fetch(url, {
28468
28776
  method: "POST",
28469
28777
  headers,
@@ -28681,6 +28989,13 @@ function register(api) {
28681
28989
  onChatDeliver: async (frame) => {
28682
28990
  const walletAddress = parseWalletFromIdentity(frame.from);
28683
28991
  const marker = `[PERKOS_CHAT:${frame.convId}]`;
28992
+ const recentPage = await chatClient?.getStore().readPage(frame.convId, {
28993
+ limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
28994
+ });
28995
+ const recentContext = formatRecentChatContext(
28996
+ recentPage?.messages ?? [],
28997
+ frame.id
28998
+ );
28684
28999
  const eventText = [
28685
29000
  marker,
28686
29001
  `From: ${frame.from}`,
@@ -28694,6 +29009,27 @@ function register(api) {
28694
29009
  walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
28695
29010
  ` text: <your reply>`,
28696
29011
  "",
29012
+ "Recent conversation context (oldest to newest):",
29013
+ recentContext,
29014
+ "",
29015
+ "Use the recent context. Do not ask again for information the user already provided.",
29016
+ "",
29017
+ "Message body:",
29018
+ frame.text
29019
+ ].filter(Boolean).join("\n");
29020
+ const directRunPrompt = [
29021
+ marker,
29022
+ `From: ${frame.from}`,
29023
+ `Conversation: ${frame.convId}`,
29024
+ frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
29025
+ "",
29026
+ "Reply to this PerkOS chat message using your current runtime configuration.",
29027
+ "Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
29028
+ "Use the recent context and do not ask again for information already provided.",
29029
+ "",
29030
+ "Recent conversation context (oldest to newest):",
29031
+ recentContext,
29032
+ "",
28697
29033
  "Message body:",
28698
29034
  frame.text
28699
29035
  ].filter(Boolean).join("\n");
@@ -28710,14 +29046,38 @@ function register(api) {
28710
29046
  logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
28711
29047
  return;
28712
29048
  }
29049
+ const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
29050
+ await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
29051
+ if (walletAddress) {
29052
+ const reply = await runOpenClawChatTurn(
29053
+ api,
29054
+ pluginConfig,
29055
+ chatSessionKey,
29056
+ directRunPrompt,
29057
+ frame.text,
29058
+ logger
29059
+ );
29060
+ if (reply) {
29061
+ const result = await chatClient?.sendReply({
29062
+ convId: frame.convId,
29063
+ walletAddress,
29064
+ text: reply,
29065
+ replyTo: frame.id
29066
+ });
29067
+ logger.info(
29068
+ `[perkos-chat] embedded reply ${result?.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`
29069
+ );
29070
+ return;
29071
+ }
29072
+ }
28713
29073
  if (enqueueSystemEvent) {
28714
- enqueueSystemEvent(eventText, { sessionKey: openclawChatSessionKey(pluginConfig, frame.convId) });
29074
+ enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
28715
29075
  }
28716
29076
  wakeGatewayAgent(
28717
29077
  requestHeartbeatNow,
28718
29078
  `[PerkOS-Chat] inbound message in ${frame.convId}`,
28719
29079
  logger,
28720
- openclawChatSessionKey(pluginConfig, frame.convId)
29080
+ chatSessionKey
28721
29081
  );
28722
29082
  },
28723
29083
  onChannelJoin: async (frame) => {
@@ -28741,6 +29101,29 @@ function register(api) {
28741
29101
  }
28742
29102
  });
28743
29103
  }
29104
+ if (pluginConfig.platform?.heartbeatUrl) {
29105
+ let platformHeartbeat = null;
29106
+ api.registerService({
29107
+ id: "perkos-a2a-platform-heartbeat",
29108
+ start: () => {
29109
+ platformHeartbeat = startPlatformHeartbeat({
29110
+ logger: {
29111
+ info: (message) => logger.info(message),
29112
+ warn: (message) => (logger.warn || logger.info).call(logger, message)
29113
+ },
29114
+ url: pluginConfig.platform?.heartbeatUrl,
29115
+ relayKey: pluginConfig.relay?.apiKey,
29116
+ runtimeKind: "openclaw",
29117
+ version: "0.12.26",
29118
+ intervalMs: pluginConfig.platform?.heartbeatIntervalMs
29119
+ });
29120
+ },
29121
+ stop: () => {
29122
+ platformHeartbeat?.stop();
29123
+ platformHeartbeat = null;
29124
+ }
29125
+ });
29126
+ }
28744
29127
  api.registerTool({
28745
29128
  name: "perkos_a2a_send",
28746
29129
  description: "Send a task to another agent via PerkOS A2A. Use this to delegate work, ask a peer to research, or route a chain/circular workflow. The message may name additional agents and a final notification target; the bridge preserves the route and prevents loops.",
@@ -29122,10 +29505,15 @@ export {
29122
29505
  ChatStore,
29123
29506
  RelayClient,
29124
29507
  RelayHub,
29508
+ alignOpenClawChatSessionModel,
29125
29509
  register as default,
29126
29510
  detectNetworking,
29511
+ extractOpenClawChatReply,
29512
+ formatRecentChatContext,
29127
29513
  isWebhookEvent,
29128
- openclawChatSessionKey
29514
+ omitUnavailableAgentHarness,
29515
+ openclawChatSessionKey,
29516
+ runOpenClawChatTurn
29129
29517
  };
29130
29518
  /*! Bundled license information:
29131
29519