refactor: simplify cross-session messaging
This commit is contained in:
+6
-10
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user