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.
Files changed (66) hide show
  1. package/README.md +55 -130
  2. package/assets/LICENSE.lucide +43 -0
  3. package/assets/icon.png +0 -0
  4. package/assets/icon.svg +1 -0
  5. package/client/agents.ts +200 -0
  6. package/client/charts.tsx +277 -152
  7. package/client/chat.tsx +122 -0
  8. package/client/display.tsx +55 -33
  9. package/client/guard/index.tsx +48 -0
  10. package/client/guard/paused.tsx +164 -0
  11. package/client/guard/pill-icons.tsx +26 -0
  12. package/client/guard/pills.ts +132 -0
  13. package/client/guard/settings.tsx +114 -0
  14. package/client/guard/surfaces.ts +3 -0
  15. package/client/guard/timeline.tsx +154 -0
  16. package/client/kit.tsx +144 -0
  17. package/client/live.ts +49 -0
  18. package/client/motion.tsx +94 -0
  19. package/client/overview.tsx +373 -0
  20. package/client/pill-store.ts +77 -0
  21. package/client/pill.tsx +91 -380
  22. package/client/popover.tsx +113 -0
  23. package/client/settings.tsx +105 -0
  24. package/client/theme.ts +56 -0
  25. package/docs/media/smart-session-chat.png +0 -0
  26. package/docs/media/smart-session-flow.gif +0 -0
  27. package/docs/media/smart-session-guard.png +0 -0
  28. package/docs/media/smart-session-hero-light.png +0 -0
  29. package/docs/media/smart-session-hero.png +0 -0
  30. package/docs/media/smart-session-mobile.png +0 -0
  31. package/docs/media/smart-session-pill.png +0 -0
  32. package/docs/navigation-verification.md +6 -6
  33. package/docs/screenshots/display-settings-desktop.png +0 -0
  34. package/docs/screenshots/display-settings-mobile.png +0 -0
  35. package/docs/session-guard.md +88 -0
  36. package/index.client.tsx +52 -35
  37. package/index.server.ts +19 -11
  38. package/mcp.mjs +22 -3
  39. package/package.json +2 -1
  40. package/paseo-plugin.json +11 -0
  41. package/server/guard/client.ts +27 -0
  42. package/server/guard/engine.ts +554 -0
  43. package/server/guard/index.ts +395 -0
  44. package/server/guard/labels.ts +77 -0
  45. package/server/guard/processes.ts +147 -0
  46. package/server/guard/standalone.ts +61 -0
  47. package/server/guard/store.ts +113 -0
  48. package/server/guard/timeline.ts +24 -0
  49. package/server/guard/usage.ts +57 -0
  50. package/server/recorder.ts +2 -0
  51. package/server/settings.ts +10 -0
  52. package/server/store.ts +25 -1
  53. package/shared/blocks.ts +15 -0
  54. package/shared/chat.ts +174 -0
  55. package/shared/forecast.ts +233 -0
  56. package/shared/format.ts +47 -1
  57. package/shared/governor.ts +3 -0
  58. package/shared/guard/contracts.ts +65 -0
  59. package/shared/guard/policy.ts +128 -0
  60. package/shared/guard/presentation.ts +85 -0
  61. package/shared/guard/timeline.ts +34 -0
  62. package/shared/lifecycle.ts +18 -0
  63. package/shared/smart-session.ts +16 -0
  64. package/client/navigation.tsx +0 -14
  65. package/client/surface.tsx +0 -574
  66. package/docs/screenshots/smart-compact-handoff.png +0 -0
package/README.md CHANGED
@@ -5,188 +5,113 @@
5
5
  [![Paseo](https://img.shields.io/badge/Paseo-%E2%89%A5%200.14.0-8A63D2?style=for-the-badge)](https://paseo.sh)
6
6
  [![License](https://img.shields.io/github/license/tomgrin10/paseo-smart-session?style=for-the-badge&color=2563eb)](LICENSE)
7
7
 
8
- Give Paseo agents a safe way to manage long-running work.
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
- Smart Session lets an agent see how full its context is, save durable task state, and compact itself when it is ready. It also records session and plan-usage statistics, so you can understand where tokens are going and when limits reset.
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
- ![A Smart Session compaction handoff: the requested compact preserves the task state, then the resumed session is told to re-read its state file and continue](docs/screenshots/smart-compact-handoff.png)
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
- ```sh
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
- An agent can check how much context it has used, checkpoint the work that must survive a reset, and request compaction when it reaches a safe stopping point. The agent also decides whether another turn should start afterwards and can write the exact follow-up message. A completed task can compact and stop; work in progress can resume from its saved state.
25
+ ## Use
35
26
 
36
- The saved state captures the goal, the current step, decisions already made, and approaches that have already failed. That prevents a resumed agent from redoing work or asking a person how to continue.
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
- Smart Session never chooses to compact an agent by itself. It can let an agent know that context is filling, but the agent decides whether to checkpoint, compact, or defer.
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
- ### Shows session and plan usage
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 keeps a local history of:
36
+ ![Pressing the context ring above the message box, then following it to the Smart Session screen and sweeping the window chart: recorded usage up to now, the projection at this pace, and the stretch after it runs out](docs/media/smart-session-flow.gif)
43
37
 
44
- - Context-window usage for each agent session
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
+ ![A session's context popover: 34% of a 1M context, 336k tokens, compacts at 30%, past its compact line; task state saved 10m ago; last compaction 3h ago, 318k to 31k; the Smart Compact switch for this session](docs/media/smart-session-pill.png)
48
39
 
49
- That history remains useful after an individual session ends or a provider cache changes.
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` | Shows current context usage, filling rate, and relevant thresholds. |
58
- | `checkpoint` | Writes durable task state for the current session. |
59
- | `request_compaction` | Queues compaction and chooses whether and how to continue afterwards. |
60
- | `defer_compaction` | Defers a compaction request and records why. |
61
- | `budget_status` | Shows plan usage, burn rate, and reset times. |
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` refuses when the state file is missing or stale. The agent can checkpoint and request again in the same turn.
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
- ## Smart Compact
52
+ ![A chat after a compaction: the agent's own compaction request and the continuation as cards, a context manually compacted divider, and one-line rows for Checked plan usage and Put off compacting](docs/media/smart-session-chat.png)
72
53
 
73
- Smart Compact is the session-control feature. As a session fills, hooks can provide context notices at useful thresholds. When the compact threshold is reached at the end of a turn, the agent is asked whether it wants to compact.
54
+ ## How Smart Compact works
74
55
 
75
- There is no forced compaction. An agent can compact immediately, defer with a reason, or keep working. A request is delivered only when the agent is idle and its state file is current.
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
- Every request sends the `/compact` command. The agent can also set:
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
- - `continue_after_compaction: false` to finish without starting another turn.
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
- Omitting the new options preserves the earlier behavior: Smart Session continues with the default state-aware prompt. The durable state wins if it disagrees with the automatic summary. A `/compact` entered by a person never receives a plugin continuation.
62
+ **Settings → Plugins → Smart Session → Smart Compact:**
84
63
 
85
- ### The composer pill
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
- | **Smart Compact** | On | Enables context notices and agent-requested compaction. |
101
- | **Large-window compact threshold** | 30% | Sets when Smart Compact asks on context windows of 400k tokens or more. |
102
- | **Small-window compact threshold** | 85% | Sets when Smart Compact asks on smaller context windows. |
103
- | **Enrol sessions automatically** | On | Enrols a session after it writes task state. |
104
- | **Show the pill on every agent** | On | Shows the Smart Compact control in the composer. |
105
- | **Register the hooks with Claude Code** | On | Keeps the context and compaction hooks installed. |
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
- The remaining settings apply only while Smart Compact is enabled. Thresholds accept whole percentages from 1% to 99%; lowering one also moves its earlier notice bands down when needed.
73
+ ## Session Guard
108
74
 
109
- ## Usage history and state
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
- Smart Session stores its data under:
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
- ```text
114
- $PASEO_HOME/plugin-data/smart-session/
115
- ```
79
+ ![Session Guard: a pause card in the chat with the reset time, Resume now and Dismiss pause, and the Paused sessions screen listing sessions waiting for the reset](docs/media/smart-session-guard.png)
116
80
 
117
- | File | Contains |
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
- Usage and context observations are append-only. A damaged line is skipped rather than putting the rest of the history at risk. Existing compaction rows are read as “continue with the default prompt,” so upgrading requires no data migration.
83
+ On a phone, the pill popovers open as sheets and every screen fits one column.
127
84
 
128
- ## Password-protected daemons
85
+ ![Smart Session at phone width: the overview, the context popover as a sheet, and Paused sessions](docs/media/smart-session-mobile.png)
129
86
 
130
- When the Paseo daemon requires a password, Smart Session resolves it in this order:
87
+ **Settings → Plugins → Smart Session → Display** hides the sidebar row or the composer pills; neither turns a feature off.
131
88
 
132
- 1. `PASEO_PASSWORD`
133
- 2. The file named by `PASEO_PASSWORD_FILE`
134
- 3. `~/paseo-hub/secrets/daemon-password`
89
+ ## Data and access
135
90
 
136
- The secret is used only to connect to the daemon. It is never written or logged.
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 "$PWD"
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
- - [Graphite](https://github.com/tomgrin10/paseo-graphite) — Graphite stacks and PR status.
185
- - [Vitals](https://github.com/tomgrin10/paseo-vitals) — Host, agent, and Docker health.
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
- - [Send to Paseo](https://github.com/tomgrin10/send-to-paseo) — Send GitHub and Graphite PRs to Paseo from Chrome.
190
- - [Dispatch](https://github.com/tomgrin10/paseo-dispatch) — Route tasks to existing workspaces (development preview).
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.
Binary file
@@ -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>
@@ -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
+ }