@sumicom/quicksave 0.6.3 → 0.7.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 (71) hide show
  1. package/dist/ai/cardBuilder.d.ts +27 -0
  2. package/dist/ai/cardBuilder.d.ts.map +1 -1
  3. package/dist/ai/cardBuilder.js +69 -0
  4. package/dist/ai/cardBuilder.js.map +1 -1
  5. package/dist/ai/claudeCliProvider.d.ts +1 -0
  6. package/dist/ai/claudeCliProvider.d.ts.map +1 -1
  7. package/dist/ai/claudeCliProvider.js +168 -35
  8. package/dist/ai/claudeCliProvider.js.map +1 -1
  9. package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
  10. package/dist/ai/claudeSdkProvider.js +17 -6
  11. package/dist/ai/claudeSdkProvider.js.map +1 -1
  12. package/dist/ai/commitSummary.d.ts.map +1 -1
  13. package/dist/ai/commitSummary.js +37 -9
  14. package/dist/ai/commitSummary.js.map +1 -1
  15. package/dist/ai/commitSummaryCli.d.ts +67 -0
  16. package/dist/ai/commitSummaryCli.d.ts.map +1 -0
  17. package/dist/ai/commitSummaryCli.js +416 -0
  18. package/dist/ai/commitSummaryCli.js.map +1 -0
  19. package/dist/ai/commitSummaryStore.d.ts +53 -0
  20. package/dist/ai/commitSummaryStore.d.ts.map +1 -0
  21. package/dist/ai/commitSummaryStore.js +136 -0
  22. package/dist/ai/commitSummaryStore.js.map +1 -0
  23. package/dist/ai/provider.d.ts +13 -2
  24. package/dist/ai/provider.d.ts.map +1 -1
  25. package/dist/ai/sessionManager.d.ts +32 -3
  26. package/dist/ai/sessionManager.d.ts.map +1 -1
  27. package/dist/ai/sessionManager.js +229 -36
  28. package/dist/ai/sessionManager.js.map +1 -1
  29. package/dist/ai/sessionRegistry.d.ts +2 -0
  30. package/dist/ai/sessionRegistry.d.ts.map +1 -1
  31. package/dist/ai/sessionRegistry.js +9 -0
  32. package/dist/ai/sessionRegistry.js.map +1 -1
  33. package/dist/config.d.ts +4 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +15 -5
  36. package/dist/config.js.map +1 -1
  37. package/dist/connection/connection.d.ts +0 -12
  38. package/dist/connection/connection.d.ts.map +1 -1
  39. package/dist/connection/connection.js +1 -44
  40. package/dist/connection/connection.js.map +1 -1
  41. package/dist/connection/pubsub.d.ts +5 -5
  42. package/dist/connection/pubsub.d.ts.map +1 -1
  43. package/dist/connection/pubsub.js +5 -7
  44. package/dist/connection/pubsub.js.map +1 -1
  45. package/dist/handlers/messageHandler.d.ts +32 -2
  46. package/dist/handlers/messageHandler.d.ts.map +1 -1
  47. package/dist/handlers/messageHandler.js +395 -176
  48. package/dist/handlers/messageHandler.js.map +1 -1
  49. package/dist/index.js +3 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/messageBus/busServerTransport.d.ts +29 -0
  52. package/dist/messageBus/busServerTransport.d.ts.map +1 -0
  53. package/dist/messageBus/busServerTransport.js +54 -0
  54. package/dist/messageBus/busServerTransport.js.map +1 -0
  55. package/dist/service/ipcClient.js +1 -1
  56. package/dist/service/pushClient.d.ts +69 -0
  57. package/dist/service/pushClient.d.ts.map +1 -0
  58. package/dist/service/pushClient.js +119 -0
  59. package/dist/service/pushClient.js.map +1 -0
  60. package/dist/service/run.d.ts.map +1 -1
  61. package/dist/service/run.js +280 -36
  62. package/dist/service/run.js.map +1 -1
  63. package/dist/service/types.d.ts +2 -0
  64. package/dist/service/types.d.ts.map +1 -1
  65. package/dist/service/types.js +13 -8
  66. package/dist/service/types.js.map +1 -1
  67. package/dist/storage/eventStore.d.ts +65 -0
  68. package/dist/storage/eventStore.d.ts.map +1 -0
  69. package/dist/storage/eventStore.js +122 -0
  70. package/dist/storage/eventStore.js.map +1 -0
  71. package/package.json +6 -2
@@ -1,6 +1,6 @@
1
- import type { AgentId, CardEvent, CardStreamEnd } from '@sumicom/quicksave-shared';
1
+ import type { AgentId, CardEvent, CardStreamEnd, ContextUsageBreakdown } from '@sumicom/quicksave-shared';
2
2
  import type { StreamCardBuilder } from './cardBuilder.js';
3
- export type PermissionLevel = 'bypassPermissions' | 'acceptEdits' | 'default' | 'plan';
3
+ export type PermissionLevel = 'bypassPermissions' | 'acceptEdits' | 'default' | 'plan' | 'auto';
4
4
  export type ProviderHistoryMode = 'claude-jsonl' | 'memory';
5
5
  /** Represents a running provider session. */
6
6
  export interface ProviderSession {
@@ -8,6 +8,10 @@ export interface ProviderSession {
8
8
  interrupt(): void;
9
9
  kill(): void;
10
10
  readonly alive: boolean;
11
+ /** Optional — ask the provider for a breakdown of current context window
12
+ * usage. Only supported by the Claude Code CLI (via `get_context_usage`
13
+ * control_request). Returns null on providers that don't support it. */
14
+ getContextUsage?(): Promise<ContextUsageBreakdown | null>;
11
15
  }
12
16
  /** Callbacks the provider uses to communicate back to SessionManager. */
13
17
  export interface ProviderCallbacks {
@@ -23,6 +27,12 @@ export interface ProviderCallbacks {
23
27
  updatedInput?: Record<string, unknown>;
24
28
  }>;
25
29
  onModelDetected(model: string): void;
30
+ /** Fired when the underlying provider process has fully exited. SessionManager
31
+ * uses this to remove the session from its in-memory map and emit
32
+ * `session-updated { isActive: false }` so the PWA's badge reflects reality.
33
+ * The `providerSession` reference lets the manager ignore stale callbacks
34
+ * from a provider that has already been replaced by cold resume. */
35
+ onSessionExited?(sessionId: string, providerSession: ProviderSession): void;
26
36
  }
27
37
  export interface StartSessionOpts {
28
38
  prompt: string;
@@ -38,6 +48,7 @@ export interface ResumeSessionOpts {
38
48
  prompt: string;
39
49
  cwd: string;
40
50
  streamId: string;
51
+ model?: string;
41
52
  permissionLevel: PermissionLevel;
42
53
  sandboxed: boolean;
43
54
  systemPrompt?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;AACvF,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE5D,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,SAAS,IAAI,IAAI,CAAC;IAClB,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC3C,uBAAuB,CACrB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAC/E,OAAO,CAAC;QACT,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAE1C,YAAY,CACV,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,iBAAiB,EAC9B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IAE5D,aAAa,CACX,IAAI,EAAE,iBAAiB,EACvB,WAAW,EAAE,iBAAiB,EAC9B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;CAC7D"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC1G,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAChG,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE5D,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,SAAS,IAAI,IAAI,CAAC;IAClB,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;4EAEwE;IACxE,eAAe,CAAC,IAAI,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;CAC3D;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC3C,uBAAuB,CACrB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAC/E,OAAO,CAAC;QACT,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;wEAIoE;IACpE,eAAe,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;CAC7E;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAE1C,YAAY,CACV,IAAI,EAAE,gBAAgB,EACtB,WAAW,EAAE,iBAAiB,EAC9B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IAE5D,aAAa,CACX,IAAI,EAAE,iBAAiB,EACvB,WAAW,EAAE,iBAAiB,EAC9B,SAAS,EAAE,iBAAiB,GAC3B,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;CAC7D"}
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
- import type { AgentId, ClaudeSessionSummary, ClaudeUserInputRequestPayload, ClaudeUserInputResponsePayload, ClaudePreferences, ConfigValue, CardHistoryResponse } from '@sumicom/quicksave-shared';
2
+ import type { AgentId, ClaudeSessionSummary, ClaudeUserInputRequestPayload, ClaudeUserInputResponsePayload, ClaudePreferences, ConfigValue, CardHistoryResponse, SessionUpdatePayload } from '@sumicom/quicksave-shared';
3
3
  import { StreamCardBuilder } from './cardBuilder.js';
4
4
  import type { PermissionLevel, ProviderSession, ProviderCallbacks, CodingAgentProvider } from './provider.js';
5
5
  export interface ManagedSession {
@@ -11,6 +11,8 @@ export interface ManagedSession {
11
11
  permissionLevel: PermissionLevel;
12
12
  sandboxed: boolean;
13
13
  cardBuilder: StreamCardBuilder | null;
14
+ /** Model the current provider process was spawned with. Used to force cold resume when model changes. */
15
+ spawnedModel?: string;
14
16
  }
15
17
  export declare class SessionManager extends EventEmitter {
16
18
  private sessions;
@@ -33,7 +35,10 @@ export declare class SessionManager extends EventEmitter {
33
35
  getPreferences(): ClaudePreferences;
34
36
  setPreferences(prefs: Partial<ClaudePreferences>): ClaudePreferences;
35
37
  getSessionConfig(sessionId: string): Record<string, ConfigValue>;
36
- setSessionConfig(sessionId: string, key: string, value: ConfigValue): Record<string, ConfigValue>;
38
+ /** Snapshot of every tracked session's config. Used by the bus
39
+ * `/sessions/config` subscription snapshot. */
40
+ getAllSessionConfigs(): Record<string, Record<string, ConfigValue>>;
41
+ setSessionConfig(sessionId: string, key: string, value: ConfigValue): Promise<Record<string, ConfigValue>>;
37
42
  startSession(opts: {
38
43
  prompt: string;
39
44
  cwd: string;
@@ -53,8 +58,10 @@ export declare class SessionManager extends EventEmitter {
53
58
  agent?: AgentId;
54
59
  }): Promise<string>;
55
60
  cancelSession(sessionId: string): Promise<boolean>;
61
+ /** Send a raw control_request to the active provider session (CLI only). */
62
+ sendControlRequest(sessionId: string, subtype: string, params?: Record<string, unknown>): Promise<unknown>;
56
63
  closeSession(sessionId: string): boolean;
57
- setPermissionLevel(sessionId: string, level: PermissionLevel): boolean;
64
+ setPermissionLevel(sessionId: string, level: PermissionLevel): Promise<boolean>;
58
65
  getPermissionLevel(sessionId: string): PermissionLevel;
59
66
  resolveUserInput(response: ClaudeUserInputResponsePayload): boolean;
60
67
  getActiveSessions(): {
@@ -68,12 +75,20 @@ export declare class SessionManager extends EventEmitter {
68
75
  isStreaming(sessionId: string): boolean;
69
76
  isOpen(sessionId: string): boolean;
70
77
  getSessionCwd(sessionId: string): string | undefined;
78
+ /** Fetch a live context-window breakdown from the provider. Returns null if
79
+ * the session has no live provider process or the provider doesn't support
80
+ * `get_context_usage` (only the claude-code CLI does). */
81
+ getSessionContextUsage(sessionId: string): Promise<import('@sumicom/quicksave-shared').ContextUsageBreakdown | null>;
71
82
  getSessionAgent(sessionId: string, cwd?: string): AgentId;
72
83
  getActiveSessionCount(): number;
73
84
  getCardBuilder(sessionId: string): StreamCardBuilder | null;
74
85
  listAvailableSessions(cwd: string): Promise<ClaudeSessionSummary[]>;
75
86
  getCards(sessionId: string, cwd: string, offset?: number, limit?: number): Promise<CardHistoryResponse>;
76
87
  getPendingInputRequests(): ClaudeUserInputRequestPayload[];
88
+ /** True when a user-input-request is outstanding for this session. Used by
89
+ * the daemon to avoid firing a redundant "session idle" push when the
90
+ * card-stream-end is actually a permission-prompt pause. */
91
+ hasPendingInputForSession(sessionId: string): boolean;
77
92
  getDebugState(): {
78
93
  pendingInputs: {
79
94
  requestId: string;
@@ -97,7 +112,21 @@ export declare class SessionManager extends EventEmitter {
97
112
  private shouldAutoApprove;
98
113
  /** Update session title (triggered by SetTitle MCP tool). */
99
114
  private updateSessionTitle;
115
+ /** Move per-session state from oldId to newId after the CLI forks on
116
+ * --resume. `sessionAgents[newId]` is already set by the caller; this
117
+ * helper moves the remaining side maps and clears the old key. */
118
+ private migrateSessionIdState;
100
119
  private emitSessionUpdate;
120
+ /**
121
+ * Build the payload normally shipped via the `session-updated` event without
122
+ * emitting. Used by daemon-level code that wants to push a fresh snapshot to
123
+ * a specific peer (e.g. PWA reconnect) instead of triggering a broadcast.
124
+ */
125
+ buildSessionUpdatePayload(sessionId: string): SessionUpdatePayload;
126
+ /** Snapshot of every active (in-memory) session's state. Delivered via the
127
+ * bus `/sessions/active` snap frame on subscribe; subsequent incremental
128
+ * updates are published per session via the same path. */
129
+ snapshotActiveSessions(): SessionUpdatePayload[];
101
130
  private persistRegistryField;
102
131
  private persistAllowPattern;
103
132
  private waitForUserInput;
@@ -1 +1 @@
1
- {"version":3,"file":"sessionManager.d.ts","sourceRoot":"","sources":["../../src/ai/sessionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EACV,OAAO,EACP,oBAAoB,EACpB,6BAA6B,EAC7B,8BAA8B,EAC9B,iBAAiB,EACjB,WAAW,EAEX,mBAAmB,EAEpB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAE,iBAAiB,EAA6C,MAAM,kBAAkB,CAAC;AAGhG,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,eAAe,CAAC;AA+CvB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACvC;AAOD,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,kBAAkB,CAA2C;IACrE,OAAO,CAAC,gBAAgB,CAAmC;IAC3D,OAAO,CAAC,cAAc,CAAuD;IAC7E,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,oBAAoB,CAAgB;IAC5C,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,SAAS,CAAoC;IACrD,OAAO,CAAC,cAAc,CAAU;IAEhC,kGAAkG;IAClG,OAAO,CAAC,kBAAkB,CAAuD;gBAErE,SAAS,EAAE,mBAAmB,EAAE,EAAE,cAAc,GAAE,OAAuB;IAQrF,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,cAAc;IAsChB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,cAAc,IAAI,iBAAiB;IAInC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB;IAwBpE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;IAIhE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;IAsC3F,YAAY,CAAC,IAAI,EAAE;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,MAAM,CAAC;IAsEb,aAAa,CAAC,IAAI,EAAE;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,GAAG,OAAO,CAAC,MAAM,CAAC;IAgGb,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAcxC,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO;IActE,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe;IAItD,gBAAgB,CAAC,QAAQ,EAAE,8BAA8B,GAAG,OAAO;IA2BnE,iBAAiB,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,eAAe,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE;IAetJ,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIvC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO;IAIzD,qBAAqB,IAAI,MAAM;IAI/B,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAIrD,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IA6BnE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAyEpG,uBAAuB,IAAI,6BAA6B,EAAE;IAI1D,aAAa;;;;;;;;;uBAjJqB,MAAM;iBAAO,MAAM;yBAAe,OAAO;6BAAmB,OAAO;4BAAkB,MAAM;wBAAc,OAAO;;;IA4JlJ,OAAO,IAAI,IAAI;IAiBf,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB;YA8BpC,uBAAuB;IAiFrC,OAAO,CAAC,iBAAiB;IAkCzB,6DAA6D;IAC7D,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,oBAAoB;YAiBd,mBAAmB;IAsBjC,OAAO,CAAC,gBAAgB;CAQzB"}
1
+ {"version":3,"file":"sessionManager.d.ts","sourceRoot":"","sources":["../../src/ai/sessionManager.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@ import { DEFAULT_AGENT, DEFAULT_MODEL, matchAllowPattern, } from '@sumicom/quick
5
5
  import { StreamCardBuilder, buildCardsFromHistory, loadPersistedCards } from './cardBuilder.js';
6
6
  import { SANDBOX_BASH_TOOL, SET_TITLE_TOOL } from './sandboxMcp.js';
7
7
  import { getSessionRegistry } from './sessionRegistry.js';
8
+ import { getEventStore } from '../storage/eventStore.js';
8
9
  /**
9
10
  * Events emitted:
10
11
  * 'card-event' (event: CardEvent)
@@ -28,6 +29,7 @@ const AUTO_APPROVE = {
28
29
  acceptEdits: new Set(['Edit', 'Write', 'NotebookEdit', 'TodoWrite', 'Agent', 'EnterWorktree', 'ExitWorktree', 'EnterPlanMode']),
29
30
  default: new Set(['TodoWrite', 'EnterWorktree', 'ExitWorktree', 'Agent', 'EnterPlanMode']),
30
31
  plan: new Set(['EnterPlanMode']),
32
+ auto: new Set(['TodoWrite', 'EnterWorktree', 'ExitWorktree', 'Agent', 'EnterPlanMode']),
31
33
  };
32
34
  const DEFAULT_SYSTEM_PROMPT = [
33
35
  'For non-destructive shell commands (ls, cat, find, git log, git status, git diff, etc.), prefer SandboxBash over Bash. SandboxBash runs in a sandboxed environment. Use Bash only for commands that modify state.',
@@ -130,7 +132,16 @@ export class SessionManager extends EventEmitter {
130
132
  getSessionConfig(sessionId) {
131
133
  return { ...this.sessionConfigs.get(sessionId) };
132
134
  }
133
- setSessionConfig(sessionId, key, value) {
135
+ /** Snapshot of every tracked session's config. Used by the bus
136
+ * `/sessions/config` subscription snapshot. */
137
+ getAllSessionConfigs() {
138
+ const out = {};
139
+ for (const [id, cfg] of this.sessionConfigs.entries()) {
140
+ out[id] = { ...cfg };
141
+ }
142
+ return out;
143
+ }
144
+ async setSessionConfig(sessionId, key, value) {
134
145
  const prev = this.sessionConfigs.get(sessionId) ?? {};
135
146
  const next = { ...prev, [key]: value };
136
147
  this.sessionConfigs.set(sessionId, next);
@@ -141,7 +152,16 @@ export class SessionManager extends EventEmitter {
141
152
  this.setPreferences({ reasoningEffort: value });
142
153
  }
143
154
  else if (key === 'permissionMode' && typeof value === 'string') {
144
- this.setPermissionLevel(sessionId, value);
155
+ try {
156
+ await this.setPermissionLevel(sessionId, value);
157
+ }
158
+ catch (err) {
159
+ // CLI rejected the mode (e.g. auto mode not supported for this model/plan).
160
+ // Roll back the optimistic config change and rethrow.
161
+ this.sessionConfigs.set(sessionId, prev);
162
+ this.emit('session-config-updated', { sessionId, config: prev });
163
+ throw err;
164
+ }
145
165
  this.persistRegistryField(sessionId, 'permissionMode', value);
146
166
  }
147
167
  else if (key === 'sandboxed' && typeof value === 'boolean') {
@@ -170,7 +190,7 @@ export class SessionManager extends EventEmitter {
170
190
  }
171
191
  // ── Session Lifecycle ──
172
192
  async startSession(opts) {
173
- const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
193
+ const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'auto'];
174
194
  const level = validModes.includes(opts.permissionMode)
175
195
  ? opts.permissionMode : 'acceptEdits';
176
196
  const sandboxed = !!opts.sandboxed;
@@ -220,6 +240,7 @@ export class SessionManager extends EventEmitter {
220
240
  permissionLevel: level,
221
241
  sandboxed,
222
242
  cardBuilder,
243
+ spawnedModel: opts.model,
223
244
  };
224
245
  this.sessions.set(sessionId, managed);
225
246
  this.emitSessionUpdate(sessionId);
@@ -228,14 +249,28 @@ export class SessionManager extends EventEmitter {
228
249
  async resumeSession(opts) {
229
250
  const existing = this.sessions.get(opts.sessionId);
230
251
  const agentId = this.resolveAgentId(opts.sessionId, opts.cwd, opts.agent);
231
- // Hot resume: session is streaming and provider session is alive
252
+ // Force cold resume if the user changed model since the session was spawned.
253
+ // The active provider process was started with `--model X`, so changing model
254
+ // requires killing it and respawning with the new flag.
255
+ const desiredModel = this.sessionConfigs.get(opts.sessionId)?.model
256
+ ?? this.preferences.model;
257
+ const modelChanged = existing?.providerSession?.alive
258
+ && existing.spawnedModel !== undefined
259
+ && desiredModel !== undefined
260
+ && existing.spawnedModel !== desiredModel;
261
+ if (modelChanged) {
262
+ console.log(`[session-manager] model changed (${existing.spawnedModel} → ${desiredModel}) — killing provider for cold resume session=${opts.sessionId.slice(0, 8)}`);
263
+ existing.providerSession.kill();
264
+ existing.streaming = false;
265
+ existing.providerSession = null;
266
+ }
267
+ // Hot resume (active turn): provider is mid-turn. Queue the streamId so
268
+ // consumeStream picks it up after the current turn's result event.
232
269
  if (existing?.streaming && existing.providerSession?.alive) {
233
- console.log(`[session-manager] hot resume session=${opts.sessionId.slice(0, 8)} streaming=true`);
234
- // Add user prompt to cardBuilder for getCards on reconnect
270
+ console.log(`[session-manager] hot resume (active) session=${opts.sessionId.slice(0, 8)}`);
235
271
  if (existing.cardBuilder) {
236
272
  existing.cardBuilder.userMessage(opts.prompt);
237
273
  }
238
- // Queue the streamId so consumeStream picks it up after the current turn's result
239
274
  const ps = existing.providerSession;
240
275
  if (ps.pendingStreamIds) {
241
276
  ps.pendingStreamIds.push(opts.streamId);
@@ -243,6 +278,25 @@ export class SessionManager extends EventEmitter {
243
278
  existing.providerSession.sendUserMessage(opts.prompt);
244
279
  return opts.sessionId;
245
280
  }
281
+ // Hot resume (idle): CLI alive but between turns — reuse the same process
282
+ // instead of cold-resuming. Saves a kill+spawn per follow-up prompt and
283
+ // avoids the brief isActive=false window that caused the "ghost inactive"
284
+ // badge flicker between turns.
285
+ if (existing?.providerSession?.alive && !modelChanged) {
286
+ console.log(`[session-manager] hot resume (idle) session=${opts.sessionId.slice(0, 8)}`);
287
+ const ps = existing.providerSession;
288
+ if (existing.cardBuilder) {
289
+ existing.cardBuilder.cancelDeferredClear?.();
290
+ existing.cardBuilder.userMessage(opts.prompt);
291
+ existing.cardBuilder.startNewTurn(opts.streamId);
292
+ }
293
+ ps.currentStreamId = opts.streamId;
294
+ ps.resultEmitted = false;
295
+ existing.streaming = true;
296
+ existing.providerSession.sendUserMessage(opts.prompt);
297
+ this.emitSessionUpdate(opts.sessionId);
298
+ return opts.sessionId;
299
+ }
246
300
  // Cold resume already in flight — queue the prompt instead of spawning again
247
301
  const inFlight = this.coldResumeInFlight.get(opts.sessionId);
248
302
  if (inFlight) {
@@ -258,7 +312,7 @@ export class SessionManager extends EventEmitter {
258
312
  // Restore session settings from in-memory maps, falling back to persisted
259
313
  // registry (survives daemon restarts).
260
314
  const registryEntry = getSessionRegistry().getEntry(opts.cwd, opts.sessionId);
261
- const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
315
+ const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'auto'];
262
316
  const restoredLevel = this.sessionPermissions.get(opts.sessionId)
263
317
  ?? (validModes.includes(registryEntry?.permissionMode) ? registryEntry.permissionMode : undefined);
264
318
  const level = restoredLevel ?? 'acceptEdits';
@@ -267,11 +321,14 @@ export class SessionManager extends EventEmitter {
267
321
  const cardBuilder = existing?.cardBuilder ?? new StreamCardBuilder(opts.sessionId, opts.streamId, opts.cwd);
268
322
  await cardBuilder.snapshotCutoff();
269
323
  const callbacks = this.makeCallbacks(provider.id);
324
+ const sessionConfig = this.sessionConfigs.get(opts.sessionId);
325
+ const resumeModel = sessionConfig?.model ?? this.preferences.model;
270
326
  const { sessionId, session: providerSession } = await provider.resumeSession({
271
327
  sessionId: opts.sessionId,
272
328
  prompt: opts.prompt,
273
329
  cwd: opts.cwd,
274
330
  streamId: opts.streamId,
331
+ model: resumeModel,
275
332
  permissionLevel: level,
276
333
  sandboxed,
277
334
  systemPrompt: buildSystemPrompt(),
@@ -280,8 +337,21 @@ export class SessionManager extends EventEmitter {
280
337
  this.sessionAgents.set(sessionId, provider.id);
281
338
  if (existing) {
282
339
  existing.agentId = provider.id;
340
+ existing.sessionId = sessionId;
283
341
  existing.providerSession = providerSession;
284
342
  existing.streaming = true;
343
+ existing.spawnedModel = resumeModel;
344
+ // Cold resume can fork a new CLI session_id. Rekey the map + side
345
+ // maps to the new ID so emitSessionUpdate finds the entry and the
346
+ // PWA sees isActive=true (the ghost-inactive bug otherwise).
347
+ if (sessionId !== opts.sessionId) {
348
+ this.sessions.delete(opts.sessionId);
349
+ this.sessions.set(sessionId, existing);
350
+ this.migrateSessionIdState(opts.sessionId, sessionId);
351
+ // Tell PWA the old id is defunct so its store doesn't keep a
352
+ // stale active entry for the previous session_id.
353
+ this.emitSessionUpdate(opts.sessionId);
354
+ }
285
355
  }
286
356
  else {
287
357
  const managed = {
@@ -293,6 +363,7 @@ export class SessionManager extends EventEmitter {
293
363
  permissionLevel: level,
294
364
  sandboxed,
295
365
  cardBuilder,
366
+ spawnedModel: resumeModel,
296
367
  };
297
368
  this.sessions.set(sessionId, managed);
298
369
  }
@@ -316,6 +387,18 @@ export class SessionManager extends EventEmitter {
316
387
  ps.providerSession.interrupt();
317
388
  return true;
318
389
  }
390
+ /** Send a raw control_request to the active provider session (CLI only). */
391
+ async sendControlRequest(sessionId, subtype, params) {
392
+ const ps = this.sessions.get(sessionId);
393
+ if (!ps?.providerSession?.alive) {
394
+ throw new Error('Session is not active');
395
+ }
396
+ const session = ps.providerSession;
397
+ if (typeof session.sendControlRequest !== 'function') {
398
+ throw new Error('Provider does not support raw control_request');
399
+ }
400
+ return session.sendControlRequest(subtype, params);
401
+ }
319
402
  closeSession(sessionId) {
320
403
  const ps = this.sessions.get(sessionId);
321
404
  if (!ps)
@@ -329,18 +412,27 @@ export class SessionManager extends EventEmitter {
329
412
  return true;
330
413
  }
331
414
  // ── Permission ──
332
- setPermissionLevel(sessionId, level) {
415
+ async setPermissionLevel(sessionId, level) {
333
416
  const ps = this.sessions.get(sessionId);
417
+ const prevLevel = ps?.permissionLevel ?? this.sessionPermissions.get(sessionId) ?? 'acceptEdits';
334
418
  if (ps)
335
419
  ps.permissionLevel = level;
336
420
  this.sessionPermissions.set(sessionId, level);
337
- this.emit('session-updated', {
338
- sessionId,
339
- isActive: !!ps,
340
- isStreaming: ps?.streaming ?? false,
341
- hasPendingInput: ps ? Array.from(this.pendingInputRequests.values()).some(p => p.request.sessionId === sessionId) : false,
342
- permissionMode: level,
343
- });
421
+ const session = ps?.providerSession;
422
+ if (session?.alive && typeof session.sendControlRequest === 'function') {
423
+ try {
424
+ await session.sendControlRequest('set_permission_mode', { mode: level });
425
+ }
426
+ catch (err) {
427
+ // CLI rejected — roll back in-memory state so config stays consistent with the CLI.
428
+ if (ps)
429
+ ps.permissionLevel = prevLevel;
430
+ this.sessionPermissions.set(sessionId, prevLevel);
431
+ this.emitSessionUpdate(sessionId);
432
+ throw err;
433
+ }
434
+ }
435
+ this.emitSessionUpdate(sessionId);
344
436
  return true;
345
437
  }
346
438
  getPermissionLevel(sessionId) {
@@ -391,6 +483,17 @@ export class SessionManager extends EventEmitter {
391
483
  getSessionCwd(sessionId) {
392
484
  return this.sessions.get(sessionId)?.cwd;
393
485
  }
486
+ /** Fetch a live context-window breakdown from the provider. Returns null if
487
+ * the session has no live provider process or the provider doesn't support
488
+ * `get_context_usage` (only the claude-code CLI does). */
489
+ async getSessionContextUsage(sessionId) {
490
+ const ps = this.sessions.get(sessionId);
491
+ if (!ps?.providerSession?.alive)
492
+ return null;
493
+ if (typeof ps.providerSession.getContextUsage !== 'function')
494
+ return null;
495
+ return ps.providerSession.getContextUsage();
496
+ }
394
497
  getSessionAgent(sessionId, cwd) {
395
498
  return this.resolveAgentId(sessionId, cwd);
396
499
  }
@@ -405,24 +508,38 @@ export class SessionManager extends EventEmitter {
405
508
  const registryEntries = registry.getEntriesForProject(cwd)
406
509
  .filter(entry => !entry.archived);
407
510
  const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map(p => p.request.sessionId));
408
- return registryEntries.map(entry => ({
409
- sessionId: entry.sessionId,
410
- summary: entry.title ?? entry.firstPrompt ?? entry.sessionId.slice(0, 8),
411
- lastModified: entry.lastAccessedAt,
412
- createdAt: entry.createdAt,
413
- cwd: entry.cwd,
414
- agent: this.sessions.get(entry.sessionId)?.agentId
415
- ?? this.sessionAgents.get(entry.sessionId)
416
- ?? entry.agent
417
- ?? (entry.provider === 'codex-mcp' ? 'codex' : entry.provider ? 'claude-code' : undefined),
418
- gitBranch: entry.gitBranch,
419
- messageCount: entry.messageCount,
420
- isActive: this.sessions.has(entry.sessionId),
421
- isStreaming: this.sessions.get(entry.sessionId)?.streaming ?? false,
422
- hasPendingInput: pendingSessionIds.has(entry.sessionId),
423
- permissionMode: this.sessions.get(entry.sessionId)?.permissionLevel
424
- ?? this.sessionPermissions.get(entry.sessionId),
425
- }));
511
+ const eventStore = getEventStore();
512
+ return registryEntries.map(entry => {
513
+ const stats = eventStore.getSessionStats(entry.sessionId);
514
+ const lastTurn = eventStore.getLastTurn(entry.sessionId);
515
+ return {
516
+ sessionId: entry.sessionId,
517
+ summary: entry.title ?? entry.firstPrompt ?? entry.sessionId.slice(0, 8),
518
+ lastModified: entry.lastAccessedAt,
519
+ createdAt: entry.createdAt,
520
+ cwd: entry.cwd,
521
+ agent: this.sessions.get(entry.sessionId)?.agentId
522
+ ?? this.sessionAgents.get(entry.sessionId)
523
+ ?? entry.agent
524
+ ?? (entry.provider === 'codex-mcp' ? 'codex' : entry.provider ? 'claude-code' : undefined),
525
+ gitBranch: entry.gitBranch,
526
+ messageCount: entry.messageCount,
527
+ isActive: this.sessions.has(entry.sessionId),
528
+ isStreaming: this.sessions.get(entry.sessionId)?.streaming ?? false,
529
+ hasPendingInput: pendingSessionIds.has(entry.sessionId),
530
+ permissionMode: this.sessions.get(entry.sessionId)?.permissionLevel
531
+ ?? this.sessionPermissions.get(entry.sessionId),
532
+ lastPromptAt: stats.lastPromptAt ?? undefined,
533
+ turnCount: stats.turnCount,
534
+ totalInputTokens: stats.totalInputTokens,
535
+ totalOutputTokens: stats.totalOutputTokens,
536
+ totalCostUsd: stats.totalCostUsd,
537
+ lastTurnInputTokens: lastTurn?.inputTokens,
538
+ lastTurnCacheCreationTokens: lastTurn?.cacheCreationTokens,
539
+ lastTurnCacheReadTokens: lastTurn?.cacheReadTokens,
540
+ lastTurnContextUsage: lastTurn?.contextUsage,
541
+ };
542
+ });
426
543
  }
427
544
  async getCards(sessionId, cwd, offset = 0, limit = 50) {
428
545
  const ps = this.sessions.get(sessionId);
@@ -496,6 +613,16 @@ export class SessionManager extends EventEmitter {
496
613
  getPendingInputRequests() {
497
614
  return Array.from(this.pendingInputRequests.values()).map(p => p.request);
498
615
  }
616
+ /** True when a user-input-request is outstanding for this session. Used by
617
+ * the daemon to avoid firing a redundant "session idle" push when the
618
+ * card-stream-end is actually a permission-prompt pause. */
619
+ hasPendingInputForSession(sessionId) {
620
+ for (const p of this.pendingInputRequests.values()) {
621
+ if (p.request.sessionId === sessionId)
622
+ return true;
623
+ }
624
+ return false;
625
+ }
499
626
  getDebugState() {
500
627
  const pendingInputs = Array.from(this.pendingInputRequests.values()).map(p => ({
501
628
  requestId: p.request.requestId,
@@ -541,10 +668,23 @@ export class SessionManager extends EventEmitter {
541
668
  return this.handlePermissionRequest(sessionId, req);
542
669
  },
543
670
  onModelDetected: (model) => {
671
+ console.log(`[session-manager] model detected: ${model} (agent=${agentId})`);
544
672
  if (agentId === 'claude-code') {
545
673
  this.preferences.model = model;
546
674
  }
547
675
  },
676
+ onSessionExited: (sessionId, providerSession) => {
677
+ const ps = this.sessions.get(sessionId);
678
+ // Stale callback — a newer provider has already taken this slot
679
+ // (e.g. cold resume killed this one and spawned a fresh CLI).
680
+ if (!ps || ps.providerSession !== providerSession)
681
+ return;
682
+ console.log(`[session-manager] provider exited session=${sessionId.slice(0, 8)} — marking inactive`);
683
+ ps.providerSession = null;
684
+ ps.streaming = false;
685
+ this.sessions.delete(sessionId);
686
+ this.emitSessionUpdate(sessionId);
687
+ },
548
688
  };
549
689
  }
550
690
  // ── Private: Permission Handling ──
@@ -658,19 +798,72 @@ export class SessionManager extends EventEmitter {
658
798
  }
659
799
  }
660
800
  }
801
+ /** Move per-session state from oldId to newId after the CLI forks on
802
+ * --resume. `sessionAgents[newId]` is already set by the caller; this
803
+ * helper moves the remaining side maps and clears the old key. */
804
+ migrateSessionIdState(oldId, newId) {
805
+ const permission = this.sessionPermissions.get(oldId);
806
+ if (permission !== undefined) {
807
+ this.sessionPermissions.set(newId, permission);
808
+ this.sessionPermissions.delete(oldId);
809
+ }
810
+ const sandboxed = this.sessionSandboxed.get(oldId);
811
+ if (sandboxed !== undefined) {
812
+ this.sessionSandboxed.set(newId, sandboxed);
813
+ this.sessionSandboxed.delete(oldId);
814
+ }
815
+ const config = this.sessionConfigs.get(oldId);
816
+ if (config !== undefined) {
817
+ this.sessionConfigs.set(newId, config);
818
+ this.sessionConfigs.delete(oldId);
819
+ }
820
+ this.sessionAgents.delete(oldId);
821
+ }
661
822
  emitSessionUpdate(sessionId) {
823
+ this.emit('session-updated', this.buildSessionUpdatePayload(sessionId));
824
+ }
825
+ /**
826
+ * Build the payload normally shipped via the `session-updated` event without
827
+ * emitting. Used by daemon-level code that wants to push a fresh snapshot to
828
+ * a specific peer (e.g. PWA reconnect) instead of triggering a broadcast.
829
+ */
830
+ buildSessionUpdatePayload(sessionId) {
662
831
  const ps = this.sessions.get(sessionId);
663
832
  const hasPendingInput = Array.from(this.pendingInputRequests.values())
664
833
  .some(p => p.request.sessionId === sessionId);
665
- this.emit('session-updated', {
834
+ const eventStore = getEventStore();
835
+ const stats = eventStore.getSessionStats(sessionId);
836
+ const lastTurn = eventStore.getLastTurn(sessionId);
837
+ return {
666
838
  sessionId,
667
839
  isActive: !!ps,
840
+ // `archived` = session has been removed from the in-memory map
841
+ // (cold-resume rekey, onSessionExited, or closeSession). PWA uses this
842
+ // as a signal to navigate away from the now-defunct session page.
843
+ // Note: isActive=false alone is ambiguous (can also mean "emitted for
844
+ // an unknown id during reconcile"); archived=true is the strong signal.
845
+ archived: !ps,
668
846
  agent: ps?.agentId ?? this.sessionAgents.get(sessionId),
669
847
  isStreaming: ps?.streaming ?? false,
670
848
  hasPendingInput,
671
849
  permissionMode: ps?.permissionLevel ?? this.sessionPermissions.get(sessionId),
672
850
  sandboxed: ps?.sandboxed ?? this.sessionSandboxed.get(sessionId) ?? false,
673
- });
851
+ lastPromptAt: stats.lastPromptAt ?? undefined,
852
+ turnCount: stats.turnCount,
853
+ totalInputTokens: stats.totalInputTokens,
854
+ totalOutputTokens: stats.totalOutputTokens,
855
+ totalCostUsd: stats.totalCostUsd,
856
+ lastTurnInputTokens: lastTurn?.inputTokens,
857
+ lastTurnCacheCreationTokens: lastTurn?.cacheCreationTokens,
858
+ lastTurnCacheReadTokens: lastTurn?.cacheReadTokens,
859
+ lastTurnContextUsage: lastTurn?.contextUsage,
860
+ };
861
+ }
862
+ /** Snapshot of every active (in-memory) session's state. Delivered via the
863
+ * bus `/sessions/active` snap frame on subscribe; subsequent incremental
864
+ * updates are published per session via the same path. */
865
+ snapshotActiveSessions() {
866
+ return Array.from(this.sessions.keys()).map((id) => this.buildSessionUpdatePayload(id));
674
867
  }
675
868
  persistRegistryField(sessionId, key, value) {
676
869
  const registry = getSessionRegistry();