openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
237 lines (236 loc) • 9.09 kB
JavaScript
import { a as writeRuntimeJson } from "./runtime-CF2WjnNZ.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { _ as resolveConfiguredAgentId } from "./agent-scope-config-DcbEhP0R.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import "./config-Cs0XXL3x.js";
import { r as callGatewayFromCliWithTransport } from "./gateway-rpc-C9fDQ0vT.js";
import { c as rethrowExpectedCliError, r as formatCliJsonFailure } from "./failure-output-Dc5ce9_F.js";
import { t as createClackPrompter } from "./clack-prompter-BSpWH_ak.js";
//#region src/shared/session-archive-timeout.ts
/** Cloud workspace reconciliation may take several minutes before archive can commit. */
const SESSION_ARCHIVE_REQUEST_TIMEOUT_MS = 6e5;
//#endregion
//#region src/commands/sessions-lifecycle.ts
const SESSION_TARGET_PAGE_SIZE = 200;
function resolveLifecycleAgentId(rawAgent) {
const requested = rawAgent?.trim();
if (rawAgent !== void 0 && !requested) throw new Error("--agent must not be blank");
return requested ? resolveConfiguredAgentId(getRuntimeConfig(), requested) : void 0;
}
function listHint(agent) {
const agentFlag = agent ? ` --agent ${agent}` : "";
return formatCliCommand(`openclaw sessions list${agentFlag} --json`);
}
function notFoundResult(key, agent) {
return {
key,
ok: false,
status: "not_found",
error: `Session not found. Run ${listHint(agent)} to choose a valid key.`
};
}
const WORKTREE_PRESERVATION_REASON_COPY = {
"owner-mismatch": "registered to another owner",
busy: "still in use by a live run or another cleanup",
"foreign-lock": "Git reports a lock owned outside OpenClaw",
"snapshot-failed": "OpenClaw could not create a safety snapshot",
"cleanup-failed": "cleanup did not finish normally"
};
async function listRequestedSessions(keys, agent, rpcOptions) {
const wanted = new Set(keys);
const found = /* @__PURE__ */ new Map();
let offset = 0;
while (wanted.size > found.size) {
const page = await callGatewayFromCliWithTransport("sessions.list", rpcOptions, {
limit: SESSION_TARGET_PAGE_SIZE,
...offset > 0 ? { offset } : {},
archived: "all",
includeGlobal: true,
includeUnknown: true,
configuredAgentsOnly: true,
...agent ? { agentId: agent } : {}
}, { defaultTimeoutMs: 3e4 });
if (!page || !Array.isArray(page.sessions)) throw new Error("Gateway returned an invalid sessions.list response.");
for (const row of page.sessions) if (wanted.has(row.key) && !found.has(row.key)) found.set(row.key, row);
if (found.size === wanted.size || page.hasMore !== true) break;
const nextOffset = page.nextOffset;
if (typeof nextOffset !== "number" || nextOffset <= offset) throw new Error("Gateway returned invalid sessions.list pagination.");
offset = nextOffset;
}
return found;
}
function outputLifecycleResults(operation, dryRun, results, runtime, json) {
const ok = results.every((result) => result.ok);
if (json) writeRuntimeJson(runtime, ok ? {
ok,
operation,
dryRun,
results
} : {
...formatCliJsonFailure(`Session ${operation} did not complete for every requested key.`),
operation,
dryRun,
results
});
else for (const result of results) switch (result.status) {
case "archived":
runtime.log(`Archived session ${result.key}.`);
break;
case "already_archived":
runtime.log(`Session ${result.key} is already archived.`);
break;
case "deleted":
runtime.log(`Deleted session ${result.key}.`);
for (const archived of result.archived ?? []) runtime.log(`Archived transcript: ${archived}`);
if (result.worktreePreserved) {
const preserved = result.worktreePreserved;
runtime.error(`Worktree ${preserved.branch} at ${preserved.path} needs attention: ${WORKTREE_PRESERVATION_REASON_COPY[preserved.reason]}. Inspect it with ${formatCliCommand("openclaw worktrees list")}.`);
}
break;
case "would_archive":
runtime.log(`[dry-run] archive session ${result.key}`);
break;
case "would_delete":
runtime.log(`[dry-run] delete session ${result.key} and its live transcript state`);
break;
case "not_found":
case "failed": runtime.error(`${operation} ${result.key}: ${result.error ?? "Unknown failure."}`);
}
if (!ok) runtime.exit(1);
}
async function runSessionsLifecycleCommand(operation, opts, runtime) {
const keys = opts.keys.map((key) => key.trim());
const rpcOptions = {
url: opts.url,
token: opts.token,
password: opts.password,
timeout: opts.timeout,
json: opts.json
};
let sessions;
let agent;
try {
agent = resolveLifecycleAgentId(opts.agent);
sessions = await listRequestedSessions(keys.filter(Boolean), agent, rpcOptions);
} catch (error) {
rethrowExpectedCliError(error);
const message = formatErrorMessage(error);
outputLifecycleResults(operation, Boolean(opts.dryRun), keys.map((key) => ({
key,
ok: false,
status: "failed",
error: message
})), runtime, Boolean(opts.json));
return;
}
const results = keys.map((key) => key && sessions.has(key) ? void 0 : notFoundResult(key, agent));
const validTargets = keys.flatMap((key, index) => {
const session = sessions.get(key);
return session ? [{
index,
session
}] : [];
}).filter(({ index, session }) => {
if (!(!opts.dryRun && !(operation === "archive" && session.archived === true)) || session.sessionId) return true;
results[index] = {
key: session.key,
ok: false,
status: "failed",
error: "Session has no durable identity; lifecycle mutation was not attempted."
};
return false;
});
if (operation === "delete" && !opts.dryRun && !opts.yes && validTargets.length > 0) {
if (opts.json || !process.stdin.isTTY) {
const error = "Deletion requires confirmation. Pass --yes to delete non-interactively.";
for (const { index, session } of validTargets) results[index] = {
key: session.key,
ok: false,
status: "failed",
error
};
outputLifecycleResults(operation, false, results.filter((result) => result !== void 0), runtime, Boolean(opts.json));
return;
}
if (!await createClackPrompter().confirm({
message: `Delete ${validTargets.length} session${validTargets.length === 1 ? "" : "s"} and remove live transcript state?`,
initialValue: false
})) {
runtime.log("Cancelled.");
return;
}
}
for (const { index, session } of validTargets) {
if (operation === "archive" && session.archived === true) {
results[index] = {
key: session.key,
ok: true,
status: "already_archived"
};
continue;
}
try {
if (opts.dryRun) {
if (session.isMain === true && session.key !== "global") throw new Error(operation === "archive" ? "Cannot archive an agent's main session." : `Cannot delete the main session (${session.key}).`);
results[index] = {
key: session.key,
ok: true,
status: operation === "archive" ? "would_archive" : "would_delete"
};
continue;
}
if (operation === "archive") {
const response = await callGatewayFromCliWithTransport("sessions.patch", rpcOptions, {
key: session.key,
...agent ? { agentId: agent } : {},
...session.sessionId ? { expectedSessionId: session.sessionId } : {},
archived: true
}, { defaultTimeoutMs: SESSION_ARCHIVE_REQUEST_TIMEOUT_MS });
if (response?.ok !== true || response.entry?.archivedAt === void 0) throw new Error("Gateway did not confirm that the session was archived.");
results[index] = {
key: response.key ?? session.key,
ok: true,
status: "archived"
};
} else {
const response = await callGatewayFromCliWithTransport("sessions.delete", rpcOptions, {
key: session.key,
...agent ? { agentId: agent } : {},
...session.sessionId ? { expectedSessionId: session.sessionId } : {},
deleteTranscript: true,
...session.archived === true ? { archivedOnly: true } : {}
}, { defaultTimeoutMs: SESSION_ARCHIVE_REQUEST_TIMEOUT_MS });
if (!response.deleted) {
results[index] = notFoundResult(session.key, agent);
continue;
}
results[index] = {
key: response.key ?? session.key,
ok: true,
status: "deleted",
archived: response.archived ?? [],
...response.worktreePreserved ? { worktreePreserved: response.worktreePreserved } : {}
};
}
} catch (error) {
results[index] = {
key: session.key,
ok: false,
status: "failed",
error: formatErrorMessage(error)
};
}
}
outputLifecycleResults(operation, Boolean(opts.dryRun), results.filter((result) => result !== void 0), runtime, Boolean(opts.json));
}
/** Archive one or more stored sessions through the same Gateway patch used by Control UI. */
async function sessionsArchiveCommand(opts, runtime) {
await runSessionsLifecycleCommand("archive", opts, runtime);
}
/** Delete one or more stored sessions through the same Gateway lifecycle owner used by Control UI. */
async function sessionsDeleteCommand(opts, runtime) {
await runSessionsLifecycleCommand("delete", opts, runtime);
}
//#endregion
export { sessionsArchiveCommand, sessionsDeleteCommand };