openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,350 lines • 58.4 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { s as asFiniteNumber } from "./number-coercion-CkhMiRw8.mjs";
import { t as AsyncWorkScope } from "./async-work-scope-DDiH0P7W.mjs";
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { n as filterStringEntries } from "./string-normalization-D6DKCpwE.mjs";
import { n as sliceUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { i as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CFqA9Dvi.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { d as readDatabasePathIdentitySync } from "./sqlite-worker-identity-BRNRpRyc.mjs";
import { s as throwSqliteLifecycleErrors } from "./sqlite-lifecycle-errors-DuOygjcn.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-De68tE_W.mjs";
import { n as hydrateOpenClawStateWorkerError, r as retainOpenClawStateWorkerErrorPayload } from "./openclaw-state-worker-error-Bi-BnNrV.mjs";
import { r as normalizeAgentIdStrict } from "./agent-id-D1-Q6vR4.mjs";
import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs";
import { c as normalizeOptionalAgentId, l as resolveAgentIdFromSessionKey } from "./session-key-DY3MOX4N.mjs";
import { h as parseAgentSessionKey, s as isSubagentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs";
import { n as normalizeAccountId } from "./account-id-ifCOO7_9.mjs";
import { i as listAgentIds, n as listAgentEntries } from "./agent-roster-DH6r_8f4.mjs";
import { c as resolveSessionStorePathCore } from "./paths-Bonddxfk.mjs";
import { o as resolveAgentEntry } from "./agent-scope-config-BEk6-ThS.mjs";
import { l as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs";
import { u as isSqliteWorkerError } from "./sqlite-worker-contract-C51cJHzg.mjs";
import { i as sessionChanges } from "./session-row-changes-Dj2yVWo-.mjs";
import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-BzUE_dOh.mjs";
import { t as executeExistingOpenClawStateRead } from "./openclaw-state-db-readonly-ywwvuxdQ.mjs";
import { n as runOpenClawAgentWriteAdmission } from "./openclaw-agent-write-admission-CXJiE5VB.mjs";
import { l as deliveryContextFromSession } from "./store-entry-ClyG-tia.mjs";
import { r as hasDeliveryTargetFields } from "./delivery-context.shared-BYdFKp9k.mjs";
import { _ as resolveSessionAgentIds, h as resolveSessionAgentId } from "./agent-scope-bJOokm5r.mjs";
import { h as onAgentEventForRun } from "./agent-events-jWdQ_ESE.mjs";
import { d as sessionEntryCommitGuardOptions } from "./session-source-authority-h1UO3u0I.mjs";
import { r as getBoundLegacyPluginSdkResourceHost } from "./legacy-sdk-resource-host-Ck09VxpO.mjs";
import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs";
import "./config-VnHbY_QV.mjs";
import { n as captureOpenClawAgentDatabaseExecution } from "./openclaw-agent-execution-C2B2fucX.mjs";
import { n as openOpenClawAgentSqliteWorkerStore } from "./openclaw-agent-worker-store-BE9hhRoh.mjs";
import { s as areSessionEventWakesEnabled } from "./heartbeat-wake-C26T6nJF.mjs";
import { n as getGlobalHookRunner } from "./hook-runner-global-Cn3xHNvt.mjs";
import { n as isExecutionIdentityCollectionEnabled } from "./audit-config-BXFCjLO0.mjs";
import { r as resolveChannelAccountEntry } from "./account-lookup-BAaS-Jak.mjs";
import { a as inheritSessionGitContributorProfileIds, r as buildSessionCreationStamp } from "./session-entry-provenance-C8CX2hws.mjs";
import { a as normalizeAssistantPhase } from "./chat-message-content-C-d0FKNG.mjs";
import { _ as upsertSessionEntryCore, s as loadSessionEntryReadOnly } from "./session-accessor.sqlite-entry-DLa7_9ui.mjs";
import "./session-accessor-C3ljJ974.mjs";
import { i as readSessionEntryReadOnlyInWorker, l as withSessionEntryReadOnlyInWorker } from "./session-entry-read-runtime-BdnQjoL6.mjs";
import { p as recordSubagentSpawned } from "./session-state-events-tNQ7VRZz.mjs";
import { r as captureSessionEventTargetForHost, t as enqueueSessionEventForHost } from "./session-event-handoff-BkyE9Uzx.mjs";
import { t as acpSessionRowMatchesEntry } from "./session-meta-keys-C29RYca5.mjs";
import { n as listActiveAcpSessionsForOwner } from "./active-turns-Cmaiqrih.mjs";
import { t as getAcpRuntimeBackend } from "./registry-CwtcY73s.mjs";
import { d as captureAcpSessionReadContext } from "./session-meta-CSN8P5u2.mjs";
import { d as formatAcpInheritedToolAllowError, f as formatAcpInheritedToolDenyError, l as findAcpUnsupportedInheritedToolAllow, m as inheritedToolDenyPatch, p as inheritedToolAllowPatch, s as resolveSubagentCapabilityStore, t as isSubagentEnvelopeSession, u as findAcpUnsupportedInheritedToolDeny } from "./subagent-capabilities-DBYrBW0F.mjs";
import { p as isSessionBindingError } from "./current-conversation-bindings.kernel-DWqaaBL6.mjs";
import { a as listSessionBindingsBySessionAsync, k as resolveInboundConversationResolution, t as getSessionBindingService } from "./session-binding-service-wvWqRl2A.mjs";
import { D as resolveChannelStreamingProgressCommentary } from "./streaming-DF0Ecn4m.mjs";
import { n as truncateUtf16WithEllipsis } from "./text-truncate-DrDnz8rI.mjs";
import { m as prepareSubagentSessionListReadCache } from "./subagent-registry-state-6clmFwBl.mjs";
import { o as getSubagentSessionListRunByChildSessionKey } from "./subagent-registry-read-kqDz6PtN.mjs";
import { n as resolveEventSessionRoutingPolicy, t as resolveEventSessionKeyForPolicy } from "./event-session-routing-Dm8aw0lJ.mjs";
import { c as callSubagentGateway, f as buildSubagentExecutionSessionSpawnContext, p as withSubagentGatewayExecutionIdentity, u as readGatewayRunId } from "./subagent-spawn-cleanup-D0pAau7y.mjs";
import { r as resolveGatewaySessionStoreTargetInWorker } from "./session-utils-store-worker-9uML4l0-.mjs";
import { t as AGENT_LANE_SUBAGENT } from "./lanes-BJoXx6GG.mjs";
import { c as resolveSpawnAdmission, f as reserveChildAdmissionSlot, l as resolveSpawnChannelAccountId, m as summarizeSpawnError, n as resolveConfiguredSubagentRunTimeoutSeconds, o as mintSpawnSessionKey, p as runSpawnPipeline, s as prepareSpawnThreadBinding, u as resolveSpawnMode } from "./spawn-thread-binding-CxKbcyi_.mjs";
import { o as resolveInternalSessionKey, s as resolveMainSessionAlias } from "./sessions-resolution-DvbCWXwD.mjs";
import "./sessions-helpers-CA0CJ1ys.mjs";
import { n as formatConversationTarget, t as deliveryContextFromConversation } from "./route-projection-CySxH0Gu.mjs";
import { n as waitForSessionParticipantRecording, t as recordSessionParticipantBestEffort } from "./session-participant-recording-Cfoa8flN.mjs";
import { n as resolveAcpAgentPolicyError, t as isAcpEnabledByPolicy } from "./policy-B6crihax.mjs";
import { i as readParentExecutionIdentity, n as resolveSubagentSpawnOwnership, r as resolveRequesterOriginForChild, t as captureSpawnParentLineage } from "./spawn-parent-lineage-DmPvhKI2.mjs";
import { t as recordSessionCreated } from "./session-created-CYn2su72.mjs";
import { i as resolveSpawnedWorkspaceInheritance } from "./spawned-context-Dw3ZU-zS.mjs";
import { i as resolveHeartbeatConfig, o as resolveHeartbeatIntervalMs } from "./heartbeat-config-BsKBfvT-.mjs";
import { t as isHeartbeatEnabledForAgent } from "./heartbeat-summary-DanEvL-Q.mjs";
import { a as resolveAcpSenderSpawnError, i as resolveRuntimeCwdForAcpSpawn, n as initializeAcpSpawnRuntime, o as resolveAcpSpawnRuntimePolicyError, r as resolveAcpSpawnRuntimeOptions, s as cleanupFailedAcpSpawn, t as bindPreparedAcpThread } from "./acp-spawn-runtime-DcfazyEf.mjs";
import { n as resolveAcpProjectionSettings, t as isAcpTagVisible } from "./acp-stream-settings-Ca7bZwio.mjs";
import crypto from "node:crypto";
//#region src/acp/runtime/session-meta-resume.ts
/** Resolve only indexed resume candidates; canonical entries still own requester authorization. */
async function readAcpResumeSessionOwner(params) {
const { agentId, backendId, resumeSessionId } = params;
const runtimeAgentId = normalizeOptionalAgentId(params.runtimeAgentId);
const { cfg, env, databasePath, assertCurrent } = await captureAcpSessionReadContext(params);
if (!runtimeAgentId) return;
const lookup = async (sessionKey) => {
const result = await executeExistingOpenClawStateRead({
env,
path: databasePath
}, {
type: "acpSessions.resume",
agentId,
backendId,
resumeSessionId,
sessionKey
}, { current: true });
assertCurrent();
if (!result) return [];
if (!result.ok || result.type !== "acpSessions.resume") throw new Error("Unexpected ACP resume lookup result");
return result.rows;
};
const rows = await lookup();
const storePath = resolveSessionStorePathCore(cfg.session?.store, {
agentId,
env
});
for (const row of rows) {
if (normalizeOptionalAgentId(row.agent) !== runtimeAgentId) continue;
params.onCandidate?.(row.sessionKey);
const owner = await withSessionEntryReadOnlyInWorker({
agentId,
storePath,
sessionKey: row.sessionKey,
env,
clone: false
}, assertCurrent, async (read) => {
if (!read.ok) throw read.error;
if (!read.value || !acpSessionRowMatchesEntry(row, read.value)) return;
const [current] = await lookup(row.sessionKey);
return current && acpSessionRowMatchesEntry(current, read.value) && normalizeOptionalAgentId(current.agent) === runtimeAgentId ? {
sessionKey: row.sessionKey,
entry: read.value
} : void 0;
});
if (owner) return owner;
}
}
//#endregion
//#region src/acp/runtime/session-meta-resume-authorization.ts
async function withAcpResumeSessionAuthorization(ownership, initialize) {
if (!normalizeOptionalString(ownership.resumeSessionId)) return initialize();
let current = true;
const sources = /* @__PURE__ */ new Map();
const unsubscribe = sessionChanges.subscribeFacts((change) => {
if ("sessionKey" in change && !change.factsInvalidated && (change.facts?.kind === "unchanged" || change.facts?.kind === "participants" || change.facts?.kind === "category")) return;
if ("all" in change || sources.has(change.sessionKey) && (!change.agentId || sources.get(change.sessionKey)?.has(change.agentId))) current = false;
});
const assertCurrent = () => {
ownership.assertCurrent?.();
if (!current) throw new Error("ACP resume source authority changed; retry from the current session.");
};
try {
return await initialize(async () => {
assertCurrent();
const result = await validateAcpResumeSessionOwnership({
...ownership,
assertCurrent
}, (agentId, sessionKey) => {
const agents = sources.get(sessionKey) ?? /* @__PURE__ */ new Set();
agents.add(agentId);
sources.set(sessionKey, agents);
});
if (!result.ok) throw new Error(result.error);
return assertCurrent;
});
} finally {
current = false;
unsubscribe();
}
}
async function validateAcpResumeSessionOwnership(params, observeSource) {
const resumeSessionId = normalizeOptionalString(params.resumeSessionId);
if (!resumeSessionId) return { ok: true };
const requesterSessionKey = normalizeOptionalString(params.requesterSessionKey);
if (!requesterSessionKey) return {
ok: false,
error: "sessions_spawn resumeSessionId requires an active requester session context."
};
for (const agentId of /* @__PURE__ */ new Set([params.ownerAgentId, params.runtimeAgentId])) {
const owner = await readAcpResumeSessionOwner({
...params,
agentId,
resumeSessionId,
onCandidate: (sessionKey) => observeSource?.(agentId, sessionKey)
});
params.assertCurrent?.();
if (!owner) continue;
if (owner.sessionKey === requesterSessionKey || normalizeOptionalString(owner.entry.spawnedBy) === requesterSessionKey || normalizeOptionalString(owner.entry.parentSessionKey) === requesterSessionKey) return { ok: true };
break;
}
return {
ok: false,
error: "sessions_spawn resumeSessionId is only allowed for ACP sessions previously recorded for this requester. Omit resumeSessionId to start a fresh ACP session."
};
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-admission.ts
function countUntrackedActiveAcpRunsForOwner(ownerKey, pendingChildSessionKeys) {
if (!ownerKey?.trim()) return 0;
const sessions = listActiveAcpSessionsForOwner(ownerKey.trim());
return new Set(sessions.filter((sessionKey) => {
const run = getSubagentSessionListRunByChildSessionKey(sessionKey);
return !pendingChildSessionKeys?.has(sessionKey) && !(run && typeof run.execution.endedAt !== "number");
})).size;
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-bootstrap-delivery.ts
function toGatewayImageAttachments(attachments) {
if (!attachments || attachments.length === 0) return;
return attachments.map((attachment) => ({
type: "image",
source: {
type: "base64",
media_type: attachment.mediaType,
data: attachment.data
}
}));
}
function resolveAcpSpawnBootstrapDeliveryPlan(params) {
const boundThreadIdRaw = params.binding?.conversation.conversationId;
const boundThreadId = boundThreadIdRaw ? normalizeOptionalString(boundThreadIdRaw) : void 0;
const fallbackThreadIdRaw = params.requester.origin?.threadId;
const fallbackThreadId = fallbackThreadIdRaw != null ? normalizeOptionalString(String(fallbackThreadIdRaw)) : void 0;
const deliveryThreadId = boundThreadId ?? fallbackThreadId;
const requesterConversationRef = resolveInboundConversationResolution({
cfg: params.cfg,
channel: params.requester.origin?.channel,
accountId: params.requester.origin?.accountId,
threadId: fallbackThreadId,
to: params.requester.origin?.to
});
const requesterAccountId = resolveSpawnChannelAccountId({
cfg: params.cfg,
channel: params.requester.origin?.channel,
accountId: params.requester.origin?.accountId
});
const bindingMatchesRequesterConversation = Boolean(params.requester.origin?.channel && params.binding?.conversation.channel === params.requester.origin.channel && params.binding?.conversation.accountId === requesterAccountId && requesterConversationRef?.conversationId && params.binding?.conversation.conversationId === requesterConversationRef.conversationId && (params.binding?.conversation.parentConversationId ?? void 0) === (requesterConversationRef.parentConversationId ?? void 0));
const boundDeliveryTarget = deliveryContextFromConversation(params.binding?.conversation);
const inferredDeliveryTo = (bindingMatchesRequesterConversation ? normalizeOptionalString(params.requester.origin?.to) : void 0) ?? boundDeliveryTarget?.to ?? normalizeOptionalString(params.requester.origin?.to) ?? formatConversationTarget({
channel: params.requester.origin?.channel,
conversationId: deliveryThreadId
});
const resolvedDeliveryThreadId = bindingMatchesRequesterConversation ? fallbackThreadId : boundDeliveryTarget?.threadId ?? deliveryThreadId;
const useInlineDelivery = Boolean(params.requester.origin?.channel && inferredDeliveryTo) && !params.effectiveStreamToParent && params.spawnMode === "session";
return {
useInlineDelivery,
channel: useInlineDelivery ? params.requester.origin?.channel : void 0,
accountId: useInlineDelivery ? requesterAccountId : void 0,
to: useInlineDelivery ? inferredDeliveryTo : void 0,
threadId: useInlineDelivery && resolvedDeliveryThreadId != null ? normalizeOptionalString(String(resolvedDeliveryThreadId)) : void 0
};
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-gateway.ts
async function launchAcpChildThroughGateway(params) {
const promptedAt = Date.now();
const response = await callSubagentGateway(withSubagentGatewayExecutionIdentity({
method: "agent",
assertDispatchCurrent: params.assertDispatchCurrent,
params: {
message: params.task,
sessionKey: params.sessionKey,
channel: params.deliveryPlan.channel,
to: params.deliveryPlan.to,
accountId: params.deliveryPlan.accountId,
threadId: params.deliveryPlan.threadId,
idempotencyKey: params.childIdem,
deliver: params.deliveryPlan.useInlineDelivery,
lane: AGENT_LANE_SUBAGENT,
acpTurnSource: "manual_spawn",
timeout: params.runTimeoutSeconds,
label: params.label || void 0,
...params.attachments ? { attachments: params.attachments } : {}
},
timeoutMs: 1e4
}, {
sessionSpawnContext: buildSubagentExecutionSessionSpawnContext(params.lineage),
parentExecutionIdentityToken: params.parentExecutionIdentityToken
}));
recordSessionParticipantBestEffort({
promptedAt,
identity: {
type: "agent",
id: params.lineage.parentAgentId
},
agentId: params.lineage.targetAgentId,
sessionKey: params.sessionKey,
storePath: params.participantStorePath
});
return response;
}
//#endregion
//#region src/agents/subagents/spawn/acp-parent-stream-store.sqlite.ts
/** Captures the child and physical store before delayed relay flushes can yield. */
function createAcpParentStreamRecorder(input) {
const options = {
...input,
env: cloneEnvWithPlatformSemantics(input.env ?? process.env)
};
const identity = readDatabasePathIdentitySync(resolveOpenClawAgentSqlitePath(options));
if (!identity.key.startsWith("file:")) throw new Error("ACP parent-stream diagnostics require the existing child database");
const execution = captureOpenClawAgentDatabaseExecution(options, { expectedIdentity: {
kind: "file",
physicalIdentity: identity.key.slice(5),
nativeLocation: identity.canonicalPath,
birthtime: identity.birthtime
} });
options.path = execution.path;
const worker = openOpenClawAgentSqliteWorkerStore(options, { execution }, {
moduleUrl: resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.acpParentStreamStore),
input: void 0
});
worker.catch(() => {});
return {
async record(events) {
const prepared = events.flatMap((entry) => {
try {
const eventJson = JSON.stringify(entry.event);
if (eventJson !== void 0) return [{
eventJson,
createdAt: entry.createdAt
}];
} catch {}
return [];
});
if (prepared.length === 0) return {
ok: true,
value: void 0
};
const result = await runOpenClawAgentWriteAdmission(options, async () => (await worker).execute({
type: "record",
input: {
sessionId: options.sessionId,
runId: options.runId,
events: prepared
}
}, () => execution.assertCurrent()), true);
if (result.ok) return result;
const error = /* @__PURE__ */ new Error("ACP parent-stream transaction rolled back");
retainOpenClawStateWorkerErrorPayload(error, result.error);
return {
ok: false,
error: hydrateOpenClawStateWorkerError(error, { includeOrdinary: true })
};
},
async close() {
const failures = [];
for (const close of [async () => (await worker).close(), () => execution.release()]) try {
await close();
} catch (error) {
failures.push(error);
}
throwSqliteLifecycleErrors(failures, "ACP parent-stream recorder cleanup failed");
}
};
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-parent-stream.ts
const STREAM_FLUSH_MS = 2500;
const NO_OUTPUT_NOTICE_MS = 6e4;
const NO_OUTPUT_POLL_MS = 15e3;
const MAX_RELAY_LIFETIME_MS = 216e5;
const STREAM_BUFFER_MAX_CHARS = 4e3;
const STREAM_SNIPPET_MAX_CHARS = 220;
const STREAM_LOG_BATCH_SIZE = 100;
const STREAM_LOG_FLUSH_MS = 1e3;
const STREAM_LOG_MAX_PENDING_EVENTS = 256;
const STREAM_LOG_MAX_RETRY_MS = 3e4;
const log = createSubsystemLogger("agents/acp-parent-stream");
function mergeStreamingConfig(base, override) {
const baseRecord = asOptionalRecord(base);
const overrideRecord = asOptionalRecord(override);
if (!baseRecord || !overrideRecord) return override ?? base;
const merged = {
...baseRecord,
...overrideRecord
};
const baseProgress = asOptionalRecord(baseRecord.progress);
const overrideProgress = asOptionalRecord(overrideRecord.progress);
if (baseProgress && overrideProgress) merged.progress = {
...baseProgress,
...overrideProgress
};
else if (overrideProgress ?? baseProgress) merged.progress = overrideProgress ?? baseProgress;
else delete merged.progress;
return merged;
}
function resolveParentProgressStreamingEntry(params) {
const channelId = normalizeOptionalString(params.deliveryContext?.channel);
if (!params.cfg || !channelId) return;
const channelCfg = params.cfg.channels?.[channelId];
if (!channelCfg) return;
const accountCfg = resolveChannelAccountEntry(channelCfg.accounts, normalizeAccountId(params.deliveryContext?.accountId), channelId, normalizeAccountId);
return accountCfg ? {
...channelCfg,
...accountCfg,
streaming: mergeStreamingConfig(channelCfg.streaming, accountCfg.streaming)
} : channelCfg;
}
function startAcpSpawnParentStreamRelay(params) {
const { runId, parentSessionKey, eventRouting } = params;
const relayLabel = truncateUtf16WithEllipsis(params.agentId.replace(/\s+/g, " ").trim(), 40) || "ACP child";
const contextPrefix = `acp-spawn:${runId}`;
const childSessionId = normalizeOptionalString(params.childSessionId);
const stateEnv = { ...params.env ?? process.env };
const host = getBoundLegacyPluginSdkResourceHost();
const closingSignal = host?.scheduler.signal;
const acceptedWork = new AsyncWorkScope();
let recorder;
try {
if (childSessionId) recorder = createAcpParentStreamRecorder({
agentId: params.ownerAgentId ?? params.agentId,
env: stateEnv,
sessionId: childSessionId,
runId
});
} catch (error) {
log.warn("Failed to capture ACP parent stream diagnostic store", {
runId,
error: String(error)
});
}
const pendingLogEvents = [];
let logFlush;
let disposal;
let logFlushTimer;
let logFailureWarned = false;
let logBufferWarned = false;
let consecutiveLogFailures = 0;
let disposed = false;
const capPendingLogEvents = () => {
const overflow = pendingLogEvents.length - STREAM_LOG_MAX_PENDING_EVENTS;
if (overflow <= 0) return;
pendingLogEvents.splice(0, overflow);
if (!logBufferWarned) {
log.warn("Capped ACP parent stream diagnostic buffer", {
runId,
childSessionId,
maxPendingEvents: STREAM_LOG_MAX_PENDING_EVENTS
});
logBufferWarned = true;
}
};
const clearLogFlushTimer = () => {
clearTimeout(logFlushTimer);
logFlushTimer = void 0;
};
function flushLogEvents() {
clearLogFlushTimer();
if (logFlush || !recorder || pendingLogEvents.length === 0) return logFlush;
const events = pendingLogEvents.splice(0);
const writer = recorder;
logFlush = acceptedWork.track(async () => {
let retryable = false;
try {
const result = await writer.record(events);
if (!result.ok) {
retryable = true;
throw result.error;
}
logFailureWarned = false;
logBufferWarned = false;
consecutiveLogFailures = 0;
} catch (error) {
retryable ||= isSqliteWorkerError(error, "overloaded") || isSqliteWorkerError(error, "unavailable");
const retrying = retryable && !disposed;
if (retrying) {
pendingLogEvents.unshift(...events);
capPendingLogEvents();
consecutiveLogFailures += 1;
clearLogFlushTimer();
scheduleLogFlush(Math.min(STREAM_LOG_FLUSH_MS * 2 ** consecutiveLogFailures, STREAM_LOG_MAX_RETRY_MS));
}
if (!logFailureWarned || disposed) {
log.warn("Failed to persist ACP parent stream diagnostics", {
runId,
childSessionId,
retrying,
error: String(error)
});
logFailureWarned = true;
}
}
}).finally(() => {
logFlush = void 0;
scheduleLogFlush();
});
return logFlush;
}
function scheduleLogFlush(delayMs = STREAM_LOG_FLUSH_MS) {
if (disposed || logFlushTimer || pendingLogEvents.length === 0) return;
logFlushTimer = setTimeout(() => {
flushLogEvents();
}, delayMs);
logFlushTimer.unref?.();
}
const logEvent = (kind, fields) => {
if (disposed || !recorder) return;
const createdAt = Date.now();
pendingLogEvents.push({
createdAt,
event: {
ts: new Date(createdAt).toISOString(),
epochMs: createdAt,
runId,
parentSessionKey,
childSessionKey: params.childSessionKey,
agentId: params.agentId,
kind,
...fields
}
});
capPendingLogEvents();
if (consecutiveLogFailures === 0 && pendingLogEvents.length >= STREAM_LOG_BATCH_SIZE) {
flushLogEvents();
return;
}
scheduleLogFlush();
};
const shouldRelayProgressCommentary = resolveChannelStreamingProgressCommentary(resolveParentProgressStreamingEntry({
cfg: params.cfg,
deliveryContext: params.deliveryContext
}), true);
const acpProjectionSettings = resolveAcpProjectionSettings(params.cfg ?? {});
const eventSessionKey = resolveEventSessionKeyForPolicy(parentSessionKey, eventRouting);
const deliveryContext = structuredClone(params.deliveryContext);
const emit = (text, contextKey) => {
const cleaned = truncateUtf16WithEllipsis(text.trim(), STREAM_BUFFER_MAX_CHARS);
if (disposed || !cleaned) return;
logEvent("system_event", {
contextKey,
text: cleaned
});
try {
enqueueSessionEventForHost(cleaned, {
agentId: params.requesterAgentId,
sessionKey: eventSessionKey,
source: "task",
contextKey,
deliveryContext,
expectedTarget: params.expectedTarget
}).settled.then((outcome) => {
if (outcome.status !== "completed") log.warn("ACP parent relay follow-up failed", {
runId,
contextKey,
...outcome
});
});
} catch (error) {
log.warn("ACP parent relay follow-up rejected", {
runId,
contextKey,
error: String(error)
});
}
};
const emitStartNotice = () => {
emit(`Started ${relayLabel} session ${params.childSessionKey}. Streaming progress updates to parent session.`, `${contextPrefix}:start`);
};
let pendingText = "";
let pendingProgressKind;
let replaceableAssistantSnapshot;
const itemProgressTextById = /* @__PURE__ */ new Map();
let lastProgressAt = Date.now();
let stallNotified = false;
let promptSubmittedAt;
let firstRuntimeEventAt;
let firstVisibleOutputAt;
let lastRuntimeEventType;
let proxyEnvKeysAtPrompt = [];
let flushTimer;
const clearFlushTimer = () => {
clearTimeout(flushTimer);
flushTimer = void 0;
};
const flushPending = () => {
clearFlushTimer();
if (!pendingText) return;
const snippet = truncateUtf16WithEllipsis(pendingText.replace(/\s+/g, " ").trim(), STREAM_SNIPPET_MAX_CHARS);
pendingText = "";
pendingProgressKind = void 0;
if (!snippet) return;
emit(`${relayLabel}: ${snippet}`, `${contextPrefix}:progress`);
};
const scheduleFlush = () => {
if (disposed || flushTimer) return;
flushTimer = setTimeout(flushPending, STREAM_FLUSH_MS);
flushTimer.unref?.();
};
const appendVisibleProgress = (delta, kind) => {
if (stallNotified) {
stallNotified = false;
emit(`${relayLabel} resumed output.`, `${contextPrefix}:resumed`);
}
lastProgressAt = Date.now();
firstVisibleOutputAt ??= lastProgressAt;
if (pendingText && pendingProgressKind && pendingProgressKind !== kind) flushPending();
pendingProgressKind = kind;
pendingText += delta;
if (pendingText.length > STREAM_BUFFER_MAX_CHARS) pendingText = sliceUtf16Safe(pendingText, -4e3);
if (pendingText.length >= STREAM_SNIPPET_MAX_CHARS || delta.includes("\n\n")) {
flushPending();
return;
}
scheduleFlush();
};
const flushReplaceableAssistantSnapshot = () => {
const snapshot = replaceableAssistantSnapshot;
replaceableAssistantSnapshot = void 0;
if (!snapshot?.trim()) return;
appendVisibleProgress(snapshot, "assistant:replaceable");
};
const appendItemProgressSnapshot = (snapshot) => {
const previous = itemProgressTextById.get(snapshot.itemId) ?? "";
if (snapshot.text === previous) return;
const kind = `item:${snapshot.itemId}`;
const isPrefixUpdate = Boolean(previous && snapshot.text.startsWith(previous));
if (previous && !isPrefixUpdate && pendingProgressKind === kind && Boolean(pendingText)) pendingText = "";
itemProgressTextById.set(snapshot.itemId, snapshot.text);
const delta = isPrefixUpdate ? snapshot.text.slice(previous.length) : snapshot.text;
appendVisibleProgress(delta, kind);
};
const buildNoOutputNotice = () => {
const seconds = Math.round(NO_OUTPUT_NOTICE_MS / 1e3);
if (!promptSubmittedAt) return `${relayLabel} session started but no prompt submission was observed for ${seconds}s.`;
if (!firstRuntimeEventAt) {
const proxySummary = `proxy env: ${proxyEnvKeysAtPrompt.join(", ") || "none"}`;
return `${relayLabel} prompt was submitted but no ACP runtime event arrived for ${seconds}s (${proxySummary}). Check upstream connectivity, auth, or proxy/network access in the gateway child environment.`;
}
if (!firstVisibleOutputAt) {
const lastEvent = lastRuntimeEventType ? ` Last ACP event: ${lastRuntimeEventType}.` : "";
return `${relayLabel} has ACP runtime activity but no visible assistant output for ${seconds}s.${lastEvent} It may be working, blocked on a tool, or failing before visible output.`;
}
return `${relayLabel} has produced no visible output for ${seconds}s. It may be waiting for interactive input.`;
};
const noOutputWatcherTimer = setInterval(() => {
if (disposed || stallNotified || Date.now() - lastProgressAt < NO_OUTPUT_NOTICE_MS) return;
stallNotified = true;
emit(buildNoOutputNotice(), `${contextPrefix}:stall`);
}, NO_OUTPUT_POLL_MS);
noOutputWatcherTimer.unref?.();
const relayLifetimeTimer = setTimeout(() => {
if (disposed) return;
emit(`${relayLabel} stream relay timed out after ${Math.round(MAX_RELAY_LIFETIME_MS / 1e3)}s without completion.`, `${contextPrefix}:timeout`);
dispose();
}, MAX_RELAY_LIFETIME_MS);
relayLifetimeTimer.unref?.();
const unsubscribe = onAgentEventForRun(runId, (event) => {
if (disposed || event.runId !== runId) return;
if (event.stream === "assistant") {
const data = event.data;
const assistantPhase = normalizeAssistantPhase(data.phase);
const textCandidate = data.text;
const deltaCandidate = data.delta;
const snapshot = typeof textCandidate === "string" ? textCandidate : typeof deltaCandidate === "string" ? deltaCandidate : void 0;
if (data.replaceable === true) {
if (snapshot?.trim()) {
replaceableAssistantSnapshot = snapshot;
lastProgressAt = Date.now();
logEvent("assistant_replaceable_snapshot", {
text: snapshot,
...assistantPhase ? { phase: assistantPhase } : {}
});
}
return;
}
const delta = typeof deltaCandidate === "string" ? deltaCandidate : snapshot;
if (!delta || !delta.trim()) return;
logEvent("assistant_delta", {
delta,
...assistantPhase ? { phase: assistantPhase } : {}
});
if (assistantPhase === "commentary" && !shouldRelayProgressCommentary) {
lastProgressAt = Date.now();
return;
}
replaceableAssistantSnapshot = void 0;
appendVisibleProgress(delta, `assistant:${assistantPhase ?? "unknown"}`);
return;
}
if (event.stream === "item") {
const data = event.data;
const itemId = normalizeOptionalString(data?.itemId);
const kind = normalizeOptionalString(data?.kind);
const progressText = normalizeOptionalString(data?.progressText);
if (kind === "preamble" && progressText) {
lastProgressAt = Date.now();
if (shouldRelayProgressCommentary && itemId) appendItemProgressSnapshot({
itemId,
text: progressText
});
}
return;
}
if (event.stream === "acp") {
const data = event.data;
const phase = normalizeOptionalString(data?.phase);
logEvent("acp", {
phase: phase ?? "unknown",
data: event.data
});
if (phase === "prompt_submitted") {
const at = asFiniteNumber(data?.at) ?? Date.now();
promptSubmittedAt ??= at;
proxyEnvKeysAtPrompt = filterStringEntries(data?.proxyEnvKeys).filter(Boolean);
lastProgressAt = Date.now();
return;
}
if (phase === "runtime_event") {
const eventType = normalizeOptionalString(data?.eventType);
const text = normalizeOptionalString(data?.text);
const tag = normalizeOptionalString(data?.tag);
firstRuntimeEventAt ??= Date.now();
lastRuntimeEventType = eventType;
if (shouldRelayProgressCommentary && eventType === "status" && text && isAcpTagVisible(acpProjectionSettings, tag)) {
appendVisibleProgress(`${text}\n\n`, "acp:status");
return;
}
lastProgressAt = Date.now();
return;
}
return;
}
if (event.stream !== "lifecycle") return;
const phase = normalizeOptionalString(event.data.phase);
logEvent("lifecycle", {
phase: phase ?? "unknown",
data: event.data
});
if (phase !== "end" && phase !== "error") return;
flushReplaceableAssistantSnapshot();
flushPending();
if (phase === "end") {
const startedAt = asFiniteNumber(event.data.startedAt);
const endedAt = asFiniteNumber(event.data.endedAt);
const durationMs = startedAt != null && endedAt != null && endedAt >= startedAt ? endedAt - startedAt : void 0;
emit(durationMs != null ? `${relayLabel} run completed in ${Math.max(1, Math.round(durationMs / 1e3))}s.` : `${relayLabel} run completed.`, `${contextPrefix}:done`);
} else {
const errorText = normalizeOptionalString(event.data.error);
emit(errorText ? `${relayLabel} run failed: ${errorText}` : `${relayLabel} run failed.`, `${contextPrefix}:error`);
}
dispose();
});
const dispose = () => {
if (disposal) return disposal;
disposed = true;
clearFlushTimer();
clearLogFlushTimer();
clearTimeout(relayLifetimeTimer);
clearInterval(noOutputWatcherTimer);
unsubscribe();
closingSignal?.removeEventListener("abort", onClose);
disposal = (async () => {
await logFlush;
await flushLogEvents();
await acceptedWork.drain();
await recorder?.close();
})().catch((error) => {
log.warn("Failed to close ACP parent stream diagnostics", {
runId,
error: String(error)
});
});
const completion = disposal;
host?.releaseClaim({ release: () => completion });
return disposal;
};
const onClose = () => {
dispose();
};
closingSignal?.addEventListener("abort", onClose, { once: true });
return {
dispose,
notifyStarted: emitStartNotice
};
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-heartbeat.ts
function isHeartbeatEnabledForSessionAgent(params) {
if (!areSessionEventWakesEnabled()) return false;
if (!params.sessionKey?.trim()) return true;
const requesterAgentId = resolveSessionAgentIds({
config: params.cfg,
agentId: params.requesterAgentId,
sessionKey: params.sessionKey
}).sessionAgentId;
if (!isHeartbeatEnabledForAgent(params.cfg, requesterAgentId)) return false;
return resolveHeartbeatIntervalMs(params.cfg, void 0, resolveHeartbeatConfig(params.cfg, requesterAgentId)) !== null;
}
function hasSessionLocalHeartbeatRelayRoute(params) {
if ((params.cfg.session?.scope ?? "per-sender") === "global") return false;
const heartbeat = resolveHeartbeatConfig(params.cfg, params.requesterAgentId);
if (heartbeat?.target !== "last") return false;
if (normalizeOptionalString(heartbeat.to) || normalizeOptionalString(heartbeat.accountId)) return false;
const storePath = resolveSessionStorePathCore(params.cfg.session?.store, { agentId: params.requesterAgentId });
const parentEntry = loadSessionEntryReadOnly({
storePath,
sessionKey: params.parentSessionKey,
clone: false
});
const parentDeliveryContext = deliveryContextFromSession(parentEntry);
return hasDeliveryTargetFields(parentDeliveryContext);
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-requester.ts
async function readAcpSpawnParentDeliveryContext(params) {
return await withSessionEntryReadOnlyInWorker({
sessionKey: params.parentSessionKey,
agentId: resolveAgentIdFromSessionKey(params.parentSessionKey, params.requesterAgentId),
clone: false
}, () => params.assertActive?.(), async (read) => {
if (!read.ok) throw read.error;
return deliveryContextFromSession(read.value);
});
}
function resolveRequesterInternalSessionKey(params) {
const { alias } = resolveMainSessionAlias(params.cfg);
const requesterSessionKey = normalizeOptionalString(params.requesterSessionKey);
return requesterSessionKey ? resolveInternalSessionKey({
key: requesterSessionKey,
alias
}) : alias;
}
async function resolveAcpSpawnRequesterState(params) {
const requesterParsedSession = parseAgentSessionKey(params.parentSessionKey);
const isSubagentSession = Boolean(requesterParsedSession) && isSubagentSessionKey(params.parentSessionKey);
return {
isSubagentSession,
hasActiveSubagentBinding: isSubagentSession && params.parentSessionKey ? (await listSessionBindingsBySessionAsync(params.parentSessionKey)).some((record) => record.targetKind === "subagent" && record.status !== "ended") : false,
hasThreadContext: typeof params.ctx.agentThreadId === "string" ? Boolean(normalizeOptionalString(params.ctx.agentThreadId)) : params.ctx.agentThreadId != null,
heartbeatEnabled: isHeartbeatEnabledForSessionAgent({
cfg: params.cfg,
requesterAgentId: params.requesterAgentId,
sessionKey: params.parentSessionKey
}),
heartbeatRelayRouteUsable: params.parentSessionKey && params.requesterAgentId ? hasSessionLocalHeartbeatRelayRoute({
cfg: params.cfg,
parentSessionKey: params.parentSessionKey,
requesterAgentId: params.requesterAgentId
}) : false,
origin: resolveRequesterOriginForChild({
cfg: params.cfg,
targetAgentId: params.ownerAgentId,
requesterAgentId: params.requesterAgentId,
requesterChannel: params.ctx.agentChannel,
requesterAccountId: params.ctx.agentAccountId,
requesterTo: params.ctx.agentTo,
requesterThreadId: params.ctx.agentThreadId,
requesterGroupSpace: params.ctx.agentGroupSpace,
requesterMemberRoleIds: params.ctx.agentMemberRoleIds
})
};
}
function shouldStreamAcpSpawnToParent(params) {
const implicitStreamToParent = params.spawnMode === "run" && !params.requestThreadBinding && params.requester.isSubagentSession && !params.requester.hasActiveSubagentBinding && !params.requester.hasThreadContext && params.requester.heartbeatEnabled && params.requester.heartbeatRelayRouteUsable;
return params.streamToParentRequested || implicitStreamToParent;
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-result.ts
function buildAcpSpawnError(errorCode, error, status = "error") {
return {
status,
errorCode,
error
};
}
function buildAcpSpawnFailureResult(result, childSessionKey) {
const { phase, error, runId } = result;
const bindingError = phase === "initialize" && isSessionBindingError(error);
return {
status: "error",
errorCode: bindingError ? "thread_binding_invalid" : phase === "dispatch" ? "dispatch_failed" : "spawn_failed",
error: bindingError ? error.message : phase === "register" ? `Failed to register ACP run: ${summarizeSpawnError(error)}. Cleanup was attempted, but the already-started ACP run may still finish in the background.` : summarizeSpawnError(error),
...phase !== "initialize" ? { childSessionKey } : {},
...phase === "register" && runId ? { runId } : {}
};
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn-target.ts
function resolveAcpAgentTarget(params) {
const backendId = normalizeOptionalString(params.agentBackend) ?? normalizeOptionalString(params.cfg.acp?.backend) ?? getAcpRuntimeBackend()?.id;
return {
ok: true,
agentId: params.agentId,
...params.configAgentId ? { configAgentId: params.configAgentId } : {},
...backendId ? { backendId } : {}
};
}
function resolveTargetAcpAgentId(params) {
const normalizedRequest = params.requestedAgentId === void 0 ? null : normalizeAgentIdStrict(params.requestedAgentId);
if (normalizedRequest && !normalizedRequest.ok) return {
ok: false,
error: `agentId "${params.requestedAgentId}" was not found`
};
const requested = normalizedRequest?.value ?? normalizeOptionalAgentId(params.cfg.acp?.defaultAgent);
if (requested) {
const configuredAgent = resolveAgentEntry(params.cfg, requested);
if (configuredAgent?.runtime?.type === "acp") return resolveAcpAgentTarget({
cfg: params.cfg,
agentId: normalizeOptionalAgentId(configuredAgent.runtime.acp?.agent) ?? requested,
configAgentId: requested,
agentBackend: configuredAgent.runtime.acp?.backend
});
if (configuredAgent && !isExplicitlyAllowedAcpAgent(params.cfg, requested)) return {
ok: false,
error: `agentId "${requested}" is an OpenClaw config agent, not an ACP harness. Use runtime="subagent" or omit runtime for OpenClaw config agents. Use runtime="acp" only with external ACP harness ids such as codex, claude, droid, gemini, or opencode, or configure agents.entries.*.runtime.type="acp" with runtime.acp.agent.`
};
return resolveAcpAgentTarget({
cfg: params.cfg,
agentId: requested,
...configuredAgent ? { configAgentId: requested } : {}
});
}
return {
ok: false,
error: "ACP target agent is not configured. Pass `agentId` in `sessions_spawn` or set `acp.defaultAgent` in config."
};
}
function isExplicitlyAllowedAcpAgent(cfg, agentId) {
return (cfg.acp?.allowedAgents ?? []).some((entry) => entry.trim() === "*" || normalizeOptionalAgentId(entry) === agentId);
}
function resolveConfiguredAcpSubagentTargetIds(cfg) {
const ids = new Set(listAgentIds(cfg));
const candidates = [
...listAgentEntries(cfg).flatMap((agent) => agent.runtime?.type === "acp" ? [agent.runtime.acp?.agent] : []),
cfg.acp?.defaultAgent,
...(cfg.acp?.allowedAgents ?? []).filter((entry) => entry.trim() !== "*")
];
for (const entry of candidates) {
const id = normalizeOptionalAgentId(entry);
if (id) ids.add(id);
}
return Array.from(ids);
}
//#endregion
//#region src/agents/subagents/spawn/acp-spawn.ts
const ACP_SPAWN_ACCEPTED_NOTE = "initial ACP task queued in isolated session; follow-ups continue in the bound thread.";
const ACP_SPAWN_SESSION_ACCEPTED_NOTE = "thread-bound ACP session stays active after this task; continue in-thread for follow-ups.";
async function spawnAcpDirect(params, ctx) {
const cfg = getRuntimeConfig();
const runTimeoutSeconds = resolveConfiguredSubagentRunTimeoutSeconds({
cfg,
runTimeoutSeconds: params.runTimeoutSeconds
});
const requesterInternalKey = resolveRequesterInternalSessionKey({
cfg,
requesterSessionKey: ctx.agentSessionKey
});
if (!isAcpEnabledByPolicy(cfg)) return buildAcpSpawnError("acp_disabled", "ACP is disabled by policy (`acp.enabled=false`).", "forbidden");
const streamToParentRequested = params.streamTo === "parent";
const parentSessionKey = normalizeOptionalString(ctx.agentSessionKey);
if (streamToParentRequested && !parentSessionKey) return buildAcpSpawnError("requester_session_required", "sessions_spawn streamTo=\"parent\" requires an active requester session context.");
const requestThreadBinding = params.thread === true;
const requesterAgentId = resolveSessionAgentId({
config: cfg,
sessionKey: requesterInternalKey,
agentId: ctx.requesterAgentIdOverride
});
const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({
cfg,
requesterAgentId,
requesterSessionKey: ctx.agentSessionKey,
requesterSandboxed: ctx.sandboxed,
sandbox: params.sandbox
});
if (runtimePolicyError) return buildAcpSpawnError("runtime_policy", runtimePolicyError, "forbidden");
const acpUnsupportedInheritedTool = findAcpUnsupportedInheritedToolDeny(ctx.inheritedToolDenylist);
if (acpUnsupportedInheritedTool) return buildAcpSpawnError("runtime_policy", formatAcpInheritedToolDenyError(acpUnsupportedInheritedTool), "forbidden");
const acpUnsupportedInheritedAllow = findAcpUnsupportedInheritedToolAllow(ctx.inheritedToolAllowlist);
if (acpUnsupportedInheritedAllow) return buildAcpSpawnError("runtime_policy", formatAcpInheritedToolAllowError(acpUnsupportedInheritedAllow), "forbidden");
const spawnMode = resolveSpawnMode({
requestedMode: params.mode,
threadRequested: requestThreadBinding
});
if (spawnMode === "session" && !requestThreadBinding) return buildAcpSpawnError("thread_required", "sessions_spawn(runtime=\"acp\", mode=\"session\") requires thread=true so the ACP session can stay bound to a channel thread. Retry with { mode: \"session\", thread: true } on a channel that exposes threads (e.g. Discord, Slack, Telegram topics), or use mode=\"run\" for one-shot work.");
const targetAgentResult = resolveTargetAcpAgentId({
requestedAgentId: params.agentId,
cfg
});
if (!targetAgentResult.ok) return buildAcpSpawnError(params.agentId && normalizeOptionalAgentId(params.agentId) ? "runtime_agent_mismatch" : "target_agent_required", targetAgentResult.error);
const { agentId: targetAgentId, configAgentId, backendId } = targetAgentResult;
const ownerAgentId = configAgentId ?? requesterAgentId;
const senderRestricted = ctx.inheritedToolPolicySource === "sender";
const requesterRoot = ctx.sessionPermissionPolicy?.root ?? ctx.workspaceDir;
const requesterPolicyError = resolveAcpSenderSpawnError({
...ctx,
requesterAgentId,
targetAgentId,
cwd: params.cwd
});
if (requesterPolicyError) return buildAcpSpawnError("runtime_policy", requesterPolicyError, "forbidden");
const agentPolicyError = resolveAcpAgentPolicyError(cfg, targetAgentId);
if (agentPolicyError) return buildAcpSpawnError("agent_forbidden", agentPolicyError.message, "forbidden");
const subagentStore = resolveSubagentCapabilityStore(parentSessionKey, { cfg });
const requesterState = await resolveAcpSpawnRequesterState({
cfg,
parentSessionKey,
requesterAgentId,
ownerAgentId,
ctx
});
ctx.assertActive?.();
const ownership = resolveSubagentSpawnOwnership({
cfg,
agentSessionKey: ctx.agentSessionKey,
completionOwnerKey: ctx.completionOwnerKey
});
const requesterTarget = await resolveGatewaySessionStoreTargetInWorker({
cfg,
key: ownership.completionRequesterSessionKey,
agentId: ctx.requesterAgentIdOverride,
assertActive: ctx.assertActive
});
ctx.assertActive?.();
const requesterEntry = requesterTarget.store[requesterTarget.canonicalKey];
const completionRequesterSessionId = requesterEntry?.sessionId;
const completionRequesterLifecycleRevision = requesterEntry?.lifecycleRevision;
const hasSubagentEnvelope = isSubagentEnvelopeSession(requesterInternalKey, {
cfg,
store: subagentStore
});
await prepareSubagentSessionListReadCache();
ctx.assertActive?.();
const resolveAdmission = (pendingChildren = 0, pendingChildSessionKeys) => resolveSpawnAdmission({
cfg,
inheritedToolPolicySource: ctx.inheritedToolPolicySource,
enabled: hasSubagentEnvelope || senderRestricted,
requesterSessionKey: requesterInternalKey,
requesterAgentId,
targetAgentId,
requestedAgentId: params.agentId,
configuredAgentIds: resolveConfiguredAcpSubagentTargetIds(cfg),
additionalActiveChildren: hasSubagentEnvelope ? countUntrackedActiveAcpRunsForOwner(requesterInternalKey, pendingChildSessionKeys) + pendingChildren : 0
});
const admission = resolveAdmission();
if (!admission.ok) return buildAcpSpawnError("subagent_policy", admission.error, "forbidden");
const resumeOwnership = {
cfg,
ownerAgentId,
runtimeAgentId: targetAgentId,
backendId,
requesterSessionKey: requesterInternalKey,
resumeSessionId: params.resumeSessionId,
assertCurrent: ctx.assertActive
};
const resumeAuthorization = await validateAcpResumeSessionOwnership(resumeOwnership);
ctx.assertActive?.();
if (!resumeAuthorization.ok) return buildAcpSpawnError("resume_forbidden", resumeAuthorization.error, "forbidden");
const runtimeOptionsResult = resolveAcpSpawnRuntimeOptions({
cfg,
targetAgentId,
configAgentId: targetAgentResult.configAgentId,
model: params.model,
thinking: params.thinking,
runTimeoutSeconds
});
if (!runtimeOptionsResult.ok) return buildAcpSpawnError("spawn_failed", runtimeOptionsResult.error);
const effectiveStreamToParent = shouldStreamAcpSpawnToParent({
spawnMode,
requestThreadBinding,
streamToParentRequested,
requester: requesterState
});
const sessionKey = mintSpawnSessionKey({
targetAgentId: ownerAgentId,
backend: "acp"
});
const resolvedCwd = resolveSpawnedWorkspaceInheritance({
config: cfg,
targetAgentId: ownerAgentId,
requesterSessionKey: ctx.agentSessionKey,
explicitWorkspaceDir: senderRestricted ? requesterRoot : params.cwd
});
let runtimeCwd;
try {
runtimeCwd = await resolveRuntimeCwdForAcpSpawn({
resolvedCwd,
explicitCwd: senderRestricted ? requesterRoot : params.cwd
});
} catch (error) {
return buildAcpSpawnError("cwd_resolution_failed", formatErrorMessage(error));
}
let preparedBinding = null;
if (requestThreadBinding) {
const prepared = await prepareSpawnThreadBinding({
cfg,
kind: "acp",
mode: spawnMode,
bindingService: {
...getSessionBindingService(),
listBySession: listSessionBindingsBySessionAsync
},
channel: requesterState.origin?.channel,
accountId: requesterState.origin?.accountId,
to: requesterState.origin?.to,
threadId: requesterState.origin?.threadId,
groupId: ctx.agentGroupId
});
ctx.assertActive?.();
if (!prepared.ok) return buildAcpSpawnError("thread_binding_invalid", prepared.error);
preparedBinding = prepared.binding;
}
let childCreationEntry;
let closeRuntimeOnFailure;
const childIdem = crypto.randomUUID();
const parentDeliveryCtx = effectiveStreamToParent && parentSessionKey ? await readAcpSpawnParentDeliveryContext({
parentSessionKey,
requesterAgentId,
assertActive: ctx.assertActive
}) : void 0;
ctx.assertActive?.();
const parentRelayStateEnv = { ...process.env };
const parentEventRouting = parentSessionKey ? resolveEventSessionRoutingPolicy({
cfg,
sessionKey: parentSessionKey
}) : void 0;
const parentEventTarget = effectiveStreamToParent && parentSessionKey && parentEventRouting ? await captureSessionEventTargetForHost(requesterAgentId, resolveEventSessionKeyForPolicy(parentSessionKey, parentEventRouting)) : void 0;
ctx.assertActive?.();
const gatewayAttachments = toGatewayImageAttachments(params.attachments);
const requesterOrigin = requesterState.origin;
const progressOrigin = {
channel: requesterOrigin?.channel,
accountId: requesterOrigin?.accountId,
to: ctx.currentMessagingTarget ?? ctx.currentChannelId ?? requesterOrigin?.to,
threadId: requesterOrigin?.threadId,
channelId: ctx.currentChannelId,
messageId: ctx.currentMessageId
};
const adapter = {
async initialize() {
const parentTarget = await resolveGatewaySessionStoreTargetInWorker({
cfg,
key: requesterInternalKey,
agentId: requesterAgentId,
assertActive: ctx.assertActive
});
const parentStorePath = parentTarget.readSource?.path ?? parentTarget.storePath;
await waitForSessionParticipantRecording({
agentId: requesterAgentId,
sessionKey: parentTarget.canonicalKey,
storePath: parentStorePath
});
ctx.assertActive?.();
const readParentEntry = () => readSessionEntryReadOnlyInWorker({
agentId: requesterAgentId,
sessionKey: parentTarget.canonicalKey,
storePath: parentStorePath
}, () => ctx.assertActive?.());
const parentEntry = isIncognitoSessionKey(requesterInternalKey) ? void 0 : await readParentEntry();
const parentLineage = captureSpawnParentLineage({
parentEntry,
expectedParentSessionId: ctx.expectedParentSessionId,
senderIsOwner: ctx.senderIsOwner,
readParentEntry
});
const creationStamp = buildSessionCreationStamp({
via: "spawn",
actor: {
type: "agent",
id: requesterAgentId
},
inheritedGitContributorProfileIds: inheritSessionGitContributorProfileIds(parentEntry)
});
const storePath = resolveSessionStorePathCore(cfg.session?.store, { agentId: ownerAgentId });
const childSessionPatch = admission.childSessionPatch ? {
spawnDepth: admission.childSessionPatch.spawnDepth,
...admission.childSessionPatch.subagentRole ? { subagentRole: admission.childSessionPatch.subagentRole } : {},
subagentControlScope: admission.childSessionPatch.subagentControlScope
} : {};
await parentLineage.assertParentUnchanged();
ctx.assertActive?.();
childCreationEntry = await upsertSessionEntryCore({
storePath,
sessionKey,
agentId: ownerAgentId
}, {
...creationStamp,
spawnedBy: requesterInternalKey,
completionOwnerSessionKey: ownership.completionRequesterSessionKey,
parentSessionKey: requesterInternalKey,
...childSessionPatch,
inheritedToolPolicyVersion: 1,
...ctx.inheritedToolPolicySource ? { inheritedToolPolicySource: ctx.inheritedToolPolicySource } : {},
...inheritedToolAllowPatch(ctx.inheritedToolAllowlist),
...inheritedToolDenyPatch(ctx.inheritedToolDenylist),
...senderRestricted ? {
spawnedWorkspaceDir: ctx.workspaceDir ?? requesterRoot,
spawnedCwd: runtimeCwd,
sessionRoot: requesterRoot,
permissionMode: ctx.sessionPermissionPolicy?.mode
} : {},
...params.label ? { label: params.label } : {},
...parentLineage.receipt
}, sessionEntryCommitGuardOptions(ctx.assertActive)) ?? void 0;
const initializedSession = await withAcpResumeSessionAuthorization(resumeOwnership, (revalidateResume) => initializeAcpSpawnRuntime({
assertActive: ctx.assertActive,
revalidateResume,
cfg,
sessionKey,
ownerAgentId,
runtimeAgentId: targetAgentId,
runtimeMode: spawnMode === "session" ? "persistent" : "oneshot",
backendId,
resumeSessionId: params.resumeSessionId,
runtimeOptions: runtimeOptionsResult.runtimeOptions,
modelExplicit: runtimeOptionsResult.modelExplicit,
thinkingExplicit: runtimeOptionsResult.thinkingExplicit,
cwd: runtimeCwd
}));
closeRuntimeOnFailure = initializedSession.initialized.closeRuntimeOnFailure;
ctx.assertActive?.();
return {
initializedSession,
binding: preparedBinding ? await bindPreparedAcpThread({
assertActive: ctx.assertActive,
cfg,
sessionKey,
targetAgentId: ownerAgentId,
label: params.label,
preparedBinding,
initializedRuntime: initializedSession
}) : null
};
},
async dispatchTurn(state) {
state.deliveryPlan = resolveAcpSpawnBootstrapDeliveryPlan({
cfg,
spawnMode,
effectiveStreamToParent,
requester: requesterState,
binding: state.binding
});
if (childCreationEntry) await recordSessionCreated(cfg, {
sessionKey,
agentId: ownerAgentId,
entry: childCreationEntry
});
await recordSubagentSpawned({
childSessionKey: sessionKey,
childRunId: childIdem,
requesterSessionKey: requesterInternalKey,
agentId: ownerAgentId
});
const startParentRelay = (runId) => effectiveStreamToParent && parentSessionKey && parentEventRouting && parentEventTarget ? startAcpSpawnParentStreamRelay({
runId,
parentSessionKey,
requesterAgentId,
expectedTarget: parentEventTarget,
childSessionKey: sessionKey,
childSessionId: state.initializedSession.sessionId,
agentId: targetAgentId,
ownerAgentId,
env: parentRelayStateEnv,
eventRouting: parentEventRouting,
deliveryContext: parentDeliveryCtx,
cfg
}) : void 0;
state.parentRelay = startParentRelay(childIdem);
const response = await launchAcpChildThroughGateway({
assertDispatchCurrent: ctx.assertActive,
task: params.task,
sessionKey,
deliveryPlan: state.deliveryPlan,
childIdem,
runTimeoutSeconds,
label: params.label,
attachments: gatewayAttachments,
lineage: {
enabled: isExecutionIdentityCollectionEnabled(cfg),
backend: "acp",
parentAgentId: requesterAgentId,
requesterRef: requesterInternalKey,
controllerRef: ownership.controllerSessionKey,
depth: admission.childSessionPatch?.spawnDepth ?? 1,
maxDepth: admission.maxSpawnDepth,
targetAgentId: ownerAgentId,
sandbox: params.sandbox === "require" ? "require" : "inherit",
inheritedToolAllowlist: ctx.inheritedToolAllowlist,
inheritedToolDenylist: ctx.inheritedToolDenylist
},
parentExecutionIdentityToken: readParentExecutionIdentity(ctx),
participantStorePath: resolveSessionStorePathCore(cfg.session?.store, { agentId: ownerAgentId })
});
const runId = readGatewayRunId(response) ?? childIdem;
if (state.parentRelay && runId !== childIdem) {
state.parentRelay.dispose();
state.parentRelay = startParentRelay(runId);
}
state.parentRelay?.notifyStarted();
return { runId };
},
async cleanupOnFailure({ state }) {
await state?.parentRelay?.dispose();
await cleanupFailedAcpSpawn({
cfg,
sessionKey,
agentId: ownerAgentId,
sessionEntry: childCreationEntry,
deleteTranscript: true,
closeRuntimeOnFailure
});
}
};
const { controllerSessionKey } = ownership;
ctx.assertActive?.();
const admissionReservation = hasSubagentEnvelope ? reserveChildAdmissionSlot({
controllerSessionKey,
childSessionKey: sessionKey,
resolveAdmission
}) : void 0;
if (admissionReservation && !admissionReservation.ok) return buildAcpSpawnError("subagent_policy", admissionReservation.error, "forbidden");
ctx.onSpawnEffectsStart?.();
let expectsCompletionMessage = false;
const pipelineResult = await runSpawnPipeline({
adapter,
assertActive: ctx.assertActive,
admissionReservation,
hookRunner: getGlobalHookRunner(),
progressOrigin,
progressSessionKey: ownership.completionRequesterSessionKey,
buildRegistration: (state, runId) => {
expectsCompletionMessage = !(state.deliveryPlan?.useInlineDelivery === true) && params.expectsCompletionMessage !== false;
return {
runId,
requesterTurnRunId: ctx.requesterTurnRunId,
childSessionKey: sessionKey,
controllerSessionKey,
sessionEntry: state.initializedSession.sessionEntry,
requesterSessionKey: ownership.completionRequesterSessionKey,
completionRequesterSessionId,
completionRequesterLifecycleRevision,
requesterOrigin,
progressOrigin,
requesterDisplayKey: ownership.completionRequesterDisplayKey,
task: params.task,
taskName: params.taskName,
agentId: ownerAgentId,
requesterAgentId,
cleanup: spawnMode === "session" ? "keep" : params.cleanup === "delete" ? "delete" : "keep",
label: params.label,
runTimeoutSeconds,
expectsCompletionMessage,
spawnMode
};
}
});
if (!pipelineResult.ok) return buildAcpSpawnFailureResult(pipelineResult, sessionKey);
return {
status: "accepted",
childSessionKey: sessionKey,
runId: pipelineResult.runId,
mode: spawnMode,
runTimeoutSeconds,
expectsCompletionMessage,
...pipelineResult.state.deliveryPlan?.useInlineDelivery ? { inlineDelivery: true } : {},
note: spawnMode === "session" ? ACP_SPAWN_SESSION_ACCEPTED_NOTE : ACP_SPAWN_ACCEPTED_NOTE
};
}
//#endregion
export { spawnAcpDirect };