Fork assistant turns into new drafts (#1788)
* feat(chat): fork assistant turns into new drafts Builds chat-history attachments on the daemon and routes them into draft composers so users can continue an assistant turn in an existing workspace tab or New Workspace. * fix(chat): clean up fork draft handling * fix(chat): address fork draft review feedback * fix(server): build fork context from bounded timeline * fix(app): restore assistant turn footer in streams * fix(app): preserve fork draft setup * fix(app): tighten assistant fork footers * fix(app): lock active fork draft handoff * fix(app): preserve attachment-only draft retries * fix(app): unify composer attachment scopes * fix(app): align chat history attachment labels * fix(app): centralize attachment pill content * fix(app): trim fork context for workspace naming
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { curateAgentActivity } from "./activity-curator.js";
|
||||
import { buildAgentForkContextAttachment, curateAgentActivity } from "./activity-curator.js";
|
||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import type { AgentTimelineRow } from "./agent-timeline-store-types.js";
|
||||
|
||||
function toolCallItem(params: {
|
||||
callId: string;
|
||||
@@ -29,6 +30,14 @@ function toolCallItem(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function row(seq: number, item: AgentTimelineItem): AgentTimelineRow {
|
||||
return {
|
||||
seq,
|
||||
timestamp: `2026-06-28T00:00:${String(seq).padStart(2, "0")}.000Z`,
|
||||
item,
|
||||
};
|
||||
}
|
||||
|
||||
describe("curateAgentActivity", () => {
|
||||
it("renders user/assistant/reasoning entries", () => {
|
||||
const timeline: AgentTimelineItem[] = [
|
||||
@@ -217,4 +226,140 @@ second line'`,
|
||||
it("returns a default message when timeline is empty", () => {
|
||||
expect(curateAgentActivity([])).toBe("No activity to display.");
|
||||
});
|
||||
|
||||
it("builds fork context from user messages, assistant messages, and tool summaries", () => {
|
||||
const result = buildAgentForkContextAttachment({
|
||||
agentTitle: "Source Agent",
|
||||
cwd: "/repo",
|
||||
boundaryMessageId: "assistant-1",
|
||||
rows: [
|
||||
row(1, { type: "user_message", text: "Ship the thing", messageId: "user-1" }),
|
||||
row(2, { type: "reasoning", text: "private chain of thought" }),
|
||||
row(
|
||||
3,
|
||||
toolCallItem({
|
||||
callId: "read-1",
|
||||
name: "read_file",
|
||||
detail: {
|
||||
type: "read",
|
||||
filePath: "src/index.ts",
|
||||
content: "console.log('hi')",
|
||||
},
|
||||
}),
|
||||
),
|
||||
row(
|
||||
4,
|
||||
toolCallItem({
|
||||
callId: "external-1",
|
||||
name: "paseo__create_agent",
|
||||
input: { initialPrompt: "do not include raw external tool input" },
|
||||
}),
|
||||
),
|
||||
row(5, {
|
||||
type: "assistant_message",
|
||||
text: "Done.",
|
||||
messageId: "assistant-1",
|
||||
}),
|
||||
row(6, {
|
||||
type: "assistant_message",
|
||||
text: "Later answer.",
|
||||
messageId: "assistant-2",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.boundaryMessageId).toBe("assistant-1");
|
||||
expect(result.attachment).toMatchObject({
|
||||
type: "text",
|
||||
mimeType: "text/plain",
|
||||
contextKind: "chat_history",
|
||||
title: "Chat history",
|
||||
});
|
||||
expect(result.attachment.text).toContain("Source agent: Source Agent");
|
||||
expect(result.attachment.text).toContain("Source directory: /repo");
|
||||
expect(result.attachment.text).toContain("[User] Ship the thing");
|
||||
expect(result.attachment.text).toContain("[Read] src/index.ts");
|
||||
expect(result.attachment.text).toContain("[paseo__create_agent]");
|
||||
expect(result.attachment.text).toContain("[Assistant] Done.");
|
||||
expect(result.attachment.text).not.toContain("private chain of thought");
|
||||
expect(result.attachment.text).not.toContain("do not include raw external tool input");
|
||||
expect(result.attachment.text).not.toContain("Later answer.");
|
||||
});
|
||||
|
||||
it("does not cap fork context to the generic recent activity limit", () => {
|
||||
const messageRows = Array.from({ length: 25 }, (_, index) =>
|
||||
row(index + 1, {
|
||||
type: "user_message",
|
||||
text: `Message ${index + 1}`,
|
||||
messageId: `user-${index + 1}`,
|
||||
}),
|
||||
);
|
||||
const result = buildAgentForkContextAttachment({
|
||||
boundaryMessageId: "assistant-1",
|
||||
rows: [
|
||||
...messageRows,
|
||||
row(26, {
|
||||
type: "assistant_message",
|
||||
text: "Done.",
|
||||
messageId: "assistant-1",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.itemCount).toBe(26);
|
||||
expect(result.attachment.text).toContain("[User] Message 1");
|
||||
expect(result.attachment.text).toContain("[User] Message 25");
|
||||
expect(result.attachment.text).toContain("[Assistant] Done.");
|
||||
});
|
||||
|
||||
it("selects the fork boundary before collapsing later tool updates", () => {
|
||||
const result = buildAgentForkContextAttachment({
|
||||
boundaryMessageId: "assistant-1",
|
||||
rows: [
|
||||
row(1, { type: "user_message", text: "Run it", messageId: "user-1" }),
|
||||
row(
|
||||
2,
|
||||
toolCallItem({
|
||||
callId: "terminal-1",
|
||||
name: "terminal",
|
||||
status: "running",
|
||||
detail: {
|
||||
type: "plain_text",
|
||||
label: "before boundary",
|
||||
},
|
||||
}),
|
||||
),
|
||||
row(3, {
|
||||
type: "assistant_message",
|
||||
text: "Partial result.",
|
||||
messageId: "assistant-1",
|
||||
}),
|
||||
row(
|
||||
4,
|
||||
toolCallItem({
|
||||
callId: "terminal-1",
|
||||
name: "terminal",
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "plain_text",
|
||||
label: "after boundary",
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.attachment.text).toContain("[Terminal] before boundary");
|
||||
expect(result.attachment.text).toContain("[Assistant] Partial result.");
|
||||
expect(result.attachment.text).not.toContain("after boundary");
|
||||
});
|
||||
|
||||
it("rejects missing assistant boundaries instead of silently using the wrong context", () => {
|
||||
expect(() =>
|
||||
buildAgentForkContextAttachment({
|
||||
boundaryMessageId: "missing",
|
||||
rows: [row(1, { type: "assistant_message", text: "Done.", messageId: "assistant-1" })],
|
||||
}),
|
||||
).toThrow("Selected assistant message is no longer available.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import type { AgentAttachment } from "@getpaseo/protocol/messages";
|
||||
import type { AgentTimelineRow } from "./agent-timeline-store-types.js";
|
||||
import { isLikelyExternalToolName } from "@getpaseo/protocol/tool-name-normalization";
|
||||
import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
|
||||
import { projectTimelineRows } from "./timeline-projection.js";
|
||||
@@ -10,12 +12,16 @@ const MAX_TOOL_SUMMARY_CHARS = 200;
|
||||
interface ActivityCuratorOptions {
|
||||
maxItems?: number;
|
||||
labelAssistantMessages?: boolean;
|
||||
includeKinds?: readonly AgentTimelineItem["type"][];
|
||||
includeExternalToolInput?: boolean;
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
text: string;
|
||||
}
|
||||
|
||||
type TextAgentAttachment = Extract<AgentAttachment, { type: "text" }>;
|
||||
|
||||
function appendText(buffer: string, text: string): string {
|
||||
const normalized = text.trim();
|
||||
if (!normalized) {
|
||||
@@ -95,24 +101,56 @@ function projectForCuration(items: readonly AgentTimelineItem[]): AgentTimelineI
|
||||
return projectTimelineRows({ rows, mode: "projected" }).map((entry) => entry.item);
|
||||
}
|
||||
|
||||
function curateAgentActivityEntries(
|
||||
timeline: AgentTimelineItem[],
|
||||
function shouldIncludeItem(item: AgentTimelineItem, options?: ActivityCuratorOptions): boolean {
|
||||
if (!options?.includeKinds) {
|
||||
return true;
|
||||
}
|
||||
return options.includeKinds.includes(item.type);
|
||||
}
|
||||
|
||||
function formatToolCallEntry(
|
||||
item: Extract<AgentTimelineItem, { type: "tool_call" }>,
|
||||
options?: ActivityCuratorOptions,
|
||||
): ActivityEntry {
|
||||
const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail));
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
error: item.error,
|
||||
detail: item.detail,
|
||||
metadata: item.metadata,
|
||||
});
|
||||
const displayName = display.displayName;
|
||||
const summary = formatToolSummary(display.summary);
|
||||
if (
|
||||
(options?.includeExternalToolInput ?? true) &&
|
||||
isLikelyExternalToolName(item.name) &&
|
||||
inputJson
|
||||
) {
|
||||
return activityEntry(`[${displayName}] ${inputJson}`);
|
||||
}
|
||||
return activityEntry(summary ? `[${displayName}] ${summary}` : `[${displayName}]`);
|
||||
}
|
||||
|
||||
function curateProjectedActivityEntries(
|
||||
items: readonly AgentTimelineItem[],
|
||||
options?: ActivityCuratorOptions,
|
||||
): ActivityEntry[] {
|
||||
if (timeline.length === 0) {
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collapsed = projectForCuration(timeline);
|
||||
|
||||
const maxItems = options?.maxItems ?? DEFAULT_MAX_ITEMS;
|
||||
const recentItems =
|
||||
maxItems > 0 && collapsed.length > maxItems ? collapsed.slice(-maxItems) : collapsed;
|
||||
const recentItems = maxItems > 0 && items.length > maxItems ? items.slice(-maxItems) : items;
|
||||
|
||||
const entries: ActivityEntry[] = [];
|
||||
const buffers = { message: "", thought: "" };
|
||||
|
||||
for (const item of recentItems) {
|
||||
if (!shouldIncludeItem(item, options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case "user_message":
|
||||
flushBuffers(entries, buffers, options);
|
||||
@@ -126,25 +164,7 @@ function curateAgentActivityEntries(
|
||||
break;
|
||||
case "tool_call": {
|
||||
flushBuffers(entries, buffers, options);
|
||||
const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail));
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
error: item.error,
|
||||
detail: item.detail,
|
||||
metadata: item.metadata,
|
||||
});
|
||||
const displayName = display.displayName;
|
||||
const summary = formatToolSummary(display.summary);
|
||||
if (isLikelyExternalToolName(item.name) && inputJson) {
|
||||
entries.push(activityEntry(`[${displayName}] ${inputJson}`));
|
||||
break;
|
||||
}
|
||||
if (summary) {
|
||||
entries.push(activityEntry(`[${displayName}] ${summary}`));
|
||||
} else {
|
||||
entries.push(activityEntry(`[${displayName}]`));
|
||||
}
|
||||
entries.push(formatToolCallEntry(item, options));
|
||||
break;
|
||||
}
|
||||
case "todo":
|
||||
@@ -172,6 +192,14 @@ function curateAgentActivityEntries(
|
||||
return entries;
|
||||
}
|
||||
|
||||
function curateAgentActivityEntries(
|
||||
timeline: AgentTimelineItem[],
|
||||
options?: ActivityCuratorOptions,
|
||||
): ActivityEntry[] {
|
||||
const collapsed = projectForCuration(timeline);
|
||||
return curateProjectedActivityEntries(collapsed, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normalized agent timeline items into a concise text summary.
|
||||
*/
|
||||
@@ -184,3 +212,90 @@ export function curateAgentActivity(
|
||||
? entries.map((entry) => entry.text).join("\n")
|
||||
: "No activity to display.";
|
||||
}
|
||||
|
||||
function selectForkContextRows(input: {
|
||||
rows: readonly AgentTimelineRow[];
|
||||
boundaryMessageId?: string | null;
|
||||
}): { items: AgentTimelineItem[]; boundaryMessageId: string | null } {
|
||||
const boundaryMessageId = input.boundaryMessageId?.trim() || null;
|
||||
if (!boundaryMessageId) {
|
||||
const projected = projectTimelineRows({ rows: input.rows, mode: "projected" });
|
||||
return {
|
||||
items: projected.map((entry) => entry.item),
|
||||
boundaryMessageId: null,
|
||||
};
|
||||
}
|
||||
|
||||
const boundaryIndex = input.rows.findLastIndex(
|
||||
(row) => row.item.type === "assistant_message" && row.item.messageId === boundaryMessageId,
|
||||
);
|
||||
if (boundaryIndex < 0) {
|
||||
throw new Error("Selected assistant message is no longer available.");
|
||||
}
|
||||
const selectedRows = input.rows.slice(0, boundaryIndex + 1);
|
||||
const projected = projectTimelineRows({ rows: selectedRows, mode: "projected" });
|
||||
|
||||
return {
|
||||
items: projected.map((entry) => entry.item),
|
||||
boundaryMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
function trimContextMetadata(value: string | null | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function buildForkContextText(input: {
|
||||
body: string;
|
||||
agentTitle?: string | null;
|
||||
cwd?: string | null;
|
||||
}): string {
|
||||
const header = ["Chat history from a previous Paseo agent."];
|
||||
const agentTitle = trimContextMetadata(input.agentTitle);
|
||||
const cwd = trimContextMetadata(input.cwd);
|
||||
if (agentTitle) {
|
||||
header.push(`Source agent: ${agentTitle}`);
|
||||
}
|
||||
if (cwd) {
|
||||
header.push(`Source directory: ${cwd}`);
|
||||
}
|
||||
return `${header.join("\n")}\n\n${input.body}`;
|
||||
}
|
||||
|
||||
export function buildAgentForkContextAttachment(input: {
|
||||
rows: readonly AgentTimelineRow[];
|
||||
boundaryMessageId?: string | null;
|
||||
agentTitle?: string | null;
|
||||
cwd?: string | null;
|
||||
}): { attachment: TextAgentAttachment; itemCount: number; boundaryMessageId: string | null } {
|
||||
const selected = selectForkContextRows({
|
||||
rows: input.rows,
|
||||
boundaryMessageId: input.boundaryMessageId,
|
||||
});
|
||||
const entries = curateProjectedActivityEntries(selected.items, {
|
||||
maxItems: 0,
|
||||
labelAssistantMessages: true,
|
||||
includeKinds: ["user_message", "assistant_message", "tool_call"],
|
||||
includeExternalToolInput: false,
|
||||
});
|
||||
const body =
|
||||
entries.length > 0
|
||||
? entries.map((entry) => entry.text).join("\n")
|
||||
: "No chat history to display.";
|
||||
return {
|
||||
attachment: {
|
||||
type: "text",
|
||||
mimeType: "text/plain",
|
||||
contextKind: "chat_history",
|
||||
title: "Chat history",
|
||||
text: buildForkContextText({
|
||||
body,
|
||||
agentTitle: input.agentTitle,
|
||||
cwd: input.cwd,
|
||||
}),
|
||||
},
|
||||
itemCount: selected.items.length,
|
||||
boundaryMessageId: selected.boundaryMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ const MODELS: AgentModelDefinition[] = [
|
||||
|
||||
interface ActiveTurn {
|
||||
turnId: string;
|
||||
assistantMessageId: string;
|
||||
prompt: AgentPromptInput;
|
||||
startedAt: number;
|
||||
cycle: number;
|
||||
@@ -609,12 +610,14 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
|
||||
const profile = resolveModelProfile(this.modelId);
|
||||
const turnId = randomUUID();
|
||||
const assistantMessageId = randomUUID();
|
||||
let resolve!: (result: AgentRunResult) => void;
|
||||
const completed = new Promise<AgentRunResult>((promiseResolve) => {
|
||||
resolve = promiseResolve;
|
||||
});
|
||||
const turn: ActiveTurn = {
|
||||
turnId,
|
||||
assistantMessageId,
|
||||
prompt,
|
||||
startedAt: Date.now(),
|
||||
cycle: 0,
|
||||
@@ -860,6 +863,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
this.emitTimeline(turn.turnId, {
|
||||
type: "assistant_message",
|
||||
text: finalText,
|
||||
messageId: turn.assistantMessageId,
|
||||
});
|
||||
this.activeTurn = null;
|
||||
this.emit({
|
||||
@@ -874,6 +878,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
{
|
||||
type: "assistant_message",
|
||||
text: finalText,
|
||||
messageId: turn.assistantMessageId,
|
||||
},
|
||||
],
|
||||
canceled: false,
|
||||
@@ -989,6 +994,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
? {
|
||||
type: "assistant_message",
|
||||
text: `stress-update-${index}`,
|
||||
messageId: turn.assistantMessageId,
|
||||
}
|
||||
: {
|
||||
type: "todo",
|
||||
@@ -1067,6 +1073,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
this.emitTimeline(turn.turnId, {
|
||||
type: "assistant_message",
|
||||
text: `data:image/png;base64,${payload}`,
|
||||
messageId: turn.assistantMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1133,6 +1140,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
this.emitTimeline(turn.turnId, {
|
||||
type: "assistant_message",
|
||||
text: event.text,
|
||||
messageId: turn.assistantMessageId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1178,6 +1186,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
this.emitTimeline(turn.turnId, {
|
||||
type: "assistant_message",
|
||||
text: "\n\n_(end of synthetic stream)_\n",
|
||||
messageId: turn.assistantMessageId,
|
||||
});
|
||||
this.finishTurnWithText(turn, "Synthetic load test complete");
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ import {
|
||||
type TimelineProjectionEntry,
|
||||
type TimelineProjectionMode,
|
||||
} from "./agent/timeline-projection.js";
|
||||
import { buildAgentForkContextAttachment } from "./agent/activity-curator.js";
|
||||
import type { StructuredGenerationDaemonConfig } from "./agent/structured-generation-providers.js";
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
@@ -1371,6 +1372,7 @@ export class Session {
|
||||
this.dispatchVoiceAndControlMessage(msg) ??
|
||||
this.dispatchAgentRewindMessage(msg) ??
|
||||
this.dispatchAgentRelationshipMessage(msg) ??
|
||||
this.dispatchAgentTimelineMessage(msg) ??
|
||||
this.dispatchAgentLifecycleMessage(msg) ??
|
||||
this.dispatchAgentConfigMessage(msg) ??
|
||||
this.dispatchCheckoutMessage(msg) ??
|
||||
@@ -1450,6 +1452,17 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchAgentTimelineMessage(msg: SessionInboundMessage): Promise<void> | undefined {
|
||||
switch (msg.type) {
|
||||
case "fetch_agent_timeline_request":
|
||||
return this.handleFetchAgentTimelineRequest(msg);
|
||||
case "agent.fork_context.request":
|
||||
return this.handleAgentForkContextRequest(msg);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchAgentLifecycleMessage(msg: SessionInboundMessage): Promise<void> | undefined {
|
||||
switch (msg.type) {
|
||||
case "fetch_agents_request":
|
||||
@@ -1484,8 +1497,6 @@ export class Session {
|
||||
return this.handleRefreshAgentRequest(msg);
|
||||
case "cancel_agent_request":
|
||||
return this.handleCancelAgentRequest(msg.agentId, msg.requestId);
|
||||
case "fetch_agent_timeline_request":
|
||||
return this.handleFetchAgentTimelineRequest(msg);
|
||||
case "agent_permission_response":
|
||||
return this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
|
||||
case "clear_agent_attention":
|
||||
@@ -5355,6 +5366,57 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAgentForkContextRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "agent.fork_context.request" }>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const snapshot = await ensureAgentLoaded(msg.agentId, {
|
||||
agentManager: this.agentManager,
|
||||
agentStorage: this.agentStorage,
|
||||
logger: this.sessionLogger,
|
||||
});
|
||||
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||
const rows = this.agentManager.fetchTimeline(msg.agentId, {
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
}).rows;
|
||||
const forkContext = buildAgentForkContextAttachment({
|
||||
rows,
|
||||
boundaryMessageId: msg.boundaryMessageId,
|
||||
agentTitle: agentPayload.title,
|
||||
cwd: snapshot.cwd,
|
||||
});
|
||||
|
||||
this.emit({
|
||||
type: "agent.fork_context.response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
agentId: msg.agentId,
|
||||
attachment: forkContext.attachment,
|
||||
itemCount: forkContext.itemCount,
|
||||
boundaryMessageId: forkContext.boundaryMessageId,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sessionLogger.error(
|
||||
{ err: error, agentId: msg.agentId },
|
||||
"Failed to handle agent.fork_context.request",
|
||||
);
|
||||
this.emit({
|
||||
type: "agent.fork_context.response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
agentId: msg.agentId,
|
||||
attachment: null,
|
||||
itemCount: 0,
|
||||
boundaryMessageId: msg.boundaryMessageId ?? null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSendAgentMessageRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "send_agent_message_request" }>,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -1200,6 +1200,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
daemonDiagnostics: true,
|
||||
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
|
||||
daemonSelfUpdate: true,
|
||||
// COMPAT(agentForkContext): added in v0.1.102, remove gate after 2026-12-28.
|
||||
agentForkContext: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user