Files
openclaw/src/gateway/session-event-payload.ts
T
Peter Steinberger 3d9b912b88 feat: sidebar session state slot, worktree/automation badges, archive disables bound cron jobs (#104730)
* feat(gateway): flag sessions with attached automations and disable bound cron jobs on archive

Session rows now carry hasAutomation, derived from a lifecycle-owned index
over the cron service's in-memory jobs; cron events push refreshed rows so
badges stay live. sessions.patch { archived: true } disables enabled cron
jobs bound to the session (locked binding re-check, internal/operator-admin
callers only); restore intentionally does not re-enable them.

Refs #104700

* feat(ui): sidebar session state slot and worktree/automation badges

The run spinner moves into a leading state slot shared with the unread dot,
keeping the timestamp visible during runs; muted fork/clock badges sit after
the title (outside the trail/action overlap so pinned rows and touch devices
keep them). New strings localized via ui:i18n:sync (English fallback pending
a keyed translation run).

Refs #104700

* test(gateway): pin cron binding broadcast test to the event mechanism

The shard shares one process; session-store state from earlier tests can make
the row load return null, which legitimately produces a keyless refresh
payload. Row-field projection stays covered by session-utils and
session-automation-index tests.
2026-07-12 06:19:01 +01:00

95 lines
4.0 KiB
TypeScript

import type { GatewaySessionRow } from "./session-utils.js";
/**
* Project a catalog-less session row for websocket merge events.
* Picker metadata comes from catalog-backed list/patch responses; emitting a
* locally reconstructed subset here would replace richer client state.
*/
export function buildGatewaySessionEventRow(sessionRow: GatewaySessionRow): GatewaySessionRow {
const session = { ...sessionRow };
delete session.thinkingLevels;
delete session.thinkingOptions;
delete session.thinkingDefault;
return session;
}
export function buildGatewaySessionEventFields(params: {
sessionRow: GatewaySessionRow;
agentId?: string;
label?: string;
displayName?: string;
parentSessionKey?: string;
hasActiveRun?: boolean;
activeRunIds?: string[];
}): Record<string, unknown> {
const { sessionRow } = params;
const omitUnscopedGlobalGoal = sessionRow.key === "global" && !params.agentId;
return {
updatedAt: sessionRow.updatedAt ?? undefined,
sessionId: sessionRow.sessionId,
kind: sessionRow.kind,
channel: sessionRow.channel,
subject: sessionRow.subject,
groupChannel: sessionRow.groupChannel,
space: sessionRow.space,
chatType: sessionRow.chatType,
origin: sessionRow.origin,
archived: sessionRow.archived ?? false,
archivedAt: sessionRow.archivedAt ?? null,
pinned: sessionRow.pinned ?? false,
pinnedAt: sessionRow.pinnedAt ?? null,
unread: sessionRow.unread ?? false,
lastReadAt: sessionRow.lastReadAt,
lastActivityAt: sessionRow.lastActivityAt,
spawnedBy: sessionRow.spawnedBy,
spawnedWorkspaceDir: sessionRow.spawnedWorkspaceDir,
spawnedCwd: sessionRow.spawnedCwd,
forkedFromParent: sessionRow.forkedFromParent,
spawnDepth: sessionRow.spawnDepth,
subagentRole: sessionRow.subagentRole,
subagentControlScope: sessionRow.subagentControlScope,
label: params.label ?? sessionRow.label ?? null,
// Explicit null so subscribed clients drop a cleared category during merge-reconcile.
category: sessionRow.category ?? null,
displayName: params.displayName ?? sessionRow.displayName ?? null,
deliveryContext: sessionRow.deliveryContext,
parentSessionKey: params.parentSessionKey ?? sessionRow.parentSessionKey,
childSessions: sessionRow.childSessions,
// Explicit null lets subscribed clients clear an override during merge-reconcile.
thinkingLevel: sessionRow.thinkingLevel ?? null,
fastMode: sessionRow.fastMode,
verboseLevel: sessionRow.verboseLevel,
reasoningLevel: sessionRow.reasoningLevel,
elevatedLevel: sessionRow.elevatedLevel,
sendPolicy: sessionRow.sendPolicy,
systemSent: sessionRow.systemSent,
abortedLastRun: sessionRow.abortedLastRun,
inputTokens: sessionRow.inputTokens,
outputTokens: sessionRow.outputTokens,
lastChannel: sessionRow.lastChannel,
lastTo: sessionRow.lastTo,
lastAccountId: sessionRow.lastAccountId,
lastThreadId: sessionRow.lastThreadId,
totalTokens: sessionRow.totalTokens,
totalTokensFresh: sessionRow.totalTokensFresh,
...(omitUnscopedGlobalGoal ? {} : { goal: sessionRow.goal ?? null }),
contextTokens: sessionRow.contextTokens,
estimatedCostUsd: sessionRow.estimatedCostUsd,
responseUsage: sessionRow.responseUsage,
effectiveResponseUsage: sessionRow.effectiveResponseUsage,
modelProvider: sessionRow.modelProvider,
model: sessionRow.model,
agentRuntime: sessionRow.agentRuntime,
status: sessionRow.status,
// Explicit false lets subscribed clients drop the flag during merge-reconcile.
hasAutomation: sessionRow.hasAutomation ?? false,
...(params.hasActiveRun === undefined ? {} : { hasActiveRun: params.hasActiveRun }),
...(params.activeRunIds === undefined ? {} : { activeRunIds: params.activeRunIds }),
startedAt: sessionRow.startedAt,
endedAt: sessionRow.endedAt,
runtimeMs: sessionRow.runtimeMs,
compactionCheckpointCount: sessionRow.compactionCheckpointCount,
latestCompactionCheckpoint: sessionRow.latestCompactionCheckpoint,
};
}