openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
3,032 lines • 112 kB
JavaScript
import { R as timestampMsToIsoString, j as resolveIntegerOption } from "./number-coercion-CLj0HTDM.js";
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-DItp3I4t.js";
import { a as normalizeFastMode, g as readStringValue, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty, t as hasNonEmptyString } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { t as isMainModule } from "./is-main-CH4EEB_R.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { a as routeLogsToStderr } from "./console-BSaonU6s.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { n as closeOpenClawStateDatabase } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { _t as coerceRequiredSqliteNumber, i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import { n as THINKING_LEVELS_HELP, t as BASE_THINKING_LEVELS } from "./thinking.shared-bHYuuc1L.js";
import { t as hasHttpUrlPrefix } from "./url-protocol-OU3K-ySz.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES, t as GATEWAY_CLIENT_CAPS } from "./client-info-B1bPgeKr.js";
import "./config-Cs0XXL3x.js";
import { r as resolveGatewayClientBootstrap } from "./client-bootstrap-BE0hdooM.js";
import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-B1nULpha.js";
import { t as GatewayClient } from "./client-I-RoP1Al.js";
import { i as readNonNegativeInteger, n as readMetadataNumber, r as readMetadataString, t as readBool } from "./meta-CCPwyPnB.js";
import { t as toAcpSessionLineageMeta } from "./session-lineage-meta-C_v-FnZ-.js";
import { t as createInMemorySessionStore } from "./session-DC5zxuKu.js";
import { t as normalizeAcpProvenanceMode } from "./types-BDs6k5lu.js";
import { t as readSecretFromFile } from "./secret-file-C0S9kU0Q.js";
import { t as normalizeTerminalChatSendAckStatus } from "./chat-send-ack-status-DrkAG_6U.js";
import { t as ACP_AGENT_INFO } from "./types-C3lEWSlg.js";
import { fileURLToPath } from "node:url";
import path from "node:path";
import os from "node:os";
import { randomUUID } from "node:crypto";
import { Readable, Writable } from "node:stream";
import { AGENT_METHODS, AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
//#region src/acp/event-ledger.types.ts
const DEFAULT_MAX_SESSIONS = 200;
const DEFAULT_MAX_EVENTS_PER_SESSION = 5e3;
const DEFAULT_MAX_SERIALIZED_BYTES = 16777216;
function normalizeAcpLedgerOptions(options = {}) {
return {
maxSessions: resolveIntegerOption(options.maxSessions, DEFAULT_MAX_SESSIONS, { min: 1 }),
maxEventsPerSession: resolveIntegerOption(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION, { min: 1 }),
maxSerializedBytes: resolveIntegerOption(options.maxSerializedBytes, DEFAULT_MAX_SERIALIZED_BYTES, { min: 1024 }),
now: options.now ?? Date.now
};
}
function cloneAcpLedgerValue(value) {
return structuredClone(value);
}
function createAcpPromptUpdates(prompt) {
return prompt.map((content) => ({
sessionUpdate: "user_message_chunk",
content: cloneAcpLedgerValue(content)
}));
}
function normalizeAcpLedgerEvent(raw) {
if (!isRecord(raw) || !isRecord(raw.update)) return;
const seq = raw.seq;
const at = raw.at;
const sessionId = raw.sessionId;
const sessionKey = raw.sessionKey;
const runId = raw.runId;
const sessionUpdate = raw.update.sessionUpdate;
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 0 || typeof at !== "number" || !Number.isFinite(at) || typeof sessionId !== "string" || typeof sessionKey !== "string" || typeof sessionUpdate !== "string") return;
return {
seq,
at,
sessionId,
sessionKey,
...typeof runId === "string" && runId ? { runId } : {},
update: cloneAcpLedgerValue(raw.update)
};
}
//#endregion
//#region src/acp/event-ledger.memory.ts
const LEDGER_VERSION = 1;
function createEmptyStore() {
return {
version: LEDGER_VERSION,
sessions: {}
};
}
function getSerializedLedgerByteLength(store) {
return Buffer.byteLength(JSON.stringify(store), "utf8");
}
function getOrCreateSession(state, params) {
const now = state.now();
const existing = state.store.sessions[params.sessionId];
if (!params.reset && existing) {
existing.sessionKey = params.sessionKey;
if (params.cwd) existing.cwd = params.cwd;
existing.complete = existing.complete || params.complete;
existing.updatedAt = now;
return existing;
}
const session = {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
cwd: params.cwd,
complete: params.complete,
createdAt: now,
updatedAt: now,
nextSeq: 1,
events: []
};
state.store.sessions[params.sessionId] = session;
return session;
}
function trimLedger(state) {
const sessions = Object.values(state.store.sessions);
for (const session of sessions) {
if (session.events.length <= state.maxEventsPerSession) continue;
session.events = session.events.slice(-state.maxEventsPerSession);
session.complete = false;
}
if (sessions.length > state.maxSessions) for (const session of sessions.toSorted((a, b) => b.updatedAt - a.updatedAt).slice(state.maxSessions)) delete state.store.sessions[session.sessionId];
let serializedBytes = getSerializedLedgerByteLength(state.store);
if (serializedBytes <= state.maxSerializedBytes) return;
const oldestFirst = Object.values(state.store.sessions).toSorted((a, b) => a.updatedAt - b.updatedAt);
for (const session of oldestFirst) while (serializedBytes > state.maxSerializedBytes && session.events.length > 0) {
session.events.shift();
session.complete = false;
serializedBytes = getSerializedLedgerByteLength(state.store);
}
for (const session of oldestFirst) {
if (serializedBytes <= state.maxSerializedBytes) break;
delete state.store.sessions[session.sessionId];
serializedBytes = getSerializedLedgerByteLength(state.store);
}
}
function appendUpdate(state, params) {
const session = getOrCreateSession(state, {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
cwd: "",
complete: false
});
const now = state.now();
session.updatedAt = now;
session.events.push({
seq: session.nextSeq,
at: now,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
...params.runId ? { runId: params.runId } : {},
update: cloneAcpLedgerValue(params.update)
});
session.nextSeq += 1;
trimLedger(state);
}
function createLedgerApi(params) {
const buildReplay = (session) => ({
complete: true,
sessionId: session.sessionId,
sessionKey: session.sessionKey,
events: session.events.map((event) => cloneAcpLedgerValue(event))
});
return {
async startSession(sessionParams) {
await params.mutate(() => {
getOrCreateSession(params.state, sessionParams);
trimLedger(params.state);
});
},
async recordUserPrompt(promptParams) {
await params.mutate(() => {
for (const update of createAcpPromptUpdates(promptParams.prompt)) appendUpdate(params.state, {
sessionId: promptParams.sessionId,
sessionKey: promptParams.sessionKey,
runId: promptParams.runId,
update
});
});
},
async recordUpdate(updateParams) {
await params.mutate(() => {
appendUpdate(params.state, updateParams);
});
},
async markIncomplete(markParams) {
await params.mutate(() => {
const session = params.state.store.sessions[markParams.sessionId];
if (!session || session.sessionKey !== markParams.sessionKey) return;
session.complete = false;
session.updatedAt = params.state.now();
});
},
async readReplay(replayParams) {
return params.read(() => {
const session = params.state.store.sessions[replayParams.sessionId];
if (!session || session.sessionKey !== replayParams.sessionKey || !session.complete) return {
complete: false,
events: []
};
return buildReplay(session);
});
},
async readReplayBySessionId(replayParams) {
return params.read(() => {
const session = params.state.store.sessions[replayParams.sessionId];
if (!session || !session.complete) return {
complete: false,
events: []
};
return buildReplay(session);
});
},
async readReplayBySessionKey(replayParams) {
return params.read(() => {
const session = Object.values(params.state.store.sessions).filter((candidate) => candidate.sessionKey === replayParams.sessionKey && candidate.complete).toSorted((a, b) => b.updatedAt - a.updatedAt)[0];
if (!session) return {
complete: false,
events: []
};
return buildReplay(session);
});
}
};
}
/** Creates an in-memory ACP event ledger for tests and ephemeral runtimes. */
function createInMemoryAcpEventLedger(options = {}) {
const normalized = normalizeAcpLedgerOptions(options);
return createLedgerApi({
state: {
store: createEmptyStore(),
...normalized
},
mutate: async (fn) => {
fn();
},
read: async (fn) => fn()
});
}
//#endregion
//#region src/acp/event-ledger.ts
function normalizeSqliteInteger(value) {
return value === null ? 0 : coerceRequiredSqliteNumber(value);
}
function sqliteRowToLedgerEvent(row) {
let update;
try {
update = JSON.parse(row.update_json);
} catch {
return;
}
return normalizeAcpLedgerEvent({
seq: normalizeSqliteInteger(row.seq),
at: normalizeSqliteInteger(row.at),
sessionId: row.session_id,
sessionKey: row.session_key,
...row.run_id ? { runId: row.run_id } : {},
update
});
}
function sqliteSessionMetadataQuery(db) {
return getNodeSqliteKysely(db).selectFrom("acp_replay_sessions").select([
"session_id",
"session_key",
"cwd",
"complete",
"next_seq"
]);
}
function readSqliteSessionById(db, sessionId) {
return executeSqliteQueryTakeFirstSync(db, sqliteSessionMetadataQuery(db).where("session_id", "=", sessionId));
}
function readLatestCompleteSqliteSessionByKey(db, sessionKey) {
return executeSqliteQueryTakeFirstSync(db, sqliteSessionMetadataQuery(db).where("session_key", "=", sessionKey).where("complete", "=", 1).orderBy("updated_at", "desc").orderBy("session_id", "asc").limit(1));
}
function upsertSqliteSession(db, state, params) {
const now = state.now();
const existing = params.reset ? void 0 : readSqliteSessionById(db, params.sessionId);
if (existing) {
const cwd = params.cwd || existing.cwd;
const complete = normalizeSqliteInteger(existing.complete) === 1 || params.complete ? 1 : 0;
db.prepare(`UPDATE acp_replay_sessions
SET estimated_bytes = estimated_bytes - length(session_key) - length(cwd) + ?,
session_key = ?, cwd = ?, complete = ?, updated_at = ?
WHERE session_id = ?`).run(params.sessionKey.length + cwd.length, params.sessionKey, cwd, complete, now, params.sessionId);
return normalizeSqliteInteger(existing.next_seq);
}
if (params.reset) db.prepare("DELETE FROM acp_replay_events WHERE session_id = ?").run(params.sessionId);
const rowBytes = estimateSessionRowBytes({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
cwd: params.cwd
});
db.prepare(`INSERT INTO acp_replay_sessions (
session_id, session_key, cwd, complete, created_at, updated_at, next_seq, estimated_bytes
) VALUES (?, ?, ?, ?, ?, ?, 1, ?)
ON CONFLICT(session_id) DO UPDATE SET
session_key = excluded.session_key,
cwd = excluded.cwd,
complete = excluded.complete,
updated_at = excluded.updated_at,
next_seq = excluded.next_seq,
-- Row overhead plus whatever event rows still exist: exact after a
-- reset (events deleted, sum is 0) and on any conflicting rewrite.
estimated_bytes = excluded.estimated_bytes + COALESCE(
(SELECT SUM(e.estimated_bytes) FROM acp_replay_events e
WHERE e.session_id = excluded.session_id), 0)`).run(params.sessionId, params.sessionKey, params.cwd, params.complete ? 1 : 0, now, now, rowBytes);
return 1;
}
function estimateSqliteLedgerBytes(db) {
return normalizeSqliteInteger(db.prepare("SELECT COALESCE(SUM(estimated_bytes), 0) AS total FROM acp_replay_sessions").get()?.total ?? 0);
}
function estimateSessionRowBytes(params) {
return params.sessionId.length + params.sessionKey.length + params.cwd.length + 32;
}
function estimateEventRowBytes(params) {
return params.sessionId.length + params.sessionKey.length + params.updateJson.length + (params.runId?.length ?? 0) + 32;
}
const LEDGER_TRIM_EVENT_BATCH = 64;
function deleteOldestSqliteEvents(db, sessionId, limit) {
const rows = db.prepare(`DELETE FROM acp_replay_events
WHERE session_id = ?
AND seq IN (
SELECT seq FROM acp_replay_events
WHERE session_id = ?
ORDER BY seq ASC
LIMIT ?
)
RETURNING estimated_bytes`).all(sessionId, sessionId, limit);
if (rows.length === 0) return 0;
const freed = rows.reduce((sum, row) => sum + normalizeSqliteInteger(row.estimated_bytes), 0);
db.prepare(`UPDATE acp_replay_sessions
SET estimated_bytes = MAX(0, estimated_bytes - ?), complete = 0
WHERE session_id = ?`).run(freed, sessionId);
return rows.length;
}
function trimSqliteLedger(db, state) {
const overCapSessions = db.prepare(`SELECT session_id, event_count FROM (
SELECT s.session_id AS session_id, COUNT(e.seq) AS event_count
FROM acp_replay_sessions s
LEFT JOIN acp_replay_events e ON e.session_id = s.session_id
GROUP BY s.session_id
) WHERE event_count > ?`).all(state.maxEventsPerSession);
for (const row of overCapSessions) {
const overage = normalizeSqliteInteger(row.event_count) - state.maxEventsPerSession;
if (overage > 0) deleteOldestSqliteEvents(db, row.session_id, overage);
}
const oldSessions = db.prepare(`SELECT session_id
FROM acp_replay_sessions
ORDER BY updated_at DESC, session_id ASC
LIMIT -1 OFFSET ?`).all(state.maxSessions);
for (const session of oldSessions) db.prepare("DELETE FROM acp_replay_sessions WHERE session_id = ?").run(session.session_id);
let serializedBytes = estimateSqliteLedgerBytes(db);
while (serializedBytes > state.maxSerializedBytes) {
const session = db.prepare(`SELECT session_id
FROM acp_replay_sessions
ORDER BY updated_at ASC, session_id ASC
LIMIT 1`).get();
if (!session) break;
if (deleteOldestSqliteEvents(db, session.session_id, LEDGER_TRIM_EVENT_BATCH) === 0) db.prepare("DELETE FROM acp_replay_sessions WHERE session_id = ?").run(session.session_id);
serializedBytes = estimateSqliteLedgerBytes(db);
}
}
function appendSqliteUpdate(db, state, params) {
const nextSeq = upsertSqliteSession(db, state, {
sessionId: params.sessionId,
sessionKey: params.sessionKey,
cwd: "",
complete: false
});
const now = state.now();
const updateJson = JSON.stringify(cloneAcpLedgerValue(params.update));
const eventBytes = estimateEventRowBytes({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
...params.runId !== void 0 ? { runId: params.runId } : {},
updateJson
});
db.prepare(`INSERT INTO acp_replay_events (session_id, seq, at, session_key, run_id, update_json, estimated_bytes)
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(params.sessionId, nextSeq, now, params.sessionKey, params.runId ?? null, updateJson, eventBytes);
db.prepare(`UPDATE acp_replay_sessions
SET estimated_bytes = estimated_bytes - length(session_key) + ?,
session_key = ?, updated_at = ?, next_seq = ?
WHERE session_id = ?`).run(params.sessionKey.length + eventBytes, params.sessionKey, now, nextSeq + 1, params.sessionId);
trimSqliteLedger(db, state);
}
function buildSqliteReplay(db, session) {
if (!session || normalizeSqliteInteger(session.complete) !== 1) return {
complete: false,
events: []
};
const events = executeSqliteQuerySync(db, getNodeSqliteKysely(db).selectFrom("acp_replay_events").select([
"session_id",
"seq",
"at",
"session_key",
"run_id",
"update_json"
]).where("session_id", "=", session.session_id).orderBy("seq", "asc")).rows.flatMap((row) => {
const event = sqliteRowToLedgerEvent(row);
return event ? [event] : [];
});
return {
complete: true,
sessionId: session.session_id,
sessionKey: session.session_key,
events
};
}
/** Creates the SQLite-backed ACP event ledger used by the state database. */
function createSqliteAcpEventLedger(params = {}) {
const normalized = normalizeAcpLedgerOptions(params);
const dbOptions = {
env: params.env,
path: params.path
};
const state = { ...normalized };
const mutate = (fn) => runOpenClawStateWriteTransaction((database) => fn(database.db), dbOptions);
const read = (fn) => fn(openOpenClawStateDatabase(dbOptions).db);
return {
async startSession(sessionParams) {
mutate((db) => {
upsertSqliteSession(db, state, sessionParams);
trimSqliteLedger(db, state);
});
},
async recordUserPrompt(promptParams) {
mutate((db) => {
for (const update of createAcpPromptUpdates(promptParams.prompt)) appendSqliteUpdate(db, state, {
sessionId: promptParams.sessionId,
sessionKey: promptParams.sessionKey,
runId: promptParams.runId,
update
});
});
},
async recordUpdate(updateParams) {
mutate((db) => {
appendSqliteUpdate(db, state, updateParams);
});
},
async markIncomplete(markParams) {
mutate((db) => {
db.prepare(`UPDATE acp_replay_sessions
SET complete = 0, updated_at = ?
WHERE session_id = ? AND session_key = ?`).run(state.now(), markParams.sessionId, markParams.sessionKey);
});
},
async readReplay(replayParams) {
return read((db) => {
const session = readSqliteSessionById(db, replayParams.sessionId);
if (session?.session_key !== replayParams.sessionKey) return {
complete: false,
events: []
};
return buildSqliteReplay(db, session);
});
},
async readReplayBySessionId(replayParams) {
return read((db) => buildSqliteReplay(db, readSqliteSessionById(db, replayParams.sessionId)));
},
async readReplayBySessionKey(replayParams) {
return read((db) => buildSqliteReplay(db, readLatestCompleteSqliteSessionByKey(db, replayParams.sessionKey)));
}
};
}
//#endregion
//#region src/infra/fixed-window-rate-limit.ts
/**
* Shared fixed-window rate-limit primitive for gateway, ACP, and webhook ingress.
*
* It is intentionally in-memory and process-local; callers that need distributed
* limits must layer their own persistence before invoking request work.
*/
/** Creates a fixed-window counter that reports allowance, remaining quota, and retry delay. */
function createFixedWindowBudget(params) {
const maxRequests = resolveIntegerOption(params.maxRequests, 1, { min: 1 });
const windowMs = resolveIntegerOption(params.windowMs, 1, { min: 1 });
const now = params.now ?? Date.now;
let count = 0;
let windowStartMs = 0;
return {
consume() {
const nowMs = now();
if (nowMs - windowStartMs >= windowMs) {
windowStartMs = nowMs;
count = 0;
}
if (count >= maxRequests) return {
allowed: false,
retryAfterMs: Math.max(0, windowStartMs + windowMs - nowMs),
remaining: 0
};
count += 1;
return {
allowed: true,
retryAfterMs: 0,
remaining: Math.max(0, maxRequests - count)
};
},
reset() {
count = 0;
windowStartMs = 0;
}
};
}
//#endregion
//#region src/acp/event-mapper.ts
const TOOL_LOCATION_PATH_KEYS = [
"path",
"filePath",
"file_path",
"targetPath",
"target_path",
"targetFile",
"target_file",
"sourcePath",
"source_path",
"destinationPath",
"destination_path",
"oldPath",
"old_path",
"newPath",
"new_path",
"outputPath",
"output_path",
"inputPath",
"input_path"
];
const TOOL_LOCATION_LINE_KEYS = [
"line",
"lineNumber",
"line_number",
"startLine",
"start_line"
];
const TOOL_RESULT_PATH_MARKER_RE = /^(?:FILE|MEDIA):(.+)$/gm;
const TOOL_LOCATION_MAX_DEPTH = 4;
const TOOL_LOCATION_MAX_NODES = 100;
const INLINE_CONTROL_ESCAPE_MAP = {
"\0": "\\0",
"\r": "\\r",
"\n": "\\n",
" ": "\\t",
"\v": "\\v",
"\f": "\\f",
"\u2028": "\\u2028",
"\u2029": "\\u2029"
};
function escapeInlineControlChars(value) {
let escaped = "";
for (const char of value) {
const codePoint = char.codePointAt(0);
if (codePoint === void 0) {
escaped += char;
continue;
}
if (!(codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 8232 || codePoint === 8233)) {
escaped += char;
continue;
}
const mapped = INLINE_CONTROL_ESCAPE_MAP[char];
if (mapped) {
escaped += mapped;
continue;
}
escaped += codePoint <= 255 ? `\\x${codePoint.toString(16).padStart(2, "0")}` : `\\u${codePoint.toString(16).padStart(4, "0")}`;
}
return escaped;
}
function escapeResourceTitle(value) {
return escapeInlineControlChars(value).replace(/[()[\]]/g, (char) => `\\${char}`);
}
function normalizeToolLocationPath(value) {
const trimmed = value.trim();
if (!trimmed || trimmed.length > 4096 || trimmed.includes("\0") || trimmed.includes("\r") || trimmed.includes("\n")) return;
if (hasHttpUrlPrefix(trimmed)) return;
if (/^file:\/\//i.test(trimmed)) try {
const parsed = new URL(trimmed);
return decodeURIComponent(parsed.pathname || "") || void 0;
} catch {
return;
}
return trimmed;
}
function normalizeToolLocationLine(value) {
if (typeof value !== "number" || !Number.isFinite(value)) return;
const line = Math.floor(value);
return line > 0 ? line : void 0;
}
function extractToolLocationLine(record) {
for (const key of TOOL_LOCATION_LINE_KEYS) {
const line = normalizeToolLocationLine(record[key]);
if (line !== void 0) return line;
}
}
function addToolLocation(locations, rawPath, line) {
const path = normalizeToolLocationPath(rawPath);
if (!path) return;
for (const [existingKey, existing] of locations.entries()) {
if (existing.path !== path) continue;
if (line === void 0 || existing.line === line) return;
if (existing.line === void 0) locations.delete(existingKey);
}
const locationKey = `${path}:${line ?? ""}`;
if (locations.has(locationKey)) return;
locations.set(locationKey, line ? {
path,
line
} : { path });
}
function collectLocationsFromTextMarkers(text, locations) {
for (const match of text.matchAll(TOOL_RESULT_PATH_MARKER_RE)) {
const candidate = normalizeOptionalString(match[1]);
if (candidate) addToolLocation(locations, candidate);
}
}
function collectToolLocations(value, locations, state, depth) {
if (state.visited >= TOOL_LOCATION_MAX_NODES || depth > TOOL_LOCATION_MAX_DEPTH) return;
state.visited += 1;
if (typeof value === "string") {
collectLocationsFromTextMarkers(value, locations);
return;
}
if (!value || typeof value !== "object") return;
if (Array.isArray(value)) {
for (const item of value) {
collectToolLocations(item, locations, state, depth + 1);
if (state.visited >= TOOL_LOCATION_MAX_NODES) return;
}
return;
}
const record = value;
const line = extractToolLocationLine(record);
for (const key of TOOL_LOCATION_PATH_KEYS) {
const rawPath = record[key];
if (typeof rawPath === "string") addToolLocation(locations, rawPath, line);
}
const content = Array.isArray(record.content) ? record.content : void 0;
if (content) for (const block of content) {
const entry = asOptionalRecord(block);
if (entry?.type === "text" && typeof entry.text === "string") collectLocationsFromTextMarkers(entry.text, locations);
}
for (const [key, nested] of Object.entries(record)) {
if (key === "content") continue;
collectToolLocations(nested, locations, state, depth + 1);
if (state.visited >= TOOL_LOCATION_MAX_NODES) return;
}
}
/** Extracts bounded text content from an ACP prompt block list. */
function extractTextFromPrompt(prompt, maxBytes) {
const parts = [];
let totalBytes = 0;
for (const block of prompt) {
let blockText;
if (block.type === "text") blockText = block.text;
else if (block.type === "resource" && "text" in block.resource && block.resource.text) blockText = block.resource.text;
else if (block.type === "resource_link") {
const title = block.title ? ` (${escapeResourceTitle(block.title)})` : "";
const uri = block.uri ? escapeInlineControlChars(block.uri) : "";
blockText = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`;
}
if (blockText !== void 0) {
if (maxBytes !== void 0) {
const separatorBytes = parts.length > 0 ? 1 : 0;
totalBytes += separatorBytes + Buffer.byteLength(blockText, "utf-8");
if (totalBytes > maxBytes) throw new Error(`Prompt exceeds maximum allowed size of ${maxBytes} bytes`);
}
parts.push(blockText);
}
}
return parts.join("\n");
}
/** Extracts image/file prompt blocks into Gateway attachment payloads. */
function extractAttachmentsFromPrompt(prompt) {
const attachments = [];
for (const block of prompt) {
if (block.type !== "image") continue;
if (!block.data || !block.mimeType) continue;
attachments.push({
type: "image",
mimeType: block.mimeType,
content: block.data
});
}
return attachments;
}
/** Builds the display title used for ACP tool-call events. */
function formatToolTitle(name, args) {
const base = name ?? "tool";
if (!args || Object.keys(args).length === 0) return base;
return escapeInlineControlChars(`${base}: ${Object.entries(args).map(([key, value]) => {
const raw = typeof value === "string" ? value : JSON.stringify(value);
return `${key}: ${raw.length > 100 ? `${truncateUtf16Safe(raw, 100)}...` : raw}`;
}).join(", ")}`);
}
/** Infers ACP tool kind from a normalized tool name. */
function inferToolKind(name) {
if (!name) return "other";
const normalized = normalizeLowercaseStringOrEmpty(name);
if (normalized.includes("read")) return "read";
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
if (normalized.includes("move") || normalized.includes("rename")) return "move";
if (normalized.includes("search") || normalized.includes("find")) return "search";
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) return "execute";
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";
return "other";
}
/** Extracts textual ACP tool-call content from unknown runtime payloads. */
function extractToolCallContent(value) {
if (hasNonEmptyString(value)) return value.trim() ? [{
type: "content",
content: {
type: "text",
text: value
}
}] : void 0;
const record = asOptionalRecord(value);
if (!record) return;
const contents = [];
const blocks = Array.isArray(record.content) ? record.content : [];
for (const block of blocks) {
const entry = asOptionalRecord(block);
if (entry?.type === "text" && hasNonEmptyString(entry.text)) contents.push({
type: "content",
content: {
type: "text",
text: entry.text
}
});
}
if (contents.length > 0) return contents;
const fallbackText = readStringValue(record.text) ?? readStringValue(record.message) ?? readStringValue(record.error);
if (!hasNonEmptyString(fallbackText)) return;
return [{
type: "content",
content: {
type: "text",
text: fallbackText
}
}];
}
/** Extracts bounded file locations from nested tool-call payloads. */
function extractToolCallLocations(...values) {
const locations = /* @__PURE__ */ new Map();
for (const value of values) collectToolLocations(value, locations, { visited: 0 }, 0);
return locations.size > 0 ? [...locations.values()] : void 0;
}
//#endregion
//#region src/acp/session-mapper.ts
/** Resolves ACP request metadata into OpenClaw Gateway session keys and reset behavior. */
/** Parses ACP request metadata into OpenClaw session routing hints. */
function parseSessionMeta(meta) {
if (!meta || typeof meta !== "object") return {};
const record = meta;
return {
sessionKey: readMetadataString(record, [
"sessionKey",
"session",
"key"
]),
sessionLabel: readMetadataString(record, ["sessionLabel", "label"]),
resetSession: readBool(record, ["resetSession", "reset"]),
requireExisting: readBool(record, ["requireExistingSession", "requireExisting"]),
prefixCwd: readBool(record, ["prefixCwd"])
};
}
/** Resolves the Gateway session key for an ACP request using metadata, defaults, or fallback. */
async function resolveAcpSessionKey(params) {
const requestedLabel = params.meta.sessionLabel ?? params.opts.defaultSessionLabel;
const requestedKey = params.meta.sessionKey ?? params.opts.defaultSessionKey;
const requireExisting = params.meta.requireExisting ?? params.opts.requireExistingSession ?? false;
if (params.meta.sessionLabel) {
const resolved = await params.gateway.request("sessions.resolve", { label: params.meta.sessionLabel });
if (!resolved?.key) throw new Error(`Unable to resolve session label: ${params.meta.sessionLabel}`);
return resolved.key;
}
if (params.meta.sessionKey) {
if (!requireExisting) return params.meta.sessionKey;
const resolved = await params.gateway.request("sessions.resolve", { key: params.meta.sessionKey });
if (!resolved?.key) throw new Error(`Session key not found: ${params.meta.sessionKey}`);
return resolved.key;
}
if (requestedLabel) {
const resolved = await params.gateway.request("sessions.resolve", { label: requestedLabel });
if (!resolved?.key) throw new Error(`Unable to resolve session label: ${requestedLabel}`);
return resolved.key;
}
if (requestedKey) {
if (!requireExisting) return requestedKey;
const resolved = await params.gateway.request("sessions.resolve", { key: requestedKey });
if (!resolved?.key) throw new Error(`Session key not found: ${requestedKey}`);
return resolved.key;
}
return params.fallbackKey;
}
/** Sends a Gateway session reset when ACP metadata or server defaults request it. */
async function resetSessionIfNeeded(params) {
if (!(params.meta.resetSession ?? params.opts.resetSession ?? false)) return;
await params.gateway.request("sessions.reset", { key: params.sessionKey });
}
//#endregion
//#region src/acp/permission-relay.ts
const FALLBACK_EXEC_APPROVAL_DECISIONS = ["allow-once", "deny"];
function normalizeGatewayExecApprovalDecision(value) {
if (value === "allow-once" || value === "allow-always" || value === "deny") return value;
}
/** Normalizes allowed Gateway exec approval decisions with a conservative fallback set. */
function normalizeGatewayExecApprovalDecisions(value) {
const normalized = Array.isArray(value) ? value.map(normalizeGatewayExecApprovalDecision).filter((decision) => Boolean(decision)) : [];
return normalized.length > 0 ? normalized : [...FALLBACK_EXEC_APPROVAL_DECISIONS];
}
/** Converts Gateway exec decisions into ACP permission options. */
function buildAcpPermissionOptions(decisions) {
const unique = new Set(decisions);
const options = [];
if (unique.has("allow-once")) options.push({
optionId: "allow-once",
name: "Allow once",
kind: "allow_once"
});
if (unique.has("allow-always")) options.push({
optionId: "allow-always",
name: "Allow always",
kind: "allow_always"
});
if (unique.has("deny")) options.push({
optionId: "deny",
name: "Deny",
kind: "reject_once"
});
return options.length > 0 ? options : buildAcpPermissionOptions(FALLBACK_EXEC_APPROVAL_DECISIONS);
}
/** Parses legacy Gateway approval event data into ACP relay state. */
function parseGatewayExecApprovalEventData(data) {
if (data.phase !== "requested" || data.kind !== "exec" || data.status !== "pending") return null;
const approvalId = normalizeOptionalString(data.approvalId);
if (!approvalId) return null;
return {
approvalId,
command: normalizeOptionalString(data.command),
host: normalizeOptionalString(data.host),
title: normalizeOptionalString(data.title),
toolCallId: normalizeOptionalString(data.toolCallId)
};
}
/** Parses structured Gateway approval-request payloads into ACP relay state. */
function parseGatewayExecApprovalRequestEventPayload(payload) {
const approvalId = normalizeOptionalString(payload.id);
const request = payload.request;
if (!approvalId || !request || typeof request !== "object" || Array.isArray(request)) return null;
const requestRecord = request;
return {
approvalId,
command: normalizeOptionalString(requestRecord.command) ?? normalizeOptionalString(requestRecord.commandPreview),
host: normalizeOptionalString(requestRecord.host),
toolCallId: normalizeOptionalString(requestRecord.toolCallId)
};
}
/** Builds the ACP request_permission payload shown to a client. */
function buildAcpPermissionRequest(params) {
const command = normalizeOptionalString(params.details?.commandText) ?? normalizeOptionalString(params.details?.commandPreview) ?? params.event.command;
const host = normalizeOptionalString(params.details?.host) ?? params.event.host;
const decisions = normalizeGatewayExecApprovalDecisions(params.details?.allowedDecisions);
const rawInput = {
name: "exec",
approvalId: params.event.approvalId
};
if (command) rawInput.command = command;
if (host) rawInput.host = host;
return {
sessionId: params.sessionId,
toolCall: {
toolCallId: params.event.toolCallId ?? `exec:${params.event.approvalId}`,
title: params.event.title ?? "Command approval requested",
kind: "execute",
status: "pending",
rawInput,
_meta: {
toolName: "exec",
approvalId: params.event.approvalId
}
},
options: buildAcpPermissionOptions(decisions)
};
}
/** Maps an ACP permission response back to the Gateway exec approval decision. */
function resolveGatewayDecisionFromPermissionOutcome(response, options) {
const outcome = response?.outcome;
if (!outcome || outcome.outcome !== "selected") return;
return normalizeGatewayExecApprovalDecision(options.find((option) => option.optionId === outcome.optionId)?.optionId);
}
//#endregion
//#region src/acp/translator.agent-events.ts
var AcpTranslatorAgentEvents = class {
constructor(connection, gateway, sessionUpdates, pendingPrompts, approvalRelays, getPendingPrompt, findPendingBySessionKey, log) {
this.connection = connection;
this.gateway = gateway;
this.sessionUpdates = sessionUpdates;
this.pendingPrompts = pendingPrompts;
this.approvalRelays = approvalRelays;
this.getPendingPrompt = getPendingPrompt;
this.findPendingBySessionKey = findPendingBySessionKey;
this.log = log;
}
async handleAgentEvent(evt) {
const payload = evt.payload;
if (!payload) return;
const stream = payload.stream;
const runId = payload.runId;
const data = payload.data;
const sessionKey = payload.sessionKey;
if (!stream || !data || !sessionKey) return;
if (stream === "approval") {
await this.handleApprovalEvent({
sessionKey,
runId,
data
});
return;
}
if (stream !== "tool") return;
const phase = data.phase;
const name = data.name;
const toolCallId = data.toolCallId;
if (!toolCallId) return;
const pending = this.findPendingBySessionKey(sessionKey, runId);
if (!pending) return;
if (phase === "start") {
if (!pending.toolCalls) pending.toolCalls = /* @__PURE__ */ new Map();
if (pending.toolCalls.has(toolCallId)) return;
const args = data.args;
const title = formatToolTitle(name, args);
const kind = inferToolKind(name);
const locations = extractToolCallLocations(args);
pending.toolCalls.set(toolCallId, {
title,
kind,
rawInput: args,
locations
});
await this.sessionUpdates.emit({
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {},
runId: pending.idempotencyKey,
record: true,
update: {
sessionUpdate: "tool_call",
toolCallId,
title,
status: "in_progress",
rawInput: args,
kind,
locations
}
});
return;
}
if (phase === "update") {
const toolState = pending.toolCalls?.get(toolCallId);
const partialResult = data.partialResult;
await this.sessionUpdates.emit({
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {},
runId: pending.idempotencyKey,
record: true,
update: {
sessionUpdate: "tool_call_update",
toolCallId,
status: "in_progress",
rawOutput: partialResult,
content: extractToolCallContent(partialResult),
locations: extractToolCallLocations(toolState?.locations, partialResult)
}
});
return;
}
if (phase === "result") {
const isError = Boolean(data.isError);
const toolState = pending.toolCalls?.get(toolCallId);
pending.toolCalls?.delete(toolCallId);
await this.sessionUpdates.emit({
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {},
runId: pending.idempotencyKey,
record: true,
update: {
sessionUpdate: "tool_call_update",
toolCallId,
status: isError ? "failed" : "completed",
rawOutput: data.result,
content: extractToolCallContent(data.result),
locations: extractToolCallLocations(toolState?.locations, data.result)
}
});
}
}
handleExecApprovalRequestEvent(evt) {
const payload = evt.payload;
if (!payload) return;
const approvalEvent = parseGatewayExecApprovalRequestEventPayload(payload);
if (!approvalEvent) return;
const request = payload.request;
const sessionKey = normalizeOptionalString(request?.sessionKey);
if (!sessionKey) return;
this.startApprovalRelay({
sessionKey,
approvalEvent
});
}
clearApprovalRelaysForPrompt(sessionId, runId, opts = {}) {
for (const [approvalId, relay] of this.approvalRelays) {
if (relay.sessionId !== sessionId) continue;
if (runId && relay.runId !== runId) continue;
this.approvalRelays.delete(approvalId);
if (opts.denyActive && relay.state === "active") this.resolveGatewayApproval(approvalId, "deny");
}
}
async handleApprovalEvent(params) {
const approvalEvent = parseGatewayExecApprovalEventData(params.data);
if (!approvalEvent) return;
this.startApprovalRelay({
sessionKey: params.sessionKey,
runId: params.runId,
approvalEvent
});
}
startApprovalRelay(params) {
const approvalEvent = params.approvalEvent;
const existing = this.approvalRelays.get(approvalEvent.approvalId);
if (existing) {
this.retryApprovalRelayDecision(existing);
return;
}
const pending = this.findPendingForApproval(params);
if (!pending) return;
const pendingToolCall = approvalEvent.toolCallId ? pending.toolCalls?.get(approvalEvent.toolCallId) : void 0;
const correlatedApprovalEvent = !approvalEvent.title && pendingToolCall?.kind === "execute" ? {
...approvalEvent,
title: pendingToolCall.title
} : approvalEvent;
const relay = {
approvalId: approvalEvent.approvalId,
runId: pending.idempotencyKey,
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
state: "active"
};
this.approvalRelays.set(relay.approvalId, relay);
this.runApprovalRelay(relay, correlatedApprovalEvent);
}
findPendingForApproval(params) {
if (params.runId) return this.findPendingBySessionKey(params.sessionKey, params.runId);
const toolCallId = params.approvalEvent.toolCallId;
if (!toolCallId) return this.findUniquePendingBySessionKey(params.sessionKey);
let match;
for (const pending of this.pendingPrompts.values()) {
if (pending.sessionKey !== params.sessionKey || pending.toolCalls?.get(toolCallId)?.kind !== "execute") continue;
if (match) return;
match = pending;
}
return match;
}
async runApprovalRelay(relay, approvalEvent) {
let resolved = false;
let decision;
try {
const details = await this.getGatewayApprovalDetails(relay.approvalId);
if (!this.isApprovalRelayActive(relay)) {
resolved = await this.resolveGatewayApproval(relay.approvalId, "deny");
return;
}
const request = buildAcpPermissionRequest({
sessionId: relay.sessionId,
event: approvalEvent,
details
});
try {
decision = resolveGatewayDecisionFromPermissionOutcome(await this.connection.requestPermission(request), request.options);
} catch (err) {
this.log(`approval relay request failed for ${relay.approvalId}: ${String(err)}`);
}
const selectedDecision = this.isApprovalRelayActive(relay) && decision ? decision : "deny";
resolved = await this.resolveGatewayApproval(relay.approvalId, selectedDecision);
} finally {
const current = this.approvalRelays.get(relay.approvalId);
if (current === relay && current.state === "active") {
if (resolved) current.state = "completed";
else if (decision) current.pendingDecision = decision;
else this.approvalRelays.delete(relay.approvalId);
}
}
}
async retryApprovalRelayDecision(relay) {
const decision = relay.pendingDecision;
if (!decision || !this.isApprovalRelayActive(relay)) return;
if (!await this.resolveGatewayApproval(relay.approvalId, decision) || !this.isApprovalRelayActive(relay)) return;
relay.pendingDecision = void 0;
relay.state = "completed";
}
async replayApprovalDecisionsOnReconnect() {
for (const relay of this.approvalRelays.values()) await this.retryApprovalRelayDecision(relay);
}
async getGatewayApprovalDetails(approvalId) {
try {
return await this.gateway.request("exec.approval.get", { id: approvalId });
} catch (err) {
this.log(`approval relay hydrate failed for ${approvalId}: ${String(err)}`);
return null;
}
}
async resolveGatewayApproval(approvalId, decision) {
try {
await this.gateway.request("exec.approval.resolve", {
id: approvalId,
decision
});
return true;
} catch (err) {
this.log(`approval relay resolve failed for ${approvalId}: ${String(err)}`);
return false;
}
}
isApprovalRelayActive(relay) {
return this.approvalRelays.get(relay.approvalId) === relay && relay.state === "active" && this.getPendingPrompt(relay.sessionId, relay.runId) !== void 0;
}
findUniquePendingBySessionKey(sessionKey) {
let match;
for (const pending of this.pendingPrompts.values()) {
if (pending.sessionKey !== sessionKey) continue;
if (match) return;
match = pending;
}
return match;
}
};
//#endregion
//#region src/acp/translator.disconnects.ts
const ACP_GATEWAY_DISCONNECT_GRACE_MS = 5e3;
var AcpTranslatorDisconnects = class {
constructor(gateway, pendingPrompts, getPendingPrompt, settleRecoveredPrompt, rejectPendingPrompt, log) {
this.gateway = gateway;
this.pendingPrompts = pendingPrompts;
this.getPendingPrompt = getPendingPrompt;
this.settleRecoveredPrompt = settleRecoveredPrompt;
this.rejectPendingPrompt = rejectPendingPrompt;
this.log = log;
this.disconnectTimer = null;
this.activeDisconnectContext = null;
this.disconnectGeneration = 0;
}
get activeContext() {
return this.activeDisconnectContext;
}
shutdown() {
this.activeDisconnectContext = null;
this.clearDisconnectTimer();
}
handleGatewayReconnect() {
this.log("gateway reconnected");
const disconnectContext = this.activeDisconnectContext;
this.activeDisconnectContext = null;
if (!disconnectContext) return;
this.reconcilePendingPrompts(disconnectContext.generation, false);
}
handleGatewayDisconnect(reason) {
this.log(`gateway disconnected: ${reason}`);
const disconnectContext = {
generation: this.disconnectGeneration + 1,
reason
};
this.disconnectGeneration = disconnectContext.generation;
this.activeDisconnectContext = disconnectContext;
if (this.pendingPrompts.size === 0) return;
for (const pending of this.pendingPrompts.values()) pending.disconnectContext = disconnectContext;
this.armDisconnectTimer(disconnectContext);
}
armForActiveContext() {
if (this.activeDisconnectContext && !this.disconnectTimer) this.armDisconnectTimer(this.activeDisconnectContext);
}
clearWhenIdle() {
if (this.pendingPrompts.size === 0) this.clearDisconnectTimer();
}
clearDisconnectTimer() {
if (!this.disconnectTimer) return;
clearTimeout(this.disconnectTimer);
this.disconnectTimer = null;
}
armDisconnectTimer(disconnectContext) {
this.clearDisconnectTimer();
this.disconnectTimer = setTimeout(() => {
this.disconnectTimer = null;
this.reconcilePendingPrompts(disconnectContext.generation, true);
}, ACP_GATEWAY_DISCONNECT_GRACE_MS);
this.disconnectTimer.unref?.();
}
clearPendingDisconnectState(pending, disconnectContext) {
if (pending.disconnectContext !== disconnectContext) return;
pending.disconnectContext = void 0;
}
shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext) {
return pending.disconnectContext === disconnectContext && (!pending.sendAccepted || this.activeDisconnectContext?.generation === disconnectContext.generation);
}
async reconcilePendingPrompts(observedDisconnectGeneration, deadlineExpired) {
if (this.pendingPrompts.size === 0) {
if (this.disconnectGeneration === observedDisconnectGeneration) this.clearDisconnectTimer();
return;
}
const pendingEntries = [...this.pendingPrompts.entries()];
let keepDisconnectTimer = false;
for (const [sessionId, pending] of pendingEntries) {
if (this.pendingPrompts.get(sessionId) !== pending) continue;
if (pending.disconnectContext?.generation !== observedDisconnectGeneration) continue;
if (await this.reconcilePendingPrompt(sessionId, pending, deadlineExpired)) keepDisconnectTimer = true;
}
if (!keepDisconnectTimer && this.disconnectGeneration === observedDisconnectGeneration) this.clearDisconnectTimer();
}
async reconcilePendingPrompt(sessionId, pending, deadlineExpired) {
const disconnectContext = pending.disconnectContext;
if (!disconnectContext) return false;
let result;
try {
result = await this.gateway.request("agent.wait", {
runId: pending.idempotencyKey,
timeoutMs: 0
}, { timeoutMs: null });
} catch (err) {
this.log(`agent.wait reconcile failed for ${pending.idempotencyKey}: ${String(err)}`);
if (deadlineExpired) {
if (this.shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext)) {
await this.rejectPendingPrompt(pending, /* @__PURE__ */ new Error(`Gateway disconnected: ${disconnectContext.reason}`), { recordDisconnectNotice: true });
return false;
}
this.clearPendingDisconnectState(pending, disconnectContext);
return false;
}
return true;
}
const currentPending = this.getPendingPrompt(sessionId, pending.idempotencyKey);
if (!currentPending) return false;
const hasVisibleReply = result?.terminalReply?.disposition === "visible";
if (result?.status === "ok" || result?.status === "error" || result?.status === "timeout" && hasVisibleReply) {
await this.settleRecoveredPrompt(sessionId, currentPending, result);
return false;
}
if (deadlineExpired) {
if (this.shouldRejectPendingAtDisconnectDeadline(currentPending, disconnectContext)) {
const currentDisconnectContext = currentPending.disconnectContext;
if (!currentDisconnectContext) return false;
await this.rejectPendingPrompt(currentPending, /* @__PURE__ */ new Error(`Gateway disconnected: ${currentDisconnectContext.reason}`), { recordDisconnectNotice: true });
return false;
}
this.clearPendingDisconnectState(currentPending, disconnectContext);
return false;
}
return true;
}
};
//#endregion
//#region src/acp/translator.prompt-stream.ts
/** ACP prompt submission, Gateway chat streaming, and prompt settlement. */
const MAX_PROMPT_BYTES = 2097152;
const ACP_SHUTDOWN_ABORT_TIMEOUT_MS = 1e3;
function isAdminScopeProvenanceRejection(err) {
if (!(err instanceof Error)) return false;
const gatewayCode = typeof err.gatewayCode === "string" ? err.gatewayCode : void 0;
return err.name === "GatewayClientRequestError" && gatewayCode === "INVALID_REQUEST" && err.message.includes("system provenance fields require admin scope");
}
function isGatewayCloseError(err) {
return (err instanceof Error ? err.message : String(err)).startsWith("gateway closed (");
}
function buildSystemInputProvenance(originSessionId) {
return {
kind: "external_user",
originSessionId,
sourceChannel: "acp",
sourceTool: "openclaw_acp"
};
}
function buildSystemProvenanceReceipt(params) {
return [
"[Source Receipt]",
"bridge=openclaw-acp",
`originHost=${os.hostname()}`,
`originCwd=${shortenHomePath(params.cwd)}`,
`acpSessionId=${params.sessionId}`,
`originSessionId=${params.sessionId}`,
`targetSession=${params.sessionKey}`,
"[/Source Receipt]"
].join("\n");
}
var AcpTranslatorPromptStream = class {
constructor(connection, gateway, opts, sessionStore, sessionUpdates, sessionState, approvalRelays, log) {
this.gateway = gateway;
this.opts = opts;
this.sessionStore = sessionStore;
this.sessionUpdates = sessionUpdates;
this.sessionState = sessionState;
this.approvalRelays = approvalRelays;
this.log = log;
this.pendingPrompts = /* @__PURE__ */ new Map();
this.pendingPromptAdmissions = /* @__PURE__ */ new Map();
this.settlingPromptKeys = /* @__PURE__ */ new Set();
this.stopped = false;
this.agentEvents = new AcpTranslatorAgentEvents(connection, gateway, sessionUpdates, this.pendingPrompts, this.approvalRelays, (sessionId, runId) => this.getPendingPrompt(sessionId, runId), (sessionKey, runId) => this.findPendingBySessionKey(sessionKey, runId), log);
this.disconnects = new AcpTranslatorDisconnects(gateway, this.pendingPrompts, (sessionId, runId) => this.getPendingPrompt(sessionId, runId), (sessionId, pending, result) => this.settleRecoveredPrompt(sessionId, pending, result), (pending, error, options) => this.rejectPendingPrompt(pending, error, options), log);
}
async shutdown() {
this.stopped = true;
this.disconnects.shutdown();
const sessions = /* @__PURE__ */ new Map();
for (const pending of this.pendingPrompts.values()) sessions.set(pending.sessionId, {
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
activeRunId: pending.idempotencyKey
});
for (const admission of this.pendingPromptAdmissions.values()) sessions.set(admission.session.sessionId, admission.session);
await Promise.all([...sessions.values()].map((session) => this.cancelSessionWork(session, ACP_SHUTDOWN_ABORT_TIMEOUT_MS)));
}
handleGatewayReconnect() {
this.agentEvents.replayApprovalDecisionsOnReconnect();
this.disconnects.handleGatewayReconnect();
}
handleGatewayDisconnect(reason) {
this.disconnects.handleGatewayDisconnect(reason);
}
async handleGatewayEvent(evt) {
if (evt.event === "chat") {
await this.handleChatEvent(evt);
return;
}
if (evt.event === "exec.approval.requested") {
this.agentEvents.handleExecApprovalRequestEvent(evt);
return;
}
if (evt.event === "agent") await this.agentEvents.handleAgentEvent(evt);
}
async prompt(params) {
const session = this.sessionStore.getSession(params.sessionId);
if (!session) throw new Error(`Session ${params.sessionId} not found`);
const admission = {
session,
previous: this.pendingPromptAdmissions.get(params.sessionId),
closed: this.stopped,
closure: createDeferredCore(),
settled: createDeferredCore()
};
this.pendingPromptAdmissions.set(params.sessionId, admission);
if (admission.previous) admission.previous.closed = true;
try {
if (!this.ownsPromptAdmission(admission)) return { stopReason: "cancelled" };
if (session.abortController || this.pendingPrompts.has(params.sessionId)) {
await Promise.race([this.cancelSessionWork(session, void 0, admission), admission.closure.promise]);
if (!this.ownsPromptAdmission(admission)) return { stopReason: "cancelled" };
}
if (admission.previous) {
await Promise.race([admission.previous.settled.promise, admission.closure.promise]);
if (!this.ownsPromptAdmission(admission)) return { stopReason: "cancelled" };
admission.previous = void 0;
}
return await Promise.race([this.submitPrompt(params, session), admission.closure.promise.then(() => ({ stopReason: "cancelled" }))]);
} finally {
admission.settled.resolve();
if (this.pendingPromptAdmissions.get(params.sessionId) === admission) this.pendingPromptAdmissions.delete(params.sessionId);
}
}
submitPrompt(params, session) {
const meta = parseSessionMeta(params["_meta"]);
const userText = extractTextFromPrompt(params.prompt, MAX_PROMPT_BYTES);
const attachments = extractAttachmentsFromPrompt(params.prompt);
const prefixCwd = meta.prefixCwd ?? this.opts.prefixCwd ?? true;
const displayCwd = shortenHomePath(session.cwd);
const message = prefixCwd ? `[Working directory: ${displayCwd}]\n\n${userText}` : userText;
const provenanceMode = this.opts.provenanceMode ?? "off";
const systemInputProvenance = provenanceMode === "off" ? void 0 : buildSystemInputProvenance(params.sessionId);
const systemProvenanceReceipt = provenanceMode === "meta+receipt" ? buildSystemProvenanceReceipt({
cwd: session.cwd,
sessionId: params.sessionId,
sessionKey: session.sessionKey
}) : void 0;
if (Buffer.byteLength(message, "utf-8") > MAX_PROMPT_BYTES) throw new Error(`Prompt exceeds maximum allowed size of ${MAX_PROMPT_BYTES} bytes`);
const abortController = new AbortController();
const runId = randomUUID();
this.sessionStore.setActiveRun(params.sessionId, runId, abortController);
const requestParams = {
sessionKey: session.sessionKey,
message,
attachments: attachments.length > 0 ? attachments : void 0,
idempotencyKey: runId,
thinking: readMetadataString(params["_meta"], ["thinking", "thinkingLevel"]),
deliver: readBool(params["_meta"], ["deliver"]),
timeoutMs: readNonNegativeInteger(params["_meta"], ["timeoutMs"])
};
return new Promise((resolve, reject) => {
this.pendingPrompts.set(params.sessionId, {
sessionId: params.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
idempotencyKey: runId,
disconnectContext: this.disconnects.activeContext ?? void 0,
resolve,
reject
});
this.disconnects.armForActiveContext();
const sendWithProvenanceFallback = async () => {
const markSendAccepted = () => {
const pending = this.getPendingPrompt(params.sessionId, runId);
if (!pending) return false;
pending.sendAccepted = true;
return true;
};
const applyTerminalAck = async (ack) => {
const status = normalizeTerminalChatSendAckStatus(ack?.status);
const pending = () => this.getPendingPrompt(params.sessionId, runId);
if (status === "timeout") {
const current = pending();
if (current) await this.finishPrompt(params.sessionId, current, "cancelled");
return true;
}
if (status === "error") {
const current = pending();
if (current) await this.rejectPendingPrompt(current, /* @__PURE__ */ new Error("Chat failed before the run started; try again."));
return true;
}
if (status === "ok") {
if (!markSendAccepted()) return true;
await this.sessionUpdates.recordUserPrompt(session, runId, params.prompt);
const current = pending();
if (current) await this.finishPrompt(params.sessionId, current, "end_turn");
return true;
}
return false;
};
const sendChat = async (payload) => {
const ack = await this.gateway.request("chat.send", payload, { timeoutMs: null });
return await applyTerminalAck(ack);
};
try {
if (await sendChat({
...requestParams,
systemInputProvenance,
systemProvenanceReceipt
})) return;
if (!markSendAccepted()) return;
await this.sessionUpdates.recordUserPrompt(session, runId, params.prompt);
} catch (err) {
if ((systemInputProvenance || systemProvenanceReceipt) && isAdminScopeProvenanceRejection(err)) {
if (!this.getPendingPrompt(params.sessionId, runId)) return;
if (await sendChat(requestParams)) return;
if (!markSendAccepted()) return;
await this.sessionUpdates.recordUserPrompt(session, runId, params.prompt);
return;
}
throw err;
}
};
sendWithProvenanceFallback().catch(async (err) => {
const promptKey = this.pendingPromptKey(params.sessionId, runId);
if (this.settlingPromptKeys.has(promptKey)) return;
if (isGatewayCloseError(err) && this.getPendingPrompt(params.sessionId, runId)) return;
const error = err instanceof Error ? err : new Error(String(err));
const current = this.getPendingPrompt(params.sessionId, runId);
if (current) await this.rejectPendingPrompt(current, error);
});
});
}
async cancel(params) {
const session = this.sessionStore.getSession(params.sessionId);
if (!session) return;
await this.cancelSessionWork(session);
}
async cancelSessionWork(session, abortTimeoutMs, retainedAdmission) {
const closingAdmissions = [];
if (!retainedAdmission) {
let admission = this.pendingPromptAdmissions.get(session.sessionId);
while (admission) {
admission.closed = true;
admission.closure.resolve();
closingAdmissions.push(admission.settled.promise);
admission = admission.previous;
}
}
const pending = this.pendingPrompts.get(session.sessionId);
const scopedRunId = session.activeRunId ?? pending?.idempotencyKey;
if (!scopedRunId) {
await Promise.all(closingAdmissions);
return;
}
this.sessionStore.cancelActiveRun(session.sessionId, scopedRunId);
if (pending?.idempotencyKey === scopedRunId && this.claimPendingPrompt(pending)) pending.resolve({ stopReason: "cancelled" });
try {
const abortParams = {
sessionKey: session.sessionKey,
runId: scopedRunId
};
await (abortTimeoutMs === void 0 ? this.gateway.request("chat.abort", abortParams) : this.gateway.request("chat.abort", abortParams, { timeoutMs: abortTimeoutMs }));
} catch (err) {
this.log(`cancel error: ${String(err)}`);
}
await Promise.all(closingAdmissions);
}
ownsPromptAdmission(admission) {
return !this.stopped && !admission.closed && this.pendingPromptAdmissions.get(admission.session.sessionId) === admission && this.sessionStore.getSession(admission.session.sessionId) === admission.session;
}
pendingPromptKey(sessionId, runId) {
return `${sessionId}\u0000${runId}`;
}
getPendingPrompt(sessionId, runId) {
const pending = this.pendingPrompts.get(sessionId);
if (pending?.idempotencyKey !== runId) return;
return pending;
}
claimPendingPrompt(pending) {
if (this.getPendingPrompt(pending.sessionId, pending.idempotencyKey) !== pending) return false;
this.agentEvents.clearApprovalRelaysForPrompt(pending.sessionId, pending.idempotencyKey, { denyActive: true });
this.pendingPrompts.delete(pending.sessionId);
this.sessionStore.clearActiveRun(pending.sessionId, pending.idempotencyKey);
this.disconnects.clearWhenIdle();
return true;
}
async handleChatEvent(evt) {
const payload = evt.payload;
if (!payload) return;
const sessionKey = payload.sessionKey;
const state = payload.state;
const runId = payload.runId;
const messageData = payload.message;
if (!sessionKey || !state) return;
const pending = this.findPendingBySessionKey(sessionKey, runId);
if (!pending) return;
if (messageData && (state === "delta" || state === "final")) {
if (!await this.handleDeltaEvent(pending, messageData) || this.getPendingPrompt(pending.sessionId, pending.idempotencyKey) !== pending || state === "delta") return;
}
if (state === "final") {
const stopReason = payload.stopReason === "max_tokens" ? "max_tokens" : "end_turn";
await this.finishPrompt(pending.sessionId, pending, stopReason);
return;
}
if (state === "aborted") {
const interruption = typeof payload.errorMessage === "string" ? payload.errorMessage : void 0;
await this.finishPrompt(pending.sessionId, pending, "cancelled", { interruption });
return;
}
if (state === "error") {
const stopReason = payload.errorKind === "refusal" ? "refusal" : "end_turn";
this.finishPrompt(pending.sessionId, pending, stopReason);
}
}
async handleDeltaEvent(pending, messageData) {
const content = messageData.content;
const sessionId = pending.sessionId;
if (this.getPendingPrompt(sessionId, pending.idempotencyKey) !== pending) return false;
const fullThought = content?.filter((block) => block?.type === "thinking").map((block) => block.thinking ?? "").join("\n").trimEnd();
const sentThoughtSoFar = pending.sentThought?.length ?? 0;
if (fullThought && fullThought.length > sentThoughtSoFar) {
const newThought = fullThought.slice(sentThoughtSoFar);
pending.sentThought = fullThought;
await this.emitPromptChunk(pending, "agent_thought_chunk", newThought);
if (this.getPendingPrompt(sessionId, pending.idempotencyKey) !== pending) return false;
}
const fullText = content?.filter((block) => block?.type === "text").map((block) => block.text ?? "").join("\n").trimEnd();
const sentSoFar = pending.sentText?.length ?? 0;
if (!fullText || fullText.length <= sentSoFar) return true;
const newText = fullText.slice(sentSoFar);
pending.sentText = fullText;
await this.emitPromptChunk(pending, "agent_message_chunk", newText);
return this.getPendingPrompt(sessionId, pending.idempotencyKey) === pending;
}
async finishPrompt(sessionId, pending, stopReason, options = {}) {
if (!options.claimed && !this.claimPendingPrompt(pending)) return;
if (options.interruption) await this.emitPromptChunk(pending, "agent_message_chunk", `[OpenClaw interruption] ${options.interruption}`, false);
const promptKey = this.pendingPromptKey(sessionId, pending.idempotencyKey);
this.settlingPromptKeys.add(promptKey);
try {
const sessionSnapshot = await this.sessionState.getSnapshot(pending.sessionKey);
try {
await this.sessionState.sendSnapshotUpdate({
sessionId,
sessionKey: pending.sessionKey,
...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {}
}, sessionSnapshot, {
includeControls: false,
record: true,
runId: pending.idempotencyKey
});
} catch (err) {
this.log(`session snapshot update failed for ${sessionId}: ${String(err)}`);
}
pending.resolve({ stopReason });
} finally {
this.settlingPromptKeys.delete(promptKey);
}
}
async emitPromptChunk(pending, kind, text, waitForDelivery = true) {
await this.sessionUpdates.emit({
sessionId: pending.sessionId,
sessionKey: pending.sessionKey,
...pending.ledgerSessionId ? { ledgerSessionId: pending.ledgerSessionId } : {},
runId: pending.idempotencyKey,
record: true,
...waitForDelivery ? {} : { waitForDelivery: false },
update: {
sessionUpdate: kind,
content: {
type: "text",
text
}
}
});
}
async settleRecoveredPrompt(sessionId, pending, result) {
if (!this.claimPendingPrompt(pending)) return;
const terminalReply = result.terminalReply;
if (terminalReply?.disposition === "visible") {
const sentText = (pending.sentText ?? "").trimStart();
const recoveredText = terminalReply.text.startsWith(sentText) ? terminalReply.text.slice(sentText.length) : "";
if (recoveredText) await this.emitPromptChunk(pending, "agent_message_chunk", recoveredText, false);
}
if (result.status !== "error") {
await this.finishPrompt(sessionId, pending, "end_turn", { claimed: true });
return;
}
const message = result.error?.trim() || "run failed";
await this.emitPromptChunk(pending, "agent_message_chunk", `[OpenClaw interruption] ${message}`, false);
await this.rejectPendingPrompt(pending, new Error(message), { claimed: true });
}
findPendingBySessionKey(sessionKey, runId) {
for (const pending of this.pendingPrompts.values()) {
if (pending.sessionKey !== sessionKey) continue;
if (runId && pending.idempotencyKey !== runId) continue;
return pending;
}
if (runId) for (const pending of this.pendingPrompts.values()) {
if (pending.idempotencyKey !== runId) continue;
this.reconcilePendingSessionKey(pending, sessionKey);
return pending;
}
}
reconcilePendingSessionKey(pending, sessionKey) {
if (pending.sessionKey === sessionKey) return;
this.log(`session key reconciled: ${pending.sessionKey} -> ${sessionKey}`);
pending.sessionKey = sessionKey;
const session = this.sessionStore.getSession(pending.sessionId);
if (session?.activeRunId === pending.idempotencyKey) session.sessionKey = sessionKey;
}
async rejectPendingPrompt(pending, error, options = {}) {
if (!options.claimed && !this.claimPendingPrompt(pending)) return;
const promptKey = this.pendingPromptKey(pending.sessionId, pending.idempotencyKey);
this.settlingPromptKeys.add(promptKey);
try {
if (options.recordDisconnectNotice) {
const text = pending.sendAccepted ? "[OpenClaw interruption] The Gateway disconnected after accepting this message, so its final outcome is unknown. Check the session before retrying." : "[OpenClaw interruption] The Gateway disconnected before OpenClaw could confirm whether this message was accepted, so its final outcome is unknown. Check the session before retrying.";
await this.emitPromptChunk(pending, "agent_message_chunk", text, false);
}
} catch (noticeError) {
this.log(`disconnect notice failed for ${pending.idempotencyKey}: ${String(noticeError)}`);
} finally {
pending.reject(error);
this.settlingPromptKeys.delete(promptKey);
}
}
};
//#endregion
//#region src/acp/translator.replay.ts
function extractReplayChunks(message) {
const role = typeof message.role === "string" ? message.role : "";
if (role !== "user" && role !== "assistant") return [];
if (typeof message.content === "string") return message.content.length > 0 ? [{
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
text: message.content
}] : [];
if (!Array.isArray(message.content)) return [];
const replayChunks = [];
for (const block of message.content) {
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
const typedBlock = block;
if (typedBlock.type === "text" && typeof typedBlock.text === "string" && typedBlock.text) {
replayChunks.push({
sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk",
text: typedBlock.text
});
continue;
}
if (role === "assistant" && typedBlock.type === "thinking" && typeof typedBlock.thinking === "string" && typedBlock.thinking) replayChunks.push({
sessionUpdate: "agent_thought_chunk",
text: typedBlock.thinking
});
}
return replayChunks;
}
//#endregion
//#region src/acp/translator.session-list.ts
/** Cursor and pagination helpers for ACP session/list requests. */
const ACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE = 100;
const ACP_LIST_SESSIONS_MAX_PAGE_SIZE = 100;
const ACP_LIST_SESSIONS_MAX_CURSOR_OFFSET = 1e4;
/** Maximum rows fetched to satisfy ACP session-list pagination plus next-page detection. */
const ACP_LIST_SESSIONS_MAX_FETCH_LIMIT = 10101;
/** Encodes an ACP session-list cursor as base64url JSON. */
function encodeListSessionsCursor(cursor) {
return Buffer.from(JSON.stringify({
v: 1,
...cursor
}), "utf8").toString("base64url");
}
/** Decodes and validates an ACP session-list cursor, defaulting to the first page. */
function decodeListSessionsCursor(value) {
if (value === null || value === void 0) return { offset: 0 };
let parsed;
try {
const bytes = Buffer.from(value, "base64url");
if (bytes.toString("base64url") !== value) throw new Error("non-canonical base64url");
parsed = JSON.parse(bytes.toString("utf8"));
} catch {
throw new Error("Invalid ACP session list cursor.");
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid ACP session list cursor.");
const record = parsed;
if (record.v !== 1) throw new Error("Unsupported ACP session list cursor.");
if (typeof record.offset !== "number" || !Number.isSafeInteger(record.offset) || record.offset < 0 || record.offset > ACP_LIST_SESSIONS_MAX_CURSOR_OFFSET) throw new Error("Invalid ACP session list cursor offset.");
const cwd = normalizeOptionalString(record.cwd);
const cursor = {
offset: record.offset,
...cwd ? { cwd } : {}
};
if (encodeListSessionsCursor(cursor) !== value) throw new Error("Invalid ACP session list cursor.");
return cursor;
}
/** Throws when an ACP method receives a relative cwd filter/path. */
function assertAbsoluteCwd(cwd, method) {
if (!path.isAbsolute(cwd)) throw new Error(`ACP ${method} requires an absolute cwd.`);
}
/** Resolves requested ACP session-list page size with bridge limits. */
function resolveListSessionsPageSize(meta) {
const requested = readMetadataNumber(meta, ["limit", "pageSize"]);
if (requested === void 0) return ACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE;
return Math.min(ACP_LIST_SESSIONS_MAX_PAGE_SIZE, Math.max(1, Math.floor(requested)));
}
//#endregion
//#region src/acp/translator.session-lifecycle.ts
/** ACP session creation, loading, listing, resuming, closing, and configuration. */
const ACP_LOAD_SESSION_REPLAY_LIMIT = 1e6;
function hasExplicitSessionRouting(meta, opts) {
return Boolean(meta.sessionKey || meta.sessionLabel || opts.defaultSessionKey || opts.defaultSessionLabel);
}
var AcpTranslatorSessionLifecycle = class {
constructor(gateway, opts, sessionStore, sessionUpdates, sessionState, sessionCreateRateLimiter, cancelSessionWork, log) {
this.gateway = gateway;
this.opts = opts;
this.sessionStore = sessionStore;
this.sessionUpdates = sessionUpdates;
this.sessionState = sessionState;
this.sessionCreateRateLimiter = sessionCreateRateLimiter;
this.cancelSessionWork = cancelSessionWork;
this.log = log;
}
async newSession(params) {
this.assertSupportedSessionSetup(params.mcpServers);
this.enforceSessionCreateRateLimit("newSession");
const sessionId = randomUUID();
const meta = parseSessionMeta(params["_meta"]);
const sessionKey = await this.resolveSessionKeyFromMeta({
meta,
fallbackKey: `acp-bridge:${sessionId}`
});
const session = this.sessionStore.createSession({
sessionId,
sessionKey,
cwd: params.cwd
});
await this.sessionUpdates.startLedgerSession(session, {
complete: true,
reset: true
});
this.log(`newSession: ${session.sessionId} -> ${session.sessionKey}`);
const sessionSnapshot = await this.sessionState.getSnapshot(session.sessionKey);
await this.sessionState.sendSnapshotUpdate(session, sessionSnapshot, {
includeControls: false,
record: true
});
await this.sessionUpdates.sendAvailableCommands(session, { record: true });
const { configOptions, modes } = sessionSnapshot;
return {
sessionId: session.sessionId,
configOptions,
modes
};
}
async loadSession(params) {
this.assertSupportedSessionSetup(params.mcpServers);
if (!this.sessionStore.hasSession(params.sessionId)) this.enforceSessionCreateRateLimit("loadSession");
const meta = parseSessionMeta(params["_meta"]);
const hasExplicitRouting = hasExplicitSessionRouting(meta, this.opts);
const exactLedgerReplay = hasExplicitRouting ? {
complete: false,
events: []
} : await this.sessionUpdates.readLedgerReplayBySessionId(params.sessionId);
const listedLedgerReplay = !hasExplicitRouting && !exactLedgerReplay.complete ? await this.sessionUpdates.readLedgerReplayBySessionKey(params.sessionId) : {
complete: false,
events: []
};
const routedLedgerReplay = exactLedgerReplay.complete ? exactLedgerReplay : listedLedgerReplay;
const sessionKey = await this.resolveSessionKeyFromMeta({
meta,
fallbackKey: routedLedgerReplay.sessionKey ?? params.sessionId
});
const ledgerReplay = exactLedgerReplay.complete && exactLedgerReplay.sessionKey === sessionKey ? exactLedgerReplay : listedLedgerReplay.complete && listedLedgerReplay.sessionKey === sessionKey ? listedLedgerReplay : await this.sessionUpdates.readLedgerReplay({
sessionId: params.sessionId,
sessionKey
});
const session = this.sessionStore.createSession({
sessionId: params.sessionId,
sessionKey,
...ledgerReplay.sessionId ? { ledgerSessionId: ledgerReplay.sessionId } : {},
cwd: params.cwd
});
await this.sessionUpdates.startLedgerSession(session, { complete: ledgerReplay.complete });
this.log(`loadSession: ${session.sessionId} -> ${session.sessionKey}`);
const [sessionSnapshot, transcript] = await Promise.all([this.sessionState.getSnapshot(session.sessionKey), ledgerReplay.complete ? Promise.resolve([]) : this.getSessionTranscript(session.sessionKey).catch((err) => {
this.log(`session transcript fallback for ${session.sessionKey}: ${String(err)}`);
return [];
})]);
if (ledgerReplay.complete) await this.replayLedgerSession(session.sessionId, ledgerReplay);
else await this.replaySessionTranscript(session.sessionId, transcript);
await this.sessionState.sendSnapshotUpdate(session, sessionSnapshot, {
includeControls: false,
record: false
});
await this.sessionUpdates.sendAvailableCommands(session, { record: false });
const { configOptions, modes } = sessionSnapshot;
return {
configOptions,
modes
};
}
async listSessions(params) {
const requestedCwd = normalizeOptionalString(params.cwd);
if (requestedCwd) assertAbsoluteCwd(requestedCwd, "session/list");
const fallbackCwd = requestedCwd ?? process.cwd();
const rawCursor = params.cursor;
const cursor = decodeListSessionsCursor(rawCursor);
if (rawCursor && cursor.cwd !== requestedCwd) throw new Error("ACP session list cursor does not match the cwd filter.");
const pageSize = resolveListSessionsPageSize(params["_meta"]);
const start = cursor.offset;
const end = start + pageSize;
let fetchLimit = end + 1;
let rows = [];
while (true) {
const result = await this.gateway.request("sessions.list", {
limit: fetchLimit,
includeDerivedTitles: true
});
rows = result.sessions.filter((session) => {
if (!requestedCwd) return true;
return (normalizeOptionalString(session.spawnedCwd) ?? normalizeOptionalString(session.spawnedWorkspaceDir)) === requestedCwd;
}).map((session) => this.sessionState.mapGatewaySession(session, fallbackCwd));
if (rows.length > end || result.hasMore !== true || fetchLimit >= 10101) break;
fetchLimit = Math.min(fetchLimit * 2, ACP_LIST_SESSIONS_MAX_FETCH_LIMIT);
}
return {
sessions: rows.slice(start, end),
nextCursor: rows.length > end ? encodeListSessionsCursor({
offset: end,
...requestedCwd ? { cwd: requestedCwd } : {}
}) : null
};
}
async resumeSession(params) {
this.assertSupportedSessionSetup(params.mcpServers ?? []);
assertAbsoluteCwd(params.cwd, "session/resume");
const existingSession = this.sessionStore.getSession(params.sessionId);
if (!existingSession) this.enforceSessionCreateRateLimit("resumeSession");
const meta = parseSessionMeta(params["_meta"]);
const fallbackKey = existingSession?.sessionKey ?? params.sessionId;
const sessionKey = await this.resolveSessionKeyFromMeta({
meta,
fallbackKey
});
const sessionSnapshot = !existingSession || sessionKey !== existingSession.sessionKey ? await this.sessionState.getExistingSnapshot(sessionKey) : await this.sessionState.getSnapshot(sessionKey);
const session = this.sessionStore.createSession({
sessionId: params.sessionId,
sessionKey,
cwd: params.cwd
});
await this.sessionUpdates.startLedgerSession(session, { complete: false });
this.log(`resumeSession: ${session.sessionId} -> ${session.sessionKey}`);
await this.sessionState.sendSnapshotUpdate(session, sessionSnapshot, {
includeControls: false,
record: false
});
await this.sessionUpdates.sendAvailableCommands(session, { record: false });
const { configOptions, modes } = sessionSnapshot;
return {
configOptions,
modes
};
}
async closeSession(params) {
const session = this.sessionStore.getSession(params.sessionId);
if (!session) throw new Error(`Session ${params.sessionId} not found`);
await this.cancelSessionWork(session);
this.sessionStore.deleteSession(params.sessionId);
this.log(`closeSession: ${params.sessionId}`);
return {};
}
async authenticate(_params) {
return {};
}
async setSessionMode(params) {
const session = this.sessionStore.getSession(params.sessionId);
if (!session) throw new Error(`Session ${params.sessionId} not found`);
if (!params.modeId) return {};
try {
await this.gateway.request("sessions.patch", {
key: session.sessionKey,
thinkingLevel: params.modeId
});
this.log(`setSessionMode: ${session.sessionId} -> ${params.modeId}`);
const sessionSnapshot = await this.sessionState.getSnapshot(session.sessionKey, { thinkingLevel: params.modeId });
await this.sessionState.sendSnapshotUpdate(session, sessionSnapshot, {
includeControls: true,
record: true
});
} catch (err) {
this.log(`setSessionMode error: ${String(err)}`);
throw err instanceof Error ? err : new Error(String(err));
}
return {};
}
async setSessionConfigOption(params) {
const session = this.sessionStore.getSession(params.sessionId);
if (!session) throw new Error(`Session ${params.sessionId} not found`);
const sessionPatch = this.sessionState.resolveConfigPatch(params.configId, params.value);
try {
if (sessionPatch.patch) await this.gateway.request("sessions.patch", {
key: session.sessionKey,
...sessionPatch.patch
});
this.log(`setSessionConfigOption: ${session.sessionId} -> ${params.configId}=${params.value}`);
const sessionSnapshot = await this.sessionState.getSnapshot(session.sessionKey, sessionPatch.overrides);
await this.sessionState.sendSnapshotUpdate(session, sessionSnapshot, {
includeControls: true,
record: true
});
return { configOptions: sessionSnapshot.configOptions };
} catch (err) {
this.log(`setSessionConfigOption error: ${String(err)}`);
throw err instanceof Error ? err : new Error(String(err));
}
}
async resolveSessionKeyFromMeta(params) {
const sessionKey = await resolveAcpSessionKey({
meta: params.meta,
fallbackKey: params.fallbackKey,
gateway: this.gateway,
opts: this.opts
});
await resetSessionIfNeeded({
meta: params.meta,
sessionKey,
gateway: this.gateway,
opts: this.opts
});
return sessionKey;
}
async getSessionTranscript(sessionKey) {
const result = await this.gateway.request("sessions.get", {
key: sessionKey,
limit: ACP_LOAD_SESSION_REPLAY_LIMIT
});
if (!Array.isArray(result.messages)) return [];
return result.messages;
}
async replaySessionTranscript(sessionId, transcript) {
for (const message of transcript) {
const replayChunks = extractReplayChunks(message);
for (const chunk of replayChunks) await this.sessionUpdates.emit({
sessionId,
update: {
sessionUpdate: chunk.sessionUpdate,
content: {
type: "text",
text: chunk.text
}
}
});
}
}
async replayLedgerSession(sessionId, ledgerReplay) {
for (const event of ledgerReplay.events) await this.sessionUpdates.emit({
sessionId,
update: event.update,
record: false
});
}
assertSupportedSessionSetup(mcpServers) {
if (mcpServers.length === 0) return;
throw new Error("ACP bridge mode does not support per-session MCP servers. Configure MCP on the OpenClaw gateway or agent instead.");
}
enforceSessionCreateRateLimit(method) {
const budget = this.sessionCreateRateLimiter.consume();
if (budget.allowed) return;
throw new Error(`ACP session creation rate limit exceeded for ${method}; retry after ${Math.ceil(budget.retryAfterMs / 1e3)}s.`);
}
};
//#endregion
//#region src/acp/translator.presentation.ts
/** ACP config option ids exposed to compatible ACP clients. */
const ACP_THOUGHT_LEVEL_CONFIG_ID = "thought_level";
const ACP_FAST_MODE_CONFIG_ID = "fast_mode";
const ACP_VERBOSE_LEVEL_CONFIG_ID = "verbose_level";
const ACP_TRACE_LEVEL_CONFIG_ID = "trace_level";
const ACP_REASONING_LEVEL_CONFIG_ID = "reasoning_level";
const ACP_RESPONSE_USAGE_CONFIG_ID = "response_usage";
const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level";
const ACP_TIMEOUT_CONFIG_ID = "timeout";
const ACP_TIMEOUT_SECONDS_CONFIG_ID = "timeout_seconds";
function formatThinkingLevelName(level) {
switch (level) {
case "xhigh": return "Extra High";
case "adaptive": return "Adaptive";
default: return level.length > 0 ? `${level.charAt(0).toUpperCase()}${level.slice(1)}` : "Unknown";
}
}
function buildThinkingModeDescription(level) {
if (level === "adaptive") return "Use the Gateway session default thought level.";
}
function formatConfigValueName(value) {
switch (value) {
case "xhigh": return "Extra High";
default: return value.length > 0 ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : "Unknown";
}
}
function buildSelectConfigOption(params) {
return {
type: "select",
id: params.id,
name: params.name,
category: params.category,
description: params.description,
currentValue: params.currentValue,
options: params.values.map((value) => ({
value,
name: formatConfigValueName(value)
}))
};
}
function buildSessionPresentation(params) {
const row = {
...params.row,
...params.overrides
};
const availableLevelIds = row.thinkingLevels?.map((level) => level.id) ?? [...BASE_THINKING_LEVELS];
const currentModeId = normalizeOptionalString(row.thinkingLevel) || "adaptive";
const currentFastMode = normalizeFastMode(row.effectiveFastMode ?? row.fastMode) ?? false;
if (!availableLevelIds.includes(currentModeId)) availableLevelIds.push(currentModeId);
const modes = {
currentModeId,
availableModes: availableLevelIds.map((level) => ({
id: level,
name: formatThinkingLevelName(level),
description: buildThinkingModeDescription(level)
}))
};
return {
configOptions: [
buildSelectConfigOption({
id: ACP_THOUGHT_LEVEL_CONFIG_ID,
name: "Thought level",
category: "thought_level",
description: "Controls how much deliberate reasoning OpenClaw requests from the Gateway model.",
currentValue: currentModeId,
values: availableLevelIds
}),
buildSelectConfigOption({
id: ACP_FAST_MODE_CONFIG_ID,
name: "Fast mode",
description: "Controls whether OpenAI sessions use the Gateway fast-mode profile.",
currentValue: currentFastMode === "auto" ? "auto" : currentFastMode ? "on" : "off",
values: [
"off",
"auto",
"on"
]
}),
buildSelectConfigOption({
id: ACP_VERBOSE_LEVEL_CONFIG_ID,
name: "Tool verbosity",
description: "Controls how much tool progress and output detail OpenClaw keeps enabled for the session.",
currentValue: normalizeOptionalString(row.verboseLevel) || "off",
values: [
"off",
"on",
"full"
]
}),
buildSelectConfigOption({
id: ACP_TRACE_LEVEL_CONFIG_ID,
name: "Plugin trace",
description: "Controls whether plugin-owned trace lines are shown for the session.",
currentValue: normalizeOptionalString(row.traceLevel) || "off",
values: ["off", "on"]
}),
buildSelectConfigOption({
id: ACP_REASONING_LEVEL_CONFIG_ID,
name: "Reasoning stream",
description: "Controls whether reasoning-capable models emit reasoning text for the session.",
currentValue: normalizeOptionalString(row.reasoningLevel) || "off",
values: [
"off",
"on",
"stream"
]
}),
buildSelectConfigOption({
id: ACP_RESPONSE_USAGE_CONFIG_ID,
name: "Usage detail",
description: "Controls how much usage information OpenClaw attaches to responses for the session. 'inherit' follows the configured default; 'off' explicitly disables it for this session.",
currentValue: normalizeOptionalString(row.responseUsage) || "inherit",
values: [
"inherit",
"off",
"tokens",
"full"
]
}),
buildSelectConfigOption({
id: ACP_ELEVATED_LEVEL_CONFIG_ID,
name: "Elevated actions",
description: "Controls how aggressively the session allows elevated execution behavior.",
currentValue: normalizeOptionalString(row.elevatedLevel) || "off",
values: [
"off",
"on",
"ask",
"full"
]
})
],
modes
};
}
function buildSessionMetadata(params) {
return {
title: normalizeOptionalString(params.row?.derivedTitle) || normalizeOptionalString(params.row?.displayName) || normalizeOptionalString(params.row?.label) || params.sessionKey,
updatedAt: timestampMsToIsoString(params.row?.updatedAt) ?? null,
_meta: toAcpSessionLineageMeta(params.row ?? {
key: params.sessionKey,
kind: "unknown"
})
};
}
function buildSessionUsageSnapshot(row) {
const totalTokens = row?.totalTokens;
const contextTokens = row?.contextTokens;
if (row?.totalTokensFresh !== true || typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || typeof contextTokens !== "number" || !Number.isFinite(contextTokens) || contextTokens <= 0) return;
const size = Math.max(0, Math.floor(contextTokens));
return {
size,
used: Math.max(0, Math.min(Math.floor(totalTokens), size))
};
}
//#endregion
//#region src/acp/translator.session-state.ts
var AcpTranslatorSessionState = class {
constructor(gateway, sessionUpdates, log) {
this.gateway = gateway;
this.sessionUpdates = sessionUpdates;
this.log = log;
}
async getSnapshot(sessionKey, overrides) {
try {
const row = await this.getGatewaySessionRow(sessionKey);
return {
...buildSessionPresentation({
row,
overrides
}),
metadata: buildSessionMetadata({
row,
sessionKey
}),
usage: buildSessionUsageSnapshot(row)
};
} catch (err) {
this.log(`session presentation fallback for ${sessionKey}: ${String(err)}`);
return {
...buildSessionPresentation({ overrides }),
metadata: buildSessionMetadata({ sessionKey })
};
}
}
async getExistingSnapshot(sessionKey) {
const row = await this.getGatewaySessionRow(sessionKey);
if (!row) throw new Error(`Session ${sessionKey} not found`);
return {
...buildSessionPresentation({ row }),
metadata: buildSessionMetadata({
row,
sessionKey
}),
usage: buildSessionUsageSnapshot(row)
};
}
mapGatewaySession(session, fallbackCwd) {
const cwd = normalizeOptionalString(session.spawnedCwd) ?? normalizeOptionalString(session.spawnedWorkspaceDir) ?? fallbackCwd;
return {
sessionId: session.key,
cwd,
title: session.derivedTitle ?? session.displayName ?? session.label ?? session.key,
updatedAt: timestampMsToIsoString(session.updatedAt),
_meta: toAcpSessionLineageMeta(session)
};
}
async sendSnapshotUpdate(session, sessionSnapshot, options) {
if (options.includeControls) {
await this.sessionUpdates.emit({
sessionId: session.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
runId: options.runId,
record: options.record,
update: {
sessionUpdate: "current_mode_update",
currentModeId: sessionSnapshot.modes.currentModeId
}
});
await this.sessionUpdates.emit({
sessionId: session.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
runId: options.runId,
record: options.record,
update: {
sessionUpdate: "config_option_update",
configOptions: sessionSnapshot.configOptions
}
});
}
if (sessionSnapshot.metadata) await this.sessionUpdates.emit({
sessionId: session.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
runId: options.runId,
record: options.record,
update: {
sessionUpdate: "session_info_update",
...sessionSnapshot.metadata
}
});
if (sessionSnapshot.usage) await this.sessionUpdates.emit({
sessionId: session.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
runId: options.runId,
record: options.record,
update: {
sessionUpdate: "usage_update",
used: sessionSnapshot.usage.used,
size: sessionSnapshot.usage.size,
_meta: {
source: "gateway-session-store",
approximate: true
}
}
});
}
resolveConfigPatch(configId, value) {
if (typeof value !== "string") throw new Error(`ACP bridge does not support non-string session config option values for "${configId}".`);
switch (configId) {
case ACP_THOUGHT_LEVEL_CONFIG_ID: return {
patch: { thinkingLevel: value },
overrides: { thinkingLevel: value }
};
case ACP_FAST_MODE_CONFIG_ID: {
const fastMode = normalizeFastMode(value);
if (fastMode === void 0) throw new Error(`Unsupported fast mode value: ${value}`);
return {
patch: { fastMode },
overrides: { fastMode }
};
}
case ACP_VERBOSE_LEVEL_CONFIG_ID: return {
patch: { verboseLevel: value },
overrides: { verboseLevel: value }
};
case ACP_TRACE_LEVEL_CONFIG_ID: return {
patch: { traceLevel: value },
overrides: { traceLevel: value }
};
case ACP_REASONING_LEVEL_CONFIG_ID: return {
patch: { reasoningLevel: value },
overrides: { reasoningLevel: value }
};
case ACP_RESPONSE_USAGE_CONFIG_ID: {
const next = value === "inherit" ? null : value;
return {
patch: { responseUsage: next },
overrides: { responseUsage: next }
};
}
case ACP_ELEVATED_LEVEL_CONFIG_ID: return {
patch: { elevatedLevel: value },
overrides: { elevatedLevel: value }
};
case ACP_TIMEOUT_CONFIG_ID:
case ACP_TIMEOUT_SECONDS_CONFIG_ID: return { overrides: {} };
default: throw new Error(`ACP bridge mode does not support session config option "${configId}".`);
}
}
async getGatewaySessionRow(sessionKey) {
const session = (await this.gateway.request("sessions.list", {
limit: 200,
search: sessionKey,
includeDerivedTitles: true
})).sessions.find((entry) => entry.key === sessionKey);
if (!session) return;
return {
key: session.key,
kind: session.kind,
channel: session.channel,
parentSessionKey: session.parentSessionKey,
spawnedBy: session.spawnedBy,
spawnDepth: session.spawnDepth,
subagentRole: session.subagentRole,
subagentControlScope: session.subagentControlScope,
spawnedWorkspaceDir: session.spawnedWorkspaceDir,
spawnedCwd: session.spawnedCwd,
displayName: session.displayName,
label: session.label,
derivedTitle: session.derivedTitle,
updatedAt: session.updatedAt,
thinkingLevel: session.thinkingLevel,
thinkingLevels: session.thinkingLevels,
modelProvider: session.modelProvider,
model: session.model,
fastMode: session.fastMode,
effectiveFastMode: session.effectiveFastMode,
verboseLevel: session.verboseLevel,
traceLevel: session.traceLevel,
reasoningLevel: session.reasoningLevel,
responseUsage: session.responseUsage,
elevatedLevel: session.elevatedLevel,
totalTokens: session.totalTokens,
totalTokensFresh: session.totalTokensFresh,
contextTokens: session.contextTokens
};
}
};
//#endregion
//#region src/acp/commands.ts
const BASE_AVAILABLE_COMMANDS = [
{
name: "help",
description: "Show help and common commands."
},
{
name: "commands",
description: "List available commands."
},
{
name: "status",
description: "Show current status."
},
{
name: "context",
description: "Explain context usage (list|detail|json).",
input: { hint: "list | detail | json" }
},
{
name: "whoami",
description: "Show sender id (alias: /id)."
},
{
name: "id",
description: "Alias for /whoami."
},
{
name: "subagents",
description: "List or manage sub-agents."
},
{
name: "config",
description: "Read or write config (owner-only)."
},
{
name: "debug",
description: "Set runtime-only overrides (owner-only)."
},
{
name: "usage",
description: "Toggle usage footer (off|tokens|full|reset). 'reset'/'inherit'/'clear'/'default' clears the session override to re-inherit the configured default."
},
{
name: "stop",
description: "Stop the current run."
},
{
name: "restart",
description: "Restart the gateway (if enabled)."
},
{
name: "activation",
description: "Set group activation (mention|always)."
},
{
name: "send",
description: "Set send mode (on|off|inherit)."
},
{
name: "reset",
description: "Reset the session (/new)."
},
{
name: "new",
description: "Reset the session (/reset)."
},
{
name: "think",
description: `Set thinking level (${THINKING_LEVELS_HELP}).`
},
{
name: "verbose",
description: "Set verbose mode (on|full|off)."
},
{
name: "trace",
description: "Set plugin trace mode (on|off)."
},
{
name: "reasoning",
description: "Toggle reasoning output (on|off|stream)."
},
{
name: "elevated",
description: "Toggle elevated mode (on|off)."
},
{
name: "model",
description: "Select a model (list|status|<name>)."
},
{
name: "queue",
description: "Adjust queue mode and options."
},
{
name: "bash",
description: "Run a host command (if enabled)."
},
{
name: "compact",
description: "Compact the session history."
}
];
/** Returns the built-in ACP commands. */
function getAvailableCommands() {
return [...BASE_AVAILABLE_COMMANDS];
}
//#endregion
//#region src/acp/translator.session-updates.ts
function resolveLedgerSessionId(session) {
return session.ledgerSessionId ?? session.sessionId;
}
/** Helper that keeps ACP client updates and replay ledger writes in sync. */
var AcpTranslatorSessionUpdates = class {
constructor(options) {
this.options = options;
this.stopped = false;
this.ledgerMutationTails = /* @__PURE__ */ new Map();
}
stop() {
this.stopped = true;
}
async startLedgerSession(session, options) {
if (this.stopped) return;
try {
await this.options.eventLedger.startSession({
sessionId: resolveLedgerSessionId(session),
sessionKey: session.sessionKey,
cwd: session.cwd,
complete: options.complete,
...options.reset ? { reset: true } : {}
});
} catch (err) {
this.options.log(`event ledger session start failed for ${session.sessionId}: ${String(err)}`);
}
}
async readLedgerReplay(params) {
if (this.stopped) return {
complete: false,
events: []
};
try {
return await this.options.eventLedger.readReplay(params);
} catch (err) {
this.options.log(`event ledger replay fallback for ${params.sessionId}: ${String(err)}`);
return {
complete: false,
events: []
};
}
}
async readLedgerReplayBySessionId(sessionId) {
if (this.stopped) return {
complete: false,
events: []
};
try {
return await this.options.eventLedger.readReplayBySessionId({ sessionId });
} catch (err) {
this.options.log(`event ledger exact replay fallback for ${sessionId}: ${String(err)}`);
return {
complete: false,
events: []
};
}
}
async readLedgerReplayBySessionKey(sessionKey) {
if (this.stopped) return {
complete: false,
events: []
};
try {
return await this.options.eventLedger.readReplayBySessionKey({ sessionKey });
} catch (err) {
this.options.log(`event ledger session-key replay fallback for ${sessionKey}: ${String(err)}`);
return {
complete: false,
events: []
};
}
}
async recordUserPrompt(session, runId, prompt) {
await this.enqueueLedgerMutation(resolveLedgerSessionId(session), async () => {
if (this.stopped) return;
try {
await this.options.eventLedger.recordUserPrompt({
sessionId: resolveLedgerSessionId(session),
sessionKey: session.sessionKey,
runId,
prompt
});
} catch (err) {
this.options.log(`event ledger prompt record failed for ${session.sessionId}: ${String(err)}`);
await this.markLedgerIncomplete(session);
}
});
}
async emit(params) {
if (this.stopped) return;
const delivery = this.options.connection.sessionUpdate({
sessionId: params.sessionId,
update: params.update
});
const recording = params.record && params.sessionKey ? this.recordLedgerUpdate({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
...params.ledgerSessionId ? { ledgerSessionId: params.ledgerSessionId } : {},
...params.runId ? { runId: params.runId } : {},
update: params.update
}) : void 0;
if (params.waitForDelivery === false) delivery.catch((err) => {
this.options.log(`session update delivery failed for ${params.sessionId}: ${String(err)}`);
});
else await delivery;
await recording;
}
async sendAvailableCommands(session, options) {
await this.emit({
sessionId: session.sessionId,
sessionKey: session.sessionKey,
...session.ledgerSessionId ? { ledgerSessionId: session.ledgerSessionId } : {},
record: options.record,
update: {
sessionUpdate: "available_commands_update",
availableCommands: getAvailableCommands()
}
});
}
async recordLedgerUpdate(params) {
await this.enqueueLedgerMutation(params.ledgerSessionId ?? params.sessionId, async () => {
if (this.stopped) return;
try {
await this.options.eventLedger.recordUpdate({
sessionId: params.ledgerSessionId ?? params.sessionId,
sessionKey: params.sessionKey,
...params.runId ? { runId: params.runId } : {},
update: params.update
});
} catch (err) {
this.options.log(`event ledger update record failed for ${params.sessionId}: ${String(err)}`);
await this.markLedgerIncomplete({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
...params.ledgerSessionId ? { ledgerSessionId: params.ledgerSessionId } : {}
});
}
});
}
enqueueLedgerMutation(ledgerSessionId, mutation) {
const pending = (this.ledgerMutationTails.get(ledgerSessionId) ?? Promise.resolve()).then(mutation, mutation);
const tail = pending.catch(() => {});
this.ledgerMutationTails.set(ledgerSessionId, tail);
tail.then(() => {
if (this.ledgerMutationTails.get(ledgerSessionId) === tail) this.ledgerMutationTails.delete(ledgerSessionId);
});
return pending;
}
async markLedgerIncomplete(session) {
if (this.stopped) return;
try {
await this.options.eventLedger.markIncomplete({
sessionId: resolveLedgerSessionId(session),
sessionKey: session.sessionKey
});
} catch (err) {
this.options.log(`event ledger incomplete mark failed for ${session.sessionId}: ${String(err)}`);
}
}
};
//#endregion
//#region src/acp/translator.ts
const SESSION_CREATE_RATE_LIMIT_DEFAULT_MAX_REQUESTS = 120;
const SESSION_CREATE_RATE_LIMIT_DEFAULT_WINDOW_MS = 1e4;
const loadAcpSdkModule = createLazyRuntimeModule(() => import("@agentclientprotocol/sdk"));
/** ACP Agent implementation backed by the OpenClaw Gateway and replay ledger. */
var AcpGatewayAgent = class {
constructor(connection, gateway, opts = {}) {
this.approvalRelays = /* @__PURE__ */ new Map();
this.log = opts.verbose ? (msg) => process.stderr.write(`[acp] ${msg}\n`) : () => {};
let sessionStore;
if (opts.sessionStore === void 0) {
this.ownedSessionStore = createInMemorySessionStore();
sessionStore = this.ownedSessionStore;
} else {
this.ownedSessionStore = void 0;
sessionStore = opts.sessionStore;
}
this.sessionUpdates = new AcpTranslatorSessionUpdates({
connection,
eventLedger: opts.eventLedger ?? createInMemoryAcpEventLedger(),
log: this.log
});
const sessionState = new AcpTranslatorSessionState(gateway, this.sessionUpdates, this.log);
this.promptStream = new AcpTranslatorPromptStream(connection, gateway, opts, sessionStore, this.sessionUpdates, sessionState, this.approvalRelays, this.log);
const sessionCreateRateLimiter = createFixedWindowBudget({
maxRequests: resolveIntegerOption(opts.sessionCreateRateLimit?.maxRequests, SESSION_CREATE_RATE_LIMIT_DEFAULT_MAX_REQUESTS, { min: 1 }),
windowMs: resolveIntegerOption(opts.sessionCreateRateLimit?.windowMs, SESSION_CREATE_RATE_LIMIT_DEFAULT_WINDOW_MS, { min: 1e3 })
});
this.sessionLifecycle = new AcpTranslatorSessionLifecycle(gateway, opts, sessionStore, this.sessionUpdates, sessionState, sessionCreateRateLimiter, (session) => this.promptStream.cancelSessionWork(session), this.log);
}
start() {
this.log("ready");
}
async shutdown() {
this.sessionUpdates.stop();
try {
await this.promptStream.shutdown();
} finally {
this.ownedSessionStore?.dispose();
}
}
handleGatewayReconnect() {
this.promptStream.handleGatewayReconnect();
}
handleGatewayDisconnect(reason) {
this.promptStream.handleGatewayDisconnect(reason);
}
async handleGatewayEvent(evt) {
await this.promptStream.handleGatewayEvent(evt);
}
async initialize(_params) {
return {
protocolVersion: (await loadAcpSdkModule()).PROTOCOL_VERSION,
agentCapabilities: {
loadSession: true,
promptCapabilities: {
image: true,
audio: false,
embeddedContext: true
},
mcpCapabilities: {
http: false,
sse: false
},
sessionCapabilities: {
list: {},
resume: {},
close: {}
}
},
agentInfo: ACP_AGENT_INFO,
authMethods: []
};
}
async newSession(params) {
return await this.sessionLifecycle.newSession(params);
}
async loadSession(params) {
return await this.sessionLifecycle.loadSession(params);
}
async listSessions(params) {
return await this.sessionLifecycle.listSessions(params);
}
async resumeSession(params) {
return await this.sessionLifecycle.resumeSession(params);
}
async closeSession(params) {
return await this.sessionLifecycle.closeSession(params);
}
async authenticate(params) {
return await this.sessionLifecycle.authenticate(params);
}
async setSessionMode(params) {
return await this.sessionLifecycle.setSessionMode(params);
}
async setSessionConfigOption(params) {
return await this.sessionLifecycle.setSessionConfigOption(params);
}
async prompt(params) {
return await this.promptStream.prompt(params);
}
async cancel(params) {
await this.promptStream.cancel(params);
}
};
//#endregion
//#region src/acp/server.ts
/** ACP stdio server that bridges Agent Client Protocol clients to the OpenClaw Gateway. */
const MAX_STARTUP_ACP_BUFFER_BYTES = 1048576;
function createStartupInputMonitor(input) {
const [monitor, readable] = input.tee();
const reader = monitor.getReader();
let readableTaken = false;
let monitorCancelled = false;
const cancelMonitor = (reason) => {
if (monitorCancelled) return;
monitorCancelled = true;
reader.cancel(reason).catch(() => {});
};
const cancelBoth = (reason) => {
cancelMonitor(reason);
readable.cancel(reason).catch(() => {});
};
return {
dispose: () => {
if (!readableTaken) cancelBoth();
else cancelMonitor();
},
ended: (async () => {
try {
let bufferedBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) return;
bufferedBytes += value.byteLength;
if (bufferedBytes > MAX_STARTUP_ACP_BUFFER_BYTES) {
const error = /* @__PURE__ */ new Error("ACP startup input exceeded the 1 MiB buffer limit");
cancelBoth(error);
throw error;
}
}
} finally {
reader.releaseLock();
}
})(),
takeReadable: () => {
readableTaken = true;
return readable;
}
};
}
/** Starts the ACP Gateway bridge and serves AgentSideConnection over stdio. */
async function serveAcpGateway(opts = {}) {
routeLogsToStderr();
const cfg = getRuntimeConfig();
const bootstrap = await resolveGatewayClientBootstrap({
config: cfg,
gatewayUrl: opts.gatewayUrl,
explicitAuth: {
token: opts.gatewayToken,
password: opts.gatewayPassword
},
env: process.env
});
let agent = null;
let onClosed;
const closed = new Promise((resolve) => {
onClosed = resolve;
});
const startupAbortController = new AbortController();
let stopped = false;
let gatewayConnected = false;
let onGatewayReadyResolve;
let onGatewayReadyReject;
let gatewayReadySettled = false;
const gatewayReady = new Promise((resolve, reject) => {
onGatewayReadyResolve = resolve;
onGatewayReadyReject = reject;
});
const resolveGatewayReady = () => {
if (gatewayReadySettled) return;
gatewayReadySettled = true;
onGatewayReadyResolve();
};
const rejectGatewayReady = (err) => {
if (gatewayReadySettled) return;
gatewayReadySettled = true;
onGatewayReadyReject(err instanceof Error ? err : new Error(String(err)));
};
const closeStateDatabase = () => {
try {
closeOpenClawStateDatabase();
} catch (err) {
console.warn(`acp: state database close failed during shutdown: ${formatErrorMessage(err)}`);
}
};
const gateway = new GatewayClient({
url: bootstrap.url,
token: bootstrap.auth.token,
password: bootstrap.auth.password,
preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs,
tlsFingerprint: bootstrap.tlsFingerprint,
clientName: GATEWAY_CLIENT_NAMES.CLI,
clientDisplayName: "ACP",
clientVersion: "acp",
mode: GATEWAY_CLIENT_MODES.CLI,
caps: [GATEWAY_CLIENT_CAPS.EXEC_APPROVALS, GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
onEvent: (evt) => {
if (stopped) return;
agent?.handleGatewayEvent(evt).catch((err) => {
process.stderr.write(`openclaw acp: gateway event ${evt.event} failed\n`);
if (opts.verbose) process.stderr.write(`openclaw acp: gateway event ${evt.event} error: ${formatErrorMessage(err)}\n`);
});
},
onHelloOk: () => {
gatewayConnected = true;
resolveGatewayReady();
agent?.handleGatewayReconnect();
},
onConnectError: (err) => {
rejectGatewayReady(err);
},
onClose: (code, reason) => {
if (stopped) return;
rejectGatewayReady(/* @__PURE__ */ new Error(`gateway closed before ready (${code}): ${reason}`));
agent?.handleGatewayDisconnect(`${code}: ${reason}`);
}
});
const startupInput = createStartupInputMonitor(Readable.toWeb(process.stdin));
const shutdown = async () => {
if (stopped) return;
stopped = true;
startupAbortController.abort();
startupInput.dispose();
process.stdin.pause();
resolveGatewayReady();
const activeAgent = agent;
agent = null;
await activeAgent?.shutdown();
await gateway.stopAndWait().catch((err) => {
console.warn(`acp: gateway stop failed during shutdown: ${formatErrorMessage(err)}`);
});
closeStateDatabase();
onClosed();
};
startupInput.ended.then(() => {
if (!gatewayConnected) shutdown();
}, shutdown);
process.once("SIGINT", () => {
shutdown();
});
process.once("SIGTERM", () => {
shutdown();
});
if (!(await startGatewayClientWhenEventLoopReady(gateway, {
clientOptions: { preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs },
signal: startupAbortController.signal
})).ready) rejectGatewayReady(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
await gatewayReady.catch(async (err) => {
await shutdown();
throw err;
});
if (stopped) return closed;
const bufferedInput = startupInput.takeReadable();
startupInput.dispose();
const output = Writable.toWeb(process.stdout);
const stream = ndJsonStream(output, bufferedInput);
const readable = stream.readable.pipeThrough(new TransformStream({ transform(message, controller) {
controller.enqueue(normalizeAcpInitializeProtocolVersion(message));
} }));
const eventLedger = createSqliteAcpEventLedger();
new AgentSideConnection((conn) => {
agent = new AcpGatewayAgent(conn, gateway, {
...opts,
eventLedger
});
agent.start();
return agent;
}, {
...stream,
readable
}).closed.then(shutdown, shutdown);
return closed;
}
function normalizeAcpInitializeProtocolVersion(message) {
if (!isRecord(message)) return message;
const messageObject = message;
if (messageObject.method !== AGENT_METHODS.initialize) return message;
const params = messageObject.params;
if (!isRecord(params) || isUint16Integer(params.protocolVersion)) return message;
return {
...message,
params: {
...params,
protocolVersion: PROTOCOL_VERSION
}
};
}
function isUint16Integer(value) {
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 65535;
}
function parseArgs(args) {
const opts = {};
let tokenFile;
let passwordFile;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "--url" || arg === "--gateway-url") {
opts.gatewayUrl = args[i + 1];
i += 1;
continue;
}
if (arg === "--token" || arg === "--gateway-token") {
opts.gatewayToken = args[i + 1];
i += 1;
continue;
}
if (arg === "--token-file" || arg === "--gateway-token-file") {
tokenFile = args[i + 1];
i += 1;
continue;
}
if (arg === "--password" || arg === "--gateway-password") {
opts.gatewayPassword = args[i + 1];
i += 1;
continue;
}
if (arg === "--password-file" || arg === "--gateway-password-file") {
passwordFile = args[i + 1];
i += 1;
continue;
}
if (arg === "--session") {
opts.defaultSessionKey = args[i + 1];
i += 1;
continue;
}
if (arg === "--session-label") {
opts.defaultSessionLabel = args[i + 1];
i += 1;
continue;
}
if (arg === "--require-existing") {
opts.requireExistingSession = true;
continue;
}
if (arg === "--reset-session") {
opts.resetSession = true;
continue;
}
if (arg === "--no-prefix-cwd") {
opts.prefixCwd = false;
continue;
}
if (arg === "--provenance") {
const provenanceMode = normalizeAcpProvenanceMode(args[i + 1]);
if (!provenanceMode) throw new Error("Invalid --provenance value. Use off, meta, or meta+receipt.");
opts.provenanceMode = provenanceMode;
i += 1;
continue;
}
if (arg === "--verbose" || arg === "-v") {
opts.verbose = true;
continue;
}
if (arg === "--help" || arg === "-h") {
printHelp();
process.exit(0);
}
}
const gatewayToken = normalizeOptionalString(opts.gatewayToken);
const gatewayPassword = normalizeOptionalString(opts.gatewayPassword);
const normalizedTokenFile = normalizeOptionalString(tokenFile);
const normalizedPasswordFile = normalizeOptionalString(passwordFile);
if (gatewayToken && normalizedTokenFile) throw new Error("Use either --token or --token-file.");
if (gatewayPassword && normalizedPasswordFile) throw new Error("Use either --password or --password-file.");
if (normalizedTokenFile) opts.gatewayToken = readSecretFromFile(normalizedTokenFile, "Gateway token");
if (normalizedPasswordFile) opts.gatewayPassword = readSecretFromFile(normalizedPasswordFile, "Gateway password");
return opts;
}
function printHelp() {
console.log(`Usage: openclaw acp [options]
Gateway-backed ACP server for IDE integration.
Options:
--url <url> Gateway WebSocket URL
--token <token> Gateway auth token
--token-file <path> Read gateway auth token from file
--password <password> Gateway auth password
--password-file <path> Read gateway auth password from file
--session <key> Default session key (e.g. "agent:main:main")
--session-label <label> Default session label to resolve
--require-existing Fail if the session key/label does not exist
--reset-session Reset the session key before first use
--no-prefix-cwd Do not prefix prompts with the working directory
--provenance <mode> ACP provenance mode: off, meta, or meta+receipt
--verbose, -v Verbose logging to stderr
--help, -h Show this help message
`);
}
if (isMainModule({ currentFile: fileURLToPath(import.meta.url) })) {
const argv = process.argv.slice(2);
if (argv.includes("--token") || argv.includes("--gateway-token")) console.error("Warning: --token can be exposed via process listings. Prefer --token-file or OPENCLAW_GATEWAY_TOKEN.");
if (argv.includes("--password") || argv.includes("--gateway-password")) console.error("Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.");
serveAcpGateway(parseArgs(argv)).catch((err) => {
console.error(formatErrorMessage(err));
process.exit(1);
});
}
//#endregion
export { serveAcpGateway };