mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 15:03:31 +00:00
Consolidate session events: remove session_before_new/session_new, add reason field to switch events
- Remove session_before_new and session_new hook events - Add reason: 'new' | 'resume' to session_before_switch and session_switch events - Remove 'new' reason from custom tool onSession (use 'switch' for both /new and /resume) - Rename reset() to newSession(options?) in AgentSession - Add NewSessionOptions with optional parentSession for lineage tracking - Rename branchedFrom to parentSession in SessionHeader - Rename RPC reset command to new_session with optional parentSession - Update example hooks to use new event structure - Update documentation and changelog Based on discussion in #293
This commit is contained in:
parent
1d9fa13d58
commit
484d7e06bb
19 changed files with 117 additions and 117 deletions
|
|
@ -23,7 +23,8 @@ The hooks API has been restructured with more granular events and better session
|
|||
- `HookCommandContext` removed (use `HookContext` for command handlers)
|
||||
|
||||
**Event changes:**
|
||||
- The monolithic `session` event is now split into granular events: `session_start`, `session_before_switch`, `session_switch`, `session_before_new`, `session_new`, `session_before_branch`, `session_branch`, `session_before_compact`, `session_compact`, `session_shutdown`
|
||||
- The monolithic `session` event is now split into granular events: `session_start`, `session_before_switch`, `session_switch`, `session_before_branch`, `session_branch`, `session_before_compact`, `session_compact`, `session_shutdown`
|
||||
- `session_before_switch` and `session_switch` events now include `reason: "new" | "resume"` to distinguish between `/new` and `/resume`
|
||||
- New `session_before_tree` and `session_tree` events for `/tree` navigation (hook can provide custom branch summary)
|
||||
- New `before_agent_start` event: inject messages before the agent loop starts
|
||||
- New `context` event: modify messages non-destructively before each LLM call
|
||||
|
|
@ -71,7 +72,7 @@ The new `ctx: CustomToolContext` provides `sessionManager`, `modelRegistry`, and
|
|||
**Session event changes:**
|
||||
- `CustomToolSessionEvent` now only has `reason` and `previousSessionFile`
|
||||
- Session entries are no longer in the event. Use `ctx.sessionManager.getBranch()` or `ctx.sessionManager.getEntries()` to reconstruct state
|
||||
- New reasons: `"tree"` (for `/tree` navigation) and `"shutdown"` (for cleanup on exit)
|
||||
- Reasons: `"start" | "switch" | "branch" | "tree" | "shutdown"` (no separate `"new"` reason; `/new` triggers `"switch"`)
|
||||
- `dispose()` method removed. Use `onSession` with `reason: "shutdown"` for cleanup
|
||||
|
||||
See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](examples/custom-tools/) for the current API.
|
||||
|
|
@ -85,10 +86,11 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex
|
|||
- `model` returns `Model | undefined` (was `Model | null`)
|
||||
- `Attachment` type removed. Use `ImageContent` from `@mariozechner/pi-ai` instead. Add images directly to message content arrays.
|
||||
|
||||
**AgentSession branching API:**
|
||||
**AgentSession API:**
|
||||
- `branch(entryIndex: number)` → `branch(entryId: string)`
|
||||
- `getUserMessagesForBranching()` returns `{ entryId, text }` instead of `{ entryIndex, text }`
|
||||
- `reset()` and `switchSession()` now return `Promise<boolean>` (false if cancelled by hook)
|
||||
- `reset()` → `newSession(options?)` where options has optional `parentSession` for lineage tracking
|
||||
- `newSession()` and `switchSession()` now return `Promise<boolean>` (false if cancelled by hook)
|
||||
- New `navigateTree(targetId, options?)` for in-place tree navigation
|
||||
|
||||
**Hook integration:**
|
||||
|
|
@ -97,8 +99,9 @@ See [docs/custom-tools.md](docs/custom-tools.md) and [examples/custom-tools/](ex
|
|||
**SessionManager API:**
|
||||
- Method renames: `saveXXX()` → `appendXXX()` (e.g., `appendMessage`, `appendCompaction`)
|
||||
- `branchInPlace()` → `branch()`
|
||||
- `reset()` → `newSession()`
|
||||
- `reset()` → `newSession(options?)` with optional `parentSession` for lineage tracking
|
||||
- `createBranchedSessionFromEntries(entries, index)` → `createBranchedSession(leafId)`
|
||||
- `SessionHeader.branchedFrom` → `SessionHeader.parentSession`
|
||||
- `saveCompaction(entry)` → `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?)`
|
||||
- `getEntries()` now excludes the session header (use `getHeader()` separately)
|
||||
- `getSessionFile()` returns `string | undefined` (undefined for in-memory sessions)
|
||||
|
|
@ -139,6 +142,9 @@ See [docs/sdk.md](docs/sdk.md) and [examples/sdk/](examples/sdk/) for the curren
|
|||
|
||||
### RPC Migration
|
||||
|
||||
**Session commands:**
|
||||
- `reset` command → `new_session` command with optional `parentSession` field
|
||||
|
||||
**Branching commands:**
|
||||
- `branch` command: `entryIndex` → `entryId`
|
||||
- `get_branch_messages` response: `entryIndex` → `entryId`
|
||||
|
|
|
|||
|
|
@ -241,19 +241,20 @@ Tools can implement `onSession` to react to session changes:
|
|||
|
||||
```typescript
|
||||
interface CustomToolSessionEvent {
|
||||
reason: "start" | "switch" | "branch" | "new" | "tree" | "shutdown";
|
||||
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
**Reasons:**
|
||||
- `start`: Initial session load on startup
|
||||
- `switch`: User switched to a different session (`/resume`)
|
||||
- `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
|
||||
- `branch`: User branched from a previous message (`/branch`)
|
||||
- `new`: User started a new session (`/new`)
|
||||
- `tree`: User navigated to a different point in the session tree (`/tree`)
|
||||
- `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
|
||||
|
||||
To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
Tools that maintain state should store it in `details` of their results, not external files. This allows branching to work correctly, as the state is reconstructed from the session history.
|
||||
|
|
|
|||
|
|
@ -123,13 +123,9 @@ user sends prompt ────────────────────
|
|||
│
|
||||
user sends another prompt ◄────────────────────────────────┘
|
||||
|
||||
/new (new session)
|
||||
├─► session_before_new (can cancel)
|
||||
└─► session_new
|
||||
|
||||
/resume (switch session)
|
||||
├─► session_before_switch (can cancel)
|
||||
└─► session_switch
|
||||
/new (new session) or /resume (switch session)
|
||||
├─► session_before_switch (can cancel, has reason: "new" | "resume")
|
||||
└─► session_switch (has reason: "new" | "resume")
|
||||
|
||||
/branch
|
||||
├─► session_before_branch (can cancel)
|
||||
|
|
@ -161,34 +157,27 @@ pi.on("session_start", async (_event, ctx) => {
|
|||
|
||||
#### session_before_switch / session_switch
|
||||
|
||||
Fired when switching sessions via `/resume`.
|
||||
Fired when starting a new session (`/new`) or switching sessions (`/resume`).
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_switch", async (event, ctx) => {
|
||||
// event.targetSessionFile - session we're switching to
|
||||
return { cancel: true }; // Cancel the switch
|
||||
// event.reason - "new" (starting fresh) or "resume" (switching to existing)
|
||||
// event.targetSessionFile - session we're switching to (only for "resume")
|
||||
|
||||
if (event.reason === "new") {
|
||||
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
|
||||
if (!ok) return { cancel: true };
|
||||
}
|
||||
|
||||
return { cancel: true }; // Cancel the switch/new
|
||||
});
|
||||
|
||||
pi.on("session_switch", async (event, ctx) => {
|
||||
// event.reason - "new" or "resume"
|
||||
// event.previousSessionFile - session we came from
|
||||
});
|
||||
```
|
||||
|
||||
#### session_before_new / session_new
|
||||
|
||||
Fired when starting a new session via `/new`.
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_new", async (_event, ctx) => {
|
||||
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
|
||||
if (!ok) return { cancel: true };
|
||||
});
|
||||
|
||||
pi.on("session_new", async (_event, ctx) => {
|
||||
// New session started
|
||||
});
|
||||
```
|
||||
|
||||
#### session_before_branch / session_branch
|
||||
|
||||
Fired when branching via `/branch`.
|
||||
|
|
|
|||
|
|
@ -76,22 +76,27 @@ Response:
|
|||
{"type": "response", "command": "abort", "success": true}
|
||||
```
|
||||
|
||||
#### reset
|
||||
#### new_session
|
||||
|
||||
Clear context and start a fresh session. Can be cancelled by a `before_clear` hook.
|
||||
Start a fresh session. Can be cancelled by a `session_before_switch` hook.
|
||||
|
||||
```json
|
||||
{"type": "reset"}
|
||||
{"type": "new_session"}
|
||||
```
|
||||
|
||||
With optional parent session tracking:
|
||||
```json
|
||||
{"type": "new_session", "parentSession": "/path/to/parent-session.jsonl"}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{"type": "response", "command": "reset", "success": true, "data": {"cancelled": false}}
|
||||
{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": false}}
|
||||
```
|
||||
|
||||
If a hook cancelled the reset:
|
||||
If a hook cancelled:
|
||||
```json
|
||||
{"type": "response", "command": "reset", "success": true, "data": {"cancelled": true}}
|
||||
{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": true}}
|
||||
```
|
||||
|
||||
### State
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ interface AgentSession {
|
|||
isStreaming: boolean;
|
||||
|
||||
// Session management
|
||||
reset(): Promise<boolean>; // Returns false if cancelled by hook
|
||||
newSession(options?: { parentSession?: string }): Promise<boolean>; // Returns false if cancelled by hook
|
||||
switchSession(sessionPath: string): Promise<boolean>;
|
||||
|
||||
// Branching
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ First line of the file. Metadata only, not part of the tree (no `id`/`parentId`)
|
|||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
|
||||
```
|
||||
|
||||
For branched sessions (created via `/branch` command):
|
||||
For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
|
||||
|
||||
```json
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","branchedFrom":"/path/to/original/session.jsonl"}
|
||||
{"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
|
||||
```
|
||||
|
||||
### SessionMessageEntry
|
||||
|
|
|
|||
|
|
@ -5,24 +5,26 @@
|
|||
* Demonstrates how to cancel session events using the before_* events.
|
||||
*/
|
||||
|
||||
import type { HookAPI, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_before_new", async (_event, ctx) => {
|
||||
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const confirmed = await ctx.ui.confirm("Clear session?", "This will delete all messages in the current session.");
|
||||
if (event.reason === "new") {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Clear session?",
|
||||
"This will delete all messages in the current session.",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Clear cancelled", "info");
|
||||
return { cancel: true };
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Clear cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_switch", async (_event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
// Check if there are unsaved changes (messages since last assistant response)
|
||||
// reason === "resume" - check if there are unsaved changes (messages since last assistant response)
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const hasUnsavedWork = entries.some(
|
||||
(e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
|
||||
|
|
|
|||
|
|
@ -41,12 +41,9 @@ async function checkDirtyRepo(pi: HookAPI, ctx: HookContext, action: string): Pr
|
|||
}
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_before_new", async (_event, ctx) => {
|
||||
return checkDirtyRepo(pi, ctx, "new session");
|
||||
});
|
||||
|
||||
pi.on("session_before_switch", async (_event, ctx) => {
|
||||
return checkDirtyRepo(pi, ctx, "switch session");
|
||||
pi.on("session_before_switch", async (event, ctx) => {
|
||||
const action = event.reason === "new" ? "new session" : "switch session";
|
||||
return checkDirtyRepo(pi, ctx, action);
|
||||
});
|
||||
|
||||
pi.on("session_before_branch", async (_event, ctx) => {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ export default function (pi: HookAPI) {
|
|||
ctx.ui.setStatus("status-demo", check + text);
|
||||
});
|
||||
|
||||
pi.on("session_new", async (_event, ctx) => {
|
||||
turnCount = 0;
|
||||
const theme = ctx.ui.theme;
|
||||
ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready"));
|
||||
pi.on("session_switch", async (event, ctx) => {
|
||||
if (event.reason === "new") {
|
||||
turnCount = 0;
|
||||
const theme = ctx.ui.theme;
|
||||
ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import type {
|
|||
HookRunner,
|
||||
SessionBeforeBranchResult,
|
||||
SessionBeforeCompactResult,
|
||||
SessionBeforeNewResult,
|
||||
SessionBeforeSwitchResult,
|
||||
SessionBeforeTreeResult,
|
||||
TreePreparation,
|
||||
|
|
@ -43,7 +42,7 @@ import type {
|
|||
} from "./hooks/index.js";
|
||||
import type { BashExecutionMessage, HookMessage } from "./messages.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager.js";
|
||||
import type { SettingsManager, SkillsSettings } from "./settings-manager.js";
|
||||
import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
|
||||
|
||||
|
|
@ -664,19 +663,21 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset agent and session to start fresh.
|
||||
* Start a new session, optionally with initial messages and parent tracking.
|
||||
* Clears all messages and starts a new session.
|
||||
* Listeners are preserved and will continue receiving events.
|
||||
* @returns true if reset completed, false if cancelled by hook
|
||||
* @param options - Optional initial messages and parent session path
|
||||
* @returns true if completed, false if cancelled by hook
|
||||
*/
|
||||
async reset(): Promise<boolean> {
|
||||
async newSession(options?: NewSessionOptions): Promise<boolean> {
|
||||
const previousSessionFile = this.sessionFile;
|
||||
|
||||
// Emit session_before_new event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_new")) {
|
||||
// Emit session_before_switch event with reason "new" (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session_before_new",
|
||||
})) as SessionBeforeNewResult | undefined;
|
||||
type: "session_before_switch",
|
||||
reason: "new",
|
||||
})) as SessionBeforeSwitchResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return false;
|
||||
|
|
@ -686,19 +687,21 @@ export class AgentSession {
|
|||
this._disconnectFromAgent();
|
||||
await this.abort();
|
||||
this.agent.reset();
|
||||
this.sessionManager.newSession();
|
||||
this.sessionManager.newSession(options);
|
||||
this._queuedMessages = [];
|
||||
this._reconnectToAgent();
|
||||
|
||||
// Emit session_new event to hooks
|
||||
// Emit session_switch event with reason "new" to hooks
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session_new",
|
||||
type: "session_switch",
|
||||
reason: "new",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
||||
// Emit session event to custom tools
|
||||
await this.emitCustomToolSessionEvent("new", previousSessionFile);
|
||||
await this.emitCustomToolSessionEvent("switch", previousSessionFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1446,6 +1449,7 @@ export class AgentSession {
|
|||
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session_before_switch",
|
||||
reason: "resume",
|
||||
targetSessionFile: sessionPath,
|
||||
})) as SessionBeforeSwitchResult | undefined;
|
||||
|
||||
|
|
@ -1468,6 +1472,7 @@ export class AgentSession {
|
|||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session_switch",
|
||||
reason: "resume",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ export interface CustomToolContext {
|
|||
/** Session event passed to onSession callback */
|
||||
export interface CustomToolSessionEvent {
|
||||
/** Reason for the session event */
|
||||
reason: "start" | "switch" | "branch" | "new" | "tree" | "shutdown";
|
||||
/** Previous session file path, or undefined for "start", "new", and "shutdown" */
|
||||
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
||||
/** Previous session file path, or undefined for "start" and "shutdown" */
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -154,27 +154,21 @@ export interface SessionStartEvent {
|
|||
/** Fired before switching to another session (can be cancelled) */
|
||||
export interface SessionBeforeSwitchEvent {
|
||||
type: "session_before_switch";
|
||||
/** Session file we're switching to */
|
||||
targetSessionFile: string;
|
||||
/** Reason for the switch */
|
||||
reason: "new" | "resume";
|
||||
/** Session file we're switching to (only for "resume") */
|
||||
targetSessionFile?: string;
|
||||
}
|
||||
|
||||
/** Fired after switching to another session */
|
||||
export interface SessionSwitchEvent {
|
||||
type: "session_switch";
|
||||
/** Reason for the switch */
|
||||
reason: "new" | "resume";
|
||||
/** Session file we came from */
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired before creating a new session (can be cancelled) */
|
||||
export interface SessionBeforeNewEvent {
|
||||
type: "session_before_new";
|
||||
}
|
||||
|
||||
/** Fired after creating a new session */
|
||||
export interface SessionNewEvent {
|
||||
type: "session_new";
|
||||
}
|
||||
|
||||
/** Fired before branching a session (can be cancelled) */
|
||||
export interface SessionBeforeBranchEvent {
|
||||
type: "session_before_branch";
|
||||
|
|
@ -255,8 +249,6 @@ export type SessionEvent =
|
|||
| SessionStartEvent
|
||||
| SessionBeforeSwitchEvent
|
||||
| SessionSwitchEvent
|
||||
| SessionBeforeNewEvent
|
||||
| SessionNewEvent
|
||||
| SessionBeforeBranchEvent
|
||||
| SessionBranchEvent
|
||||
| SessionBeforeCompactEvent
|
||||
|
|
@ -505,12 +497,6 @@ export interface SessionBeforeSwitchResult {
|
|||
cancel?: boolean;
|
||||
}
|
||||
|
||||
/** Return type for session_before_new handlers */
|
||||
export interface SessionBeforeNewResult {
|
||||
/** If true, cancel the new session */
|
||||
cancel?: boolean;
|
||||
}
|
||||
|
||||
/** Return type for session_before_branch handlers */
|
||||
export interface SessionBeforeBranchResult {
|
||||
/**
|
||||
|
|
@ -600,8 +586,6 @@ export interface HookAPI {
|
|||
on(event: "session_start", handler: HookHandler<SessionStartEvent>): void;
|
||||
on(event: "session_before_switch", handler: HookHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;
|
||||
on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
|
||||
on(event: "session_before_new", handler: HookHandler<SessionBeforeNewEvent, SessionBeforeNewResult>): void;
|
||||
on(event: "session_new", handler: HookHandler<SessionNewEvent>): void;
|
||||
on(event: "session_before_branch", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;
|
||||
on(event: "session_branch", handler: HookHandler<SessionBranchEvent>): void;
|
||||
on(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ export interface SessionHeader {
|
|||
id: string;
|
||||
timestamp: string;
|
||||
cwd: string;
|
||||
branchedFrom?: string;
|
||||
parentSession?: string;
|
||||
}
|
||||
|
||||
export interface NewSessionOptions {
|
||||
parentSession?: string;
|
||||
}
|
||||
|
||||
export interface SessionEntryBase {
|
||||
|
|
@ -508,7 +512,7 @@ export class SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
newSession(): string | undefined {
|
||||
newSession(options?: NewSessionOptions): string | undefined {
|
||||
this.sessionId = randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const header: SessionHeader = {
|
||||
|
|
@ -517,11 +521,13 @@ export class SessionManager {
|
|||
id: this.sessionId,
|
||||
timestamp,
|
||||
cwd: this.cwd,
|
||||
parentSession: options?.parentSession,
|
||||
};
|
||||
this.fileEntries = [header];
|
||||
this.byId.clear();
|
||||
this.leafId = null;
|
||||
this.flushed = false;
|
||||
|
||||
// Only generate filename if persisting and not already set (e.g., via --session flag)
|
||||
if (this.persist && !this.sessionFile) {
|
||||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
|
|
@ -929,7 +935,7 @@ export class SessionManager {
|
|||
id: newSessionId,
|
||||
timestamp,
|
||||
cwd: this.cwd,
|
||||
branchedFrom: this.persist ? this.sessionFile : undefined,
|
||||
parentSession: this.persist ? this.sessionFile : undefined,
|
||||
};
|
||||
|
||||
// Collect labels for entries in the path
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export {
|
|||
getLatestCompactionEntry,
|
||||
type ModelChangeEntry,
|
||||
migrateSessionEntries,
|
||||
type NewSessionOptions,
|
||||
parseSessionEntries,
|
||||
type SessionContext,
|
||||
type SessionEntry,
|
||||
|
|
|
|||
|
|
@ -2148,8 +2148,8 @@ export class InteractiveMode {
|
|||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Reset via session (emits hook and tool session events)
|
||||
await this.session.reset();
|
||||
// New session via session (emits hook and tool session events)
|
||||
await this.session.newSession();
|
||||
|
||||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
|
|
|
|||
|
|
@ -187,11 +187,12 @@ export class RpcClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset session (clear all messages).
|
||||
* @returns Object with `cancelled: true` if a hook cancelled the reset
|
||||
* Start a new session, optionally with parent tracking.
|
||||
* @param parentSession - Optional parent session path for lineage tracking
|
||||
* @returns Object with `cancelled: true` if a hook cancelled the new session
|
||||
*/
|
||||
async reset(): Promise<{ cancelled: boolean }> {
|
||||
const response = await this.send({ type: "reset" });
|
||||
async newSession(parentSession?: string): Promise<{ cancelled: boolean }> {
|
||||
const response = await this.send({ type: "new_session", parentSession });
|
||||
return this.getData(response);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -239,9 +239,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
return success(id, "abort");
|
||||
}
|
||||
|
||||
case "reset": {
|
||||
const cancelled = !(await session.reset());
|
||||
return success(id, "reset", { cancelled });
|
||||
case "new_session": {
|
||||
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
||||
const cancelled = !(await session.newSession(options));
|
||||
return success(id, "new_session", { cancelled });
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export type RpcCommand =
|
|||
| { id?: string; type: "prompt"; message: string; images?: ImageContent[] }
|
||||
| { id?: string; type: "queue_message"; message: string }
|
||||
| { id?: string; type: "abort" }
|
||||
| { id?: string; type: "reset" }
|
||||
| { id?: string; type: "new_session"; parentSession?: string }
|
||||
|
||||
// State
|
||||
| { id?: string; type: "get_state" }
|
||||
|
|
@ -87,7 +87,7 @@ export type RpcResponse =
|
|||
| { id?: string; type: "response"; command: "prompt"; success: true }
|
||||
| { id?: string; type: "response"; command: "queue_message"; success: true }
|
||||
| { id?: string; type: "response"; command: "abort"; success: true }
|
||||
| { id?: string; type: "response"; command: "reset"; success: true; data: { cancelled: boolean } }
|
||||
| { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
|
||||
|
||||
// State
|
||||
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
expect(stats.assistantMessages).toBeGreaterThanOrEqual(1);
|
||||
}, 90000);
|
||||
|
||||
test("should reset session", async () => {
|
||||
test("should create new session", async () => {
|
||||
await client.start();
|
||||
|
||||
// Send a prompt
|
||||
|
|
@ -247,8 +247,8 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
let state = await client.getState();
|
||||
expect(state.messageCount).toBeGreaterThan(0);
|
||||
|
||||
// Reset
|
||||
await client.reset();
|
||||
// New session
|
||||
await client.newSession();
|
||||
|
||||
// Verify messages cleared
|
||||
state = await client.getState();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue