refactor: simplify cross-session messaging

This commit is contained in:
chengyongru
2026-08-19 01:15:56 +08:00
committed by chengyongru
parent 0e184965e8
commit 251a1ccd40
78 changed files with 1578 additions and 7569 deletions
+6 -10
View File
@@ -23,7 +23,7 @@ import type {
ProviderOAuthLoginResult,
ProviderSettingsUpdate,
SessionDeleteResult,
SessionListHandle,
SessionHandle,
SessionAutomationsPayload,
SettingsPayload,
SettingsUpdate,
@@ -167,20 +167,17 @@ function splitKey(key: string): { channel: string; chatId: string } {
return { channel: key.slice(0, idx), chatId: key.slice(idx + 1) };
}
function normalizeSessionListHandle(value: unknown): SessionListHandle | null {
function normalizeSessionHandle(value: unknown): SessionHandle | null {
if (!value || typeof value !== "object") return null;
const handle = value as Partial<SessionListHandle>;
const handle = value as Partial<SessionHandle>;
const id = typeof handle.id === "string" ? handle.id.trim() : "";
const name = typeof handle.name === "string" ? handle.name.trim() : "";
if (
!/^handle_[a-f0-9]{32}$/i.test(id)
|| !name
|| !/^[\p{L}\p{N}_-]+$/u.test(name)
|| !Number.isInteger(handle.color_slot)
|| (handle.color_slot ?? -1) < 0
|| (handle.color_slot ?? 8) >= 8
) return null;
return { id, name, color_slot: handle.color_slot as number };
return { id, name };
}
export async function listSessions(
@@ -196,7 +193,7 @@ export async function listSessions(
model_preset?: string | null;
run_started_at?: number | null;
workspace_scope?: WorkspaceScopePayload | null;
handle?: SessionListHandle | null;
handle?: SessionHandle | null;
};
const body = await request<{ sessions: Row[] }>(
`${base}/api/sessions`,
@@ -205,8 +202,7 @@ export async function listSessions(
API_READ_TIMEOUT_MS,
);
return body.sessions.map((s) => {
const rawSession = normalizeSessionListHandle(s.handle);
const handle = rawSession ? { ...rawSession, session_key: s.key } : null;
const handle = normalizeSessionHandle(s.handle);
return {
key: s.key,
...splitKey(s.key),
+4 -20
View File
@@ -5,7 +5,6 @@ import type {
OutboundCliAppMention,
OutboundMcpPresetMention,
OutboundMedia,
SessionHandle,
SessionMention,
SidebarStatePayload,
GoalStateWsPayload,
@@ -196,7 +195,7 @@ export class NanobotClient {
private knownChats = new Set<string>();
/** Temporary chats are connection-owned and intentionally not reattached. */
private temporaryChatIds = new Set<string>();
/** Per-chat run projection, started optimistically and reconciled by lifecycle events. */
/** Wall-clock run strip: updated from ``goal_status`` even with no ``onChat`` subscriber. */
private runStartedAtByChatId = new Map<string, number>();
/** Per-turn clocks let a rejected newer turn fall back without borrowing its timer. */
private runStartedAtByTurnKey = new Map<string, number>();
@@ -538,14 +537,6 @@ export class NanobotClient {
}
}
private startRunLocally(chatId: string, turnId: string): void {
const startedAt = Date.now() / 1000;
this.runStartedAtByTurnKey.set(this.runSendKey(chatId, turnId), startedAt);
const previous = this.runStartedAtByChatId.get(chatId);
this.runStartedAtByChatId.set(chatId, startedAt);
if (previous !== startedAt) this.emitRunStatus(chatId, startedAt);
}
private settleRunTurn(chatId: string, turnId?: string): void {
if (!turnId) return;
this.clearPendingMessageSend(chatId, turnId);
@@ -725,7 +716,7 @@ export class NanobotClient {
}
}
private recordRunStatus(chatId: string, ev: InboundEvent): void {
private recordGoalStatusForRunStrip(chatId: string, ev: InboundEvent): void {
if (ev.event === "turn_end") {
this.recordRunCompletion(chatId, ev.turn_id);
return;
@@ -976,7 +967,6 @@ export class NanobotClient {
cliApps?: OutboundCliAppMention[];
mcpPresets?: OutboundMcpPresetMention[];
sessionMentions?: SessionMention[];
sessionHandles?: SessionHandle[];
quotedContext?: string;
workspaceScope?: WorkspaceScopePayload | null;
turnId?: string;
@@ -996,9 +986,6 @@ export class NanobotClient {
...(options?.sessionMentions?.length
? { session_mentions: options.sessionMentions }
: {}),
...(options?.sessionHandles?.length
? { session_handles: options.sessionHandles }
: {}),
...(options?.quotedContext?.trim() ? { quoted_context: options.quotedContext.trim() } : {}),
...(options?.workspaceScope ? { workspace_scope: options.workspaceScope } : {}),
...(options?.turnId ? { turn_id: options.turnId } : {}),
@@ -1017,10 +1004,7 @@ export class NanobotClient {
}
if (options?.turnId && !isSystemCommandTurnId(options.turnId)) {
const startsNewRun = options.startsNewRun !== false;
if (startsNewRun) {
this.advanceRunGeneration(chatId, options.turnId);
this.startRunLocally(chatId, options.turnId);
}
if (startsNewRun) this.advanceRunGeneration(chatId, options.turnId);
this.trackPendingMessageSend(chatId, options.turnId, startsNewRun);
}
this.queueSend(frame);
@@ -1256,7 +1240,7 @@ export class NanobotClient {
if (chatId) {
if (this.isCanonicalCompletedTurnEvent(chatId, parsed)) return;
const supersededRunCompletion = this.isSupersededRunCompletion(chatId, parsed);
this.recordRunStatus(chatId, parsed);
this.recordGoalStatusForRunStrip(chatId, parsed);
if (supersededRunCompletion) return;
this.recordGoalStateSnapshot(chatId, parsed);
this.dispatch(chatId, parsed);
+9
View File
@@ -0,0 +1,9 @@
export function sessionHandleColor(handleId: string): string {
let hash = 2166136261;
for (const char of handleId) {
hash ^= char.codePointAt(0) ?? 0;
hash = Math.imul(hash, 16777619);
}
const hue = (hash >>> 0) % 360;
return `oklch(var(--session-handle-lightness) var(--session-handle-chroma) ${hue})`;
}
+8 -21
View File
@@ -66,8 +66,6 @@ export interface UIMessage {
mcpPresets?: UIMcpPresetAttachment[];
/** Persisted sessions explicitly referenced by this user turn. */
sessionMentions?: SessionMention[];
/** Active session handles structurally selected by this user turn. */
sessionHandles?: SessionHandle[];
/** Assistant turn: accumulated model reasoning / thinking text. Built up
* incrementally from ``reasoning_delta`` frames; finalized when
* ``reasoning_end`` arrives. */
@@ -114,29 +112,24 @@ export interface UIMcpPresetAttachment {
}
export interface SessionMention {
/** Text token inserted in the composer, without the leading #. */
/** Stable public identity. Older transcript rows may not include it. */
id?: string;
/** Text token inserted in the composer, without the leading @. */
name: string;
/** Stable persisted-session identifier used by read_session. */
session_key: string;
title: string;
}
/** Exact public handle DTO returned by the session-list endpoint. */
export interface SessionListHandle {
/** Stable public handle returned by the session-list endpoint. */
export interface SessionHandle {
id: string;
name: string;
color_slot: number;
}
/** Public session handle enriched with its UI navigation target. */
export interface SessionHandle extends SessionListHandle {
session_key: string;
}
export interface UISessionMessage {
direction: "incoming" | "outgoing";
message_id: string;
session: SessionListHandle;
session: SessionHandle;
}
export interface SessionAutomationJob {
@@ -1249,10 +1242,12 @@ export type InboundEvent =
active_turn_id?: string;
starts_turn: boolean;
started_at?: number;
created_at_ms?: number;
media_urls?: UIMediaAttachment[];
cli_apps?: UICliAppAttachment[];
mcp_presets?: UIMcpPresetAttachment[];
session_mentions?: SessionMention[];
provenance?: { session_message?: UISessionMessage };
}
| ({
event: "message";
@@ -1272,13 +1267,6 @@ export type InboundEvent =
/** Optional structured payload on progress frames (channel-specific). */
agent_ui?: AgentUIBlob;
} & InboundTurnMetadata)
| ({
event: "session_message";
chat_id: string;
text: string;
created_at_ms: number;
session_message: UISessionMessage;
} & InboundTurnMetadata)
| ({
event: "file_edit";
chat_id: string;
@@ -1473,7 +1461,6 @@ export type Outbound =
cli_apps?: OutboundCliAppMention[];
mcp_presets?: OutboundMcpPresetMention[];
session_mentions?: SessionMention[];
session_handles?: SessionHandle[];
quoted_context?: string;
workspace_scope?: WorkspaceScopePayload;
turn_id?: string;