mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* test: align frozen-base fixtures with current lifecycle contracts Apply the prerequisite fixture repairs already landed in0b6f02faae(#159836) so the narration comparison base can complete validation. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * perf(gateway): throttle background sidebar narration with digests Declare narration intent on targeted session subscriptions and pace bounded text snapshots on the server. Preserve full streams whenever any SDK owner renders a foreground transcript, and flush corrected final text before terminal events. Update bundled clients, protocol artifacts, and the event contract. * fix(gateway): preserve foreground ownership across narration addresses Track independent SDK observer IDs at the server so every full-stream interest survives alias collisions, rollback, and unrelated releases. Keep global requests bound to their acknowledged owner and pace distinct logical sessions separately. Wait for foreground admission before history reads, retire stale admissions, and load subscription runtime on demand. Share the existing browser-safe UUID implementation and extend protocol and lifecycle regressions. * test(gateway): expect resolved agent ownership in shared approval replay ACKs * test(gateway): isolate the shared subscription lookup assertion Narration also reads subscription intent for eligible recipients, so the visible chat path legitimately performs three registry lookups. Check the single admission lookup after visibility is revoked, when narration is not entered. Preserve delivery, unrelated-recipient, visibility-revocation, and event-sequencing assertions. * ci: split UI component tests into their own core type shard Apply the upstream partition fix so the narration merge retains complete root coverage within the unchanged local shard-size budget. (cherry picked from commit52ae7e314d) * test(ui): align narration fixtures with stream admission --------- Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
388 lines
16 KiB
TypeScript
388 lines
16 KiB
TypeScript
import type { GatewaySessionMessageSubscription } from "@openclaw/gateway-client/browser";
|
|
import type {
|
|
PreservedSessionWorktree,
|
|
SessionOwner,
|
|
SessionsAssignOwnerParams,
|
|
SessionsDeleteResult,
|
|
SessionsDescribeParams,
|
|
SessionsPatchManyParams,
|
|
SessionsPatchManyResult,
|
|
SessionsRecoverResult,
|
|
} from "../../../../packages/gateway-protocol/src/index.js";
|
|
import type { SessionCatalogPullRequestSummary } from "../../../../packages/gateway-protocol/src/schema/sessions-catalog.js";
|
|
import type { GatewayBrowserClient, GatewayEventFrame, GatewayHelloOk } from "../../api/gateway.ts";
|
|
import type {
|
|
GatewaySessionRow,
|
|
SessionBranch,
|
|
SessionsBranchesSwitchResult,
|
|
SessionsForkResult,
|
|
SessionsListResult,
|
|
SessionsRewindResult,
|
|
SessionWorkspaceGetResult,
|
|
SessionWorkspaceListResult,
|
|
SessionWorkspaceSetResult,
|
|
} from "../../api/types.ts";
|
|
import type { ApplicationGatewayPhase, ApplicationGatewaySnapshot } from "../../app/gateway.ts";
|
|
import type { AuthenticatedUser } from "../../app/user-profile.ts";
|
|
import type { GatewayConnectionScope } from "../gateway-connection-lifecycle.ts";
|
|
import type { SessionCreateOutcome, SessionCreateParams } from "./create.ts";
|
|
import type { SessionGroupSettings } from "./custom-groups.ts";
|
|
import type {
|
|
GitHubPublicationController,
|
|
GitHubPublicationPresentationBinding,
|
|
} from "./github-publication-controller.ts";
|
|
import type { SessionArchivedFilter } from "./navigation.ts";
|
|
import type { SessionPatchRoute } from "./patch.ts";
|
|
import type { SessionReconcileOptions } from "./reconcile.ts";
|
|
import type { SessionChangedRowResult } from "./session-row-reconcile.ts";
|
|
import type { SessionRunTerminal } from "./session-run-terminal.ts";
|
|
|
|
export type SessionState = {
|
|
result: SessionsListResult | null;
|
|
resultCached?: boolean;
|
|
agentId: string | null;
|
|
modelOverrides: Readonly<Record<string, string | null>>;
|
|
loading: boolean;
|
|
error: string | null;
|
|
deletedSessions: readonly SessionDeletionFact[];
|
|
/** Gateway-owned custom group catalog in display order. */
|
|
groups: readonly string[];
|
|
/** New Session defaults associated with each gateway-owned group. */
|
|
groupSettings: readonly SessionGroupSettings[];
|
|
/** Gateway-owned sidebar section order; pinned is intentionally absent. */
|
|
sectionOrder: readonly string[];
|
|
};
|
|
|
|
type SessionDeletionFact = {
|
|
key: string;
|
|
agentId?: string;
|
|
retireBeforeRevision: number;
|
|
};
|
|
|
|
export type SessionGroupMutationResult = "completed" | "stale";
|
|
export type SessionGroupDefaultsStatus = "idle" | "loading" | "ready" | "unavailable";
|
|
|
|
export type SessionListOptions = {
|
|
agentId?: string;
|
|
spawnedBy?: string;
|
|
boardFace?: "chat" | "dashboard";
|
|
hasBoard?: boolean;
|
|
activeMinutes?: number;
|
|
search?: string;
|
|
ownerId?: string;
|
|
ownerFirst?: boolean;
|
|
involvingMe?: boolean;
|
|
offset?: number;
|
|
limit?: number;
|
|
includeGlobal?: boolean;
|
|
includeUnknown?: boolean;
|
|
configuredAgentsOnly?: boolean;
|
|
excludeSubagents?: boolean;
|
|
excludeCron?: boolean;
|
|
excludeSystem?: boolean;
|
|
includeDerivedTitles?: boolean;
|
|
includeLastMessage?: boolean;
|
|
includeOwnerSessionCounts?: boolean;
|
|
archivedFilter?: SessionArchivedFilter;
|
|
append?: boolean;
|
|
};
|
|
|
|
export type SessionRefreshOptions = SessionListOptions & {
|
|
force?: boolean;
|
|
// Sidebar startup hydration must not block session creation or drop the open session.
|
|
backgroundHydrate?: boolean;
|
|
};
|
|
|
|
export type SessionRefreshOutcome =
|
|
| { status: "refreshed" | "stale" }
|
|
| { status: "failed"; error: string };
|
|
|
|
export type SessionListScope = Readonly<Omit<SessionListOptions, "offset" | "append">>;
|
|
|
|
export type SessionListSnapshot = Pick<SessionState, "result" | "agentId" | "loading" | "error">;
|
|
|
|
export type SessionRowTarget = Readonly<{ key: string; agentId: string }>;
|
|
|
|
type SessionRowReadOutcome =
|
|
| { status: "current"; row: GatewaySessionRow | null }
|
|
| { status: "invalidated" }
|
|
| { status: "retired" };
|
|
|
|
export type SessionRowObservation = {
|
|
readonly row: GatewaySessionRow | null;
|
|
readonly sessionId: string | null;
|
|
/** False until this owner accepts a row or confirms its absence. */
|
|
readonly hasObserved: boolean;
|
|
isCurrent: () => boolean;
|
|
captureReconcile: () => (row: GatewaySessionRow | undefined) => SessionRowReadOutcome;
|
|
dispose: () => void;
|
|
};
|
|
|
|
export type SessionRowEventListener = (
|
|
event: GatewayEventFrame,
|
|
/** Rejected generations can wake recovery but cannot mutate transcript or lifecycle state. */
|
|
result: SessionChangedRowResult & { generationRejected?: true },
|
|
) => void;
|
|
|
|
export type SessionRowListener = (
|
|
row: GatewaySessionRow | null,
|
|
/** This retiring registration still owns delivery of the captured frame. */
|
|
notification?: { eventPending: true },
|
|
) => void;
|
|
|
|
export type SessionDeleteOptions = {
|
|
agentId?: string;
|
|
deleteTranscript?: boolean;
|
|
expectedSessionId?: string;
|
|
archivedOnly?: boolean;
|
|
};
|
|
|
|
export type SessionDeleteTarget = SessionDeleteOptions & {
|
|
key: string;
|
|
};
|
|
|
|
export type SessionDeleteOutcome = Pick<SessionsDeleteResult, "deleted" | "worktreePreserved">;
|
|
|
|
export type SessionDeleteBatchResult = {
|
|
deleted: string[];
|
|
errors: { target: SessionDeleteTarget; error: unknown }[];
|
|
preservedWorktrees: PreservedSessionWorktree[];
|
|
};
|
|
|
|
export type SessionCompactResult = {
|
|
ok?: boolean;
|
|
compacted?: boolean;
|
|
reason?: string;
|
|
result?: { tokensBefore?: number; tokensAfter?: number };
|
|
};
|
|
|
|
export type SessionResetOptions = {
|
|
agentId?: string | null;
|
|
};
|
|
|
|
export type SessionResetResult = "completed" | "not-started" | "uncertain";
|
|
|
|
export type SessionGateway = {
|
|
readonly connection?: { readonly gatewayUrl: string; readonly token?: string };
|
|
readonly connectionRevision?: number;
|
|
readonly snapshot: {
|
|
client: GatewayBrowserClient | null;
|
|
phase: ApplicationGatewayPhase;
|
|
restartPending?: boolean;
|
|
suspensionPhase?: ApplicationGatewaySnapshot["suspensionPhase"];
|
|
hello: GatewayHelloOk | null;
|
|
assistantAgentId?: string | null;
|
|
sessionKey?: string;
|
|
selfUser?: AuthenticatedUser | null;
|
|
};
|
|
subscribe: (listener: (snapshot: SessionGateway["snapshot"]) => void) => () => void;
|
|
subscribeEvents: (listener: (event: GatewayEventFrame) => void) => () => void;
|
|
};
|
|
|
|
export type SessionRequestClient = Pick<GatewayBrowserClient, "request">;
|
|
|
|
export type SessionConnectionScope = GatewayConnectionScope;
|
|
|
|
export type SessionConnectionOwner = {
|
|
capture: () => SessionConnectionScope | null;
|
|
isCurrent: (scope: SessionConnectionScope) => boolean;
|
|
};
|
|
|
|
export type SessionCreateReconciliation = "blocking" | "background";
|
|
|
|
export type SessionMessageSubscription = GatewaySessionMessageSubscription;
|
|
export type SessionArchiveVisibility = "pending" | "archived";
|
|
|
|
export type GitHubPublicationBinding = GitHubPublicationPresentationBinding & {
|
|
matches: (row: GatewaySessionRow) => boolean;
|
|
};
|
|
|
|
export type SessionCapability = {
|
|
readonly githubPublication: {
|
|
// The lazy presentation supplies code; this session owner keeps operation custody.
|
|
attach: (
|
|
row: GatewaySessionRow,
|
|
changed: () => void,
|
|
Controller: typeof GitHubPublicationController,
|
|
) => GitHubPublicationBinding | null;
|
|
};
|
|
readonly state: SessionState;
|
|
/** Memory-only roster presentation; never authority for mutations or live row observations. */
|
|
readonly presentation: Pick<SessionState, "result" | "agentId" | "resultCached">;
|
|
/** Advances only when a canonical sessions.list result is published. */
|
|
readonly canonicalListRevision: number;
|
|
whenCachedRosterSettled: () => Promise<void>;
|
|
/** Captures the current Gateway connection generation for read-only requests. */
|
|
captureConnectionScope: () => SessionConnectionScope | null;
|
|
/** Whether a captured read-only request still belongs to the active connection. */
|
|
isConnectionScopeCurrent: (scope: SessionConnectionScope) => boolean;
|
|
/** Shares exact descriptor reads until the session changes; refresh supersedes earlier reads. */
|
|
describe: (
|
|
params: SessionsDescribeParams,
|
|
options?: { refresh?: boolean; timeoutMs?: number; client?: SessionRequestClient },
|
|
) => Promise<{ session?: GatewaySessionRow | null }>;
|
|
list: (options?: SessionListOptions) => Promise<SessionsListResult | null>;
|
|
listSnapshot: (scope: SessionListScope) => SessionListSnapshot;
|
|
subscribeList: (
|
|
scope: SessionListScope,
|
|
listener: (snapshot: SessionListSnapshot) => void,
|
|
) => () => void;
|
|
/** Observes an independent query; refresh rejects after failure, retirement or disposal. */
|
|
observeList: (
|
|
scope: SessionListScope,
|
|
listener: (snapshot: SessionListSnapshot) => void,
|
|
) => { refresh: () => Promise<void>; dispose: () => void };
|
|
refreshList: (options?: SessionRefreshOptions) => Promise<void>;
|
|
/** Admits history through lifecycle fences; defaults-only never authorizes row publication. */
|
|
reconcile: (
|
|
row: GatewaySessionRow | undefined,
|
|
defaults?: SessionsListResult["defaults"],
|
|
options?: SessionReconcileOptions & {
|
|
sourceCanonicalListRevision?: number;
|
|
sourceListScope?: SessionListScope;
|
|
},
|
|
) => boolean | "defaults-only";
|
|
/** Captures request ordering before a supplemental row read begins. */
|
|
captureReconcile: () => SessionCapability["reconcile"];
|
|
/** Owns a routed descriptor through reads and events until its consumer retires. */
|
|
observeRow: (
|
|
target: SessionRowTarget,
|
|
listener: SessionRowListener,
|
|
/** Matching events can omit descriptor-only fields; re-read those without watching roster revisions. */
|
|
options?: {
|
|
onInvalidate?: (reason?: string) => void;
|
|
/** Receives the frame after shared reconciliation, even without an admitted row. */
|
|
onEvent?: SessionRowEventListener;
|
|
},
|
|
) => SessionRowObservation;
|
|
/** Preserve an existing row observation through a local presentation copy. */
|
|
inheritRow: (
|
|
row: GatewaySessionRow,
|
|
previous: GatewaySessionRow | undefined,
|
|
donor?: GatewaySessionRow,
|
|
) => GatewaySessionRow;
|
|
/** Projects held field observations without changing the input rows' keys or membership. */
|
|
projectRows: (rows: readonly GatewaySessionRow[]) => GatewaySessionRow[];
|
|
reconcileRunTerminal: (terminal: SessionRunTerminal) => boolean;
|
|
refresh: (options?: SessionRefreshOptions) => Promise<void>;
|
|
/** Schedules background list refreshes without replacing queued foreground queries. */
|
|
invalidate: () => void;
|
|
/** Refreshes the remembered query without superseding queued foreground intent. */
|
|
refreshReplacement: () => Promise<SessionsListResult | null>;
|
|
/** Reconciles an operation's agent without claiming the foreground query. */
|
|
reconcileMutation: (agentId?: string | null) => Promise<SessionRefreshOutcome>;
|
|
/** Captures freshness of this conversation's permission facts. */
|
|
capturePermissionObservation: (key: string, agentId?: string | null) => () => boolean;
|
|
createResult: (
|
|
params?: SessionCreateParams,
|
|
options?: { reconciliation?: SessionCreateReconciliation },
|
|
) => Promise<SessionCreateOutcome | null>;
|
|
create: (params?: SessionCreateParams) => Promise<string | null>;
|
|
recover: (params: { key: string; agentId?: string }) => Promise<SessionsRecoverResult | null>;
|
|
patch: SessionPatchRoute;
|
|
patchMany: (
|
|
targets: SessionsPatchManyParams["targets"],
|
|
patch: SessionsPatchManyParams["patch"],
|
|
) => Promise<SessionsPatchManyResult | null>;
|
|
archiveVisibility: (key: string) => SessionArchiveVisibility | undefined;
|
|
beginArchive: (key: string, sessionId: string | undefined) => (() => void) | null;
|
|
assignOwner: (
|
|
key: string,
|
|
owner: SessionsAssignOwnerParams["owner"],
|
|
options?: { agentId?: string | null },
|
|
) => Promise<SessionOwner | null>;
|
|
retireModelOverride: (key: string) => void;
|
|
think: (key: string, agentId?: string | null) => string | undefined;
|
|
/** Pending settings may render before roster membership or physical identity exists. */
|
|
settingsPreview: (
|
|
key: string,
|
|
agentId?: string,
|
|
) =>
|
|
| Pick<GatewaySessionRow, "thinkingLevel" | "fastMode" | "effectiveFastMode" | "contextWindow">
|
|
| undefined;
|
|
/** Local previews update the primary snapshot; explicit targets also update held incarnations. */
|
|
patchRowLocal: (
|
|
key: string,
|
|
patch: Partial<GatewaySessionRow>,
|
|
target?: { agentId: string; sessionId: string },
|
|
) => void;
|
|
/** True while a just-created work session awaits its canonical placement row. */
|
|
isPreparedWorkSession: (key: string) => boolean;
|
|
pullRequestSummary: (key: string) => SessionCatalogPullRequestSummary | undefined;
|
|
capturePullRequestEpoch: (key: string) => object;
|
|
setPullRequestSummary: (
|
|
key: string,
|
|
summary: SessionCatalogPullRequestSummary | undefined,
|
|
epoch?: object,
|
|
) => void;
|
|
deletionState: (
|
|
key: string,
|
|
agentId?: string | null,
|
|
sessionId?: string,
|
|
) => "pending" | "confirmed" | undefined;
|
|
delete: (key: string, options?: SessionDeleteOptions) => Promise<SessionDeleteOutcome>;
|
|
deleteMany: (targets: readonly SessionDeleteTarget[]) => Promise<SessionDeleteBatchResult>;
|
|
reset: (key: string, options?: SessionResetOptions) => Promise<SessionResetResult>;
|
|
compact: (key: string, options?: { agentId?: string | null }) => Promise<SessionCompactResult>;
|
|
listFiles: (
|
|
key: string,
|
|
options?: { agentId?: string | null; path?: string; search?: string },
|
|
) => Promise<SessionWorkspaceListResult | null>;
|
|
getFile: (
|
|
key: string,
|
|
path: string,
|
|
options?: { agentId?: string | null },
|
|
) => Promise<SessionWorkspaceGetResult | null>;
|
|
setFile: (
|
|
key: string,
|
|
path: string,
|
|
content: string,
|
|
options: { agentId?: string | null; expectedHash: string },
|
|
) => Promise<SessionWorkspaceSetResult | null>;
|
|
subscribeMessages: (
|
|
key: string,
|
|
options?: { agentId?: string | null; includeApprovals?: boolean; mode?: "narration" },
|
|
) => Promise<SessionMessageSubscription>;
|
|
unsubscribeMessages: (subscription: SessionMessageSubscription) => Promise<void>;
|
|
rewind: (
|
|
key: string,
|
|
entryId: string,
|
|
options?: { agentId?: string | null },
|
|
) => Promise<SessionsRewindResult>;
|
|
forkAtMessage: (
|
|
key: string,
|
|
entryId: string,
|
|
options?: { agentId?: string | null },
|
|
) => Promise<SessionsForkResult>;
|
|
listBranches: (key: string, options?: { agentId?: string | null }) => Promise<SessionBranch[]>;
|
|
switchBranch: (
|
|
key: string,
|
|
leafEntryId: string,
|
|
options?: { agentId?: string | null },
|
|
) => Promise<SessionsBranchesSwitchResult>;
|
|
/** Loads one connection-owned group catalog; null means the attempt retired or failed. */
|
|
groupsLoad: () => Promise<readonly SessionGroupSettings[] | null>;
|
|
/** Generation of the catalog/defaults snapshot used by group-target routes. */
|
|
groupsGeneration: () => number;
|
|
/** Whether group defaults are current enough for a group-target route. */
|
|
groupsStatus: () => SessionGroupDefaultsStatus;
|
|
/** Invalidates the connection-owned group catalog before an explicit route retry. */
|
|
groupsInvalidate: () => void;
|
|
/** Replaces the group catalog; stale means the initiating connection retired. */
|
|
groupsPut: (
|
|
names: readonly string[],
|
|
sectionOrder?: readonly string[],
|
|
) => Promise<SessionGroupMutationResult>;
|
|
/** Renames a group; stale means the initiating connection retired before reconciliation. */
|
|
groupsRename: (from: string, to: string) => Promise<SessionGroupMutationResult>;
|
|
/** Updates the New Session defaults for one group. */
|
|
groupsUpdate: (
|
|
name: string,
|
|
defaults: { cwd: string | null; worktree: boolean },
|
|
) => Promise<SessionGroupMutationResult>;
|
|
/** Deletes a group; stale means the initiating connection retired before reconciliation. */
|
|
groupsDelete: (name: string) => Promise<SessionGroupMutationResult>;
|
|
subscribeCreated: (listener: (key: string) => void) => () => void;
|
|
subscribe: (listener: (state: SessionState) => void) => () => void;
|
|
dispose: () => void;
|
|
};
|