@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.
- package/dist/ai/cardBuilder.d.ts +27 -0
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +69 -0
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +1 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +168 -35
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +17 -6
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +37 -9
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/commitSummaryCli.d.ts +67 -0
- package/dist/ai/commitSummaryCli.d.ts.map +1 -0
- package/dist/ai/commitSummaryCli.js +416 -0
- package/dist/ai/commitSummaryCli.js.map +1 -0
- package/dist/ai/commitSummaryStore.d.ts +53 -0
- package/dist/ai/commitSummaryStore.d.ts.map +1 -0
- package/dist/ai/commitSummaryStore.js +136 -0
- package/dist/ai/commitSummaryStore.js.map +1 -0
- package/dist/ai/provider.d.ts +13 -2
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sessionManager.d.ts +32 -3
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +229 -36
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts +2 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +9 -0
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -5
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +0 -12
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +1 -44
- package/dist/connection/connection.js.map +1 -1
- package/dist/connection/pubsub.d.ts +5 -5
- package/dist/connection/pubsub.d.ts.map +1 -1
- package/dist/connection/pubsub.js +5 -7
- package/dist/connection/pubsub.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +32 -2
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +395 -176
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/messageBus/busServerTransport.d.ts +29 -0
- package/dist/messageBus/busServerTransport.d.ts.map +1 -0
- package/dist/messageBus/busServerTransport.js +54 -0
- package/dist/messageBus/busServerTransport.js.map +1 -0
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/pushClient.d.ts +69 -0
- package/dist/service/pushClient.d.ts.map +1 -0
- package/dist/service/pushClient.js +119 -0
- package/dist/service/pushClient.js.map +1 -0
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +280 -36
- package/dist/service/run.js.map +1 -1
- package/dist/service/types.d.ts +2 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service/types.js +13 -8
- package/dist/service/types.js.map +1 -1
- package/dist/storage/eventStore.d.ts +65 -0
- package/dist/storage/eventStore.d.ts.map +1 -0
- package/dist/storage/eventStore.js +122 -0
- package/dist/storage/eventStore.js.map +1 -0
- package/package.json +6 -2
package/dist/ai/provider.d.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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)}
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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();
|