paseo-smart-session 2.0.0 → 2.2.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/README.md +55 -130
- package/assets/LICENSE.lucide +43 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +1 -0
- package/client/agents.ts +200 -0
- package/client/charts.tsx +277 -152
- package/client/chat.tsx +122 -0
- package/client/display.tsx +55 -33
- package/client/guard/index.tsx +48 -0
- package/client/guard/paused.tsx +164 -0
- package/client/guard/pill-icons.tsx +26 -0
- package/client/guard/pills.ts +132 -0
- package/client/guard/settings.tsx +114 -0
- package/client/guard/surfaces.ts +3 -0
- package/client/guard/timeline.tsx +154 -0
- package/client/kit.tsx +144 -0
- package/client/live.ts +49 -0
- package/client/motion.tsx +94 -0
- package/client/overview.tsx +373 -0
- package/client/pill-store.ts +77 -0
- package/client/pill.tsx +91 -380
- package/client/popover.tsx +113 -0
- package/client/settings.tsx +105 -0
- package/client/theme.ts +56 -0
- package/docs/media/smart-session-chat.png +0 -0
- package/docs/media/smart-session-flow.gif +0 -0
- package/docs/media/smart-session-guard.png +0 -0
- package/docs/media/smart-session-hero-light.png +0 -0
- package/docs/media/smart-session-hero.png +0 -0
- package/docs/media/smart-session-mobile.png +0 -0
- package/docs/media/smart-session-pill.png +0 -0
- package/docs/navigation-verification.md +6 -6
- package/docs/screenshots/display-settings-desktop.png +0 -0
- package/docs/screenshots/display-settings-mobile.png +0 -0
- package/docs/session-guard.md +88 -0
- package/index.client.tsx +52 -35
- package/index.server.ts +19 -11
- package/mcp.mjs +22 -3
- package/package.json +2 -1
- package/paseo-plugin.json +11 -0
- package/server/guard/client.ts +27 -0
- package/server/guard/engine.ts +554 -0
- package/server/guard/index.ts +395 -0
- package/server/guard/labels.ts +77 -0
- package/server/guard/processes.ts +147 -0
- package/server/guard/standalone.ts +61 -0
- package/server/guard/store.ts +113 -0
- package/server/guard/timeline.ts +24 -0
- package/server/guard/usage.ts +57 -0
- package/server/recorder.ts +2 -0
- package/server/settings.ts +10 -0
- package/server/store.ts +25 -1
- package/shared/blocks.ts +15 -0
- package/shared/chat.ts +174 -0
- package/shared/forecast.ts +233 -0
- package/shared/format.ts +47 -1
- package/shared/governor.ts +3 -0
- package/shared/guard/contracts.ts +65 -0
- package/shared/guard/policy.ts +128 -0
- package/shared/guard/presentation.ts +85 -0
- package/shared/guard/timeline.ts +34 -0
- package/shared/lifecycle.ts +18 -0
- package/shared/smart-session.ts +16 -0
- package/client/navigation.tsx +0 -14
- package/client/surface.tsx +0 -574
- package/docs/screenshots/smart-compact-handoff.png +0 -0
package/README.md
CHANGED
|
@@ -5,188 +5,113 @@
|
|
|
5
5
|
[](https://paseo.sh)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Know when your plan runs out, and let agents manage their own long-running work. Smart Session records your Claude plan usage and turns it into a forecast ("Session runs out around 5:40 PM"), shows how full each session's context is, lets an agent save its task state and compact itself at a safe point, and with Session Guard pauses work at the session limit and continues it after the reset.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/media/smart-session-hero-light.png">
|
|
12
|
+
<img alt="The Smart Session screen: the headline Session runs out around 8:55 PM, 44m before the 9:39 PM reset. Below it the session window at 77% with a chart of this window's usage and the projection crossing 100% before the reset, the earlier session windows of the last 14 days, the week and the Opus week, then each session's context with its compact line" src="docs/media/smart-session-hero.png">
|
|
13
|
+
</picture>
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
Screenshots use synthetic usage history and sessions.
|
|
13
16
|
|
|
14
17
|
## Install
|
|
15
18
|
|
|
16
|
-
Install from npm using the latest Paseo release:
|
|
17
|
-
|
|
18
19
|
```sh
|
|
19
20
|
paseo plugin install paseo-smart-session
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
paseo plugin ls # confirm it is running
|
|
24
|
-
paseo plugin update smart-session
|
|
25
|
-
paseo plugin remove smart-session
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
On first load, Smart Session registers its Claude Code hooks. Its MCP server is attached by Paseo when an interactive session opens. It only manages entries that point at its own scripts; other Claude Code configuration is left alone.
|
|
29
|
-
|
|
30
|
-
## What it does
|
|
31
|
-
|
|
32
|
-
### Lets agents compact themselves
|
|
23
|
+
On first load it registers its Claude Code hooks, touching only entries that point at its own scripts. Paseo attaches its MCP server when an interactive session opens.
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
## Use
|
|
35
26
|
|
|
36
|
-
|
|
27
|
+
**Smart Session** in the sidebar opens the overview. Its headline says when the session window runs out at the current pace, or that it lasts until the reset.
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
- **Plan limits.** Each window (the 5-hour session, the week, per-model weekly caps) shows how much is used, when it resets, and where it lands at the current pace: a faint extension of the meter, and a tick where an even pace would be. The session window adds a chart of its recorded usage and the projection to the reset, plus every session window of the last 14 days, so you can see how often you hit the limit.
|
|
30
|
+
- **Session context.** Every open session with its share of the context window, the line where Smart Compact asks it to compact, how fast it is filling, and whether it is enrolled. Press a row to open the session.
|
|
31
|
+
- **Smart Compact and Session Guard.** One switch each, what they will do, and links to their settings.
|
|
32
|
+
- **Tokens, last 30 days.** Billable tokens, the cache hit rate and the subagent share, per day, by hour of the week, by workspace and by model, reconstructed from Claude Code's transcripts.
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
The pill above each message box is a ring of that session's context, with a tick at its compact line. Press it to see the detail, when it last saved its task state and compacted, and to enrol the session or take it out.
|
|
41
35
|
|
|
42
|
-
Smart Session
|
|
36
|
+

|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
- Claude plan usage, burn rate, and reset times
|
|
46
|
-
- Token spend by model, workspace, and main agent versus subagent
|
|
47
|
-
- Compaction requests and their before/after context sizes
|
|
38
|
+

|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## How an agent uses it
|
|
52
|
-
|
|
53
|
-
Smart Session exposes these MCP tools to Paseo agents:
|
|
40
|
+
Agents get these tools:
|
|
54
41
|
|
|
55
42
|
| Tool | What it does |
|
|
56
43
|
| --- | --- |
|
|
57
|
-
| `context_status` |
|
|
58
|
-
| `checkpoint` |
|
|
59
|
-
| `request_compaction` |
|
|
60
|
-
| `defer_compaction` |
|
|
61
|
-
| `budget_status` |
|
|
62
|
-
|
|
63
|
-
The usual flow is:
|
|
64
|
-
|
|
65
|
-
```text
|
|
66
|
-
context_status → checkpoint → request_compaction → stop, or re-read state and continue
|
|
67
|
-
```
|
|
44
|
+
| `context_status` | Current context usage, fill rate and thresholds |
|
|
45
|
+
| `checkpoint` | Save durable task state for this session |
|
|
46
|
+
| `request_compaction` | Queue `/compact`, and choose whether and how to continue afterwards |
|
|
47
|
+
| `defer_compaction` | Put off a compaction and record why |
|
|
48
|
+
| `budget_status` | Plan usage, burn rate and reset times, and whether Session Guard will pause this session |
|
|
68
49
|
|
|
69
|
-
`request_compaction
|
|
50
|
+
The usual flow is `context_status → checkpoint → request_compaction`, then stop or re-read the state and continue. Saved state holds the goal, current step, decisions made and approaches that failed, so a resumed agent neither redoes work nor asks how to continue. In the chat each call is one line ("Saved task state · Cap retries at five", "Asked to compact · …"), and the messages Smart Session sends show as small cards with the full text one press away.
|
|
70
51
|
|
|
71
|
-
|
|
52
|
+

|
|
72
53
|
|
|
73
|
-
Smart Compact
|
|
54
|
+
## How Smart Compact works
|
|
74
55
|
|
|
75
|
-
|
|
56
|
+
As a session fills, hooks send context notices. At the compact line, the agent is asked at the end of its turn whether it wants to compact. Nothing is forced: the agent can compact, defer with a reason, or keep working. A request is delivered only when the agent is idle and its state file is current; `request_compaction` refuses a missing or stale one.
|
|
76
57
|
|
|
77
|
-
|
|
58
|
+
After compaction, the agent chooses to stop (`continue_after_compaction: false`), continue with the default prompt to re-read its state, or continue with its own `continue_message`. A `/compact` typed by a person never gets a plugin continuation.
|
|
78
59
|
|
|
79
|
-
|
|
80
|
-
- `continue_after_compaction: true` to start another turn after compaction.
|
|
81
|
-
- `continue_message` to choose the exact follow-up; when omitted, the default tells the agent to re-read the state file and continue from its **Current step**.
|
|
60
|
+
The forecast uses the last half hour for the session window and the window's own average for a week, so a burst of work doesn't read as "the week runs out tonight".
|
|
82
61
|
|
|
83
|
-
|
|
62
|
+
**Settings → Plugins → Smart Session → Smart Compact:**
|
|
84
63
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
The composer pill shows whether the current session is enrolled:
|
|
88
|
-
|
|
89
|
-
| State | Meaning |
|
|
90
|
-
| --- | --- |
|
|
91
|
-
| **Smart Compact on** | The session can receive context notices and request compaction. |
|
|
92
|
-
| **Smart Compact off** | The session is not enrolled. |
|
|
93
|
-
|
|
94
|
-
If Smart Compact is disabled globally, the pill is not shown.
|
|
95
|
-
|
|
96
|
-
## Settings
|
|
97
|
-
|
|
98
|
-
| Setting | Default | Description |
|
|
64
|
+
| Setting | Default | |
|
|
99
65
|
| --- | --- | --- |
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
66
|
+
| Smart Compact | On | Context notices and agent-requested compaction |
|
|
67
|
+
| Enrol sessions automatically | On | Enrol a session once it writes task state |
|
|
68
|
+
| Context pill on every session | On | The ring above each message box |
|
|
69
|
+
| Windows of 400k or more | 30% | Where large windows are asked to compact |
|
|
70
|
+
| Smaller windows | 85% | Where smaller windows are asked to compact |
|
|
71
|
+
| Register the hooks | On | Keep the context and compaction hooks installed in Claude Code |
|
|
106
72
|
|
|
107
|
-
|
|
73
|
+
## Session Guard
|
|
108
74
|
|
|
109
|
-
|
|
75
|
+
Session Guard pauses agents at the provider's session limit and continues the same conversations after the reset, even with the app closed. It is the [Session Guard](https://github.com/tomgrin10/paseo-session-guard) plugin folded into Smart Session, and it is **off by default**: turn it on from the overview or **Settings → Plugins → Smart Session → Session Guard**.
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+
On Linux, Claude and Codex sessions pause in place together with their native subagents; elsewhere the turn is interrupted and one continuation is sent after the reset. Each paused session gets a card in its chat with the reset time and a countdown, **Resume now** and **Dismiss pause**; once the pause ends the card folds into one line. **Paused sessions** lists every pause by its reset time, and the session's pill and its workspace read **Paused**. For Claude, a reset is confirmed from whichever is fresher, Paseo's cached usage or Smart Session's own recorder.
|
|
112
78
|
|
|
113
|
-
|
|
114
|
-
$PASEO_HOME/plugin-data/smart-session/
|
|
115
|
-
```
|
|
79
|
+

|
|
116
80
|
|
|
117
|
-
|
|
118
|
-
| --- | --- |
|
|
119
|
-
| `usage-YYYY-MM.jsonl` | Plan-usage observations and reset times. |
|
|
120
|
-
| `context-YYYY-MM.jsonl` | Context-window usage changes. |
|
|
121
|
-
| `compactions.json` | Compaction requests, continuation choices, and context sizes. |
|
|
122
|
-
| `spend-index.json` | Hourly token spend by model, workspace, and agent type. |
|
|
123
|
-
| `state/<agentId>.md` | Durable task state for one agent. |
|
|
124
|
-
| `settings.json`, `enrolment.json` | Plugin settings and per-session enrolment. |
|
|
81
|
+
While the standalone plugin is enabled, Smart Session leaves session protection to it, so a session is never paused twice. Disable or uninstall Session Guard to hand over; per-session choices and waiting pauses in `$PASEO_HOME/plugin-data/session-guard/` carry over. See the [Session Guard guide](docs/session-guard.md) for settings and limits.
|
|
125
82
|
|
|
126
|
-
|
|
83
|
+
On a phone, the pill popovers open as sheets and every screen fits one column.
|
|
127
84
|
|
|
128
|
-
|
|
85
|
+

|
|
129
86
|
|
|
130
|
-
|
|
87
|
+
**Settings → Plugins → Smart Session → Display** hides the sidebar row or the composer pills; neither turns a feature off.
|
|
131
88
|
|
|
132
|
-
|
|
133
|
-
2. The file named by `PASEO_PASSWORD_FILE`
|
|
134
|
-
3. `~/paseo-hub/secrets/daemon-password`
|
|
89
|
+
## Data and access
|
|
135
90
|
|
|
136
|
-
|
|
91
|
+
Everything lives in `$PASEO_HOME/plugin-data/smart-session/`: monthly usage and context logs, compaction records, an hourly spend index by model, workspace and agent type, and one `state/<agentId>.md` per agent. Logs are append-only, and a damaged line is skipped rather than risking the rest.
|
|
92
|
+
|
|
93
|
+
Paseo plugins run trusted, unsandboxed code on the daemon host. Smart Session reads local Claude Code usage data and talks only to the local daemon. For password-protected daemons it reads `PASEO_PASSWORD`, then `PASEO_PASSWORD_FILE`, then `~/paseo-hub/secrets/daemon-password`, and never writes or logs it.
|
|
137
94
|
|
|
138
95
|
## Development
|
|
139
96
|
|
|
140
97
|
```sh
|
|
141
|
-
git clone https://github.com/tomgrin10/paseo-smart-session.git
|
|
142
|
-
cd paseo-smart-session
|
|
143
98
|
npm ci
|
|
144
99
|
npm run verify
|
|
145
|
-
paseo plugin install
|
|
100
|
+
paseo plugin install .
|
|
101
|
+
paseo plugin reload smart-session # after changes
|
|
146
102
|
```
|
|
147
103
|
|
|
148
|
-
After editing:
|
|
149
|
-
|
|
150
|
-
```sh
|
|
151
|
-
npm run verify
|
|
152
|
-
paseo plugin reload smart-session
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Reloading the plugin is enough. Do not restart the Paseo daemon: that interrupts every running agent.
|
|
156
|
-
|
|
157
|
-
## Data and privacy
|
|
158
|
-
|
|
159
|
-
Smart Session reads local Claude Code usage information and communicates with the local Paseo daemon. Its usage history, settings, and task state stay under `$PASEO_HOME`. Apart from narrowly maintaining its own Claude Code hook entries, it does not modify user configuration. MCP attachment is local to each Paseo session.
|
|
160
|
-
|
|
161
104
|
## License
|
|
162
105
|
|
|
163
106
|
[MIT](LICENSE) © 2026 Tom Gringauz.
|
|
164
107
|
|
|
165
|
-
## Navigation and display
|
|
166
|
-
|
|
167
|
-
Open this plugin from Command-K or its sidebar entry. **Overview** and **Display settings** link the main view and settings directly, without opening the Plugins settings page. Hiding a sidebar entry keeps Command-K available.
|
|
168
|
-
|
|
169
|
-
Display settings can also hide this plugin's workspace header or composer buttons independently. Hidden buttons stay hidden for newly opened workspaces; restoring them retains the latest button status.
|
|
170
|
-
|
|
171
|
-
## Paseo 0.14 agent integration
|
|
172
|
-
|
|
173
|
-
The Smart Session MCP attaches automatically when an interactive Claude or Codex session opens, including resume, refresh and import. Normal plugin startup no longer registers a global Claude MCP. Existing global entries are preserved; usage history and checkpoints remain in place. The bridge also respects a custom daemon listen address.
|
|
174
|
-
|
|
175
|
-
Requires Paseo **0.14.0 or newer**. Agent tools are scoped to interactive Claude
|
|
176
|
-
and Codex sessions; history-only opens receive none. They become available on
|
|
177
|
-
the next idle session open after install or reload. The agent label
|
|
178
|
-
`plugin-tools/enabled: "false"` opts a session out. Existing plugin data and
|
|
179
|
-
settings are retained.
|
|
180
|
-
|
|
181
108
|
## Other plugins
|
|
182
109
|
|
|
183
110
|
- [Defer](https://github.com/tomgrin10/paseo-defer) — Schedule messages for later delivery.
|
|
184
|
-
- [
|
|
185
|
-
- [
|
|
111
|
+
- [Follow-up](https://github.com/tomgrin10/paseo-follow-up) — Track workspace PRs, remaining tasks and completion.
|
|
112
|
+
- [Send to Paseo](https://github.com/tomgrin10/send-to-paseo) — Send GitHub and Graphite PRs to Paseo from Chrome.
|
|
186
113
|
- [Session Guard](https://github.com/tomgrin10/paseo-session-guard) — Pause at session limits and continue after reset.
|
|
187
|
-
- [Transfer](https://github.com/tomgrin10/paseo-transfer) — Copy or move workspaces between hosts.
|
|
188
114
|
- [Transcript Search](https://github.com/tomgrin10/paseo-transcript-search) — Search conversations across connected hosts.
|
|
189
|
-
- [
|
|
190
|
-
- [
|
|
191
|
-
|
|
192
|
-
- [Follow-up](https://github.com/tomgrin10/paseo-follow-up) — Track workspace PRs, remaining tasks and completion.
|
|
115
|
+
- [Transfer](https://github.com/tomgrin10/paseo-transfer) — Copy or move workspaces between hosts.
|
|
116
|
+
- [Vitals](https://github.com/tomgrin10/paseo-vitals) — Host, process, agent and Docker monitoring.
|
|
117
|
+
- [Workspace Switcher](https://github.com/tomgrin10/paseo-switcher) — Arc-style Ctrl+Tab between recent workspaces.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The following Lucide icons are derived from the Feather project:
|
|
20
|
+
|
|
21
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT) (for the icons listed above)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2013-present Cole Bemis
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
package/assets/icon.png
ADDED
|
Binary file
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><title>Smart Session</title><rect width="256" height="256" rx="52" fill="#1c2229"/><svg x="52" y="52" xmlns="http://www.w3.org/2000/svg" width="152" height="152" viewBox="0 0 24 24" fill="none" stroke="#f3f0e8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-gauge" aria-hidden="true"><path d="m12 14 4-4"></path><path d="M3.34 19a10 10 0 1 1 17.32 0"></path></svg></svg>
|
package/client/agents.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one agent directory this client bundle keeps, shared by every composer pill.
|
|
3
|
+
*
|
|
4
|
+
* Both the Smart Compact pill and the Session Guard pill need to know which agents
|
|
5
|
+
* exist and which workspace each sits in, since a pill registration bakes in its
|
|
6
|
+
* workspace. Following the host's agents twice would open two observations of the
|
|
7
|
+
* same directory; this opens one and lets each pill read it.
|
|
8
|
+
*
|
|
9
|
+
* It must be an *owned* observation, `agents.list({ subscribe: {}, signal })`: bare
|
|
10
|
+
* `agents.subscribe()` only listens to observations this API instance already holds,
|
|
11
|
+
* so on its own it never discovers a new agent (RESEARCH.md §5.1). A 0.8 runtime
|
|
12
|
+
* returns no observation and emits its updates through that local listener instead,
|
|
13
|
+
* so the listener stays attached until an owned observation replaces it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
17
|
+
|
|
18
|
+
export interface DirectoryAgent {
|
|
19
|
+
readonly workspaceId: string;
|
|
20
|
+
/** Closed or archived: still listed, but no longer a session anyone is working in. */
|
|
21
|
+
readonly closed: boolean;
|
|
22
|
+
/** Context occupancy as of the agent's last turn, from its snapshot; null until it has run one. */
|
|
23
|
+
readonly usedTokens: number | null;
|
|
24
|
+
readonly maxTokens: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentDirectory {
|
|
28
|
+
current(): ReadonlyMap<string, DirectoryAgent>;
|
|
29
|
+
/** Called after every change; read `current()` for the new state. */
|
|
30
|
+
subscribe(listener: () => void): () => void;
|
|
31
|
+
/** Bumped on every change, for `useSyncExternalStore`. */
|
|
32
|
+
version(): number;
|
|
33
|
+
/** Releases the observation. Safe to call more than once. */
|
|
34
|
+
dispose(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The lean shapes this needs from the agent directory, which carries far more. */
|
|
38
|
+
interface ListedAgent {
|
|
39
|
+
readonly id?: string;
|
|
40
|
+
readonly workspaceId?: string | null;
|
|
41
|
+
readonly status?: string;
|
|
42
|
+
readonly archivedAt?: string | null;
|
|
43
|
+
readonly lastUsage?: { readonly contextWindowUsedTokens?: number; readonly contextWindowMaxTokens?: number } | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AgentUpdate {
|
|
47
|
+
readonly kind: "remove" | "upsert";
|
|
48
|
+
readonly agentId?: string;
|
|
49
|
+
readonly agent?: ListedAgent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface AgentListing {
|
|
53
|
+
readonly entries?: readonly { readonly agent?: ListedAgent }[];
|
|
54
|
+
readonly subscription?: {
|
|
55
|
+
subscribe(observer: {
|
|
56
|
+
snapshot(listing: AgentListing): void;
|
|
57
|
+
update(message: { readonly type?: string; readonly payload?: AgentUpdate }): void;
|
|
58
|
+
}): () => void;
|
|
59
|
+
release(): Promise<void>;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function entryOf(agent: ListedAgent | undefined): [string, DirectoryAgent] | null {
|
|
64
|
+
if (typeof agent?.id !== "string" || typeof agent.workspaceId !== "string" || agent.workspaceId === "") return null;
|
|
65
|
+
const used = agent.lastUsage?.contextWindowUsedTokens;
|
|
66
|
+
const max = agent.lastUsage?.contextWindowMaxTokens;
|
|
67
|
+
return [agent.id, {
|
|
68
|
+
workspaceId: agent.workspaceId,
|
|
69
|
+
closed: agent.status === "closed" || !!agent.archivedAt,
|
|
70
|
+
usedTokens: typeof used === "number" && Number.isFinite(used) ? used : null,
|
|
71
|
+
maxTokens: typeof max === "number" && Number.isFinite(max) && max > 0 ? max : null,
|
|
72
|
+
}];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function same(a: DirectoryAgent | undefined, b: DirectoryAgent): boolean {
|
|
76
|
+
return a !== undefined && a.workspaceId === b.workspaceId && a.closed === b.closed && a.usedTokens === b.usedTokens && a.maxTokens === b.maxTokens;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function observeAgents(client: PluginClientContext): AgentDirectory {
|
|
80
|
+
const agents = new Map<string, DirectoryAgent>();
|
|
81
|
+
const listeners = new Set<() => void>();
|
|
82
|
+
const lifetime = new AbortController();
|
|
83
|
+
let stopped = false;
|
|
84
|
+
let unsubscribeOwnedObservation: (() => void) | null = null;
|
|
85
|
+
let releaseObservation: (() => Promise<void>) | null = null;
|
|
86
|
+
|
|
87
|
+
let version = 0;
|
|
88
|
+
const notify = () => {
|
|
89
|
+
version += 1;
|
|
90
|
+
for (const listener of [...listeners]) listener();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function upsert(agent: ListedAgent | undefined): boolean {
|
|
94
|
+
let entry = entryOf(agent);
|
|
95
|
+
if (entry === null) return false;
|
|
96
|
+
// An update that carries no usage is not a reading of zero: keep the last one.
|
|
97
|
+
const known = agents.get(entry[0]);
|
|
98
|
+
if (entry[1].usedTokens === null && agent?.lastUsage == null && known !== undefined) {
|
|
99
|
+
entry = [entry[0], { ...entry[1], usedTokens: known.usedTokens, maxTokens: known.maxTokens }];
|
|
100
|
+
}
|
|
101
|
+
if (same(known, entry[1])) return false;
|
|
102
|
+
agents.set(entry[0], entry[1]);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function apply(update: AgentUpdate): void {
|
|
107
|
+
if (stopped) return;
|
|
108
|
+
if (update.kind === "remove") {
|
|
109
|
+
if (typeof update.agentId === "string" && agents.delete(update.agentId)) notify();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (upsert(update.agent)) notify();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The legacy listener is live while the snapshot is in flight, so bootstrap is additive. */
|
|
116
|
+
function add(entries: AgentListing["entries"]): void {
|
|
117
|
+
let changed = false;
|
|
118
|
+
for (const entry of entries ?? []) changed = upsert(entry.agent) || changed;
|
|
119
|
+
if (changed) notify();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A restored observation replaces its directory snapshot, including removals. */
|
|
123
|
+
function replace(entries: AgentListing["entries"]): void {
|
|
124
|
+
const present = new Set<string>();
|
|
125
|
+
let changed = false;
|
|
126
|
+
for (const entry of entries ?? []) {
|
|
127
|
+
const parsed = entryOf(entry.agent);
|
|
128
|
+
if (parsed === null) continue;
|
|
129
|
+
present.add(parsed[0]);
|
|
130
|
+
changed = upsert(entry.agent) || changed;
|
|
131
|
+
}
|
|
132
|
+
for (const agentId of [...agents.keys()]) {
|
|
133
|
+
if (!present.has(agentId)) {
|
|
134
|
+
agents.delete(agentId);
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (changed) notify();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let unsubscribeLegacy: (() => void) | null = client.paseo.agents.subscribe((update) => apply(update as AgentUpdate));
|
|
142
|
+
|
|
143
|
+
void (async () => {
|
|
144
|
+
try {
|
|
145
|
+
const listAgents = client.paseo.agents.list as unknown as (options: {
|
|
146
|
+
subscribe: Record<string, never>;
|
|
147
|
+
signal: AbortSignal;
|
|
148
|
+
}) => Promise<AgentListing>;
|
|
149
|
+
const listing = await listAgents({ subscribe: {}, signal: lifetime.signal });
|
|
150
|
+
const release = listing.subscription ? () => listing.subscription!.release() : null;
|
|
151
|
+
if (stopped) {
|
|
152
|
+
await release?.();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
releaseObservation = release;
|
|
156
|
+
add(listing.entries);
|
|
157
|
+
if (listing.subscription) {
|
|
158
|
+
unsubscribeOwnedObservation = listing.subscription.subscribe({
|
|
159
|
+
snapshot(snapshot) {
|
|
160
|
+
if (!stopped) replace(snapshot.entries);
|
|
161
|
+
},
|
|
162
|
+
update(message) {
|
|
163
|
+
if (message.type === "agent_update" && message.payload) apply(message.payload);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
unsubscribeLegacy?.();
|
|
167
|
+
unsubscribeLegacy = null;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
const release = releaseObservation;
|
|
171
|
+
releaseObservation = null;
|
|
172
|
+
await release?.().catch(() => undefined);
|
|
173
|
+
// Not fatal to the rest of the client contribution; a reload can retry.
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
current: () => agents,
|
|
179
|
+
version: () => version,
|
|
180
|
+
subscribe(listener) {
|
|
181
|
+
listeners.add(listener);
|
|
182
|
+
return () => {
|
|
183
|
+
listeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
async dispose() {
|
|
187
|
+
stopped = true;
|
|
188
|
+
lifetime.abort();
|
|
189
|
+
unsubscribeLegacy?.();
|
|
190
|
+
unsubscribeLegacy = null;
|
|
191
|
+
unsubscribeOwnedObservation?.();
|
|
192
|
+
unsubscribeOwnedObservation = null;
|
|
193
|
+
listeners.clear();
|
|
194
|
+
agents.clear();
|
|
195
|
+
const release = releaseObservation;
|
|
196
|
+
releaseObservation = null;
|
|
197
|
+
await release?.().catch(() => undefined);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|