feat: add rich run timeline backend

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-24 00:04:47 +01:00
co-authored by Copilot
parent cf8c82b779
commit b742941559
16 changed files with 1072 additions and 26 deletions
+152 -25
View File
@@ -40,6 +40,15 @@ import {
type ChatMessageRecord,
type SessionRecord,
} from '@shared/domain/session';
import {
appendRunActivityEvent,
completeSessionRunRecord,
createSessionRunRecord,
failSessionRunRecord,
upsertRunMessageEvent,
upsertSessionRunRecord,
type SessionRunRecord,
} from '@shared/domain/runTimeline';
import {
createSessionToolingSelection,
type LspProfileDefinition,
@@ -318,6 +327,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
? createScratchpadSessionConfig(normalizedPattern)
: undefined,
tooling: createSessionToolingSelection(),
runs: [],
};
workspace.sessions.unshift(session);
@@ -376,28 +386,41 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
return;
}
const requestId = createId('turn');
const workspaceKind = isScratchpadProject(project) ? 'scratchpad' : 'project';
const occurredAt = nowIso();
const userMessageId = createId('msg');
session.messages.push({
id: createId('msg'),
id: userMessageId,
role: 'user',
authorName: 'You',
content: trimmed,
createdAt: nowIso(),
createdAt: occurredAt,
});
session.title = resolveSessionTitle(session, effectivePattern, session.messages);
session.status = 'running';
session.lastError = undefined;
session.updatedAt = nowIso();
session.updatedAt = occurredAt;
session.runs = [
createSessionRunRecord({
requestId,
project,
workspaceKind,
pattern: effectivePattern,
triggerMessageId: userMessageId,
startedAt: occurredAt,
}),
...session.runs,
];
await this.persistAndBroadcast(workspace);
this.emitSessionEvent({
sessionId: session.id,
kind: 'status',
status: 'running',
occurredAt: nowIso(),
occurredAt,
});
const requestId = createId('turn');
const workspaceKind = isScratchpadProject(project) ? 'scratchpad' : 'project';
try {
const responseMessages = await this.sidecar.runTurn(
{
@@ -411,26 +434,33 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
tooling: this.buildRunTurnToolingConfig(workspace, project, session),
},
async (event) => {
await this.applyTurnDelta(workspace, session.id, event);
await this.applyTurnDelta(workspace, session.id, requestId, event);
},
(event) => {
this.emitAgentActivity(event);
async (event) => {
await this.applyAgentActivity(workspace, session.id, requestId, event);
},
);
this.finalizeTurn(workspace, session.id, responseMessages);
this.finalizeTurn(workspace, session.id, requestId, responseMessages);
await this.persistAndBroadcast(workspace);
} catch (error) {
const failedAt = nowIso();
session.status = 'error';
session.lastError = error instanceof Error ? error.message : String(error);
session.updatedAt = nowIso();
session.updatedAt = failedAt;
const failedRun = this.updateSessionRun(session, requestId, (run) =>
failSessionRunRecord(run, failedAt, session.lastError ?? 'Unknown error.'));
this.emitSessionEvent({
sessionId: session.id,
kind: 'error',
occurredAt: nowIso(),
occurredAt: failedAt,
error: session.lastError,
});
if (failedRun) {
this.emitRunUpdated(session.id, failedAt, failedRun);
}
await this.persistAndBroadcast(workspace);
}
@@ -607,18 +637,23 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
private async applyTurnDelta(
workspace: WorkspaceState,
sessionId: string,
requestId: string,
event: TurnDeltaEvent,
): Promise<void> {
if (event.content === undefined && event.contentDelta === undefined) {
return;
}
const occurredAt = nowIso();
const session = this.requireSession(workspace, sessionId);
const existing = session.messages.find((message) => message.id === event.messageId);
const content =
existing && event.content === undefined
? mergeStreamingText(existing.content, event.contentDelta)
: (event.content ?? event.contentDelta);
if (existing) {
existing.content =
event.content ?? mergeStreamingText(existing.content, event.contentDelta);
existing.content = content;
existing.pending = true;
existing.authorName = event.authorName;
} else {
@@ -626,34 +661,75 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
id: event.messageId,
role: 'assistant',
authorName: event.authorName,
content: event.content ?? event.contentDelta,
createdAt: nowIso(),
content,
createdAt: occurredAt,
pending: true,
});
}
session.updatedAt = nowIso();
const nextRun = this.updateSessionRun(session, requestId, (run) =>
upsertRunMessageEvent(run, {
messageId: event.messageId,
occurredAt,
authorName: event.authorName,
content,
status: 'running',
}));
session.updatedAt = occurredAt;
await this.workspaceRepository.save(workspace);
this.emitSessionEvent({
sessionId,
kind: 'message-delta',
occurredAt: nowIso(),
occurredAt,
messageId: event.messageId,
authorName: event.authorName,
contentDelta: event.contentDelta,
content: event.content,
});
if (nextRun) {
this.emitRunUpdated(sessionId, occurredAt, nextRun);
}
}
private emitAgentActivity(event: AgentActivityEvent): void {
private async applyAgentActivity(
workspace: WorkspaceState,
sessionId: string,
requestId: string,
event: AgentActivityEvent,
): Promise<void> {
const occurredAt = nowIso();
const session = this.requireSession(workspace, sessionId);
const activityType = event.activityType;
let nextRun: SessionRunRecord | undefined;
if (activityType !== 'completed') {
nextRun = this.updateSessionRun(session, requestId, (run) =>
appendRunActivityEvent(run, {
activityType,
occurredAt,
agentId: event.agentId,
agentName: event.agentName,
sourceAgentId: event.sourceAgentId,
sourceAgentName: event.sourceAgentName,
toolName: event.toolName,
}));
}
if (nextRun) {
session.updatedAt = occurredAt;
await this.workspaceRepository.save(workspace);
this.emitRunUpdated(sessionId, occurredAt, nextRun);
}
this.emitSessionEvent({
sessionId: event.sessionId,
sessionId,
kind: 'agent-activity',
occurredAt: nowIso(),
occurredAt,
activityType: event.activityType,
agentId: event.agentId,
agentName: event.agentName,
sourceAgentId: event.sourceAgentId,
sourceAgentName: event.sourceAgentName,
toolName: event.toolName,
});
}
@@ -683,12 +759,18 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
});
}
private finalizeTurn(workspace: WorkspaceState, sessionId: string, messages: ChatMessageRecord[]): void {
private finalizeTurn(
workspace: WorkspaceState,
sessionId: string,
requestId: string,
messages: ChatMessageRecord[],
): void {
const session = this.requireSession(workspace, sessionId);
const pattern = this.requirePattern(workspace, session.patternId);
const incomingIds = new Set(messages.map((message) => message.id));
for (const message of messages) {
const occurredAt = nowIso();
const existing = session.messages.find((current) => current.id === message.id);
if (existing) {
existing.authorName = message.authorName;
@@ -698,14 +780,25 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
session.messages.push({ ...message, pending: false });
}
const nextRun = this.updateSessionRun(session, requestId, (run) =>
upsertRunMessageEvent(run, {
messageId: message.id,
occurredAt,
authorName: message.authorName,
content: message.content,
status: 'completed',
}));
this.emitSessionEvent({
sessionId,
kind: 'message-complete',
occurredAt: nowIso(),
occurredAt,
messageId: message.id,
authorName: message.authorName,
content: message.content,
});
if (nextRun) {
this.emitRunUpdated(sessionId, occurredAt, nextRun);
}
this.emitCompletedActivity(sessionId, pattern, message);
}
@@ -716,15 +809,21 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
}
}
const completedAt = nowIso();
session.status = 'idle';
session.lastError = undefined;
session.updatedAt = nowIso();
session.updatedAt = completedAt;
const completedRun = this.updateSessionRun(session, requestId, (run) =>
completeSessionRunRecord(run, completedAt));
this.emitSessionEvent({
sessionId,
kind: 'status',
occurredAt: nowIso(),
occurredAt: completedAt,
status: 'idle',
});
if (completedRun) {
this.emitRunUpdated(sessionId, completedAt, completedRun);
}
}
private async persistAndBroadcast(workspace: WorkspaceState): Promise<WorkspaceState> {
@@ -835,6 +934,34 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
};
}
private updateSessionRun(
session: SessionRecord,
requestId: string,
updater: (run: SessionRunRecord) => SessionRunRecord,
): SessionRunRecord | undefined {
const run = session.runs.find((candidate) => candidate.requestId === requestId);
if (!run) {
return undefined;
}
const nextRun = updater(run);
if (nextRun === run) {
return undefined;
}
session.runs = upsertSessionRunRecord(session.runs, nextRun);
return nextRun;
}
private emitRunUpdated(sessionId: string, occurredAt: string, run: SessionRunRecord): void {
this.emitSessionEvent({
sessionId,
kind: 'run-updated',
occurredAt,
run,
});
}
private emitSessionEvent(event: SessionEventRecord): void {
this.emit('session-event', event);
}
@@ -3,6 +3,7 @@ import { mkdir } from 'node:fs/promises';
import { createBuiltinPatterns } from '@shared/domain/pattern';
import type { PatternDefinition } from '@shared/domain/pattern';
import { mergeScratchpadProject } from '@shared/domain/project';
import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
import { normalizeSessionToolingSelection, normalizeWorkspaceSettings } from '@shared/domain/tooling';
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
import { nowIso } from '@shared/utils/ids';
@@ -62,6 +63,7 @@ export class WorkspaceRepository {
projects,
sessions: (stored.sessions ?? []).map((session) => ({
...session,
runs: normalizeSessionRunRecords(session.runs),
tooling: normalizeSessionToolingSelection(session.tooling),
})),
settings: normalizeWorkspaceSettings(stored.settings),
+23
View File
@@ -1,4 +1,5 @@
import type { SessionEventRecord } from '@shared/domain/event';
import { upsertSessionRunRecord } from '@shared/domain/runTimeline';
import type { ChatMessageRecord, SessionRecord } from '@shared/domain/session';
import type { WorkspaceState } from '@shared/domain/workspace';
import { mergeStreamingText } from '@shared/utils/streamingText';
@@ -40,6 +41,8 @@ function applySessionEvent(session: SessionRecord, event: SessionEventRecord): S
return applyMessageDeltaEvent(session, event);
case 'message-complete':
return applyMessageCompleteEvent(session, event);
case 'run-updated':
return applyRunUpdatedEvent(session, event);
default:
return session;
}
@@ -160,3 +163,23 @@ function applyMessageCompleteEvent(session: SessionRecord, event: SessionEventRe
updatedAt: event.occurredAt,
};
}
function applyRunUpdatedEvent(session: SessionRecord, event: SessionEventRecord): SessionRecord {
if (!event.run) {
return session;
}
const nextRuns = upsertSessionRunRecord(session.runs, event.run);
if (
nextRuns.length === session.runs.length
&& nextRuns.every((run, index) => run === session.runs[index])
) {
return session;
}
return {
...session,
runs: nextRuns,
updatedAt: event.occurredAt,
};
}
+2
View File
@@ -152,6 +152,8 @@ export interface AgentActivityEvent {
activityType: AgentActivityType;
agentId?: string;
agentName?: string;
sourceAgentId?: string;
sourceAgentName?: string;
toolName?: string;
}
+6
View File
@@ -1,3 +1,5 @@
import type { SessionRunRecord } from '@shared/domain/runTimeline';
export type SessionActivityType = 'thinking' | 'tool-calling' | 'handoff' | 'completed';
export type SessionEventKind =
@@ -5,6 +7,7 @@ export type SessionEventKind =
| 'message-delta'
| 'message-complete'
| 'agent-activity'
| 'run-updated'
| 'error';
export interface SessionEventRecord {
@@ -19,6 +22,9 @@ export interface SessionEventRecord {
activityType?: SessionActivityType;
agentId?: string;
agentName?: string;
sourceAgentId?: string;
sourceAgentName?: string;
toolName?: string;
run?: SessionRunRecord;
error?: string;
}
+454
View File
@@ -0,0 +1,454 @@
import type { PatternDefinition, ReasoningEffort } from '@shared/domain/pattern';
import type { ProjectRecord } from '@shared/domain/project';
import { createId } from '@shared/utils/ids';
export type SessionRunStatus = 'running' | 'completed' | 'error';
export type SessionRunWorkspaceKind = 'project' | 'scratchpad';
export type RunTimelineEventKind =
| 'run-started'
| 'thinking'
| 'handoff'
| 'tool-call'
| 'message'
| 'run-completed'
| 'run-failed';
export type RunTimelineEventStatus = 'running' | 'completed' | 'error';
export interface RunTimelineAgentRecord {
agentId: string;
agentName: string;
model: string;
reasoningEffort?: ReasoningEffort;
}
export interface RunTimelineEventRecord {
id: string;
kind: RunTimelineEventKind;
occurredAt: string;
updatedAt?: string;
status: RunTimelineEventStatus;
agentId?: string;
agentName?: string;
sourceAgentId?: string;
sourceAgentName?: string;
targetAgentId?: string;
targetAgentName?: string;
toolName?: string;
messageId?: string;
content?: string;
error?: string;
}
export interface SessionRunRecord {
id: string;
requestId: string;
projectId: string;
projectPath: string;
workspaceKind: SessionRunWorkspaceKind;
patternId: string;
patternName: string;
patternMode: PatternDefinition['mode'];
triggerMessageId: string;
startedAt: string;
completedAt?: string;
status: SessionRunStatus;
agents: RunTimelineAgentRecord[];
events: RunTimelineEventRecord[];
}
export interface CreateSessionRunRecordInput {
requestId: string;
project: Pick<ProjectRecord, 'id' | 'path'>;
workspaceKind: SessionRunWorkspaceKind;
pattern: Pick<PatternDefinition, 'id' | 'name' | 'mode' | 'agents'>;
triggerMessageId: string;
startedAt: string;
}
export interface AppendRunActivityEventInput {
activityType: 'thinking' | 'tool-calling' | 'handoff';
occurredAt: string;
agentId?: string;
agentName?: string;
sourceAgentId?: string;
sourceAgentName?: string;
toolName?: string;
}
export interface UpsertRunMessageEventInput {
messageId: string;
occurredAt: string;
authorName?: string;
content?: string;
status: RunTimelineEventStatus;
error?: string;
}
function normalizeOptionalString(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function normalizeRunTimelineAgent(
agent: RunTimelineAgentRecord,
): RunTimelineAgentRecord | undefined {
const agentId = normalizeOptionalString(agent.agentId);
const agentName = normalizeOptionalString(agent.agentName);
const model = normalizeOptionalString(agent.model);
if (!agentId || !agentName || !model) {
return undefined;
}
return {
agentId,
agentName,
model,
reasoningEffort: agent.reasoningEffort,
};
}
function normalizeRunTimelineEvent(
event: RunTimelineEventRecord,
): RunTimelineEventRecord | undefined {
const id = normalizeOptionalString(event.id);
const occurredAt = normalizeOptionalString(event.occurredAt);
if (!id || !occurredAt) {
return undefined;
}
return {
id,
kind: event.kind,
occurredAt,
updatedAt: normalizeOptionalString(event.updatedAt),
status: event.status,
agentId: normalizeOptionalString(event.agentId),
agentName: normalizeOptionalString(event.agentName),
sourceAgentId: normalizeOptionalString(event.sourceAgentId),
sourceAgentName: normalizeOptionalString(event.sourceAgentName),
targetAgentId: normalizeOptionalString(event.targetAgentId),
targetAgentName: normalizeOptionalString(event.targetAgentName),
toolName: normalizeOptionalString(event.toolName),
messageId: normalizeOptionalString(event.messageId),
content: event.content,
error: normalizeOptionalString(event.error),
};
}
function resolveRunTimelineAgent(
run: SessionRunRecord,
agentId?: string,
agentName?: string,
): Pick<RunTimelineEventRecord, 'agentId' | 'agentName'> {
const normalizedAgentId = normalizeOptionalString(agentId);
const normalizedAgentName = normalizeOptionalString(agentName);
if (normalizedAgentId) {
const matchedAgent = run.agents.find((agent) => agent.agentId === normalizedAgentId);
if (matchedAgent) {
return {
agentId: matchedAgent.agentId,
agentName: matchedAgent.agentName,
};
}
}
if (normalizedAgentName) {
const matchedAgent = run.agents.find((agent) => agent.agentName === normalizedAgentName);
if (matchedAgent) {
return {
agentId: matchedAgent.agentId,
agentName: matchedAgent.agentName,
};
}
}
return {
agentId: normalizedAgentId,
agentName: normalizedAgentName,
};
}
function appendRunTimelineEvent(
run: SessionRunRecord,
event: Omit<RunTimelineEventRecord, 'id'> & { id?: string },
): SessionRunRecord {
const nextEvent = normalizeRunTimelineEvent({
id: event.id ?? createId('run-event'),
...event,
});
if (!nextEvent) {
return run;
}
return {
...run,
events: [...run.events, nextEvent],
};
}
function settleOpenMessageEvents(
run: SessionRunRecord,
status: Extract<RunTimelineEventStatus, 'completed' | 'error'>,
occurredAt: string,
error?: string,
): SessionRunRecord {
let changed = false;
const normalizedError = normalizeOptionalString(error);
const nextEvents = run.events.map((event) => {
if (event.kind !== 'message' || event.status !== 'running') {
return event;
}
changed = true;
return {
...event,
status,
updatedAt: occurredAt,
error: normalizedError,
};
});
if (!changed) {
return run;
}
return {
...run,
events: nextEvents,
};
}
export function createSessionRunRecord(input: CreateSessionRunRecordInput): SessionRunRecord {
return {
id: createId('run'),
requestId: input.requestId,
projectId: input.project.id,
projectPath: input.project.path,
workspaceKind: input.workspaceKind,
patternId: input.pattern.id,
patternName: input.pattern.name,
patternMode: input.pattern.mode,
triggerMessageId: input.triggerMessageId,
startedAt: input.startedAt,
status: 'running',
completedAt: undefined,
agents: input.pattern.agents
.map((agent): RunTimelineAgentRecord => ({
agentId: agent.id,
agentName: agent.name,
model: agent.model,
reasoningEffort: agent.reasoningEffort,
}))
.flatMap((agent) => {
const normalized = normalizeRunTimelineAgent(agent);
return normalized ? [normalized] : [];
}),
events: [
{
id: createId('run-event'),
kind: 'run-started',
occurredAt: input.startedAt,
status: 'completed',
messageId: input.triggerMessageId,
},
],
};
}
export function normalizeSessionRunRecords(
runs: readonly SessionRunRecord[] | undefined,
): SessionRunRecord[] {
if (!runs || runs.length === 0) {
return [];
}
return runs.flatMap((run) => {
const id = normalizeOptionalString(run.id);
const requestId = normalizeOptionalString(run.requestId);
const projectId = normalizeOptionalString(run.projectId);
const projectPath = normalizeOptionalString(run.projectPath);
const patternId = normalizeOptionalString(run.patternId);
const patternName = normalizeOptionalString(run.patternName);
const triggerMessageId = normalizeOptionalString(run.triggerMessageId);
const startedAt = normalizeOptionalString(run.startedAt);
if (!id || !requestId || !projectId || !projectPath || !patternId || !patternName || !triggerMessageId || !startedAt) {
return [];
}
return [
{
id,
requestId,
projectId,
projectPath,
workspaceKind: run.workspaceKind === 'scratchpad' ? 'scratchpad' : 'project',
patternId,
patternName,
patternMode: run.patternMode,
triggerMessageId,
startedAt,
completedAt: normalizeOptionalString(run.completedAt),
status: run.status === 'error' ? 'error' : run.status === 'running' ? 'running' : 'completed',
agents: run.agents.flatMap((agent) => {
const normalized = normalizeRunTimelineAgent(agent);
return normalized ? [normalized] : [];
}),
events: run.events.flatMap((event) => {
const normalized = normalizeRunTimelineEvent(event);
return normalized ? [normalized] : [];
}),
},
];
});
}
export function upsertSessionRunRecord(
runs: readonly SessionRunRecord[],
nextRun: SessionRunRecord,
): SessionRunRecord[] {
const runIndex = runs.findIndex((run) => run.id === nextRun.id);
if (runIndex < 0) {
return [nextRun, ...runs];
}
const nextRuns = runs.slice();
nextRuns[runIndex] = nextRun;
return nextRuns;
}
export function appendRunActivityEvent(
run: SessionRunRecord,
input: AppendRunActivityEventInput,
): SessionRunRecord {
switch (input.activityType) {
case 'thinking': {
const agent = resolveRunTimelineAgent(run, input.agentId, input.agentName);
return appendRunTimelineEvent(run, {
kind: 'thinking',
occurredAt: input.occurredAt,
status: 'completed',
...agent,
});
}
case 'tool-calling': {
const agent = resolveRunTimelineAgent(run, input.agentId, input.agentName);
return appendRunTimelineEvent(run, {
kind: 'tool-call',
occurredAt: input.occurredAt,
status: 'completed',
...agent,
toolName: normalizeOptionalString(input.toolName),
});
}
case 'handoff': {
const sourceAgent = resolveRunTimelineAgent(run, input.sourceAgentId, input.sourceAgentName);
const targetAgent = resolveRunTimelineAgent(run, input.agentId, input.agentName);
return appendRunTimelineEvent(run, {
kind: 'handoff',
occurredAt: input.occurredAt,
status: 'completed',
agentId: sourceAgent.agentId,
agentName: sourceAgent.agentName,
sourceAgentId: sourceAgent.agentId,
sourceAgentName: sourceAgent.agentName,
targetAgentId: targetAgent.agentId,
targetAgentName: targetAgent.agentName,
});
}
}
}
export function upsertRunMessageEvent(
run: SessionRunRecord,
input: UpsertRunMessageEventInput,
): SessionRunRecord {
const messageId = normalizeOptionalString(input.messageId);
if (!messageId) {
return run;
}
const author = resolveRunTimelineAgent(run, undefined, input.authorName);
const existingIndex = run.events.findIndex((event) => event.kind === 'message' && event.messageId === messageId);
const normalizedError = normalizeOptionalString(input.error);
if (existingIndex < 0) {
return appendRunTimelineEvent(run, {
kind: 'message',
occurredAt: input.occurredAt,
updatedAt: input.occurredAt,
status: input.status,
messageId,
content: input.content,
error: normalizedError,
...author,
});
}
const existingEvent = run.events[existingIndex];
const nextEvent: RunTimelineEventRecord = {
...existingEvent,
agentId: existingEvent.agentId ?? author.agentId,
agentName: existingEvent.agentName ?? author.agentName,
updatedAt: input.occurredAt,
status: input.status,
content: input.content ?? existingEvent.content,
error: normalizedError,
};
if (
nextEvent.agentId === existingEvent.agentId
&& nextEvent.agentName === existingEvent.agentName
&& nextEvent.updatedAt === existingEvent.updatedAt
&& nextEvent.status === existingEvent.status
&& nextEvent.content === existingEvent.content
&& nextEvent.error === existingEvent.error
) {
return run;
}
const nextEvents = run.events.slice();
nextEvents[existingIndex] = nextEvent;
return {
...run,
events: nextEvents,
};
}
export function completeSessionRunRecord(
run: SessionRunRecord,
completedAt: string,
): SessionRunRecord {
const settledRun = settleOpenMessageEvents(run, 'completed', completedAt);
const completedRun: SessionRunRecord = {
...settledRun,
status: 'completed',
completedAt,
};
return appendRunTimelineEvent(completedRun, {
kind: 'run-completed',
occurredAt: completedAt,
status: 'completed',
});
}
export function failSessionRunRecord(
run: SessionRunRecord,
failedAt: string,
error: string,
): SessionRunRecord {
const settledRun = settleOpenMessageEvents(run, 'error', failedAt, error);
const failedRun: SessionRunRecord = {
...settledRun,
status: 'error',
completedAt: failedAt,
};
return appendRunTimelineEvent(failedRun, {
kind: 'run-failed',
occurredAt: failedAt,
status: 'error',
error,
});
}
+2
View File
@@ -4,6 +4,7 @@ import {
normalizeSessionToolingSelection,
type SessionToolingSelection,
} from '@shared/domain/tooling';
import type { SessionRunRecord } from '@shared/domain/runTimeline';
export type ChatRole = 'system' | 'user' | 'assistant';
export type SessionStatus = 'idle' | 'running' | 'error';
@@ -38,6 +39,7 @@ export interface SessionRecord {
lastError?: string;
scratchpadConfig?: ScratchpadSessionConfig;
tooling?: SessionToolingSelection;
runs: SessionRunRecord[];
}
export function resolveSessionTitle(
+1
View File
@@ -181,6 +181,7 @@ export function duplicateSessionRecord(
enabledLspProfileIds: [...session.tooling.enabledLspProfileIds],
}
: undefined,
runs: [],
messages: session.messages.map((message): ChatMessageRecord => ({
...message,
pending: false,