SessionManager.open() without a session directory used the file's
folder for new sessions. When the session's cwd differs from the project
the folder belongs to, for example after choosing "continue in current
cwd" at the missing-cwd prompt, pi treated that folder as a custom
--session-dir: /resume All listed only that folder, the resume hint
added --session-dir, and /new, /fork and /clone saved into the old
project's folder.
For a file in the sessions directory, open() now uses the default
directory of the session's cwd. Files elsewhere, such as in a custom
--session-dir, keep their folder.
When writing a session entry failed (ENOSPC, EBUSY, ETIMEDOUT on network
drives), `SessionManager._appendEntry` had already added the entry to
memory and made it the current leaf. The process kept running, and the
next entry written to disk pointed to a parent that was not in the file.
On resume, pi follows the history only back to that gap: everything
before it disappears. In #11460 that also dropped the first system
message, which declares the tools, so resume turned off every built-in
tool.
`_appendEntry` now undoes the in-memory append if writing fails, then
rethrows. The error reaches the caller as before; later entries chain to
the last entry actually on disk.
Reports of this problem:
- #11460: ENOSPC; resume dropped all built-in tools.
- #10166: failure injected with EBUSY; the next entry on disk points to
a missing parent.
- #10661: ETIMEDOUT on Windows + NFS; resumed sessions showed only the
last few messages. That report also describes NUL holes in the file,
which this change does not address.
**Testing**
- Two tests in `test/session-manager/file-operations.test.ts` make
writes fail with real file permissions: a read-only session file (failed
append) and a read-only session directory (failed first file creation).
They check that the entry is not kept, the leaf is unchanged, the next
entry chains to the last written one, and the reopened session's branch
contains both entries. Both fail without the change and pass with it.
- `test/session-manager/` and `test/default-tools-setting.test.ts` pass
(140 tests).
- `npm run check` passes.
Fixes#11460
Requested by @davidbrai
Co-authored-by: triagent <triagent@localhost>
A new session was only written to disk once the first assistant message
existed. If pi exited during the first turn (Ctrl+C twice while streaming,
or the process was killed), the user's prompt was lost and the session
never appeared in /resume.
The file is now created as soon as the session has a user or assistant
message. A session with only setup entries (model, thinking level) still
stays in memory, so starting pi and quitting without chatting leaves no
file. Forking uses the same rule through a shared helper, so the two paths
cannot drift apart and write the header twice (#1672).
closes#10000
* feat(coding-agent): ingest external entries in in-memory sessions
SessionManager.inMemory() takes the entries to start from, so a session
held outside the filesystem can be resumed as it was stored.
Replaying through appendMessage/appendCompaction cannot do this: ids are
minted fresh, so every stored firstKeptEntryId, targetId and fromId has to
be remapped, timestamps are rewritten, and parentId always comes from the
current leaf, so a branched tree needs the original branch() choreography
reproduced call by call.
The entry-adoption half of _setSessionFile becomes _loadEntries, which the
constructor also uses when there are entries but no file. Entries carrying
a session header keep it; without one, the header comes from the options
and the entries become its body.
* fix(coding-agent): carry the stored version through restored entries
inMemory() took SessionEntry[], which excludes SessionHeader, so an
external store could not pass the header it saved and the header branch of
_loadEntries was unreachable through the public API. A restore therefore
always synthesised a current-version header, and the migration that ran
next read that header and skipped: a v2 session kept its hookMessage roles
forever.
Widening to FileEntry[] lets the stored header through, version included.
Migration moves inside the header branch, where a version exists to compare
against; without a header the entries are adopted as current-version, which
is the only thing they can mean.
* add usage info to branch summary entries
* add usage to compaction entries
* allow custom tools to report llm usage in tool results
* allow observing and patching usage in tool_result hooks
* agent-harness: save usage in entries
for compaction, branch summaries and tool results
* feat(session-manager): allow supplying custom session ID in newSession()
Add optional `id` field to `NewSessionOptions`. When provided, this ID
is used as the session ID instead of generating a random UUID. Existing
callers are unaffected since the field is optional and falls back to
`randomUUID()`.
Closes#2097
* test(session-manager): add tests for custom session ID in newSession()
Verify that newSession() uses the provided id when supplied, falls back
to randomUUID() when omitted, and includes the custom id in the session
header.
createBranchedSession() wrote the file and set flushed=true even when the
branched path had no assistant message. The next _persist() call saw no
assistant, reset flushed=false, and the subsequent flush appended all
in-memory entries to the already-populated file, duplicating the header
and entries.
Fix: defer file creation when the branched path has no assistant message,
matching the newSession() contract. _persist() creates the file on the
first assistant response.
closes#1672
Breaking changes:
- Settings: 'hooks' and 'customTools' arrays replaced with 'extensions'
- CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e'
- API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom'
- API: FileSlashCommand renamed to PromptTemplate
- API: discoverSlashCommands() renamed to discoverPromptTemplates()
- Directories: commands/ renamed to prompts/ for prompt templates
Migration:
- Session version bumped to 3 (auto-migrates v2 sessions)
- Old 'hookMessage' role entries converted to 'custom'
Structural changes:
- src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/
- src/core/slash-commands.ts renamed to src/core/prompt-templates.ts
- examples/hooks/ and examples/custom-tools/ merged into examples/extensions/
- docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md
New test coverage:
- test/extensions-runner.test.ts (10 tests)
- test/extensions-discovery.test.ts (26 tests)
- test/prompt-templates.test.ts
- CustomAgentTool renamed to CustomTool
- ToolAPI renamed to CustomToolAPI
- ToolContext renamed to CustomToolContext
- ToolSessionEvent renamed to CustomToolSessionEvent
- Added CustomToolContext parameter to execute() and onSession()
- CustomToolFactory now returns CustomTool<any, any> for type compatibility
- dispose() replaced with onSession({ reason: 'shutdown' })
- Added wrapCustomTool() to convert CustomTool to AgentTool
- Session exposes setToolUIContext() instead of leaking internals
- Fix ToolExecutionComponent to sync with toolOutputExpanded state
- Update all custom tool examples for new API
- Add test/utilities.ts with shared helpers (API_KEY, userMsg, assistantMsg, createTestSession)
- Add agent-session-tree-navigation.test.ts with e2e tests for tree navigation
- Add getChildren() method to SessionManager
- Add summaryEntry to navigateTree return type
- Update existing tests to use shared utilities
- Check response.stopReason instead of catching errors for abort detection
- Return result object from _generateBranchSummary instead of throwing
- Fix summary attachment: attach to navigation target position, not old branch
- Support root-level summaries (parentId=null) in branchWithSummary
- Remove setTimeout hack for re-showing tree selector on abort
Major changes:
- Replace monolithic SessionEvent with reason discriminator with individual
event types: session_start, session_before_switch, session_switch,
session_before_new, session_new, session_before_branch, session_branch,
session_before_compact, session_compact, session_shutdown
- Each event has dedicated result type (SessionBeforeSwitchResult, etc.)
- HookHandler type now allows bare return statements (void in return type)
- HookAPI.on() has proper overloads for each event with correct typing
Additional fixes:
- AgentSession now always subscribes to agent in constructor (was only
subscribing when external subscribe() called, breaking internal handlers)
- Standardize on undefined over null throughout codebase
- HookUIContext methods return undefined instead of null
- SessionManager methods return undefined instead of null
- Simplify hook exports to 'export type * from types.js'
- Add detailed JSDoc for skipConversationRestore vs cancel
- Fix createBranchedSession to rebuild index in persist mode
- newSession() now returns the session file path
Updated all example hooks, tests, and emission sites to use new event types.
- addMessageToChat: exhaustive switch for all AgentMessage roles
- renderSessionContext: delegates to addMessageToChat, special handling for assistant tool calls and tool results
- export-html formatMessage: exhaustive switch for all AgentMessage roles
- Removed isHookMessage, isBashExecutionMessage type guards in favor of role checks
- Fixed imports and removed unused getLatestCompactionEntry
- Add LabelEntry type with targetId and label (string | undefined)
- Add labelsById map built on load via linear scan
- Add getLabel(id) and appendLabelChange(targetId, label) methods
- Add label field to SessionTreeNode, populated by getTree()
- Update createBranchedSession to preserve labels for entries on path
- Labels are ignored by buildSessionContext (not sent to LLM)
- Add comprehensive tests for label functionality