paseo-smart-session 1.2.4 → 1.2.5

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 CHANGED
@@ -37,13 +37,13 @@ That history remains useful after an individual session ends or a provider cache
37
37
  Install from npm on Paseo 0.9.0 or newer:
38
38
 
39
39
  ```sh
40
- paseo plugin install npm:paseo-smart-session
40
+ paseo plugin install npm:paseo-smart-session@1.2.5
41
41
  ```
42
42
 
43
43
  Paseo 0.8 can install the same plugin from Git:
44
44
 
45
45
  ```sh
46
- paseo plugin add tomgrin10/paseo-smart-session
46
+ paseo plugin add tomgrin10/paseo-smart-session --ref v1.2.5
47
47
  ```
48
48
 
49
49
  ```sh
package/client/pill.tsx CHANGED
@@ -197,9 +197,27 @@ function AutoCompactIcon({
197
197
  return <Icon name="FoldVertical" size={size} color={color} />;
198
198
  }
199
199
 
200
- /** The lean shape this needs from the agent list, which carries far more. */
200
+ /** The lean shapes this needs from the agent directory, which carries far more. */
201
+ interface ListedAgent {
202
+ readonly id?: string;
203
+ readonly workspaceId?: string | null;
204
+ }
205
+
206
+ interface AgentUpdate {
207
+ readonly kind: "remove" | "upsert";
208
+ readonly agentId?: string;
209
+ readonly agent?: ListedAgent;
210
+ }
211
+
201
212
  interface AgentListing {
202
- readonly entries?: readonly { readonly agent?: { id?: string; workspaceId?: string | null } }[];
213
+ readonly entries?: readonly { readonly agent?: ListedAgent }[];
214
+ readonly subscription?: {
215
+ subscribe(observer: {
216
+ snapshot(listing: AgentListing): void;
217
+ update(message: { readonly type?: string; readonly payload?: AgentUpdate }): void;
218
+ }): () => void;
219
+ release(): Promise<void>;
220
+ };
203
221
  }
204
222
 
205
223
  /**
@@ -227,6 +245,10 @@ function updatePill(handle: PillHandle, title: string): void {
227
245
  export function contributeClient(client: PluginClientContext) {
228
246
  /** Every agent this app knows of, and its pill registration while one is up. */
229
247
  const tracked = new Map<string, { workspaceId: string; handle: PillHandle | null }>();
248
+ const lifetime = new AbortController();
249
+ let stopped = false;
250
+ let unsubscribeOwnedObservation: (() => void) | null = null;
251
+ let releaseAgentObservation: (() => Promise<void>) | null = null;
230
252
 
231
253
  function register(agentId: string, entry: { workspaceId: string; handle: PillHandle | null }): void {
232
254
  const toggle = async () => {
@@ -299,42 +321,114 @@ export function contributeClient(client: PluginClientContext) {
299
321
  tracked.delete(agentId);
300
322
  }
301
323
 
324
+ function applyAgentUpdate(update: AgentUpdate): void {
325
+ if (update.kind === "remove") {
326
+ if (typeof update.agentId === "string") forget(update.agentId);
327
+ return;
328
+ }
329
+ const agent = update.agent;
330
+ if (
331
+ typeof agent?.id !== "string" ||
332
+ typeof agent.workspaceId !== "string" ||
333
+ agent.workspaceId === ""
334
+ ) return;
335
+ track(agent.id, agent.workspaceId);
336
+ }
337
+
338
+ function trackAgents(entries: AgentListing["entries"]): Set<string> {
339
+ const present = new Set<string>();
340
+ for (const entry of entries ?? []) {
341
+ const agent = entry.agent;
342
+ if (
343
+ typeof agent?.id !== "string" ||
344
+ typeof agent.workspaceId !== "string" ||
345
+ agent.workspaceId === ""
346
+ ) continue;
347
+ present.add(agent.id);
348
+ track(agent.id, agent.workspaceId);
349
+ }
350
+ return present;
351
+ }
352
+
353
+ /** A restored observation replaces its directory snapshot, including removals. */
354
+ function replaceAgents(entries: AgentListing["entries"]): void {
355
+ const present = trackAgents(entries);
356
+ for (const agentId of [...tracked.keys()]) {
357
+ if (!present.has(agentId)) forget(agentId);
358
+ }
359
+ }
360
+
302
361
  const refresh = () => refreshPills(() => client.rpc(enrolmentState, {}));
303
362
 
304
363
  const unsubscribeStore = subscribe(sync);
305
364
 
306
- const unsubscribeAgents = client.paseo.agents.subscribe((update) => {
307
- if (update.kind === "remove") {
308
- forget(update.agentId);
309
- return;
310
- }
311
- const { id, workspaceId } = update.agent;
312
- if (typeof workspaceId !== "string" || workspaceId === "") return;
313
- track(id, workspaceId);
365
+ // v0.8 returns only the listing and emits its subscribed updates through this
366
+ // local listener. Keep it attached until a newer runtime gives us the owned
367
+ // observation whose snapshots can reconcile the whole directory on reconnect.
368
+ let unsubscribeLegacyAgents: (() => void) | null = client.paseo.agents.subscribe((update) => {
369
+ applyAgentUpdate(update as AgentUpdate);
314
370
  });
315
371
 
316
372
  // The subscription only carries agents that report in from now on, and an idle
317
373
  // one may not do that for hours.
318
374
  void (async () => {
319
375
  try {
320
- const listing = (await client.paseo.agents.list()) as unknown as AgentListing;
321
- for (const entry of listing.entries ?? []) {
322
- const agent = entry.agent;
323
- if (typeof agent?.id !== "string" || typeof agent.workspaceId !== "string") continue;
324
- track(agent.id, agent.workspaceId);
376
+ // `agents.subscribe()` listens only to observations already owned by
377
+ // this API instance. A plain list stopped receiving newly created agents
378
+ // in Paseo 0.9, which made both plugin pills disappear on new sessions.
379
+ const listAgents = client.paseo.agents.list as unknown as (options: {
380
+ subscribe: Record<string, never>;
381
+ signal: AbortSignal;
382
+ }) => Promise<AgentListing>;
383
+ const listing = await listAgents({ subscribe: {}, signal: lifetime.signal });
384
+ const release = listing.subscription
385
+ ? () => listing.subscription!.release()
386
+ : null;
387
+ if (stopped) {
388
+ await release?.();
389
+ return;
390
+ }
391
+ releaseAgentObservation = release;
392
+ // The legacy listener was already live while this snapshot was in flight,
393
+ // so bootstrap must be additive or it can erase a just-created agent.
394
+ trackAgents(listing.entries);
395
+ if (listing.subscription) {
396
+ unsubscribeOwnedObservation = listing.subscription.subscribe({
397
+ snapshot(snapshot) {
398
+ if (!stopped) replaceAgents(snapshot.entries);
399
+ },
400
+ update(message) {
401
+ if (!stopped && message.type === "agent_update" && message.payload) {
402
+ applyAgentUpdate(message.payload);
403
+ }
404
+ },
405
+ });
406
+ unsubscribeLegacyAgents?.();
407
+ unsubscribeLegacyAgents = null;
325
408
  }
326
409
  } catch {
327
- // Not fatal: the subscription still picks up every agent that speaks next.
410
+ const release = releaseAgentObservation;
411
+ releaseAgentObservation = null;
412
+ await release?.().catch(() => undefined);
413
+ // Not fatal to the rest of the client contribution; a reload can retry.
328
414
  }
329
415
  })();
330
416
 
331
417
  void refresh().catch(() => undefined);
332
418
  const timer = setInterval(() => void refresh().catch(() => undefined), REFRESH_MS);
333
419
 
334
- return () => {
420
+ return async () => {
421
+ stopped = true;
422
+ lifetime.abort();
335
423
  clearInterval(timer);
336
- unsubscribeAgents();
424
+ unsubscribeLegacyAgents?.();
425
+ unsubscribeLegacyAgents = null;
426
+ unsubscribeOwnedObservation?.();
427
+ unsubscribeOwnedObservation = null;
337
428
  unsubscribeStore();
338
429
  for (const agentId of [...tracked.keys()]) forget(agentId);
430
+ const release = releaseAgentObservation;
431
+ releaseAgentObservation = null;
432
+ await release?.().catch(() => undefined);
339
433
  };
340
434
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "paseo-smart-session",
3
3
  "type": "module",
4
- "version": "1.2.4",
4
+ "version": "1.2.5",
5
5
  "license": "MIT",
6
6
  "description": "Records Claude plan-usage history and lets a Paseo agent manage its own context.",
7
7
  "repository": {