@f5-sales-demo/xcsh 22.7.2 → 22.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/src/config/settings-schema.ts +0 -27
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +0 -4
- package/src/modes/components/settings-defs.ts +0 -7
- package/src/modes/controllers/event-controller.ts +15 -1
- package/src/modes/controllers/input-controller.ts +2 -16
- package/src/prompts/system/plan-mode-approved.md +1 -3
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/system/title-system.md +7 -7
- package/src/prompts/tools/todo-write.md +6 -18
- package/src/remote-control/session.ts +4 -9
- package/src/remote-control/voice.ts +1 -1
- package/src/session/agent-session.ts +15 -190
- package/src/session/session-manager.ts +48 -2
- package/src/tools/todo-write.ts +5 -0
- package/src/utils/title-generator.ts +181 -39
- package/src/prompts/system/eager-todo.md +0 -13
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HerdrClient } from "../herdr/client";
|
|
2
2
|
import { HerdrInteractionBridge } from "../herdr/interactions";
|
|
3
|
+
import { coordinateSessionTitle } from "../utils/title-generator";
|
|
3
4
|
/**
|
|
4
5
|
* AgentSession - Core abstraction for agent lifecycle and session management.
|
|
5
6
|
*
|
|
@@ -131,7 +132,6 @@ import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
|
131
132
|
import type { PlanModeState } from "../plan-mode/state";
|
|
132
133
|
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
|
133
134
|
import defaultModePrompt from "../prompts/system/default-mode-active.md" with { type: "text" };
|
|
134
|
-
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
135
135
|
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
|
136
136
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
137
137
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
@@ -621,8 +621,7 @@ export class AgentSession {
|
|
|
621
621
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
622
622
|
#retryResolve: (() => void) | undefined = undefined;
|
|
623
623
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
624
|
-
//
|
|
625
|
-
#todoReminderCount = 0;
|
|
624
|
+
// Session progress tracking
|
|
626
625
|
#todoPhases: TodoPhase[] = [];
|
|
627
626
|
#todoClearTimers = new Map<string, Timer>();
|
|
628
627
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
@@ -1589,7 +1588,7 @@ export class AgentSession {
|
|
|
1589
1588
|
if (toolName === "todo_write" && !isError && Array.isArray(details?.phases)) {
|
|
1590
1589
|
this.setTodoPhases(details.phases);
|
|
1591
1590
|
}
|
|
1592
|
-
if (toolName === "todo_write" && isError) {
|
|
1591
|
+
if (toolName === "todo_write" && isError && !this.#planModeState?.enabled) {
|
|
1593
1592
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
1594
1593
|
const reminderText = [
|
|
1595
1594
|
"<system-reminder>",
|
|
@@ -1667,7 +1666,7 @@ export class AgentSession {
|
|
|
1667
1666
|
const compactionTask = this.#checkCompaction(msg);
|
|
1668
1667
|
this.#trackPostPromptTask(compactionTask);
|
|
1669
1668
|
await compactionTask;
|
|
1670
|
-
//
|
|
1669
|
+
// Name only completed exchanges, never intermediate tool-use turns.
|
|
1671
1670
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
1672
1671
|
if (hasToolCalls) {
|
|
1673
1672
|
return;
|
|
@@ -1676,7 +1675,7 @@ export class AgentSession {
|
|
|
1676
1675
|
if (this.#enforceRewindBeforeYield()) {
|
|
1677
1676
|
return;
|
|
1678
1677
|
}
|
|
1679
|
-
|
|
1678
|
+
if (this.#promptInFlightCount === 0) void coordinateSessionTitle(this);
|
|
1680
1679
|
}
|
|
1681
1680
|
}
|
|
1682
1681
|
};
|
|
@@ -3447,14 +3446,6 @@ export class AgentSession {
|
|
|
3447
3446
|
}
|
|
3448
3447
|
}
|
|
3449
3448
|
|
|
3450
|
-
// Skip eager todo prelude when the user has already queued a directive
|
|
3451
|
-
const pendingDirectives = this.#toolChoiceQueue.inspect();
|
|
3452
|
-
const hasPendingUserDirective = pendingDirectives.includes("user-force");
|
|
3453
|
-
const eagerTodoPrelude =
|
|
3454
|
-
!options?.synthetic && !hasPendingUserDirective && !pendingDirectives.includes("context-selection")
|
|
3455
|
-
? this.#createEagerTodoPrelude(expandedText)
|
|
3456
|
-
: undefined;
|
|
3457
|
-
|
|
3458
3449
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
3459
3450
|
if (options?.images) {
|
|
3460
3451
|
userContent.push(...options.images);
|
|
@@ -3488,22 +3479,7 @@ export class AgentSession {
|
|
|
3488
3479
|
}
|
|
3489
3480
|
}
|
|
3490
3481
|
|
|
3491
|
-
|
|
3492
|
-
this.#toolChoiceQueue.pushOnce(eagerTodoPrelude.toolChoice, {
|
|
3493
|
-
label: "eager-todo",
|
|
3494
|
-
});
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
try {
|
|
3498
|
-
await this.#promptWithMessage(message, expandedText, {
|
|
3499
|
-
...options,
|
|
3500
|
-
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
3501
|
-
});
|
|
3502
|
-
} finally {
|
|
3503
|
-
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
3504
|
-
// (e.g., compaction aborted, validation failed).
|
|
3505
|
-
this.#toolChoiceQueue.removeByLabel("eager-todo");
|
|
3506
|
-
}
|
|
3482
|
+
await this.#promptWithMessage(message, expandedText, options);
|
|
3507
3483
|
}
|
|
3508
3484
|
|
|
3509
3485
|
async promptCustomMessage<T = unknown>(
|
|
@@ -3847,9 +3823,6 @@ export class AgentSession {
|
|
|
3847
3823
|
this.#flushPendingBashMessages();
|
|
3848
3824
|
this.#flushPendingPythonMessages();
|
|
3849
3825
|
|
|
3850
|
-
// Reset todo reminder count on new user prompt
|
|
3851
|
-
this.#todoReminderCount = 0;
|
|
3852
|
-
|
|
3853
3826
|
// Classified F5 XC API/schema intent is discovered locally before any
|
|
3854
3827
|
// compaction or provider inference. A QMD/index error is deliberately
|
|
3855
3828
|
// terminal for this turn so the model cannot substitute web search or a guess.
|
|
@@ -3996,6 +3969,15 @@ export class AgentSession {
|
|
|
3996
3969
|
if (!options?.skipPostPromptRecoveryWait) {
|
|
3997
3970
|
await this.#waitForPostPromptRecovery();
|
|
3998
3971
|
}
|
|
3972
|
+
const finalAssistant = this.#findLastAssistantMessage();
|
|
3973
|
+
if (
|
|
3974
|
+
this.#isPromptCurrent(generation) &&
|
|
3975
|
+
finalAssistant &&
|
|
3976
|
+
finalAssistant.stopReason !== "error" &&
|
|
3977
|
+
finalAssistant.stopReason !== "aborted" &&
|
|
3978
|
+
!finalAssistant.content.some(part => part.type === "toolCall")
|
|
3979
|
+
)
|
|
3980
|
+
void coordinateSessionTitle(this);
|
|
3999
3981
|
} catch (error) {
|
|
4000
3982
|
this.#turnPhase.settle(this.#findLastAssistantMessage()?.stopReason === "aborted" ? "aborted" : "error");
|
|
4001
3983
|
settled = true;
|
|
@@ -4769,7 +4751,6 @@ export class AgentSession {
|
|
|
4769
4751
|
if (this.model) this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
4770
4752
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
4771
4753
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
4772
|
-
this.#todoReminderCount = 0;
|
|
4773
4754
|
this.#reconnectToAgent();
|
|
4774
4755
|
|
|
4775
4756
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -5662,7 +5643,6 @@ export class AgentSession {
|
|
|
5662
5643
|
this.#followUpMessages = [];
|
|
5663
5644
|
this.#pendingNextTurnMessages = [];
|
|
5664
5645
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5665
|
-
this.#todoReminderCount = 0;
|
|
5666
5646
|
} else if (
|
|
5667
5647
|
this.sessionManager.getSessionId() !== preview.targetSessionId ||
|
|
5668
5648
|
this.sessionManager.getSessionFile() !== preview.targetSessionFile
|
|
@@ -5839,161 +5819,6 @@ export class AgentSession {
|
|
|
5839
5819
|
this.#pendingRewindReport = undefined;
|
|
5840
5820
|
}
|
|
5841
5821
|
|
|
5842
|
-
#createEagerTodoPrelude(promptText: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
|
|
5843
|
-
const eagerTodosEnabled = this.settings.get("todo.eager");
|
|
5844
|
-
const todosEnabled = this.settings.get("todo.enabled");
|
|
5845
|
-
if (!eagerTodosEnabled || !todosEnabled) {
|
|
5846
|
-
return undefined;
|
|
5847
|
-
}
|
|
5848
|
-
|
|
5849
|
-
if (this.#planModeState?.enabled) {
|
|
5850
|
-
return undefined;
|
|
5851
|
-
}
|
|
5852
|
-
if (this.getTodoPhases().length > 0) {
|
|
5853
|
-
return undefined;
|
|
5854
|
-
}
|
|
5855
|
-
|
|
5856
|
-
// Only inject on the first user message of the conversation. Subsequent user
|
|
5857
|
-
// turns must not receive the eager todo reminder — they often correct, clarify,
|
|
5858
|
-
// or redirect the prior task, and forcing a brand-new todo list there is wrong.
|
|
5859
|
-
const hasPriorUserMessage = this.agent.state.messages.some(m => m.role === "user");
|
|
5860
|
-
if (hasPriorUserMessage) {
|
|
5861
|
-
return undefined;
|
|
5862
|
-
}
|
|
5863
|
-
|
|
5864
|
-
const trimmedPromptText = promptText.trim();
|
|
5865
|
-
if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) {
|
|
5866
|
-
return undefined;
|
|
5867
|
-
}
|
|
5868
|
-
const normalizedPromptText = trimmedPromptText.toLowerCase();
|
|
5869
|
-
const startsWithQuestionWord = /^(?:who|what|when|where|why|how)\b/.test(normalizedPromptText);
|
|
5870
|
-
const startsWithConversationalQuestion = /^(?:am|are|do|have) you\b/.test(normalizedPromptText);
|
|
5871
|
-
const isConversationalGreeting =
|
|
5872
|
-
/^(?:hello|hi|hey|thanks|thank you|good (?:morning|afternoon|evening))\b/.test(normalizedPromptText) ||
|
|
5873
|
-
/^tell me about yourself\b/.test(normalizedPromptText);
|
|
5874
|
-
const hasSubstantiveAction =
|
|
5875
|
-
/\b(?:add|build|change|create|debug|deploy|fix|implement|inspect|investigate|modify|refactor|remove|repair|review|run|test|update|verify|write)\b/.test(
|
|
5876
|
-
normalizedPromptText,
|
|
5877
|
-
);
|
|
5878
|
-
if (
|
|
5879
|
-
!hasSubstantiveAction &&
|
|
5880
|
-
(startsWithQuestionWord || startsWithConversationalQuestion || isConversationalGreeting)
|
|
5881
|
-
) {
|
|
5882
|
-
return undefined;
|
|
5883
|
-
}
|
|
5884
|
-
|
|
5885
|
-
if (!this.agent.state.tools.some(tool => tool.name === "todo_write")) {
|
|
5886
|
-
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
5887
|
-
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
5888
|
-
});
|
|
5889
|
-
return undefined;
|
|
5890
|
-
}
|
|
5891
|
-
|
|
5892
|
-
const todoWriteToolChoice = buildNamedToolChoice("todo_write", this.model);
|
|
5893
|
-
if (!todoWriteToolChoice) {
|
|
5894
|
-
logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo_write", {
|
|
5895
|
-
modelApi: this.model?.api,
|
|
5896
|
-
modelId: this.model?.id,
|
|
5897
|
-
});
|
|
5898
|
-
return undefined;
|
|
5899
|
-
}
|
|
5900
|
-
|
|
5901
|
-
const eagerTodoReminder = prompt.render(eagerTodoPrompt);
|
|
5902
|
-
|
|
5903
|
-
return {
|
|
5904
|
-
message: {
|
|
5905
|
-
role: "custom",
|
|
5906
|
-
customType: "eager-todo-prelude",
|
|
5907
|
-
content: eagerTodoReminder,
|
|
5908
|
-
display: false,
|
|
5909
|
-
attribution: "agent",
|
|
5910
|
-
timestamp: Date.now(),
|
|
5911
|
-
},
|
|
5912
|
-
toolChoice: todoWriteToolChoice,
|
|
5913
|
-
};
|
|
5914
|
-
}
|
|
5915
|
-
/**
|
|
5916
|
-
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
5917
|
-
*/
|
|
5918
|
-
async #checkTodoCompletion(): Promise<void> {
|
|
5919
|
-
// Skip todo reminders when the most recent turn was driven by an explicit user force —
|
|
5920
|
-
// the user wanted exactly that tool, not a follow-up nag about incomplete todos.
|
|
5921
|
-
const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
|
|
5922
|
-
if (lastServedLabel === "user-force") {
|
|
5923
|
-
return;
|
|
5924
|
-
}
|
|
5925
|
-
|
|
5926
|
-
const remindersEnabled = this.settings.get("todo.reminders");
|
|
5927
|
-
const todosEnabled = this.settings.get("todo.enabled");
|
|
5928
|
-
if (!remindersEnabled || !todosEnabled) {
|
|
5929
|
-
this.#todoReminderCount = 0;
|
|
5930
|
-
return;
|
|
5931
|
-
}
|
|
5932
|
-
|
|
5933
|
-
const remindersMax = this.settings.get("todo.reminders.max");
|
|
5934
|
-
if (this.#todoReminderCount >= remindersMax) {
|
|
5935
|
-
logger.debug("Todo completion: max reminders reached", { count: this.#todoReminderCount });
|
|
5936
|
-
return;
|
|
5937
|
-
}
|
|
5938
|
-
|
|
5939
|
-
const phases = this.getTodoPhases();
|
|
5940
|
-
if (phases.length === 0) {
|
|
5941
|
-
this.#todoReminderCount = 0;
|
|
5942
|
-
return;
|
|
5943
|
-
}
|
|
5944
|
-
|
|
5945
|
-
const incompleteByPhase = phases
|
|
5946
|
-
.map(phase => ({
|
|
5947
|
-
name: phase.name,
|
|
5948
|
-
tasks: phase.tasks
|
|
5949
|
-
.filter(
|
|
5950
|
-
(task): task is TodoItem & { status: "pending" | "in_progress" } =>
|
|
5951
|
-
task.status === "pending" || task.status === "in_progress",
|
|
5952
|
-
)
|
|
5953
|
-
.map(task => ({ id: task.id, content: task.content, status: task.status })),
|
|
5954
|
-
}))
|
|
5955
|
-
.filter(phase => phase.tasks.length > 0);
|
|
5956
|
-
const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
|
|
5957
|
-
if (incomplete.length === 0) {
|
|
5958
|
-
this.#todoReminderCount = 0;
|
|
5959
|
-
return;
|
|
5960
|
-
}
|
|
5961
|
-
|
|
5962
|
-
// Build reminder message
|
|
5963
|
-
this.#todoReminderCount++;
|
|
5964
|
-
const todoList = incompleteByPhase
|
|
5965
|
-
.map(phase => `- ${phase.name}\n${phase.tasks.map(task => ` - ${task.content}`).join("\n")}`)
|
|
5966
|
-
.join("\n");
|
|
5967
|
-
const reminder =
|
|
5968
|
-
`<system-reminder>\n` +
|
|
5969
|
-
`You stopped with ${incomplete.length} incomplete todo item(s):\n${todoList}\n\n` +
|
|
5970
|
-
`Please continue working on these tasks or mark them complete if finished.\n` +
|
|
5971
|
-
`(Reminder ${this.#todoReminderCount}/${remindersMax})\n` +
|
|
5972
|
-
`</system-reminder>`;
|
|
5973
|
-
|
|
5974
|
-
logger.debug("Todo completion: sending reminder", {
|
|
5975
|
-
incomplete: incomplete.length,
|
|
5976
|
-
attempt: this.#todoReminderCount,
|
|
5977
|
-
});
|
|
5978
|
-
|
|
5979
|
-
// Emit event for UI to render notification
|
|
5980
|
-
await this.#emitSessionEvent({
|
|
5981
|
-
type: "todo_reminder",
|
|
5982
|
-
todos: incomplete,
|
|
5983
|
-
attempt: this.#todoReminderCount,
|
|
5984
|
-
maxAttempts: remindersMax,
|
|
5985
|
-
});
|
|
5986
|
-
|
|
5987
|
-
// Inject reminder and continue the conversation
|
|
5988
|
-
this.agent.appendMessage({
|
|
5989
|
-
role: "developer",
|
|
5990
|
-
content: [{ type: "text", text: reminder }],
|
|
5991
|
-
attribution: "agent",
|
|
5992
|
-
timestamp: Date.now(),
|
|
5993
|
-
});
|
|
5994
|
-
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
5995
|
-
}
|
|
5996
|
-
|
|
5997
5822
|
/**
|
|
5998
5823
|
* Attempt context promotion to a larger model.
|
|
5999
5824
|
* Returns true if promotion succeeded (caller should retry without compacting).
|
|
@@ -56,11 +56,17 @@ import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
|
56
56
|
export const CURRENT_SESSION_VERSION = 3;
|
|
57
57
|
const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
|
|
58
58
|
|
|
59
|
+
export interface AutomaticTitleState {
|
|
60
|
+
status: "provisional" | "refined";
|
|
61
|
+
exchangeId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
export interface SessionHeader {
|
|
60
65
|
type: "session";
|
|
61
66
|
version?: number; // v1 sessions don't have this
|
|
62
67
|
id: string;
|
|
63
|
-
title?: string; //
|
|
68
|
+
title?: string; // Session display name
|
|
69
|
+
automaticTitle?: AutomaticTitleState;
|
|
64
70
|
titleSource?: "auto" | "user";
|
|
65
71
|
timestamp: string;
|
|
66
72
|
cwd: string;
|
|
@@ -1497,6 +1503,7 @@ interface SessionManagerStateSnapshot {
|
|
|
1497
1503
|
|
|
1498
1504
|
export class SessionManager {
|
|
1499
1505
|
#sessionId: string = "";
|
|
1506
|
+
#titleRevision = 0;
|
|
1500
1507
|
#sessionName: string | undefined;
|
|
1501
1508
|
#titleSource: "auto" | "user" | undefined;
|
|
1502
1509
|
#sessionFile: string | undefined;
|
|
@@ -1567,6 +1574,7 @@ export class SessionManager {
|
|
|
1567
1574
|
}
|
|
1568
1575
|
|
|
1569
1576
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
1577
|
+
this.#titleRevision++;
|
|
1570
1578
|
this.#sessionId = snapshot.sessionId;
|
|
1571
1579
|
this.#sessionName = snapshot.sessionName;
|
|
1572
1580
|
this.#titleSource = snapshot.titleSource;
|
|
@@ -1599,6 +1607,7 @@ export class SessionManager {
|
|
|
1599
1607
|
|
|
1600
1608
|
/** Switch to a different session file (used for resume and branching) */
|
|
1601
1609
|
async setSessionFile(sessionFile: string): Promise<void> {
|
|
1610
|
+
this.#titleRevision++;
|
|
1602
1611
|
await this.#closePersistWriter();
|
|
1603
1612
|
this.#persistError = undefined;
|
|
1604
1613
|
this.#persistErrorReported = false;
|
|
@@ -1607,6 +1616,7 @@ export class SessionManager {
|
|
|
1607
1616
|
this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
|
|
1608
1617
|
if (this.#fileEntries.length > 0) {
|
|
1609
1618
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1619
|
+
this.#titleRevision++;
|
|
1610
1620
|
this.#sessionId = header?.id ?? Snowflake.next();
|
|
1611
1621
|
this.#sessionName = header?.title;
|
|
1612
1622
|
this.#titleSource = header?.titleSource;
|
|
@@ -1632,6 +1642,7 @@ export class SessionManager {
|
|
|
1632
1642
|
|
|
1633
1643
|
/** Start a new session. Closes any existing writer first. */
|
|
1634
1644
|
async newSession(options?: NewSessionOptions, preview?: SessionNewPreview): Promise<string | undefined> {
|
|
1645
|
+
this.#titleRevision++;
|
|
1635
1646
|
await this.#closePersistWriter();
|
|
1636
1647
|
return this.#newSessionSync(options, preview);
|
|
1637
1648
|
}
|
|
@@ -1727,6 +1738,7 @@ export class SessionManager {
|
|
|
1727
1738
|
this.#persistErrorReported = false;
|
|
1728
1739
|
|
|
1729
1740
|
// Create new session ID and header
|
|
1741
|
+
this.#titleRevision++;
|
|
1730
1742
|
this.#sessionId = preview.targetSessionId;
|
|
1731
1743
|
const timestamp = preview.timestamp;
|
|
1732
1744
|
this.#sessionFile = preview.targetSessionFile;
|
|
@@ -1739,6 +1751,7 @@ export class SessionManager {
|
|
|
1739
1751
|
id: this.#sessionId,
|
|
1740
1752
|
title: oldHeader?.title ?? this.#sessionName,
|
|
1741
1753
|
titleSource: oldHeader?.titleSource ?? this.#titleSource,
|
|
1754
|
+
automaticTitle: oldHeader?.automaticTitle,
|
|
1742
1755
|
timestamp,
|
|
1743
1756
|
cwd: this.cwd,
|
|
1744
1757
|
parentSession: oldSessionId,
|
|
@@ -1904,6 +1917,7 @@ export class SessionManager {
|
|
|
1904
1917
|
this.#persistChain = Promise.resolve();
|
|
1905
1918
|
this.#persistError = undefined;
|
|
1906
1919
|
this.#persistErrorReported = false;
|
|
1920
|
+
this.#titleRevision++;
|
|
1907
1921
|
this.#sessionId = preview?.targetSessionId ?? Snowflake.next();
|
|
1908
1922
|
this.#sessionName = options?.title ? SessionManager.#sanitizeName(options.title) || undefined : undefined;
|
|
1909
1923
|
this.#titleSource = this.#sessionName ? options?.titleSource : undefined;
|
|
@@ -2336,6 +2350,25 @@ export class SessionManager {
|
|
|
2336
2350
|
return this.#sessionName;
|
|
2337
2351
|
}
|
|
2338
2352
|
|
|
2353
|
+
get titleRevision(): number {
|
|
2354
|
+
return this.#titleRevision;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
getAutomaticTitleState(): AutomaticTitleState | undefined {
|
|
2358
|
+
return this.getHeader()?.automaticTitle;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/** Validate before mutation so late requests cannot overwrite a rename or a session transition. */
|
|
2362
|
+
setAutomaticSessionName(
|
|
2363
|
+
name: string,
|
|
2364
|
+
state: AutomaticTitleState,
|
|
2365
|
+
sessionId: string,
|
|
2366
|
+
revision: number,
|
|
2367
|
+
): Promise<boolean> {
|
|
2368
|
+
if (sessionId !== this.#sessionId || revision !== this.#titleRevision) return Promise.resolve(false);
|
|
2369
|
+
return this.setSessionName(name, "auto", state);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2339
2372
|
/** Strip C0/C1 control characters (includes ESC, so removes ANSI sequences) and collapse whitespace. */
|
|
2340
2373
|
static #sanitizeName(name: string): string {
|
|
2341
2374
|
return name
|
|
@@ -2349,7 +2382,11 @@ export class SessionManager {
|
|
|
2349
2382
|
* @param source - "user" for explicit renames (/rename command, RPC); "auto" for generated titles.
|
|
2350
2383
|
* Auto-generated titles are silently ignored when the user has already set a name.
|
|
2351
2384
|
*/
|
|
2352
|
-
async setSessionName(
|
|
2385
|
+
async setSessionName(
|
|
2386
|
+
name: string,
|
|
2387
|
+
source: "auto" | "user" = "auto",
|
|
2388
|
+
automaticTitle?: AutomaticTitleState,
|
|
2389
|
+
): Promise<boolean> {
|
|
2353
2390
|
if (name === "New Realtime Voice Chat") return false;
|
|
2354
2391
|
// User-set names take permanent precedence over auto-generated ones.
|
|
2355
2392
|
if (this.#titleSource === "user" && source === "auto") return false;
|
|
@@ -2357,6 +2394,7 @@ export class SessionManager {
|
|
|
2357
2394
|
const sanitized = SessionManager.#sanitizeName(name);
|
|
2358
2395
|
if (!sanitized) return false;
|
|
2359
2396
|
|
|
2397
|
+
this.#titleRevision++;
|
|
2360
2398
|
this.#sessionName = sanitized;
|
|
2361
2399
|
this.#titleSource = source;
|
|
2362
2400
|
|
|
@@ -2365,6 +2403,7 @@ export class SessionManager {
|
|
|
2365
2403
|
if (header) {
|
|
2366
2404
|
header.title = sanitized;
|
|
2367
2405
|
header.titleSource = source;
|
|
2406
|
+
header.automaticTitle = source === "auto" ? automaticTitle : undefined;
|
|
2368
2407
|
}
|
|
2369
2408
|
|
|
2370
2409
|
// Update the session file header with the title (if already flushed)
|
|
@@ -2911,6 +2950,7 @@ export class SessionManager {
|
|
|
2911
2950
|
if (!this.#byId.has(branchFromId)) {
|
|
2912
2951
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
2913
2952
|
}
|
|
2953
|
+
this.#titleRevision++;
|
|
2914
2954
|
this.#leafId = branchFromId;
|
|
2915
2955
|
}
|
|
2916
2956
|
|
|
@@ -2920,6 +2960,7 @@ export class SessionManager {
|
|
|
2920
2960
|
* Use this when navigating to re-edit the first user message.
|
|
2921
2961
|
*/
|
|
2922
2962
|
resetLeaf(): void {
|
|
2963
|
+
this.#titleRevision++;
|
|
2923
2964
|
this.#leafId = null;
|
|
2924
2965
|
}
|
|
2925
2966
|
|
|
@@ -2932,6 +2973,7 @@ export class SessionManager {
|
|
|
2932
2973
|
if (branchFromId !== null && !this.#byId.has(branchFromId)) {
|
|
2933
2974
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
2934
2975
|
}
|
|
2976
|
+
this.#titleRevision++;
|
|
2935
2977
|
this.#leafId = branchFromId;
|
|
2936
2978
|
const entry: BranchSummaryEntry = {
|
|
2937
2979
|
type: "branch_summary",
|
|
@@ -3044,6 +3086,7 @@ export class SessionManager {
|
|
|
3044
3086
|
}
|
|
3045
3087
|
this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
|
|
3046
3088
|
this.#fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
3089
|
+
this.#titleRevision++;
|
|
3047
3090
|
this.#sessionId = newSessionId;
|
|
3048
3091
|
this.#sessionFile = newSessionFile;
|
|
3049
3092
|
this.#flushed = true;
|
|
@@ -3067,6 +3110,7 @@ export class SessionManager {
|
|
|
3067
3110
|
parentId = labelEntry.id;
|
|
3068
3111
|
}
|
|
3069
3112
|
this.#fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
3113
|
+
this.#titleRevision++;
|
|
3070
3114
|
this.#sessionId = newSessionId;
|
|
3071
3115
|
this.#buildIndex();
|
|
3072
3116
|
return undefined;
|
|
@@ -3116,6 +3160,7 @@ export class SessionManager {
|
|
|
3116
3160
|
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
3117
3161
|
newHeader.title = sourceHeader?.title;
|
|
3118
3162
|
newHeader.titleSource = sourceHeader?.titleSource;
|
|
3163
|
+
newHeader.automaticTitle = sourceHeader?.automaticTitle;
|
|
3119
3164
|
manager.#fileEntries = [newHeader, ...historyEntries];
|
|
3120
3165
|
manager.#sessionName = newHeader.title;
|
|
3121
3166
|
manager.#titleSource = newHeader.titleSource;
|
|
@@ -3145,6 +3190,7 @@ export class SessionManager {
|
|
|
3145
3190
|
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
3146
3191
|
newHeader.title = sourceHeader?.title;
|
|
3147
3192
|
newHeader.titleSource = sourceHeader?.titleSource;
|
|
3193
|
+
newHeader.automaticTitle = sourceHeader?.automaticTitle;
|
|
3148
3194
|
manager.#fileEntries = [newHeader, ...historyEntries];
|
|
3149
3195
|
manager.#sessionName = newHeader.title;
|
|
3150
3196
|
manager.#titleSource = newHeader.titleSource;
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -368,6 +368,11 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
368
368
|
_onUpdate?: AgentToolUpdateCallback<TodoWriteToolDetails>,
|
|
369
369
|
_context?: AgentToolContext,
|
|
370
370
|
): Promise<AgentToolResult<TodoWriteToolDetails>> {
|
|
371
|
+
if (this.session.getPlanModeState?.()?.enabled) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
"todo_write is unavailable in Plan Mode. Discuss the proposed plan in the conversation; execution tracking resumes in Default mode.",
|
|
374
|
+
);
|
|
375
|
+
}
|
|
371
376
|
const previousPhases = this.session.getTodoPhases?.() ?? [];
|
|
372
377
|
const current = fileFromPhases(previousPhases);
|
|
373
378
|
const { file: updated, errors } = applyOps(current, params.ops);
|