@openclaw/acpx 2026.10.5-beta.3 → 2026.10.10-beta.1

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 CHANGED
@@ -21,10 +21,15 @@ to regenerate wrappers written by an older plugin version.
21
21
  ## What it provides
22
22
 
23
23
  - ACP-backed agent runtime sessions.
24
+ - Built-in ACPX harness targets, including `grok-build` for the Grok Build CLI.
24
25
  - Plugin-owned session and transport management.
25
26
  - MCP bridge helpers for OpenClaw tools and plugin tools.
26
27
  - Static runtime assets used by the ACP process bridge.
27
28
 
29
+ If the Gateway restarts before a one-shot session receives its first prompt, ACPX
30
+ reuses the initialized conversation when the agent supports loading or resuming
31
+ sessions. Completed one-shot runs still start a fresh conversation.
32
+
28
33
  ## Native agents in the model picker
29
34
 
30
35
  Install GitHub Copilot CLI, Kilo Code, OpenCode, Pi ACP, or Qwen Code and complete its login on the Gateway host,
@@ -83,6 +88,10 @@ Use the ACP docs for harness-specific setup, permission modes, and model/runtime
83
88
  - https://docs.openclaw.ai/tools/acp-agents-setup
84
89
  - https://docs.openclaw.ai/tools/acp-agents
85
90
 
91
+ `grok-build` is an agent target (`agentId: "grok-build"`), not the xAI model id
92
+ `xai/grok-build-0.1`. The target launches the installed Grok Build CLI through
93
+ `grok agent stdio`.
94
+
86
95
  ## Package
87
96
 
88
97
  - Plugin id: `acpx`
@@ -1,7 +1,7 @@
1
- import { createNativeSessionBindingAuthority } from "openclaw/plugin-sdk/agent-harness-session-runtime";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { consumeAcpTurnStream } from "openclaw/plugin-sdk/acp-runtime";
4
3
  import { clearActiveEmbeddedRun, emitAgentEvent, resolveAgentHarnessBeforePromptBuildResult, resolveBootstrapContextForRun, setActiveEmbeddedRun } from "openclaw/plugin-sdk/agent-harness-runtime";
4
+ import { createNativeSessionBindingScope } from "openclaw/plugin-sdk/agent-harness-session-runtime";
5
5
  import { SessionManager, buildSessionContext } from "openclaw/plugin-sdk/agent-sessions";
6
6
  import { DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS } from "openclaw/plugin-sdk/approval-runtime";
7
7
  import { toErrorObject } from "openclaw/plugin-sdk/error-runtime";
@@ -299,7 +299,7 @@ async function runAcpHarnessAttempt(params) {
299
299
  ...assistant,
300
300
  idempotencyKey: assistantItemId
301
301
  },
302
- preparation: { source: createNativeSessionBindingAuthority([], input.hostCapabilities.assertActive).assertLegacyCurrent }
302
+ preparation: { source: createNativeSessionBindingScope(input.hostCapabilities.assertActive).assertLegacyCurrent }
303
303
  });
304
304
  if (written.kind !== "result") throw new Error("ACP assistant transcript was not committed");
305
305
  assistant = written.result.message;
@@ -4,22 +4,22 @@ import { createReadStream } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
7
- import { createSessionCatalogFamily, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey } from "openclaw/plugin-sdk/session-catalog";
7
+ import { createSessionCatalogFamily } from "openclaw/plugin-sdk/session-catalog";
8
8
  import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
9
9
  import process from "node:process";
10
10
  import { resolveSessionAgentIdsStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
11
+ import { createAcpSessionCatalogAdoption, readSessionCatalogJsonlActivity } from "openclaw/plugin-sdk/session-catalog-runtime";
11
12
  import { sessionCatalogPaging } from "openclaw/plugin-sdk/session-catalog-paging";
12
13
  import { safeParseJson, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
13
14
  import fs$1 from "node:fs/promises";
14
15
  import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
15
- import { isPathStrictlyInside, readFileRangeAsync } from "openclaw/plugin-sdk/file-access-runtime";
16
+ import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
16
17
  //#region extensions/acpx/src/pi-session-store.ts
17
18
  const MAX_DISCOVERY_FILES = 1e4;
18
19
  const SUMMARY_SCAN_BATCH_SIZE = 100;
19
20
  const MAX_SUMMARY_CACHE_ENTRIES = 256;
20
21
  const MAX_SESSION_BYTES = 33554432;
21
22
  const MAX_SUMMARY_LINE_BYTES = 1048576;
22
- const APPEND_PROOF_EDGE_BYTES = 65536;
23
23
  const IO_CONCURRENCY = 8;
24
24
  const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
25
25
  const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
@@ -200,30 +200,6 @@ async function scanSummaryAppend(candidate, start, state) {
200
200
  }
201
201
  }
202
202
  }
203
- async function readAppendProof(file, size) {
204
- const length = Math.min(size, APPEND_PROOF_EDGE_BYTES);
205
- if (length === 0) return {
206
- head: Buffer.alloc(0),
207
- tail: Buffer.alloc(0)
208
- };
209
- const handle = await fs$1.open(file, "r");
210
- try {
211
- const head = Buffer.alloc(length);
212
- const tail = Buffer.alloc(length);
213
- const [headRead, tailRead] = await Promise.all([handle.read(head, 0, length, 0), handle.read(tail, 0, length, size - length)]);
214
- return {
215
- head: head.subarray(0, headRead.bytesRead),
216
- tail: tail.subarray(0, tailRead.bytesRead)
217
- };
218
- } finally {
219
- await handle.close();
220
- }
221
- }
222
- async function cachedPrefixIsUnchanged(candidate, cached) {
223
- if (cached.identity !== candidate.identity || cached.size >= candidate.size) return false;
224
- const current = await readAppendProof(candidate.file, cached.size);
225
- return current.head.equals(cached.appendProof.head) && current.tail.equals(cached.appendProof.tail);
226
- }
227
203
  async function readPiSessionSummary(candidate) {
228
204
  const cached = summaryCache.get(candidate.file);
229
205
  if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
@@ -236,9 +212,8 @@ async function readPiSessionSummary(candidate) {
236
212
  }
237
213
  let summary;
238
214
  let scanState;
239
- let appendProof;
240
215
  try {
241
- const resumable = cached && await cachedPrefixIsUnchanged(candidate, cached) ? cached : void 0;
216
+ const resumable = cached?.identity === candidate.identity && cached.size < candidate.size ? cached : void 0;
242
217
  scanState = resumable ? {
243
218
  ...resumable.scanState,
244
219
  pending: Buffer.from(resumable.scanState.pending)
@@ -248,7 +223,6 @@ async function readPiSessionSummary(candidate) {
248
223
  invalid: false
249
224
  };
250
225
  await scanSummaryAppend(candidate, resumable?.size ?? 0, scanState);
251
- appendProof = await readAppendProof(candidate.file, candidate.size);
252
226
  const projectedState = {
253
227
  ...scanState,
254
228
  pending: Buffer.from(scanState.pending)
@@ -284,8 +258,7 @@ async function readPiSessionSummary(candidate) {
284
258
  cacheSummary(candidate.file, {
285
259
  ...candidate,
286
260
  summary,
287
- scanState,
288
- appendProof
261
+ scanState
289
262
  });
290
263
  if (summary) threadFileCache.set(threadCacheKey(candidate.storeRoot, summary.threadId), candidate.file);
291
264
  return summary;
@@ -349,24 +322,20 @@ async function readPiSessionFileBaseline(threadId, env) {
349
322
  }
350
323
  async function readPiSessionById(threadId, env) {
351
324
  const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
352
- let file = threadFileCache.get(cacheKey);
353
- for (let attempt = 0; attempt < 2; attempt += 1) {
354
- if (!file) file = (await findPiSummary(threadId, env))?.file;
355
- if (!file) throw new Error("Pi session was not found");
356
- try {
357
- const stats = await fs$1.stat(file);
358
- if (!stats.isFile()) throw new Error("Pi session is not a file");
359
- if (stats.size > MAX_SESSION_BYTES) throw new RangeError("Pi session exceeds the 32 MiB read safety limit");
360
- const entries = parsePiJsonLines(await fs$1.readFile(file, "utf8"));
361
- if (entries[0]?.type === "session" && entries[0].id === threadId) return entries;
362
- } catch (error) {
363
- if (error instanceof RangeError) throw error;
364
- if (attempt > 0) throw new Error("Pi session is unavailable", { cause: error });
365
- }
325
+ const file = threadFileCache.get(cacheKey) ?? (await findPiSummary(threadId, env))?.file;
326
+ if (!file) throw new Error("Pi session was not found");
327
+ try {
328
+ const stats = await fs$1.stat(file);
329
+ if (!stats.isFile()) throw new Error("Pi session is not a file");
330
+ if (stats.size > MAX_SESSION_BYTES) throw new RangeError("Pi session exceeds the 32 MiB read safety limit");
331
+ const entries = parsePiJsonLines(await fs$1.readFile(file, "utf8"));
332
+ if (entries[0]?.type !== "session" || entries[0].id !== threadId) throw new Error("Pi session changed during read");
333
+ return entries;
334
+ } catch (error) {
366
335
  threadFileCache.delete(cacheKey);
367
- file = void 0;
336
+ if (error instanceof RangeError) throw error;
337
+ throw new Error("Pi session is unavailable", { cause: error });
368
338
  }
369
- throw new Error("Pi session changed during read");
370
339
  }
371
340
  //#endregion
372
341
  //#region extensions/acpx/src/pi-session-catalog.ts
@@ -564,27 +533,6 @@ async function readLocalPiTranscriptPage(value) {
564
533
  }
565
534
  //#endregion
566
535
  //#region extensions/acpx/src/pi-session-upstream-activity.ts
567
- const MAX_PI_UPSTREAM_SCAN_BYTES = 1048576;
568
- function parseCompletePiRows(tail) {
569
- const entries = [];
570
- let lineStart = 0;
571
- let classifiedBytes = 0;
572
- for (let index = 0; index < tail.length; index += 1) {
573
- if (tail[index] !== 10) continue;
574
- const line = tail.subarray(lineStart, index).toString("utf8").trim();
575
- if (line) {
576
- const value = safeParseJson(line);
577
- if (!isRecord(value)) break;
578
- entries.push(value);
579
- }
580
- classifiedBytes = index + 1;
581
- lineStart = index + 1;
582
- }
583
- return {
584
- entries,
585
- classifiedBytes
586
- };
587
- }
588
536
  function readFilePath(probe) {
589
537
  return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
590
538
  }
@@ -611,48 +559,20 @@ async function checkPiSessionUpstreamActivity(probe) {
611
559
  const filePath = readFilePath(probe);
612
560
  const markerOffset = readMarkerOffset(probe);
613
561
  if (!filePath || markerOffset === void 0) return;
614
- let handle;
615
- try {
616
- handle = await fs$1.open(filePath, "r");
617
- } catch (error) {
618
- return isRecord(error) && error.code === "ENOENT" ? {
619
- kind: "missing",
620
- sessionKey: probe.sessionKey
621
- } : void 0;
622
- }
623
- try {
624
- const stat = await handle.stat();
625
- if (!stat.isFile()) return {
626
- kind: "missing",
627
- sessionKey: probe.sessionKey
628
- };
629
- if (stat.size <= markerOffset) return;
630
- const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
631
- const { entries, classifiedBytes } = parseCompletePiRows(await readFileRangeAsync(handle, markerOffset, readLength));
632
- if (classifiedBytes === 0) return;
633
- let humanTurns = 0;
634
- let occurredAt;
635
- for (const entry of entries) {
636
- if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
637
- const text = piMessageText(entry.message.content);
638
- if (!isExternalUserText(probe, text)) continue;
639
- humanTurns += 1;
640
- occurredAt = Math.max(occurredAt ?? 0, parseDateFirstTimestampMs(entry.message.timestamp) ?? parseDateFirstTimestampMs(entry.timestamp) ?? stat.mtimeMs);
562
+ return readSessionCatalogJsonlActivity({
563
+ probe,
564
+ filePath,
565
+ offset: markerOffset,
566
+ classifyLine(line) {
567
+ const entry = safeParseJson(line);
568
+ if (!isRecord(entry)) return false;
569
+ if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") return;
570
+ return {
571
+ userText: piMessageText(entry.message.content),
572
+ occurredAt: parseDateFirstTimestampMs(entry.message.timestamp) ?? parseDateFirstTimestampMs(entry.timestamp)
573
+ };
641
574
  }
642
- const nextOffset = markerOffset + classifiedBytes;
643
- return {
644
- kind: "activity",
645
- sessionKey: probe.sessionKey,
646
- humanTurns,
647
- nextMarker: { offset: nextOffset },
648
- ...humanTurns > 0 ? {
649
- occurredAt: occurredAt ?? stat.mtimeMs,
650
- dedupeId: String(nextOffset)
651
- } : {}
652
- };
653
- } finally {
654
- await handle.close();
655
- }
575
+ });
656
576
  }
657
577
  async function checkPiUpstreamActivity(probes) {
658
578
  const outcomes = [];
@@ -677,7 +597,7 @@ async function requireLocalPiSession(threadId) {
677
597
  return session;
678
598
  }
679
599
  function currentPiCatalogConfig(api) {
680
- return api.runtime.config?.current?.() ?? api.config ?? {};
600
+ return api.runtime.config.current();
681
601
  }
682
602
  function resolvePiContinuationAvailability(api) {
683
603
  const availability = resolveAcpSessionAvailability({
@@ -695,64 +615,20 @@ function resolvePiContinuationAvailability(api) {
695
615
  message: "Pi CLI is unavailable"
696
616
  };
697
617
  }
698
- function listAdoptedPiSessions(api, agentId, sessionEntries) {
699
- return listAdoptedSessionCatalogSessions({
700
- ...agentId ? { agentId } : {},
701
- config: currentPiCatalogConfig(api),
702
- pluginId: api.id,
703
- runtime: api.runtime,
704
- sessionEntries,
705
- sourceFromEntry: (entry) => {
706
- const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
707
- const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
708
- return marker && typeof marker.sourceThreadId === "string" ? {
709
- hostId: PI_LOCAL_SESSION_HOST_ID,
710
- threadId: marker.sourceThreadId
711
- } : void 0;
712
- }
713
- });
714
- }
715
- async function createAdoptedPiSession(params) {
716
- const config = currentPiCatalogConfig(params.api);
717
- const marker = { sourceThreadId: params.threadId };
718
- return { sessionKey: (await params.api.runtime.agent.session.createSessionEntry({
719
- cfg: config,
720
- key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, params.threadId),
721
- agentId: params.agentId,
722
- recoverMatchingInitialEntry: true,
723
- ...params.session.name ? { displayName: params.session.name } : {},
724
- ...params.session.cwd ? { spawnedCwd: params.session.cwd } : {},
725
- initialEntry: {
726
- acpBackendId: ACPX_BACKEND_ID,
727
- acpSessionBinding: {
728
- acpAgentId: PI_ACP_AGENT_ID,
729
- agentSessionId: params.threadId
730
- },
731
- pluginExtensions: { acpx: { piSessionCatalog: marker } }
732
- },
733
- afterCreate: async (entry) => {
734
- await importSessionCatalogHistory({
735
- catalogId: "pi",
736
- threadId: params.threadId,
737
- read: async ({ cursor, limit }) => await readLocalPiTranscriptPage({
738
- threadId: params.threadId,
739
- limit,
740
- ...cursor ? { cursor } : {}
741
- }),
742
- sessionId: entry.sessionId,
743
- sessionKey: entry.key,
744
- agentId: entry.agentId,
745
- ...params.session.cwd ? { cwd: params.session.cwd } : {},
746
- config
747
- });
748
- return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
749
- }
750
- })).key };
751
- }
752
618
  function assertPiLocalAccess(hostId, allowProcessHomeFallback) {
753
619
  if (hostId === "gateway" && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new Error("local Pi sessions are unavailable in isolated state");
754
620
  }
755
621
  function createPiSessionCatalogRuntime(api) {
622
+ const adoption = createAcpSessionCatalogAdoption({
623
+ api,
624
+ config: () => currentPiCatalogConfig(api),
625
+ catalogId: PI_ACP_AGENT_ID,
626
+ hostId: PI_LOCAL_SESSION_HOST_ID,
627
+ keyPrefix: PI_ADOPTED_SESSION_KEY_PREFIX,
628
+ markerPluginId: "acpx",
629
+ markerKey: "piSessionCatalog",
630
+ read: readLocalPiTranscriptPage
631
+ });
756
632
  return createSessionCatalogFamily({
757
633
  runtime: api.runtime,
758
634
  local: {
@@ -821,15 +697,12 @@ function createPiSessionCatalogRuntime(api) {
821
697
  agentId
822
698
  }).sessionAgentId,
823
699
  availability: () => resolvePiContinuationAvailability(api),
824
- listAdopted: (agentId, sessionEntries) => listAdoptedPiSessions(api, agentId, sessionEntries),
700
+ listAdopted: (agentId, sessionEntries) => adoption.listAdopted(agentId, sessionEntries),
825
701
  loadSession: requireLocalPiSession,
826
702
  validateSession: (session) => {
827
703
  if (!session.canContinue) throw new Error("Pi session is outside the session store supported by pi-acp");
828
704
  },
829
- create: async (params) => await createAdoptedPiSession({
830
- api,
831
- ...params
832
- }),
705
+ create: (params) => adoption.create(params),
833
706
  complete: async (continued, threadId) => await linkContinuedPiSession(continued.sessionKey, threadId),
834
707
  nodeReadOnlyMessage: "paired-node Pi session rows are view-only"
835
708
  },
@@ -53,7 +53,7 @@ function createLazyAcpRuntimeProxy(resolveRuntime) {
53
53
  * immediately, then imports the heavier service only when a session needs it.
54
54
  */
55
55
  const ACPX_BACKEND_ID = "acpx";
56
- const loadServiceModule = createLazyRuntimeModule(() => import("./service-De_oVTL3.mjs"));
56
+ const loadServiceModule = createLazyRuntimeModule(() => import("./service-5eVJSjDE.mjs"));
57
57
  function unregisterOwnedRuntime(runtime) {
58
58
  if (runtime && getAcpRuntimeBackend(ACPX_BACKEND_ID)?.runtime === runtime) unregisterAcpRuntimeBackend(ACPX_BACKEND_ID);
59
59
  }