Project-Scoped Sessions
Sessions are remembered per project, with cumulative token and cost tracking across all runs
from praisonaiagents import Agent, MemoryConfig
agent = Agent(name="project-agent", instructions="Manage project-scoped agent sessions.")
agent.start("Load my marketing-campaign project session and continue where we left off.")The user resumes work in the same project folder; prior turns and cumulative usage load automatically.
cd ~/code/my-app && praisonai run "Build a TODO app"
praisonai run --continue "Add a delete endpoint"Quick Start
- Start in your project root
bash cd ~/code/my-app && praisonai run "Build a TODO app"Project context comes from the git repository โ its remote or root commit โ falling back to a path hash only outside a repo.
- Continue later
bash praisonai run --continue "Add a delete endpoint"Prior user and assistant messages replay before your new prompt โ no need to summarise what happened last time.
- List this project's sessions
bash $ praisonai session list Project: my-app (ID: a1b2c3d4, identity: git-remote) ...The
identity:label tells you which resolver won โ useful when explaining why two clones share sessions or why a fresh repo has none.Shows every session
--continuecould resume for this project โ the project’s own sessions plus any globally-stored sessions (fromchat, gateway, TUI, API, or a bareAgent( memory=MemoryConfig( session_id=..., ))). Duplicates are collapsed; the freshest record wins.
How It Works
sequenceDiagram
participant User
participant CLI
participant PS as Project session store
participant GS as Global default store
participant Agent
User->>CLI: praisonai run --continue "..."
CLI->>PS: list_sessions(limit)
CLI->>GS: list_sessions(limit)
Note over CLI: merge + dedup by session_id, freshest wins, prefer root sessions
CLI-->>CLI: last root session id
CLI->>Agent: restore history, run prompt
Agent->>PS: append new messages
Agent-->>User: response
classDef cli fill:#8B0000,stroke:#7C90A0,color:#fff
classDef store fill:#189AB4,stroke:#7C90A0,color:#fff
classDef result fill:#10B981,stroke:#7C90A0,color:#fff
class User,CLI cli
class PS,GS store
class Agent resultfind_last_session() merges the project-scoped store and the global default store so --continue resolves the most-recent session regardless of how it was created (run, chat, gateway, TUI, API, or a bare Agent( memory=MemoryConfig( session_id=..., ))). Sub-agent / forked children are skipped โ the last root session wins. It falls back to a child only if the project has none. Each store is scanned with a limit window (default 50).
The CLI now follows the repository, not the folder โ cloning, moving, or git worktree-ing a repo keeps the same session history.
| Priority | Where the id comes from | Identity source | When it applies |
|---|---|---|---|
| 1 | Normalised git remote URL (origin first, then any configured remote) | git-remote | Any repo with a remote |
| 2 | Repository root commit SHA (oldest across all refs; pinned to .git/praisonai-root-commit on first use) | root-commit | Repo has commits but no remote |
| 3 | Cached id persisted at .git/praisonai-project | cached-id | Git repo with neither remote nor commits |
| 4 | Absolute-path SHA-256 hash (legacy behaviour) | path | Non-git directories only |
All four sources collapse to the same 8-char sha256 short hash, so the on-disk session layout is unchanged.
What survives
Session history follows the repository, not the folder.
| Scenario | Session history follows you? | Why |
|---|---|---|
mv myrepo mynewrepo (rename directory) | โ Yes | Identity comes from remote / root commit, not the path |
Clone the same repo to ~/work/a and ~/work/b | โ Yes (shared history) | Both resolve to the same normalised remote |
git worktree add ../feature-x | โ Yes | Worktrees share --git-common-dir, so the same identity resolves |
git checkout --orphan feature-x + first commit | โ Yes | Oldest root wins over the newer one; the SHA is pinned so ties never flip |
| Two orphan branches created in the same second (scripted setup) | โ Yes | The pin locks the choice on first resolve โ no tie-break to flip |
| Rewritten history / shallow clone where the pinned SHA is no longer a root | โ ๏ธ Re-chosen and re-pinned | The dead pin is not trusted; the fresh choice is persisted so it stays stable from then on |
pip install a project into /venvs/foo/src/myrepo | โ Yes | Same remote resolves the same id |
Delete .git and git init again with no remote | โ ๏ธ New identity (root commit changes) | Legacy sessions are auto-migrated on first --continue |
| Non-git directory renamed | โ Old behaviour: new id | Path hash changes |
Upgrading from an earlier CLI? Your existing project sessions are stored under a directory named after a hash of the repo’s absolute path. On the first --continue / praisonai run after upgrading, sessions are copied (not moved) into the new identity-based directory automatically. No configuration, no data loss โ just re-run in the same repo and prior history is there.
After upgrading, the first resolve in each git repo also writes a small .git/praisonai-root-commit file next to the existing .git/praisonai-project. Both are internal to PraisonAI, shared across all worktrees of the repo, and safe to commit or leave untracked โ deleting either just causes the next run to recompute and re-pin.
History restore and save wiring landed in PR #1963. If no prior session exists, a warning appears and a new session starts.
Reading the identity source programmatically:
from praisonai_code.cli.utils.project import (
get_project_id,
get_project_identity_source,
resolve_project_identity,
)
pid = get_project_id() # 8-char hash โ unchanged public API
source = get_project_identity_source() # "git-remote" / "root-commit" / "cached-id" / "path"
pid, source = resolve_project_identity() # both in one callPersisted usage shape
Every session stores a usage blob in ~/.praisonai/sessions/projects/<project_id>/<session_id>.json. Config-driven consumers and scripts can read this directly.
| Key | Type | Notes |
|---|---|---|
input_tokens | int | Cumulative prompt tokens |
output_tokens | int | Cumulative completion tokens |
cached_tokens | int | Cumulative cached / prefix-hit tokens (when provider reports them) |
total_tokens | int | input + output (cached not double-counted); mirrored to flat total_tokens on session metadata for back-compat with list_sessions |
cost | float | USD, priced via get_pricing(model); mirrored to flat cost |
requests | int | Count of agent.start() runs that produced usage |
The flat total_tokens and cost fields at the session root are mirrors kept for backwards compatibility with any code reading the old schema.
SDK helpers (praisonaiagents 1.6.85+):
from praisonai.cli.state.project_sessions import (
read_session_usage,
format_usage_footer,
accumulate_session_usage,
)
usage = read_session_usage(session_id="abc12345")
print(usage)
# {"input_tokens": 10000, "output_tokens": 2345, "cached_tokens": 0,
# "total_tokens": 12345, "cost": 0.014, "requests": 3}
footer = format_usage_footer(usage)
print(footer)
# "10,000 in / 2,345 out ยท $0.0140"
updated = accumulate_session_usage(session_id="abc12345", model="gpt-4o-mini")Configuration Options
| Flag | Description |
|---|---|
--continue | Resume the most recent root session for this project โ searches both the project store and the global default store |
--session <id> | Resume a specific session |
--fork --session <id> | Branch a session to try alternatives |
--no-save | One-off prompt โ nothing persisted |
session list --all | List sessions across all projects |
Works with YAML / team runs
The same four flags apply to praisonai run agents.yaml, so an AgentTeam resumes exactly like a single-agent prompt.
sequenceDiagram
participant User
participant Adapter
participant Team as AgentTeam
participant Store as Session store
User->>Adapter: praisonai run agents.yaml --continue
Adapter->>Team: restore_session_state(id)
Team->>Team: rehydrate per-agent chat_history (deduped)
Adapter->>Team: astart()
Adapter->>Team: capture chat_history into state
Adapter->>Store: save_session_state(id)
Adapter-->>User: response
classDef actor fill:#8B0000,stroke:#7C90A0,color:#fff
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
classDef result fill:#10B981,stroke:#7C90A0,color:#fff
class User,Adapter actor
class Team process
class Store result| Command | Behavior |
|---|---|
praisonai run agents.yaml --continue | Resumes the most recent root session; prior team turns replay |
praisonai run agents.yaml --session <id> | Resumes a specific session โ merges saved team._state and re-injects each agent’s chat history |
praisonai run agents.yaml --fork --session <id> | Forks via the hierarchical store; history is copied into the fork (deduplicated) |
praisonai run agents.yaml --no-save | One-off team run โ nothing is persisted after kickoff |
Per-agent chat_history is captured into team state on save (under the reserved key _cli_session_chat_history) and re-injected on restore, so a resumed YAML team continues the actual conversation โ not just team bookkeeping.
Best Practices
Run from the project root
Start praisonai run from the repo root so git detection stays consistent โ subdirectories still resolve to the same project.
Use –fork for risky experiments
Try alternatives without altering the main thread: praisonai run --fork --session abc123 "try Redis instead".
Use –no-save for throwaway prompts
Quick questions or PII-sensitive input: praisonai run --no-save "How do I hash passwords?".
Clean up with session list –all
Review and delete stale sessions across projects periodically.
Clones and moves keep their history
Two clones of the same repo โ for example, one on your laptop and one on a build box โ automatically share session history because the id comes from the git remote. Renaming or moving a checkout does not change the id either.
Orphan branches don't reassign the project id
Creating an orphan branch (git checkout --orphan โฆ + a first commit) adds a new root commit but does not change the project id โ the oldest root wins and the choice is pinned to .git/praisonai-root-commit on first resolve, so even two roots created in the same second stay stable.