mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
feat(sessions): expose organization tools
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
export const EXEC_TOOL_DISPLAY_SUMMARY = "Run shell now.";
|
||||
export const PROCESS_TOOL_DISPLAY_SUMMARY = "Inspect/control exec sessions.";
|
||||
export const CRON_TOOL_DISPLAY_SUMMARY = "Schedule reminders, automations, wake events.";
|
||||
export const SESSIONS_LIST_TOOL_DISPLAY_SUMMARY = "List visible sessions; filters/previews.";
|
||||
export const SESSIONS_LIST_TOOL_DISPLAY_SUMMARY =
|
||||
"List visible sessions; organization, status, filters/previews.";
|
||||
export const SESSIONS_HISTORY_TOOL_DISPLAY_SUMMARY = "Read sanitized session history.";
|
||||
export const SESSIONS_SEARCH_TOOL_DISPLAY_SUMMARY = "Search past session transcripts.";
|
||||
export const SESSIONS_SEND_TOOL_DISPLAY_SUMMARY = "Run same-Gateway session/agent.";
|
||||
@@ -64,7 +65,7 @@ export function describeSessionLinkRule(base: string): string {
|
||||
/** Describes the sessions_list tool for model-facing instructions. */
|
||||
export function describeSessionsListTool(options?: SessionLinkDescriptionOptions): string {
|
||||
return [
|
||||
"List visible sessions and sidebar categories; filter kind/label/agentId/search/activity/archive.",
|
||||
"List visible sessions with category, unread, icon, owner, project/worktree, and active attention/status; filter kind/label/agentId/search/activity/archive.",
|
||||
"Preview recent messages inline via includeLastMessage/messageLimit; includeDerivedTitles adds derived titles.",
|
||||
"Use before history/send target selection.",
|
||||
...(options?.sessionLinkBase ? [describeSessionLinkRule(options.sessionLinkBase)] : []),
|
||||
|
||||
@@ -376,8 +376,18 @@ export const TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
|
||||
actions: {
|
||||
patch: {
|
||||
label: "update",
|
||||
detailKeys: ["sessionKey", "label", "pinned", "archived", "model", "thinkingLevel"],
|
||||
detailKeys: [
|
||||
"sessionKey",
|
||||
"label",
|
||||
"category",
|
||||
"unread",
|
||||
"pinned",
|
||||
"archived",
|
||||
"model",
|
||||
"thinkingLevel",
|
||||
],
|
||||
},
|
||||
patch_many: { label: "update many", detailKeys: ["category", "unread", "targets"] },
|
||||
group_list: { label: "groups" },
|
||||
group_set: { label: "set groups", detailKeys: ["names"] },
|
||||
group_rename: { label: "rename group", detailKeys: ["name", "to"] },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { normalizeOptionalString, type FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
import type {
|
||||
SessionOwner,
|
||||
SessionRow,
|
||||
SessionRunStatus,
|
||||
} from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import type { SessionAgentStatus } from "../../../packages/gateway-protocol/src/session-agent-status.js";
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { parseRawSessionConversationRef } from "../../sessions/session-key-utils.js";
|
||||
@@ -67,6 +69,12 @@ export type GatewaySessionListRow = {
|
||||
spawnedBy?: string;
|
||||
label?: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
unread?: boolean;
|
||||
owner?: SessionOwner;
|
||||
projectId?: string;
|
||||
worktree?: { id: string; branch: string; repoRoot: string };
|
||||
agentStatus?: SessionAgentStatus;
|
||||
displayName?: string;
|
||||
derivedTitle?: string;
|
||||
lastMessagePreview?: string;
|
||||
@@ -117,6 +125,12 @@ export type SessionListRow = {
|
||||
channel: string;
|
||||
label?: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
unread?: boolean;
|
||||
owner?: SessionOwner["actor"];
|
||||
projectId?: string;
|
||||
hasWorktree?: boolean;
|
||||
agentStatus?: SessionAgentStatus;
|
||||
displayName?: string;
|
||||
derivedTitle?: string;
|
||||
lastMessagePreview?: string;
|
||||
|
||||
@@ -369,7 +369,20 @@ describe("sessions-list-tool", () => {
|
||||
classification: "subagent",
|
||||
channel: "discord",
|
||||
label: "worker",
|
||||
category: "P1 issues",
|
||||
category: "Projects",
|
||||
icon: "🦞",
|
||||
unread: true,
|
||||
owner: {
|
||||
actor: {
|
||||
type: "human",
|
||||
id: "profile-owner",
|
||||
label: "Owner",
|
||||
avatarUrl: "/avatars/private",
|
||||
},
|
||||
},
|
||||
projectId: "openclaw",
|
||||
worktree: { id: "worktree-private", branch: "work", repoRoot: "/private/repo" },
|
||||
agentStatus: { note: "Needs review", attention: "flag", expiresAt: Date.now() + 60_000 },
|
||||
displayName: "Worker",
|
||||
derivedTitle: "Investigate queue",
|
||||
lastMessagePreview: "Use `[[reply_to_current]]` literally.",
|
||||
@@ -406,7 +419,7 @@ describe("sessions-list-tool", () => {
|
||||
linkedDetails.sessionLinkRule,
|
||||
);
|
||||
expect(compactToolOutputHint(tool.outputSchema)).toBe(
|
||||
'{ count: number; sessions: Array<{ agentId: string; archived: boolean; channel: string; key: string; kind: "main" | "group" | "cron" | "hook" | "node" | "other"; pinned: boolean; abortedLastRun?: boolean; category?: string; childSessions?: Array<string>; contextTokens?: number; derivedTitle?: string; displayName?: string; label?: string; lastMessagePreview?: string; messages?: Array<unknown>; model?: string; parentSessionKey?: string; sessionId?: string; stateVersion?: number; status?: "running" | "done" | "failed" | "killed" | "timeout"; totalTokens?: number; updatedAt?: number }>; sessionLinkRule?: string; visibility?: { mode: "self" | "tree" | "agent"; restricted: true; warning: string } }',
|
||||
'{ count: number; sessions: Array<{ agentId: string; archived: boolean; channel: string; key: string; kind: "main" | "group" | "cron" | "hook" | "node" | "other"; pinned: boolean; abortedLastRun?: boolean; agentStatus?: { expiresAt: number; note: string; attention?: string }; category?: string; childSessions?: Array<string>; contextTokens?: number; derivedTitle?: string; displayName?: string; hasWorktree?: boolean; icon?: string; label?: string; lastMessagePreview?: string; messages?: Array<unknown>; model?: string; owner?: { type: "human" | "agent" | "system"; id?: string; label?: string }; parentSessionKey?: string; projectId?: string; sessionId?: string; stateVersion?: number; status?: "running" | "done" | "failed" | "killed" | "timeout"; totalTokens?: number; unread?: boolean; updatedAt?: number }>; sessionLinkRule?: string; visibility?: { mode: "self" | "tree" | "agent"; restricted: true; warning: string } }',
|
||||
);
|
||||
expect(result.details).toEqual({
|
||||
count: 1,
|
||||
@@ -420,7 +433,17 @@ describe("sessions-list-tool", () => {
|
||||
archived: false,
|
||||
pinned: true,
|
||||
label: "worker",
|
||||
category: "P1 issues",
|
||||
category: "Projects",
|
||||
icon: "🦞",
|
||||
unread: true,
|
||||
owner: { type: "human", id: "profile-owner", label: "Owner" },
|
||||
projectId: "openclaw",
|
||||
hasWorktree: true,
|
||||
agentStatus: {
|
||||
note: "Needs review",
|
||||
attention: "flag",
|
||||
expiresAt: expect.any(Number),
|
||||
},
|
||||
displayName: "Worker",
|
||||
derivedTitle: "Investigate queue",
|
||||
lastMessagePreview: "Use `[[reply_to_current]]` literally.",
|
||||
|
||||
@@ -86,6 +86,34 @@ const SessionListRowOutputSchema = Type.Object(
|
||||
pinned: Type.Boolean(),
|
||||
label: Type.Optional(Type.String()),
|
||||
category: Type.Optional(Type.String()),
|
||||
icon: Type.Optional(Type.String()),
|
||||
unread: Type.Optional(Type.Boolean()),
|
||||
owner: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
type: Type.Union([
|
||||
Type.Literal("human"),
|
||||
Type.Literal("agent"),
|
||||
Type.Literal("system"),
|
||||
]),
|
||||
id: Type.Optional(Type.String()),
|
||||
label: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
projectId: Type.Optional(Type.String()),
|
||||
hasWorktree: Type.Optional(Type.Boolean()),
|
||||
agentStatus: Type.Optional(
|
||||
Type.Object(
|
||||
{
|
||||
note: Type.String(),
|
||||
expiresAt: Type.Number(),
|
||||
attention: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
),
|
||||
displayName: Type.Optional(Type.String()),
|
||||
derivedTitle: Type.Optional(Type.String()),
|
||||
lastMessagePreview: Type.Optional(Type.String()),
|
||||
@@ -379,6 +407,18 @@ export function createSessionsListTool(opts?: {
|
||||
const stateVersion = stateVersions[stateVersionAgentId]?.[key];
|
||||
const rowLabel = readStringValue(entry.label);
|
||||
const category = readStringValue(entry.category);
|
||||
const icon = readStringValue(entry.icon);
|
||||
const ownerActor = entry.owner?.actor;
|
||||
const owner = ownerActor
|
||||
? {
|
||||
type: ownerActor.type,
|
||||
...(ownerActor.id ? { id: ownerActor.id } : {}),
|
||||
...(ownerActor.label ? { label: ownerActor.label } : {}),
|
||||
}
|
||||
: undefined;
|
||||
const projectId = readStringValue(entry.projectId);
|
||||
const hasWorktree = entry.worktree ? true : undefined;
|
||||
const agentStatus = entry.agentStatus;
|
||||
const displayName = readStringValue(entry.displayName);
|
||||
const derivedTitle = readStringValue(entry.derivedTitle);
|
||||
const lastMessagePreview = readStringValue(entry.lastMessagePreview);
|
||||
@@ -431,6 +471,12 @@ export function createSessionsListTool(opts?: {
|
||||
pinned: entry.pinned === true,
|
||||
...(rowLabel ? { label: rowLabel } : {}),
|
||||
...(category ? { category } : {}),
|
||||
...(icon ? { icon } : {}),
|
||||
...(entry.unread !== undefined ? { unread: entry.unread } : {}),
|
||||
...(owner ? { owner } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
...(hasWorktree ? { hasWorktree } : {}),
|
||||
...(agentStatus ? { agentStatus } : {}),
|
||||
...(displayName ? { displayName } : {}),
|
||||
...(derivedTitle ? { derivedTitle } : {}),
|
||||
...(lastMessagePreview ? { lastMessagePreview } : {}),
|
||||
|
||||
@@ -147,6 +147,7 @@ describe("sessions tool", () => {
|
||||
type: "string",
|
||||
enum: [
|
||||
"patch",
|
||||
"patch_many",
|
||||
"reset",
|
||||
"delete",
|
||||
"assign_owner",
|
||||
@@ -158,6 +159,12 @@ describe("sessions tool", () => {
|
||||
},
|
||||
deleteTranscript: { type: "boolean" },
|
||||
label: { type: "string", description: expect.stringContaining("Empty string clears") },
|
||||
category: {
|
||||
anyOf: [{ type: "string" }, { type: "null" }],
|
||||
description: expect.stringContaining("Null or empty string clears"),
|
||||
},
|
||||
unread: { type: "boolean" },
|
||||
targets: { type: "array", maxItems: 100 },
|
||||
icon: {
|
||||
type: "string",
|
||||
description: expect.stringContaining(
|
||||
@@ -767,20 +774,20 @@ describe("sessions tool", () => {
|
||||
|
||||
const result = await tool.execute("patch-sidebar", {
|
||||
action: "patch",
|
||||
label: "Movies",
|
||||
category: "Movies",
|
||||
});
|
||||
|
||||
expect(callGateway).toHaveBeenCalledWith({
|
||||
method: "sessions.patch",
|
||||
params: {
|
||||
key: "agent:main:main",
|
||||
label: "Movies",
|
||||
category: "Movies",
|
||||
},
|
||||
});
|
||||
expect(result.details).toEqual({
|
||||
status: "updated",
|
||||
sessionKey: "agent:main:main",
|
||||
updated: ["label"],
|
||||
updated: ["category"],
|
||||
});
|
||||
const text = (result.content[0] as { text?: string } | undefined)?.text ?? "";
|
||||
expect(text).not.toContain('"entry"');
|
||||
@@ -1032,4 +1039,56 @@ describe("sessions tool", () => {
|
||||
params: expect.objectContaining({ key: "agent:main:other" }),
|
||||
});
|
||||
});
|
||||
|
||||
it("patches visible sessions in one compact batch result", async () => {
|
||||
const callGateway = vi.fn(async (request: AgentToolGatewayRequest) => {
|
||||
if (request.method === "sessions.patchMany") {
|
||||
return {
|
||||
outcomes: [
|
||||
{ ok: true, key: "agent:main:main" },
|
||||
{
|
||||
ok: false,
|
||||
key: "agent:main:dashboard:changed",
|
||||
error: { code: "INVALID_REQUEST", message: "session changed; retry" },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { key: String(request.params.sessionKey) };
|
||||
});
|
||||
const tool = createSessionsTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
config: { tools: { sessions: { visibility: "all" } } },
|
||||
callGateway: callGateway as never,
|
||||
});
|
||||
|
||||
const result = await tool.execute("batch-category", {
|
||||
action: "patch_many",
|
||||
targets: [
|
||||
{ sessionKey: "agent:main:main", expectedSessionId: "main-session" },
|
||||
{ sessionKey: "agent:main:dashboard:changed" },
|
||||
],
|
||||
category: "Research",
|
||||
unread: false,
|
||||
});
|
||||
|
||||
expect(callGateway).toHaveBeenCalledWith({
|
||||
method: "sessions.patchMany",
|
||||
params: {
|
||||
targets: [
|
||||
{ key: "agent:main:main", expectedSessionId: "main-session" },
|
||||
{ key: "agent:main:dashboard:changed" },
|
||||
],
|
||||
patch: { category: "Research", unread: false },
|
||||
},
|
||||
});
|
||||
expect(result.details).toEqual({
|
||||
status: "updated",
|
||||
requested: 2,
|
||||
updated: 1,
|
||||
failed: [
|
||||
{ sessionKey: "agent:main:dashboard:changed", error: "session changed; retry" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
|
||||
import { Type } from "typebox";
|
||||
import type {
|
||||
SessionsAssignOwnerResult,
|
||||
SessionsPatchManyResult,
|
||||
SessionsPatchResult,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import {
|
||||
@@ -47,6 +48,7 @@ import { resolveSessionReference, shouldResolveSessionIdInput } from "./sessions
|
||||
|
||||
const ACTIONS = [
|
||||
"patch",
|
||||
"patch_many",
|
||||
"reset",
|
||||
"delete",
|
||||
"assign_owner",
|
||||
@@ -108,6 +110,12 @@ const SessionsToolSchema = Type.Object(
|
||||
label: Type.Optional(
|
||||
Type.String({ description: "Sidebar title override. Empty string clears it." }),
|
||||
),
|
||||
category: Type.Optional(
|
||||
Type.Union([Type.String(), Type.Null()], {
|
||||
description: "Sidebar group/category. Null or empty string clears it to Other.",
|
||||
}),
|
||||
),
|
||||
unread: Type.Optional(Type.Boolean({ description: "Mark the session unread or read." })),
|
||||
icon: Type.Optional(
|
||||
Type.String({
|
||||
description: `Persistent sidebar icon: a single emoji, or a named icon: ${SESSION_ICON_GLYPH_DESCRIPTION}. Empty string clears it. Distinct from attention, which is temporary.`,
|
||||
@@ -158,6 +166,22 @@ const SessionsToolSchema = Type.Object(
|
||||
),
|
||||
name: Type.Optional(Type.String({ description: "Group name" })),
|
||||
to: Type.Optional(Type.String({ description: "New group name" })),
|
||||
targets: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object(
|
||||
{
|
||||
sessionKey: Type.String(),
|
||||
expectedSessionId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
{
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
description: "Visible sessions for patch_many; maximum 100.",
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
@@ -333,7 +357,7 @@ export function createSessionsTool(opts: SessionsToolOptions = {}): AnyAgentTool
|
||||
label: "Sessions",
|
||||
name: "sessions",
|
||||
description:
|
||||
"Session settings, ownership, reset, delete, and sidebar categories: patch label/icon/category/status, pin, archive/restore, model/thinking override; category assigns one session while group_set replaces the ordered category catalog; assign_owner hands responsibility to a human or agent; reset/delete visible sessions; group_list/group_set/group_rename/group_delete.",
|
||||
"Session settings, ownership, reset, delete, and groups: patch label/icon/category/status, unread, pin, archive/restore, model/thinking override; patch_many changes category/unread for up to 100 visible sessions; assign_owner hands responsibility to a human or agent; reset/delete visible sessions; group_list/group_set/group_rename/group_delete.",
|
||||
parameters: SessionsToolSchema,
|
||||
execute: async (_toolCallId, rawArgs) => {
|
||||
const params = rawArgs as Record<string, unknown>;
|
||||
@@ -444,6 +468,58 @@ export function createSessionsTool(opts: SessionsToolOptions = {}): AnyAgentTool
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (action === "patch_many") {
|
||||
if (!Array.isArray(params.targets) || params.targets.length === 0) {
|
||||
throw new ToolInputError("patch_many requires targets");
|
||||
}
|
||||
if (params.targets.length > 100) {
|
||||
throw new ToolInputError("patch_many supports at most 100 targets");
|
||||
}
|
||||
const patch = {
|
||||
...(params.category !== undefined
|
||||
? { category: readClearableString(params, "category") }
|
||||
: {}),
|
||||
...(params.unread !== undefined ? { unread: readBooleanParam(params, "unread") } : {}),
|
||||
};
|
||||
if (Object.keys(patch).length === 0) {
|
||||
throw new ToolInputError("patch_many requires category or unread");
|
||||
}
|
||||
const targets = await Promise.all(
|
||||
params.targets.map(async (rawTarget, index) => {
|
||||
if (!rawTarget || typeof rawTarget !== "object") {
|
||||
throw new ToolInputError(`targets[${index}] must be an object`);
|
||||
}
|
||||
const target = rawTarget as Record<string, unknown>;
|
||||
const sessionKey = readToolStringParam(target, "sessionKey", { required: true });
|
||||
const resolved = await resolvePatchTarget(
|
||||
{ ...opts, config: opts.config ?? getRuntimeConfig() },
|
||||
sessionKey,
|
||||
gatewayRequest,
|
||||
);
|
||||
const expectedSessionId = normalizeOptionalString(
|
||||
readToolStringParam(target, "expectedSessionId"),
|
||||
);
|
||||
return {
|
||||
key: resolved.key,
|
||||
...(!parseAgentSessionKey(resolved.key) ? { agentId: resolved.agentId } : {}),
|
||||
...(expectedSessionId ? { expectedSessionId } : {}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const result = await callGateway<SessionsPatchManyResult>("sessions.patchMany", {
|
||||
targets,
|
||||
patch,
|
||||
});
|
||||
const failed = result.outcomes.flatMap((outcome) =>
|
||||
outcome.ok ? [] : [{ sessionKey: outcome.key, error: outcome.error.message }],
|
||||
);
|
||||
return jsonResult({
|
||||
status: "updated",
|
||||
requested: targets.length,
|
||||
updated: result.outcomes.length - failed.length,
|
||||
failed,
|
||||
});
|
||||
}
|
||||
if (action !== "patch") {
|
||||
throw new ToolInputError(`Unknown action: ${action}`);
|
||||
}
|
||||
@@ -471,6 +547,10 @@ export function createSessionsTool(opts: SessionsToolOptions = {}): AnyAgentTool
|
||||
key,
|
||||
...lifecycleIdentity,
|
||||
...(params.label !== undefined ? { label: readClearableString(params, "label") } : {}),
|
||||
...(params.category !== undefined
|
||||
? { category: readClearableString(params, "category") }
|
||||
: {}),
|
||||
...(params.unread !== undefined ? { unread: readBooleanParam(params, "unread") } : {}),
|
||||
...(params.icon !== undefined ? { icon: readClearableString(params, "icon") } : {}),
|
||||
...(params.category !== undefined
|
||||
? { category: readClearableString(params, "category") }
|
||||
|
||||
@@ -112,6 +112,70 @@ test("single non-label sessions.patch avoids a whole-store projection", async ()
|
||||
});
|
||||
});
|
||||
|
||||
test("sessions.patchMany categorizes 100 sessions without transcript hydration", async () => {
|
||||
await withOpenClawTestState({ scenario: "minimal" }, async (state) => {
|
||||
const targets = Array.from({ length: 100 }, (_, index) => ({
|
||||
key: `agent:main:category-perf-${index}`,
|
||||
expectedSessionId: `session-category-perf-${index}`,
|
||||
}));
|
||||
for (const [index, target] of targets.entries()) {
|
||||
await upsertSessionEntryCore(
|
||||
{ agentId: "main", sessionKey: target.key },
|
||||
{ sessionId: target.expectedSessionId, updatedAt: index + 1 },
|
||||
);
|
||||
}
|
||||
|
||||
const database = openOpenClawAgentDatabase({ agentId: "main", env: state.env });
|
||||
const statements = trackSqliteStatementExecutions(
|
||||
database.db,
|
||||
["transcript-full-hydration"] as const,
|
||||
(sql) => {
|
||||
const normalized = sql.toLowerCase().replaceAll(/\s+/g, " ").trim();
|
||||
const fromIndex = normalized.indexOf(" from ");
|
||||
const selectedColumns = fromIndex > 0 ? normalized.slice("select ".length, fromIndex) : "";
|
||||
return normalized.startsWith("select ") &&
|
||||
/\b(?:from|join) "transcript_events"(?: |$)/.test(normalized) &&
|
||||
(selectedColumns.includes("event_json") || /(?:^|, )(?:(?:"[^"]+"\.)?\*)/.test(selectedColumns))
|
||||
? "transcript-full-hydration"
|
||||
: null;
|
||||
},
|
||||
);
|
||||
sqliteTransactionLabels.length = 0;
|
||||
const respond = vi.fn();
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
await sessionMutationHandlers["sessions.patchMany"]!({
|
||||
params: { targets, patch: { category: "Research" } },
|
||||
respond,
|
||||
context: {
|
||||
getRuntimeConfig: () => ({}),
|
||||
loadGatewayModelCatalog: vi.fn(async () => []),
|
||||
broadcastToConnIds: vi.fn(),
|
||||
getSessionEventSubscriberConnIds: () => new Set(),
|
||||
chatAbortControllers: new Map(),
|
||||
chatQueuedTurns: new Map(),
|
||||
dedupe: new Map(),
|
||||
} as unknown as GatewayRequestContext,
|
||||
client: humanClient(),
|
||||
} as never);
|
||||
} finally {
|
||||
statements.restore();
|
||||
}
|
||||
|
||||
expect(performance.now() - startedAt).toBeLessThan(1_000);
|
||||
expect(respond.mock.calls[0]?.[1]?.outcomes).toHaveLength(100);
|
||||
expect(statements.counts["transcript-full-hydration"]).toBe(0);
|
||||
expect(
|
||||
sqliteTransactionLabels.filter((label) => label === "session.entry-replacements"),
|
||||
).toHaveLength(1);
|
||||
for (const target of targets) {
|
||||
expect(loadSessionEntry({ agentId: "main", sessionKey: target.key })?.category).toBe(
|
||||
"Research",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("sessions.patchMany archives 30 human sessions without transcript hydration", async () => {
|
||||
await withOpenClawTestState({ scenario: "minimal" }, async (state) => {
|
||||
const targets = Array.from({ length: 30 }, (_, index) => ({
|
||||
|
||||
@@ -521,6 +521,7 @@ export function buildGatewaySessionRow(params: {
|
||||
? { sessionRoot: entry.sessionRoot }
|
||||
: {}),
|
||||
worktree: entry?.worktree,
|
||||
projectId: entry?.projectId,
|
||||
execNode: entry?.execNode,
|
||||
execCwd: entry?.execCwd,
|
||||
forkedFromParent: sessionEntryForkedFromParent(entry) ? true : undefined,
|
||||
|
||||
@@ -78,6 +78,7 @@ export type GatewaySessionRow = {
|
||||
sessionRoot?: string;
|
||||
/** Managed worktree bound to this session (repo checkout + branch). */
|
||||
worktree?: SessionEntry["worktree"];
|
||||
projectId?: SessionEntry["projectId"];
|
||||
/** Session-scoped exec node binding (exec host=node routing). */
|
||||
execNode?: string;
|
||||
/** Working directory interpreted only by the bound exec node. */
|
||||
|
||||
Reference in New Issue
Block a user