mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 08:31:49 -06:00
8668aeb969
* fix(discord): bind transcript capture to source account * style(agents): keep transcript tool wiring compact * fix(transcripts): declare account binding channels * fix(transcripts): report effective capture account * fix(transcripts): enforce account lifecycle ownership * fix(transcripts): preserve cross-surface control * fix(copilot): preserve transcript channel context * fix(transcripts): fail closed for legacy channel owners * fix(transcripts): add trusted legacy recovery * fix(transcripts): preserve auto-start cleanup ownership * fix(transcripts): reject untrusted account starts * fix(transcripts): keep persisted ownership authoritative * fix(transcripts): harden legacy recovery * fix(transcripts): preserve agent ownership boundary * fix(transcripts): scope account binding to source channel * fix(transcripts): preserve unattributed owner isolation * fix(transcripts): own configured captures by account * docs(plugins): clarify transcript auto-start ownership * test(transcripts): cover account-less recovery * docs(transcripts): scope legacy recovery by provider * fix(discord): reuse eligible account ordering for transcripts * test(discord): use neutral transcript account fixtures * fix(transcripts): keep accountless recovery local * fix(discord): resolve transcript accounts by voice capability * fix(transcripts): bound account resolution failures * fix(transcripts): bound account tool output * fix(transcripts): honor unresolved provider accounts * fix(transcripts): preserve binding when providers are missing * fix(transcripts): fail closed on unknown binding provenance * fix(transcripts): qualify account lifecycle capability * fix(transcripts): normalize provable legacy owners * fix(transcripts): bind scheduled capture to caller authority * fix(transcripts): preserve scheduled caller identity tuple * fix(transcripts): preserve channel-less scheduled authority * fix(plugin-sdk): publish transcript provider types * fix(transcripts): use exact lifecycle ownership tokens * fix(transcripts): preserve local ownerless lifecycle access * fix(transcripts): allow local configured capture control * fix(transcripts): preserve scheduled caller channel * fix(transcripts): retain named-agent legacy recovery * fix(transcripts): deny unrelated remote channels * fix(doctor): validate transcript owner inference * fix(transcripts): restrict legacy remote recovery * fix(ci): align transcript Doctor checks * fix(transcripts): require Doctor-owned legacy metadata * fix(transcripts): reject unowned remote capture starts * fix(transcripts): reject unbound Discord lifecycle calls * fix(transcripts): distinguish legacy owner rows * test(discord): keep unavailable account fixture typed * fix(transcripts): mark current imports for Doctor * fix(transcripts): complete account ownership validation * fix(discord): restore transcript package boundary * fix(discord): preserve bundled transcript entry boundary * docs(transcripts): clarify Discord auto-start account * fix(transcripts): bind account-owned imports * fix: preserve transcript and cron policy state * fix(cron): preserve scheduled transcript authority * fix(discord): keep legacy transcript rows local * fix(transcripts): narrow account ownership boundary * fix(transcripts): preserve trusted caller ownership * fix(discord): enforce transcript source authorization * fix(ci): bound Control UI gzip build variance * test(qa): align transcript scenario contracts * fix(agents): repair rebased caller context * fix(discord): restore rebased account ownership * test(discord): restore voice account fixtures --------- Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
695 lines
26 KiB
TypeScript
695 lines
26 KiB
TypeScript
/** Main agent command orchestration for sessions, model selection, delivery, and attempts. */
|
|
import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion";
|
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
|
import type { VerboseLevel } from "../auto-reply/thinking.js";
|
|
import type { CliDeps } from "../cli/deps.types.js";
|
|
import { resolveSessionWorkStartError } from "../config/sessions/lifecycle.js";
|
|
import { buildRestartRecoveryClaimCleanupPatch } from "../config/sessions/restart-recovery-state.js";
|
|
import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js";
|
|
import type { SessionEntry } from "../config/sessions/types.js";
|
|
import {
|
|
assertAgentRunLifecycleGenerationCurrent,
|
|
captureAgentRunLifecycleGeneration,
|
|
withAgentRunLifecycleGeneration,
|
|
} from "../infra/agent-events.js";
|
|
import { clearAgentRunContext } from "../infra/agent-run-registry.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { isSubagentSessionKey } from "../routing/session-key.js";
|
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
|
import { isAgentMediatedCompletionSourceTool } from "../sessions/input-provenance.js";
|
|
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
|
import { ensureSessionDiffBaseline } from "../sessions/session-diff-baseline.js";
|
|
import { beginSessionWorkAdmission } from "../sessions/session-lifecycle-admission.js";
|
|
import { classifySessionStateActor } from "../sessions/session-state-events.js";
|
|
import { sessionDeliveryChannel, type DeliveryContext } from "../utils/delivery-context.shared.js";
|
|
import {
|
|
executionIdentity,
|
|
prepareAgentCommandExecutionIdentity,
|
|
sanitizePublicAgentCommandIngressOpts,
|
|
type AgentCommandAdmissionIngress,
|
|
} from "./agent-command-execution-identity.js";
|
|
import { runLocalAgentCommand } from "./agent-command-local.js";
|
|
import { runWithAgentCommandRecoveryOwner } from "./agent-command-recovery-owner.js";
|
|
import {
|
|
buildCurrentRunRestartRecoveryClaim,
|
|
shouldPersistRestartRecoveryCleanup,
|
|
shouldPersistRestartRecoveryContextClaim,
|
|
} from "./agent-command-restart-recovery.js";
|
|
import { runAcpAgentCommand } from "./command/acp-execution.js";
|
|
import { repairPendingAssistantTranscriptTurns } from "./command/assistant-transcript-repair.js";
|
|
import { persistAgentSession } from "./command/attempt-execution.shared.js";
|
|
import { emitIngressModelUsageDiagnostic } from "./command/ingress-diagnostics.js";
|
|
import { resolveEmbeddedModelSelection } from "./command/model-selection.js";
|
|
import { finalizeEmbeddedAgentCommand } from "./command/post-run.js";
|
|
import { prepareAgentCommandExecution } from "./command/prepare.js";
|
|
import { runEmbeddedAgentAttempt } from "./command/run-embedded-attempt.js";
|
|
import { loadSessionStoreRuntime, resolveAgentCommandDeps } from "./command/runtime-loaders.js";
|
|
import { prepareCurrentRunDelivery } from "./command/session-helpers.js";
|
|
import { prepareEmbeddedSessionState } from "./command/session-preparation.js";
|
|
import { clearRotatedSessionMetadata } from "./command/session.js";
|
|
import type {
|
|
AgentCommandGatewayIngressOpts,
|
|
AgentCommandIngressOpts,
|
|
AgentCommandOpts,
|
|
} from "./command/types.js";
|
|
import {
|
|
removeInternalSessionEffectsSession,
|
|
resolveInternalSessionEffectsTarget,
|
|
} from "./internal-session-effects.js";
|
|
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
|
|
import type { MainSessionRecoveryPendingTarget } from "./main-session-recovery/main-session-recovery-store.js";
|
|
import type { AgentRunSessionTarget } from "./run-session-target.js";
|
|
import { createAgentRunRestartAbortError } from "./run-termination.js";
|
|
import { withAgentPluginRegistry } from "./runtime-plugins.js";
|
|
import { measureAgentStartup } from "./startup-timing.js";
|
|
|
|
const log = createSubsystemLogger("agents/agent-command");
|
|
|
|
async function agentCommandInternal(
|
|
prepared: Awaited<ReturnType<typeof prepareAgentCommandExecution>>,
|
|
initialOpts: AgentCommandOpts,
|
|
admissionIngress: AgentCommandAdmissionIngress,
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
) {
|
|
const resolvedDeps = await resolveAgentCommandDeps(deps);
|
|
const isRawModelRun = initialOpts.modelRun === true || initialOpts.promptMode === "none";
|
|
const suppressVisibleSessionEffects = initialOpts.sessionEffects === "internal";
|
|
const preserveUserFacingSessionModelState =
|
|
initialOpts.preserveUserFacingSessionModelState === true;
|
|
const lifecycleAbortController = new AbortController();
|
|
const storedDeliveryMediaUrls =
|
|
prepared.sessionEntry?.restartRecoveryDeliveryRunId === prepared.runId &&
|
|
Array.isArray(prepared.sessionEntry.restartRecoveryDeliveryMediaUrls)
|
|
? prepared.sessionEntry.restartRecoveryDeliveryMediaUrls
|
|
: undefined;
|
|
const preparedOpts =
|
|
storedDeliveryMediaUrls !== undefined
|
|
? {
|
|
...prepared.opts,
|
|
internalDeliveryMediaUrls: [...storedDeliveryMediaUrls],
|
|
internalDeliverySuppressText: prepared.sessionEntry?.restartRecoverySuppressTextDelivery,
|
|
sourceReplyDeliveryMode: prepared.sessionEntry?.restartRecoverySourceReplyDeliveryMode,
|
|
disableMessageTool: prepared.sessionEntry?.restartRecoveryDisableMessageTool,
|
|
forceRestartSafeTools: prepared.sessionEntry?.restartRecoveryForceSafeTools,
|
|
}
|
|
: prepared.opts;
|
|
if (
|
|
(preparedOpts.internalDeliverySuppressText === true &&
|
|
preparedOpts.internalDeliveryMediaUrls === undefined) ||
|
|
((preparedOpts.internalDeliveryMediaUrls !== undefined ||
|
|
preparedOpts.internalDeliverySuppressText === true) &&
|
|
(preparedOpts.forceRestartSafeTools !== true ||
|
|
preparedOpts.disableMessageTool !== true ||
|
|
preparedOpts.sourceReplyDeliveryMode !== "automatic"))
|
|
) {
|
|
throw new Error(
|
|
"internal delivery media constraints require automatic delivery with restart-safe tools and no message tool",
|
|
);
|
|
}
|
|
let opts: AgentCommandOpts = {
|
|
...preparedOpts,
|
|
abortSignal: preparedOpts.abortSignal
|
|
? AbortSignal.any([preparedOpts.abortSignal, lifecycleAbortController.signal])
|
|
: lifecycleAbortController.signal,
|
|
};
|
|
const {
|
|
body,
|
|
transcriptBody,
|
|
cfg,
|
|
configuredThinkingCatalog,
|
|
agentCfg,
|
|
thinkOverride,
|
|
thinkOnce,
|
|
verboseOverride,
|
|
sessionId,
|
|
sessionKey,
|
|
sessionStore,
|
|
storePath,
|
|
isNewSession,
|
|
persistedThinking,
|
|
persistedVerbose,
|
|
sessionAgentId,
|
|
outboundSession,
|
|
workspaceDir,
|
|
cwd,
|
|
runId,
|
|
isSubagentLane,
|
|
acpManager,
|
|
acpResolution,
|
|
pluginsEnabled,
|
|
manifestMetadataSnapshot,
|
|
modelManifestContext,
|
|
} = prepared;
|
|
let lifecycleGeneration = opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(runId);
|
|
let sessionEntry = prepared.sessionEntry,
|
|
runOwnedSessionId = sessionId;
|
|
const sessionStateActor = classifySessionStateActor({
|
|
inputProvenance: opts.inputProvenance,
|
|
internalEvents: opts.internalEvents,
|
|
sessionEffects: opts.sessionEffects,
|
|
});
|
|
// Subagent-lane turns are the parent's own task dispatch into the child (they
|
|
// carry no inter_session provenance today); classifying them as human would tell
|
|
// the parent a human interjected on every spawn, for embedded and ACP children alike.
|
|
const isSubagentLaneTurn = normalizeOptionalString(opts.lane) === AGENT_LANE_SUBAGENT;
|
|
let sessionReboundDuringRun = false;
|
|
let trackedRestartRecoveryDeliveryClaim = false;
|
|
let currentRunDeliveryContext: DeliveryContext | undefined;
|
|
let restartRecoveryTerminalDeliveryEvidence:
|
|
| RestartRecoveryTerminalDeliveryEvidenceResult
|
|
| undefined;
|
|
const preparedSessionId = sessionEntry?.sessionId;
|
|
const internalModelRunTargets =
|
|
initialOpts.modelRun === true && suppressVisibleSessionEffects
|
|
? new Map<string, AgentRunSessionTarget>()
|
|
: undefined;
|
|
const trackInternalModelRunTarget = (target: AgentRunSessionTarget | undefined) => {
|
|
if (!internalModelRunTargets || !target?.sessionKey || !target.storePath) {
|
|
return;
|
|
}
|
|
internalModelRunTargets.set(`${target.storePath}\n${target.sessionKey}`, target);
|
|
};
|
|
if (internalModelRunTargets && storePath) {
|
|
trackInternalModelRunTarget(
|
|
resolveInternalSessionEffectsTarget({ agentId: sessionAgentId, runId, storePath }),
|
|
);
|
|
}
|
|
|
|
let sessionWorkAdmission: Awaited<ReturnType<typeof beginSessionWorkAdmission>> | undefined;
|
|
let preparedRunAdmission: ReturnType<typeof executionIdentity.prepare> | undefined;
|
|
try {
|
|
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
|
|
const sessionStoreRuntime =
|
|
storePath && sessionKey ? await loadSessionStoreRuntime() : undefined;
|
|
// Reset marks its mutation before interrupting work. An aborted run must not
|
|
// queue behind that mutation or reset would wait on the run holding the queue.
|
|
sessionWorkAdmission = await beginSessionWorkAdmission({
|
|
scope: storePath ?? `agent:${sessionAgentId}`,
|
|
identities: [sessionKey, sessionId],
|
|
signal: opts.abortSignal,
|
|
onInterrupt: () => lifecycleAbortController.abort(createAgentRunRestartAbortError()),
|
|
assertAllowed: () => {
|
|
const currentEntry =
|
|
sessionStoreRuntime && storePath && sessionKey
|
|
? sessionStoreRuntime.loadSessionEntry({
|
|
storePath,
|
|
sessionKey,
|
|
readConsistency: "latest",
|
|
})
|
|
: sessionEntry;
|
|
if (!currentEntry && preparedSessionId) {
|
|
throw new Error(
|
|
`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`,
|
|
);
|
|
}
|
|
const matchesIntentionalRollover =
|
|
isNewSession && currentEntry?.sessionId === preparedSessionId;
|
|
if (currentEntry && currentEntry.sessionId !== sessionId && !matchesIntentionalRollover) {
|
|
throw new Error(
|
|
`Session "${sessionKey ?? sessionId}" changed while starting work. Retry.`,
|
|
);
|
|
}
|
|
const archivedSessionError = resolveSessionWorkStartError(
|
|
sessionKey ?? sessionId,
|
|
currentEntry,
|
|
);
|
|
if (archivedSessionError) {
|
|
throw new Error(archivedSessionError);
|
|
}
|
|
sessionEntry = currentEntry;
|
|
if (sessionStore && sessionKey) {
|
|
if (currentEntry) {
|
|
sessionStore[sessionKey] = currentEntry;
|
|
} else {
|
|
delete sessionStore[sessionKey];
|
|
}
|
|
}
|
|
},
|
|
});
|
|
return await sessionWorkAdmission.run(async () => {
|
|
preparedRunAdmission = prepareAgentCommandExecutionIdentity({
|
|
opts,
|
|
prepared,
|
|
ingress: admissionIngress,
|
|
lifecycleGeneration,
|
|
});
|
|
if (sessionStore && sessionKey && !suppressVisibleSessionEffects) {
|
|
try {
|
|
await repairPendingAssistantTranscriptTurns({
|
|
context: {
|
|
sessionKey,
|
|
sessionEntry,
|
|
sessionStore,
|
|
storePath,
|
|
sessionAgentId,
|
|
config: cfg,
|
|
},
|
|
});
|
|
sessionEntry = sessionStore[sessionKey] ?? sessionEntry;
|
|
} catch (error) {
|
|
if (!isNewSession) {
|
|
throw error;
|
|
}
|
|
// A reset starts a fresh transcript. Do not let predecessor repair
|
|
// state leak into it when the old transcript remains unavailable.
|
|
log.warn(
|
|
`Could not repair predecessor transcript before session reset for ${sessionKey}: ${formatErrorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
if (opts.deliver === true) {
|
|
const sendPolicy = resolveSendPolicy({
|
|
cfg,
|
|
entry: sessionEntry,
|
|
sessionKey,
|
|
channel: sessionDeliveryChannel(sessionEntry),
|
|
chatType: sessionEntry?.chatType,
|
|
});
|
|
if (sendPolicy === "deny") {
|
|
throw new Error("send blocked by session policy");
|
|
}
|
|
}
|
|
|
|
if (!isRawModelRun && acpResolution?.kind === "stale") {
|
|
throw acpResolution.error;
|
|
}
|
|
|
|
let currentRunDeliveryPrepared = false;
|
|
const prepareDeliveryForRun = async (candidateSessionEntry?: SessionEntry) => {
|
|
if (currentRunDeliveryPrepared || opts.deliver !== true) {
|
|
return;
|
|
}
|
|
currentRunDeliveryPrepared = true;
|
|
let preparedDelivery: Awaited<ReturnType<typeof prepareCurrentRunDelivery>>;
|
|
try {
|
|
preparedDelivery = await prepareCurrentRunDelivery({
|
|
cfg,
|
|
opts,
|
|
agentId: sessionAgentId,
|
|
currentSessionKey: sessionKey,
|
|
sessionEntry: candidateSessionEntry,
|
|
});
|
|
} catch (error) {
|
|
if (opts.bestEffortDeliver !== true) {
|
|
throw error;
|
|
}
|
|
log.warn(
|
|
`delivery preflight failed; continuing model run with requested delivery intent because bestEffortDeliver is enabled: ${coerceErrorMessage(error)}`,
|
|
);
|
|
}
|
|
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
|
|
if (preparedDelivery) {
|
|
currentRunDeliveryContext = preparedDelivery.context;
|
|
opts = {
|
|
...opts,
|
|
replyChannel: preparedDelivery.context.channel,
|
|
replyTo: preparedDelivery.context.to,
|
|
replyAccountId: preparedDelivery.context.accountId,
|
|
threadId: preparedDelivery.context.threadId,
|
|
deliveryTargetMode: preparedDelivery.targetMode,
|
|
};
|
|
}
|
|
};
|
|
|
|
if (
|
|
sessionStore &&
|
|
sessionKey &&
|
|
!suppressVisibleSessionEffects &&
|
|
!isSubagentSessionKey(sessionKey)
|
|
) {
|
|
const now = Date.now();
|
|
const currentStoreEntry = sessionStore[sessionKey];
|
|
const allowCreateRestartRecoveryEntry =
|
|
currentStoreEntry === undefined && sessionEntry === undefined;
|
|
const initialEntry = currentStoreEntry ??
|
|
sessionEntry ?? { sessionId, updatedAt: now, sessionStartedAt: now };
|
|
const isSessionRollover = isNewSession && initialEntry.sessionId !== sessionId;
|
|
const entry = isSessionRollover ? clearRotatedSessionMetadata(initialEntry) : initialEntry;
|
|
await prepareDeliveryForRun(entry);
|
|
const generatedMediaSourceRunId =
|
|
opts.internalDeliveryMediaUrls !== undefined &&
|
|
opts.inputProvenance?.kind === "inter_session" &&
|
|
isAgentMediatedCompletionSourceTool(opts.inputProvenance.sourceTool)
|
|
? runId
|
|
: undefined;
|
|
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
|
|
const next: SessionEntry = {
|
|
...entry,
|
|
sessionId,
|
|
updatedAt: now,
|
|
sessionStartedAt: isSessionRollover ? now : entry.sessionStartedAt,
|
|
lastInteractionAt: isSessionRollover ? now : entry.lastInteractionAt,
|
|
...buildCurrentRunRestartRecoveryClaim({
|
|
deliveryContext: currentRunDeliveryContext,
|
|
deliveryMediaUrls: opts.internalDeliveryMediaUrls,
|
|
disableMessageTool: opts.disableMessageTool,
|
|
entry,
|
|
forceRestartSafeTools: opts.forceRestartSafeTools,
|
|
runId,
|
|
sourceIngress: generatedMediaSourceRunId ? "internal" : undefined,
|
|
sourceRunId: generatedMediaSourceRunId,
|
|
sourceReplyDeliveryMode: opts.sourceReplyDeliveryMode,
|
|
suppressTextDelivery: opts.internalDeliverySuppressText,
|
|
}),
|
|
};
|
|
const persisted = await persistAgentSession({
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
initialEntry,
|
|
entry: next,
|
|
shouldPersist: (current) =>
|
|
isSessionRollover
|
|
? current?.sessionId === initialEntry.sessionId
|
|
: shouldPersistRestartRecoveryContextClaim(
|
|
current,
|
|
sessionId,
|
|
runId,
|
|
allowCreateRestartRecoveryEntry,
|
|
),
|
|
});
|
|
sessionEntry = persisted;
|
|
trackedRestartRecoveryDeliveryClaim = persisted?.restartRecoveryDeliveryRunId === runId;
|
|
}
|
|
if (sessionEntry && sessionKey && !suppressVisibleSessionEffects) {
|
|
try {
|
|
sessionEntry = await ensureSessionDiffBaseline({
|
|
cwd: cwd ?? workspaceDir,
|
|
entry: sessionEntry,
|
|
isNewSession,
|
|
sessionKey,
|
|
storePath,
|
|
});
|
|
if (sessionStore) {
|
|
sessionStore[sessionKey] = sessionEntry;
|
|
}
|
|
} catch (error) {
|
|
log.warn(
|
|
`session diff baseline capture failed; continuing without attribution filtering: ${coerceErrorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
await prepareDeliveryForRun(sessionEntry);
|
|
|
|
if (!isRawModelRun && acpResolution?.kind === "ready" && sessionKey) {
|
|
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
|
|
return await runAcpAgentCommand({
|
|
cfg,
|
|
deps: resolvedDeps,
|
|
runtime,
|
|
opts,
|
|
outboundSession,
|
|
sessionEntry,
|
|
sessionStore,
|
|
body,
|
|
transcriptBody,
|
|
suppressVisibleSessionEffects,
|
|
provenance: isSubagentLaneTurn ? "agent" : sessionStateActor.actorType,
|
|
sessionAgentId,
|
|
sessionId,
|
|
sessionKey,
|
|
storePath,
|
|
workspaceDir,
|
|
runId,
|
|
lifecycleGeneration,
|
|
acpManager,
|
|
acpResolution,
|
|
trackInternalModelRunTarget,
|
|
preparedRunAdmission,
|
|
});
|
|
}
|
|
|
|
const embeddedSessionState = await measureAgentStartup(
|
|
"session-state",
|
|
() =>
|
|
prepareEmbeddedSessionState({
|
|
cfg,
|
|
opts,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
sessionId,
|
|
storePath,
|
|
sessionAgentId,
|
|
lifecycleGeneration,
|
|
runId,
|
|
workspaceDir,
|
|
isNewSession,
|
|
isSubagentLaneTurn,
|
|
suppressVisibleSessionEffects,
|
|
thinkOnce,
|
|
thinkOverride,
|
|
persistedThinking,
|
|
verboseOverride,
|
|
persistedVerbose,
|
|
verboseDefault: agentCfg?.verboseDefault as VerboseLevel | undefined,
|
|
sessionStateActor,
|
|
}),
|
|
{ config: cfg },
|
|
);
|
|
sessionEntry = embeddedSessionState.sessionEntry;
|
|
const { requestedThinkLevel, runContext } = embeddedSessionState;
|
|
|
|
const modelSelection = await measureAgentStartup(
|
|
"model-selection",
|
|
() =>
|
|
resolveEmbeddedModelSelection({
|
|
cfg,
|
|
opts,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
sessionId,
|
|
storePath,
|
|
sessionAgentId,
|
|
workspaceDir,
|
|
pluginsEnabled,
|
|
manifestMetadataSnapshot,
|
|
modelManifestContext,
|
|
configuredThinkingCatalog,
|
|
requestedThinkLevel,
|
|
thinkOverride,
|
|
thinkOnce,
|
|
isSubagentLane,
|
|
suppressVisibleSessionEffects,
|
|
runContext,
|
|
}),
|
|
{ config: cfg },
|
|
);
|
|
sessionEntry = modelSelection.sessionEntry;
|
|
const embeddedAttempt = await runEmbeddedAgentAttempt({
|
|
prepared,
|
|
opts,
|
|
sessionEntry,
|
|
lifecycleGeneration,
|
|
onLifecycleGenerationChanged: (nextLifecycleGeneration) => {
|
|
lifecycleGeneration = nextLifecycleGeneration;
|
|
},
|
|
suppressVisibleSessionEffects,
|
|
preserveUserFacingSessionModelState,
|
|
modelSelection,
|
|
embeddedSessionState,
|
|
trackInternalModelRunTarget,
|
|
preparedRunAdmission,
|
|
});
|
|
if (embeddedAttempt.fallbackExhausted) {
|
|
opts.onModelFallbackExhausted?.();
|
|
}
|
|
sessionEntry = embeddedAttempt.sessionEntry;
|
|
lifecycleGeneration = embeddedAttempt.lifecycleGeneration;
|
|
const finalized = await finalizeEmbeddedAgentCommand({
|
|
prepared,
|
|
opts,
|
|
deps: resolvedDeps,
|
|
runtime,
|
|
sessionEntry,
|
|
attempt: embeddedAttempt,
|
|
embeddedSessionState,
|
|
suppressVisibleSessionEffects,
|
|
preserveUserFacingSessionModelState,
|
|
currentRunDeliveryContext,
|
|
sessionOwnership: { runOwnedSessionId, sessionReboundDuringRun },
|
|
trackInternalModelRunTarget,
|
|
onSessionOwnershipChanged: (ownership) => {
|
|
runOwnedSessionId = ownership.runOwnedSessionId;
|
|
sessionReboundDuringRun = ownership.sessionReboundDuringRun;
|
|
},
|
|
onTerminalDeliveryEvidenceChanged: (evidence) => {
|
|
restartRecoveryTerminalDeliveryEvidence = evidence;
|
|
},
|
|
});
|
|
sessionEntry = finalized.sessionEntry;
|
|
runOwnedSessionId = finalized.runOwnedSessionId;
|
|
sessionReboundDuringRun = finalized.sessionReboundDuringRun;
|
|
return finalized.deliveryResult;
|
|
});
|
|
} finally {
|
|
preparedRunAdmission?.close();
|
|
sessionWorkAdmission?.release();
|
|
if (internalModelRunTargets) {
|
|
// Compaction may rotate a private session identity. Remove every owned
|
|
// SQLite row only after delivery; transcript and trajectory rows cascade.
|
|
for (const target of internalModelRunTargets.values()) {
|
|
try {
|
|
await removeInternalSessionEffectsSession(target);
|
|
} catch (error) {
|
|
// Cleanup remains best-effort so a terminal SQLite write failure does
|
|
// not replace the completed model-run result; the DB layer warns too.
|
|
log.warn(`failed to remove model-run SQLite session: ${coerceErrorMessage(error)}`);
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
!sessionReboundDuringRun &&
|
|
trackedRestartRecoveryDeliveryClaim &&
|
|
sessionStore &&
|
|
sessionKey
|
|
) {
|
|
try {
|
|
const entry = sessionStore[sessionKey] ?? sessionEntry;
|
|
if (entry?.restartRecoveryDeliveryRunId === runId) {
|
|
const next: SessionEntry = {
|
|
...entry,
|
|
...buildRestartRecoveryClaimCleanupPatch({
|
|
entry,
|
|
recordTerminalSource: true,
|
|
terminalRunId: runId,
|
|
terminalDeliveryEvidence: restartRecoveryTerminalDeliveryEvidence,
|
|
}),
|
|
updatedAt: Date.now(),
|
|
};
|
|
const persisted = await persistAgentSession({
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
initialEntry: entry,
|
|
entry: next,
|
|
shouldPersist: (current) =>
|
|
shouldPersistRestartRecoveryCleanup(current, runOwnedSessionId, runId),
|
|
});
|
|
sessionEntry = persisted;
|
|
}
|
|
} catch (error) {
|
|
log.warn(
|
|
`failed to clear restart recovery delivery context for ${sessionKey}: ${coerceErrorMessage(error)}`,
|
|
);
|
|
}
|
|
}
|
|
clearAgentRunContext(runId, lifecycleGeneration);
|
|
}
|
|
}
|
|
|
|
async function agentCommandWithAdmissionIngress(
|
|
opts: AgentCommandOpts,
|
|
admissionIngress: AgentCommandAdmissionIngress,
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
) {
|
|
return await runLocalAgentCommand({
|
|
opts,
|
|
runtime,
|
|
deps,
|
|
operatorAuthority: admissionIngress.kind === "local-cli",
|
|
run: async (prepared, resolvedDeps) =>
|
|
await agentCommandInternal(prepared, prepared.opts, admissionIngress, runtime, resolvedDeps),
|
|
});
|
|
}
|
|
|
|
export async function agentCommand(
|
|
opts: AgentCommandOpts,
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
) {
|
|
const { localIngress } = executionIdentity;
|
|
return await agentCommandWithAdmissionIngress(opts, localIngress, runtime, deps);
|
|
}
|
|
|
|
export async function agentCommandFromSystem(
|
|
opts: AgentCommandOpts,
|
|
admission: { boundary: string },
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
) {
|
|
const ingress = executionIdentity.systemIngress(admission.boundary);
|
|
return await agentCommandWithAdmissionIngress(opts, ingress, runtime, deps);
|
|
}
|
|
|
|
async function agentCommandFromIngressInternal(
|
|
opts: AgentCommandGatewayIngressOpts,
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
recovery?: {
|
|
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
|
|
},
|
|
) {
|
|
if (typeof opts.allowModelOverride !== "boolean") {
|
|
throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
|
|
}
|
|
const lifecycleGeneration =
|
|
opts.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(opts.runId ?? "");
|
|
return await withAgentRunLifecycleGeneration(lifecycleGeneration, async () => {
|
|
let preparedAgentDir: string | undefined;
|
|
const result = await runWithAgentCommandRecoveryOwner({
|
|
lifecycleGeneration,
|
|
mode: "claim",
|
|
opts: {
|
|
...opts,
|
|
lifecycleGeneration,
|
|
senderIsOwner: opts.senderIsOwner === true,
|
|
},
|
|
prepare: async (preparedOpts) => await prepareAgentCommandExecution(preparedOpts, runtime),
|
|
restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery,
|
|
run: async (prepared) => {
|
|
preparedAgentDir = prepared.agentDir;
|
|
return await withAgentPluginRegistry({
|
|
config: prepared.cfg,
|
|
workspaceDir: prepared.workspaceDir,
|
|
run: async () =>
|
|
await agentCommandInternal(
|
|
prepared,
|
|
prepared.opts,
|
|
{ kind: "api", boundary: "agent-command.from-ingress", state: "unknown" },
|
|
runtime,
|
|
deps,
|
|
),
|
|
});
|
|
},
|
|
});
|
|
|
|
if (result && preparedAgentDir) {
|
|
emitIngressModelUsageDiagnostic(result, opts, preparedAgentDir);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/** Runs an agent turn from an inbound channel/gateway ingress context. */
|
|
export async function agentCommandFromIngress(
|
|
opts: AgentCommandIngressOpts,
|
|
runtime: RuntimeEnv = defaultRuntime,
|
|
deps?: CliDeps,
|
|
) {
|
|
// Plugin SDK callers may be plain JavaScript. Enforce the private recovery
|
|
// boundary at runtime so extra or inherited properties cannot author audit identity.
|
|
return await agentCommandFromIngressInternal(
|
|
sanitizePublicAgentCommandIngressOpts(opts),
|
|
runtime,
|
|
deps,
|
|
);
|
|
}
|
|
|
|
/** Internal Gateway entrypoint that restores a rejected restart-recovery admission. */
|
|
export async function agentCommandFromGatewayIngress(
|
|
opts: AgentCommandGatewayIngressOpts,
|
|
runtime: RuntimeEnv,
|
|
deps: CliDeps | undefined,
|
|
recovery: {
|
|
restoreAdmittedRecovery?: () => Promise<MainSessionRecoveryPendingTarget | undefined>;
|
|
},
|
|
) {
|
|
return await agentCommandFromIngressInternal(opts, runtime, deps, recovery);
|
|
}
|