@sumicom/quicksave 0.7.1 → 0.8.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/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +8 -11
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +9 -14
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/enrichEntry.d.ts +11 -0
- package/dist/ai/enrichEntry.d.ts.map +1 -0
- package/dist/ai/enrichEntry.js +27 -0
- package/dist/ai/enrichEntry.js.map +1 -0
- package/dist/ai/provider.d.ts +6 -0
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sandboxMcp.d.ts +41 -3
- package/dist/ai/sandboxMcp.d.ts.map +1 -1
- package/dist/ai/sandboxMcp.js +44 -3
- package/dist/ai/sandboxMcp.js.map +1 -1
- package/dist/ai/sandboxMcpStdio.js +103 -9
- package/dist/ai/sandboxMcpStdio.js.map +1 -1
- package/dist/ai/sessionManager.d.ts +14 -2
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +141 -19
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts +57 -6
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +237 -31
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +61 -0
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +274 -1
- package/dist/connection/connection.js.map +1 -1
- package/dist/connection/relay.d.ts +20 -0
- package/dist/connection/relay.d.ts.map +1 -1
- package/dist/connection/relay.js +62 -1
- package/dist/connection/relay.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +10 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +59 -3
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +60 -2
- package/dist/index.js.map +1 -1
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/ipcServer.d.ts.map +1 -1
- package/dist/service/ipcServer.js +10 -4
- package/dist/service/ipcServer.js.map +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +69 -21
- package/dist/service/run.js.map +1 -1
- package/dist/service/types.d.ts +15 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service/types.js +6 -2
- package/dist/service/types.js.map +1 -1
- package/dist/tombstoneCheck.d.ts +43 -0
- package/dist/tombstoneCheck.d.ts.map +1 -0
- package/dist/tombstoneCheck.js +96 -0
- package/dist/tombstoneCheck.js.map +1 -0
- package/package.json +3 -3
|
@@ -15,16 +15,25 @@
|
|
|
15
15
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
17
|
import { execFileSync } from 'child_process';
|
|
18
|
-
import { realpathSync, existsSync } from 'fs';
|
|
18
|
+
import { realpathSync, existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join, dirname } from 'path';
|
|
20
|
+
import { homedir, platform } from 'os';
|
|
20
21
|
import { fileURLToPath } from 'url';
|
|
21
22
|
import { z } from 'zod';
|
|
22
|
-
import { platform } from 'os';
|
|
23
23
|
const __ownDir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
|
|
25
|
-
const
|
|
24
|
+
function readArg(flag) {
|
|
25
|
+
const idx = process.argv.indexOf(flag);
|
|
26
|
+
return idx !== -1 ? process.argv[idx + 1] : undefined;
|
|
27
|
+
}
|
|
28
|
+
const cwd = readArg('--cwd') ?? process.cwd();
|
|
29
|
+
/** Known only on resume; undefined when the CLI spawns this MCP for a fresh session. */
|
|
30
|
+
const sessionIdHint = readArg('--session-id');
|
|
26
31
|
const realCwd = realpathSync(cwd);
|
|
27
32
|
const realHome = realpathSync(process.env.HOME ?? '/');
|
|
33
|
+
const quicksaveHome = process.env.QUICKSAVE_HOME || join(homedir(), '.quicksave');
|
|
34
|
+
const sessionRegistryDir = join(quicksaveHome, 'state', 'session-registry');
|
|
35
|
+
/** Same encoding as `apps/agent/src/ai/sessionRegistry.ts:encodeProjectPath`. */
|
|
36
|
+
const encodedCwd = cwd.replace(/\//g, '-');
|
|
28
37
|
const PROFILE_PATH = join(__ownDir, 'profiles', 'project-sandbox.sb');
|
|
29
38
|
function detectBackend() {
|
|
30
39
|
const os = platform();
|
|
@@ -111,12 +120,97 @@ server.tool('SandboxBash', 'Execute a shell command WITHOUT requiring user permi
|
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
function readStoredStatus() {
|
|
124
|
+
const empty = {
|
|
125
|
+
subject: null, stage: null, blocked: null, note: null, recentNotes: [], source: 'unknown',
|
|
126
|
+
};
|
|
127
|
+
if (!sessionIdHint)
|
|
128
|
+
return empty;
|
|
129
|
+
const path = join(sessionRegistryDir, encodedCwd, `${sessionIdHint}.json`);
|
|
130
|
+
if (!existsSync(path))
|
|
131
|
+
return empty;
|
|
132
|
+
try {
|
|
133
|
+
const entry = JSON.parse(readFileSync(path, 'utf-8'));
|
|
134
|
+
const history = Array.isArray(entry.noteHistory) ? entry.noteHistory : [];
|
|
135
|
+
return {
|
|
136
|
+
subject: typeof entry.title === 'string' ? entry.title : null,
|
|
137
|
+
stage: entry.stage ?? null,
|
|
138
|
+
blocked: typeof entry.blocked === 'boolean' ? entry.blocked : null,
|
|
139
|
+
note: typeof entry.note === 'string' ? entry.note : null,
|
|
140
|
+
recentNotes: history.slice(-5),
|
|
141
|
+
source: 'stored',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return empty;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
server.tool('UpdateSessionStatus', 'Update the ticket-style status for the current session, shown to the user on the home screen.\n' +
|
|
149
|
+
'\n' +
|
|
150
|
+
'Call at the START of every session (first response), and again whenever stage changes, ' +
|
|
151
|
+
'work gets blocked/unblocked, or progress is worth surfacing.\n' +
|
|
152
|
+
'\n' +
|
|
153
|
+
'Call with NO fields to read the current stored status without mutating ' +
|
|
154
|
+
'(dry-run). Useful on resume to check whether subject/stage already match the work.\n' +
|
|
155
|
+
'\n' +
|
|
156
|
+
'Fields:\n' +
|
|
157
|
+
' subject — What this session is solving, from the user\'s perspective.\n' +
|
|
158
|
+
' Good: "Fix auth token expiring early" Bad: "Debugging jwt.ts"\n' +
|
|
159
|
+
' stage — Ticket lifecycle stage:\n' +
|
|
160
|
+
' investigating reading code, finding root cause, understanding problem, designing\n' +
|
|
161
|
+
' working actively writing or changing code\n' +
|
|
162
|
+
' verifying running tests, confirming the fix, waiting on CI\n' +
|
|
163
|
+
' done user-visible deliverable is complete\n' +
|
|
164
|
+
' blocked — Orthogonal flag. Set true when stuck (waiting on user decision, ' +
|
|
165
|
+
'permission request, external service). Set false when unblocked. Do not change stage.\n' +
|
|
166
|
+
' note — One progress/finding entry, max ~12 words. APPENDED to the session\'s event log ' +
|
|
167
|
+
'(not overwritten). Emit one on meaningful state changes: ruling out a hypothesis, ' +
|
|
168
|
+
'completing a sub-goal, hitting a blocker, starting verification, etc. ' +
|
|
169
|
+
'Examples: "handler done, writing tests" / "permission pending on git push" / ' +
|
|
170
|
+
'"ruled out jwt.ts — secret looks right".\n' +
|
|
171
|
+
'\n' +
|
|
172
|
+
'Typical flows:\n' +
|
|
173
|
+
' Bug / debug: investigating → working → verifying → (loop or done)\n' +
|
|
174
|
+
' Feature: investigating → working → verifying → done\n' +
|
|
175
|
+
' Question: investigating → done\n' +
|
|
176
|
+
' Chore: working → verifying → done\n' +
|
|
177
|
+
'\n' +
|
|
178
|
+
'Rules:\n' +
|
|
179
|
+
'- Always call on the first response of a new session to set `subject` and `stage`.\n' +
|
|
180
|
+
'- On resume, if you cannot see a prior UpdateSessionStatus tool call in conversation ' +
|
|
181
|
+
'history, do a no-args dry-run first; then set/correct subject or stage if blank or ' +
|
|
182
|
+
'drifted from the current work.\n' +
|
|
183
|
+
'- For long-running tasks (research, large refactors), emit a `note` every time you ' +
|
|
184
|
+
'cross a sub-goal or learn something — the user opens the session to skim recent notes ' +
|
|
185
|
+
'as progress signal.\n' +
|
|
186
|
+
'- Do not skip `verifying` if you ran tests / build / repro.\n' +
|
|
187
|
+
'- Do not declare `done` until the user\'s problem is fully resolved.\n' +
|
|
188
|
+
'- For long refactors, prefer proposing to split into per-phase sub-sessions ' +
|
|
189
|
+
'over keeping one session in `investigating` for a long time.', {
|
|
190
|
+
subject: z.string().optional().describe('Subject line — what this session is solving'),
|
|
191
|
+
stage: z.enum(['investigating', 'working', 'verifying', 'done']).optional()
|
|
192
|
+
.describe('Current lifecycle stage'),
|
|
193
|
+
blocked: z.boolean().optional().describe('True when stuck, false when unblocked'),
|
|
194
|
+
note: z.string().optional()
|
|
195
|
+
.describe('One progress/finding entry (~12 words). Appended to the session event log; emit on each meaningful state change.'),
|
|
118
196
|
}, async (args) => {
|
|
119
|
-
|
|
197
|
+
const isDryRun = args.subject === undefined &&
|
|
198
|
+
args.stage === undefined &&
|
|
199
|
+
args.blocked === undefined &&
|
|
200
|
+
args.note === undefined;
|
|
201
|
+
// Daemon-side `shouldAutoApprove` has already persisted any updates by the
|
|
202
|
+
// time this handler runs, so re-reading the registry file yields the final
|
|
203
|
+
// merged state for both dry-run and write paths.
|
|
204
|
+
const snapshot = readStoredStatus();
|
|
205
|
+
const header = isDryRun
|
|
206
|
+
? (snapshot.source === 'stored' ? 'Current session status (dry-run read):' : 'No stored status for this session yet.')
|
|
207
|
+
: 'Session status updated. Current status:';
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: `${header}\n${JSON.stringify(snapshot, null, 2)}`,
|
|
212
|
+
}],
|
|
213
|
+
};
|
|
120
214
|
});
|
|
121
215
|
/** Quote a string for safe use inside a bash -c argument. */
|
|
122
216
|
function shellQuote(s) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxMcpStdio.js","sourceRoot":"","sources":["../../src/ai/sandboxMcpStdio.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"sandboxMcpStdio.js","sourceRoot":"","sources":["../../src/ai/sandboxMcpStdio.ts"],"names":[],"mappings":"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"}
|
|
@@ -61,6 +61,10 @@ export declare class SessionManager extends EventEmitter {
|
|
|
61
61
|
/** Send a raw control_request to the active provider session (CLI only). */
|
|
62
62
|
sendControlRequest(sessionId: string, subtype: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
63
63
|
closeSession(sessionId: string): boolean;
|
|
64
|
+
/** Resolve and remove every pending user-input request for a session that
|
|
65
|
+
* is going away. Used by closeSession and onSessionExited so the map
|
|
66
|
+
* doesn't leak and any awaiter unblocks with a deny. */
|
|
67
|
+
private cancelPendingInputsForSession;
|
|
64
68
|
setPermissionLevel(sessionId: string, level: PermissionLevel): Promise<boolean>;
|
|
65
69
|
getPermissionLevel(sessionId: string): PermissionLevel;
|
|
66
70
|
resolveUserInput(response: ClaudeUserInputResponsePayload): boolean;
|
|
@@ -110,8 +114,16 @@ export declare class SessionManager extends EventEmitter {
|
|
|
110
114
|
makeCallbacks(agentId: AgentId): ProviderCallbacks;
|
|
111
115
|
private handlePermissionRequest;
|
|
112
116
|
private shouldAutoApprove;
|
|
113
|
-
/**
|
|
114
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Update session ticket-model status (triggered by the UpdateSessionStatus MCP tool).
|
|
119
|
+
* Partial update: only fields present in `input` are written. Unknown-typed inputs
|
|
120
|
+
* are validated here before being applied to session config / registry.
|
|
121
|
+
*
|
|
122
|
+
* `note`, when supplied, is appended to the session's `noteHistory` log in the
|
|
123
|
+
* registry (capped at SESSION_NOTE_HISTORY_CAP, oldest-first). The latest note
|
|
124
|
+
* text is also mirrored to session config / registry `note` for quick access.
|
|
125
|
+
*/
|
|
126
|
+
private updateSessionStatus;
|
|
115
127
|
/** Move per-session state from oldId to newId after the CLI forks on
|
|
116
128
|
* --resume. `sessionAgents[newId]` is already set by the caller; this
|
|
117
129
|
* helper moves the remaining side maps and clears the old key. */
|
|
@@ -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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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { resolve, join } from 'path';
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
|
-
import { DEFAULT_AGENT, DEFAULT_MODEL, matchAllowPattern, } from '@sumicom/quicksave-shared';
|
|
4
|
+
import { DEFAULT_AGENT, DEFAULT_MODEL, SESSION_NOTE_HISTORY_CAP, matchAllowPattern, } from '@sumicom/quicksave-shared';
|
|
5
5
|
import { StreamCardBuilder, buildCardsFromHistory, loadPersistedCards } from './cardBuilder.js';
|
|
6
|
-
import { SANDBOX_BASH_TOOL,
|
|
6
|
+
import { SANDBOX_BASH_TOOL, UPDATE_SESSION_STATUS_TOOL } from './sandboxMcp.js';
|
|
7
7
|
import { getSessionRegistry } from './sessionRegistry.js';
|
|
8
8
|
import { getEventStore } from '../storage/eventStore.js';
|
|
9
9
|
/**
|
|
@@ -32,12 +32,23 @@ const AUTO_APPROVE = {
|
|
|
32
32
|
auto: new Set(['TodoWrite', 'EnterWorktree', 'ExitWorktree', 'Agent', 'EnterPlanMode']),
|
|
33
33
|
};
|
|
34
34
|
const DEFAULT_SYSTEM_PROMPT = [
|
|
35
|
-
'For non-destructive shell commands (ls, cat, find, git log, git status, git diff, etc.), prefer
|
|
36
|
-
|
|
35
|
+
'For non-destructive shell commands (ls, cat, find, git log, git status, git diff, etc.), prefer `mcp__quicksave-sandbox__SandboxBash` over Bash. SandboxBash runs in a sandboxed environment. Use Bash only for commands that modify state.',
|
|
36
|
+
// Ticket-model status updates. Sessions surface on the user\'s home screen as tickets; keep this metadata fresh.
|
|
37
|
+
'Treat each session as a ticket. The `mcp__quicksave-sandbox__UpdateSessionStatus` MCP tool is already loaded and available — call it directly, do NOT use ToolSearch. On your FIRST response in a new session, call it with at minimum `subject` and `stage` before doing other work. `subject` is what the user is trying to solve (e.g. "Fix auth token expiring early"), not what you are doing (not "Debugging jwt.ts"). On RESUME, if you do not see a prior `mcp__quicksave-sandbox__UpdateSessionStatus` tool_use in the conversation history, call it ONCE with no arguments as a dry-run to read the current stored status; if the returned subject is empty OR does not match what the user is now asking for, follow up with a real call to set/correct it.',
|
|
38
|
+
'Re-call `mcp__quicksave-sandbox__UpdateSessionStatus` whenever the stage changes (investigating → working → verifying → done), whenever work becomes blocked or unblocked (set `blocked` true/false without changing `stage`), or when a one-line `note` would give the user useful progress signal. `note` is an append-only event log — each call adds one entry — so for long-running tasks (research, large refactors) emit a fresh `note` every time you rule out an approach, cross a sub-goal, or hit a blocker. Do not skip `verifying` when you have tests/build/repro running. Do not declare `done` until the user\'s problem is fully resolved.',
|
|
37
39
|
].join('\n\n');
|
|
38
40
|
function buildSystemPrompt(extra) {
|
|
39
41
|
return extra ? `${DEFAULT_SYSTEM_PROMPT}\n\n${extra}` : DEFAULT_SYSTEM_PROMPT;
|
|
40
42
|
}
|
|
43
|
+
const SESSION_STAGES = [
|
|
44
|
+
'investigating',
|
|
45
|
+
'working',
|
|
46
|
+
'verifying',
|
|
47
|
+
'done',
|
|
48
|
+
];
|
|
49
|
+
function isSessionStage(value) {
|
|
50
|
+
return SESSION_STAGES.includes(value);
|
|
51
|
+
}
|
|
41
52
|
function normalizeAgentId(value) {
|
|
42
53
|
if (value === 'claude-code' || value === 'claude-cli' || value === 'claude-sdk') {
|
|
43
54
|
return 'claude-code';
|
|
@@ -385,6 +396,11 @@ export class SessionManager extends EventEmitter {
|
|
|
385
396
|
return false;
|
|
386
397
|
console.log(`[session-manager] cancel session=${sessionId.slice(0, 8)}`);
|
|
387
398
|
ps.providerSession.interrupt();
|
|
399
|
+
// The CLI abandons its can_use_tool RPC after interrupt, so any awaiter
|
|
400
|
+
// on our side would hang forever. Resolve outstanding permissions with
|
|
401
|
+
// deny: it unblocks the daemon promise, frees the map slot, and matches
|
|
402
|
+
// user intent ("stop" = don't do that thing).
|
|
403
|
+
this.cancelPendingInputsForSession(sessionId);
|
|
388
404
|
return true;
|
|
389
405
|
}
|
|
390
406
|
/** Send a raw control_request to the active provider session (CLI only). */
|
|
@@ -408,9 +424,24 @@ export class SessionManager extends EventEmitter {
|
|
|
408
424
|
ps.providerSession = null;
|
|
409
425
|
}
|
|
410
426
|
this.sessions.delete(sessionId);
|
|
427
|
+
// Drop any pending permission promises for this session — the CLI is dead
|
|
428
|
+
// so any later resolveUserInput would land on an orphan promise, and the
|
|
429
|
+
// map entry would leak forever.
|
|
430
|
+
this.cancelPendingInputsForSession(sessionId);
|
|
411
431
|
this.emitSessionUpdate(sessionId);
|
|
412
432
|
return true;
|
|
413
433
|
}
|
|
434
|
+
/** Resolve and remove every pending user-input request for a session that
|
|
435
|
+
* is going away. Used by closeSession and onSessionExited so the map
|
|
436
|
+
* doesn't leak and any awaiter unblocks with a deny. */
|
|
437
|
+
cancelPendingInputsForSession(sessionId) {
|
|
438
|
+
for (const [requestId, pending] of this.pendingInputRequests) {
|
|
439
|
+
if (pending.request.sessionId !== sessionId)
|
|
440
|
+
continue;
|
|
441
|
+
pending.resolve({ sessionId, requestId, action: 'deny' });
|
|
442
|
+
this.pendingInputRequests.delete(requestId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
414
445
|
// ── Permission ──
|
|
415
446
|
async setPermissionLevel(sessionId, level) {
|
|
416
447
|
const ps = this.sessions.get(sessionId);
|
|
@@ -432,7 +463,17 @@ export class SessionManager extends EventEmitter {
|
|
|
432
463
|
throw err;
|
|
433
464
|
}
|
|
434
465
|
}
|
|
435
|
-
|
|
466
|
+
if (ps) {
|
|
467
|
+
this.emitSessionUpdate(sessionId);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// Inactive session: emitSessionUpdate would carry archived=true (since
|
|
471
|
+
// the session isn't in the in-memory map), which the PWA reads as a
|
|
472
|
+
// "session retired" signal and bounces the user off the page. Persist
|
|
473
|
+
// to the registry instead so the new mode applies on the next cold
|
|
474
|
+
// resume; the PWA's optimistic state already reflects the change.
|
|
475
|
+
this.persistRegistryField(sessionId, 'permissionMode', level);
|
|
476
|
+
}
|
|
436
477
|
return true;
|
|
437
478
|
}
|
|
438
479
|
getPermissionLevel(sessionId) {
|
|
@@ -458,7 +499,12 @@ export class SessionManager extends EventEmitter {
|
|
|
458
499
|
});
|
|
459
500
|
}
|
|
460
501
|
this.emit('user-input-resolved', { requestId: response.requestId, sessionId: pending.request.sessionId });
|
|
461
|
-
|
|
502
|
+
// Skip the session-update emit when the session is no longer in memory:
|
|
503
|
+
// buildSessionUpdatePayload would set archived=true and navigate the PWA
|
|
504
|
+
// off the page. Can happen if the CLI exited (or was rekeyed) between
|
|
505
|
+
// sending the permission request and the user responding.
|
|
506
|
+
if (ps)
|
|
507
|
+
this.emitSessionUpdate(pending.request.sessionId);
|
|
462
508
|
return true;
|
|
463
509
|
}
|
|
464
510
|
// ── Query ──
|
|
@@ -505,8 +551,7 @@ export class SessionManager extends EventEmitter {
|
|
|
505
551
|
}
|
|
506
552
|
async listAvailableSessions(cwd) {
|
|
507
553
|
const registry = getSessionRegistry();
|
|
508
|
-
const registryEntries = registry.getEntriesForProject(cwd)
|
|
509
|
-
.filter(entry => !entry.archived);
|
|
554
|
+
const registryEntries = registry.getEntriesForProject(cwd);
|
|
510
555
|
const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map(p => p.request.sessionId));
|
|
511
556
|
const eventStore = getEventStore();
|
|
512
557
|
return registryEntries.map(entry => {
|
|
@@ -668,6 +713,13 @@ export class SessionManager extends EventEmitter {
|
|
|
668
713
|
handlePermissionRequest: async (sessionId, req) => {
|
|
669
714
|
return this.handlePermissionRequest(sessionId, req);
|
|
670
715
|
},
|
|
716
|
+
onToolUse: (sessionId, toolName, toolInput) => {
|
|
717
|
+
// Claude CLI's auto permission mode skips can_use_tool for MCP tools,
|
|
718
|
+
// so handlePermissionRequest can't reliably drive side effects here.
|
|
719
|
+
if (toolName === UPDATE_SESSION_STATUS_TOOL) {
|
|
720
|
+
this.updateSessionStatus(sessionId, toolInput);
|
|
721
|
+
}
|
|
722
|
+
},
|
|
671
723
|
onModelDetected: (model) => {
|
|
672
724
|
console.log(`[session-manager] model detected: ${model} (agent=${agentId})`);
|
|
673
725
|
if (agentId === 'claude-code') {
|
|
@@ -684,6 +736,7 @@ export class SessionManager extends EventEmitter {
|
|
|
684
736
|
ps.providerSession = null;
|
|
685
737
|
ps.streaming = false;
|
|
686
738
|
this.sessions.delete(sessionId);
|
|
739
|
+
this.cancelPendingInputsForSession(sessionId);
|
|
687
740
|
this.emitSessionUpdate(sessionId);
|
|
688
741
|
},
|
|
689
742
|
};
|
|
@@ -754,10 +807,10 @@ export class SessionManager extends EventEmitter {
|
|
|
754
807
|
return { action: 'allow' };
|
|
755
808
|
}
|
|
756
809
|
shouldAutoApprove(sessionId, toolName, input) {
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
810
|
+
// UpdateSessionStatus — always approve. The actual metadata write happens
|
|
811
|
+
// in the onToolUse callback (driven by the assistant stream), because
|
|
812
|
+
// CLI auto mode pre-approves MCP tools without ever sending can_use_tool.
|
|
813
|
+
if (toolName === UPDATE_SESSION_STATUS_TOOL) {
|
|
761
814
|
return true;
|
|
762
815
|
}
|
|
763
816
|
// SandboxBash: auto-approve when sandboxed, otherwise check as Bash
|
|
@@ -784,18 +837,69 @@ export class SessionManager extends EventEmitter {
|
|
|
784
837
|
}
|
|
785
838
|
return true;
|
|
786
839
|
}
|
|
787
|
-
/**
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Update session ticket-model status (triggered by the UpdateSessionStatus MCP tool).
|
|
842
|
+
* Partial update: only fields present in `input` are written. Unknown-typed inputs
|
|
843
|
+
* are validated here before being applied to session config / registry.
|
|
844
|
+
*
|
|
845
|
+
* `note`, when supplied, is appended to the session's `noteHistory` log in the
|
|
846
|
+
* registry (capped at SESSION_NOTE_HISTORY_CAP, oldest-first). The latest note
|
|
847
|
+
* text is also mirrored to session config / registry `note` for quick access.
|
|
848
|
+
*/
|
|
849
|
+
updateSessionStatus(sessionId, input) {
|
|
850
|
+
const configUpdates = {};
|
|
851
|
+
const entryUpdates = {};
|
|
852
|
+
let noteToAppend = null;
|
|
853
|
+
let changed = false;
|
|
854
|
+
if (typeof input.subject === 'string' && input.subject.length > 0) {
|
|
855
|
+
configUpdates.title = input.subject;
|
|
856
|
+
entryUpdates.title = input.subject;
|
|
857
|
+
changed = true;
|
|
858
|
+
}
|
|
859
|
+
if (typeof input.stage === 'string' && isSessionStage(input.stage)) {
|
|
860
|
+
configUpdates.stage = input.stage;
|
|
861
|
+
entryUpdates.stage = input.stage;
|
|
862
|
+
changed = true;
|
|
863
|
+
}
|
|
864
|
+
if (typeof input.blocked === 'boolean') {
|
|
865
|
+
configUpdates.blocked = input.blocked;
|
|
866
|
+
entryUpdates.blocked = input.blocked;
|
|
867
|
+
changed = true;
|
|
868
|
+
}
|
|
869
|
+
if (typeof input.note === 'string' && input.note.length > 0) {
|
|
870
|
+
configUpdates.note = input.note;
|
|
871
|
+
entryUpdates.note = input.note;
|
|
872
|
+
noteToAppend = input.note;
|
|
873
|
+
changed = true;
|
|
874
|
+
}
|
|
875
|
+
if (!changed)
|
|
876
|
+
return;
|
|
877
|
+
const prev = this.sessionConfigs.get(sessionId) ?? {};
|
|
878
|
+
const next = { ...prev, ...configUpdates };
|
|
879
|
+
this.sessionConfigs.set(sessionId, next);
|
|
880
|
+
this.emit('session-config-updated', { sessionId, config: next });
|
|
793
881
|
const ps = this.sessions.get(sessionId);
|
|
794
882
|
if (ps?.cwd) {
|
|
795
883
|
const registry = getSessionRegistry();
|
|
796
884
|
const entry = registry.getEntry(ps.cwd, sessionId);
|
|
797
885
|
if (entry) {
|
|
798
|
-
|
|
886
|
+
let nextHistory = entry.noteHistory;
|
|
887
|
+
if (noteToAppend !== null) {
|
|
888
|
+
const appended = { ts: Date.now(), text: noteToAppend };
|
|
889
|
+
const prior = Array.isArray(entry.noteHistory) ? entry.noteHistory : [];
|
|
890
|
+
const combined = [...prior, appended];
|
|
891
|
+
// Trim oldest-first once we exceed the cap so the registry payload
|
|
892
|
+
// stays broadcast-friendly.
|
|
893
|
+
nextHistory = combined.length > SESSION_NOTE_HISTORY_CAP
|
|
894
|
+
? combined.slice(combined.length - SESSION_NOTE_HISTORY_CAP)
|
|
895
|
+
: combined;
|
|
896
|
+
}
|
|
897
|
+
registry.upsertEntry({
|
|
898
|
+
...entry,
|
|
899
|
+
...entryUpdates,
|
|
900
|
+
...(nextHistory !== entry.noteHistory ? { noteHistory: nextHistory } : {}),
|
|
901
|
+
lastAccessedAt: Date.now(),
|
|
902
|
+
});
|
|
799
903
|
}
|
|
800
904
|
}
|
|
801
905
|
}
|
|
@@ -818,6 +922,15 @@ export class SessionManager extends EventEmitter {
|
|
|
818
922
|
this.sessionConfigs.set(newId, config);
|
|
819
923
|
this.sessionConfigs.delete(oldId);
|
|
820
924
|
}
|
|
925
|
+
// Rewrite any pending input request still tagged with the old sessionId
|
|
926
|
+
// so a later resolveUserInput can find the new session entry (and its
|
|
927
|
+
// cardBuilder) to clear the PWA's pending-permission UI. Without this,
|
|
928
|
+
// permission prompts emitted before the rekey stay stuck on screen.
|
|
929
|
+
for (const [, pending] of this.pendingInputRequests) {
|
|
930
|
+
if (pending.request.sessionId === oldId) {
|
|
931
|
+
pending.request = { ...pending.request, sessionId: newId };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
821
934
|
this.sessionAgents.delete(oldId);
|
|
822
935
|
}
|
|
823
936
|
emitSessionUpdate(sessionId) {
|
|
@@ -920,6 +1033,15 @@ export class SessionManager extends EventEmitter {
|
|
|
920
1033
|
waitForUserInput(requestId, request) {
|
|
921
1034
|
return new Promise((resolve) => {
|
|
922
1035
|
this.pendingInputRequests.set(requestId, { resolve, request });
|
|
1036
|
+
// Notify subscribers so the PWA's dot indicator flips to "pending"
|
|
1037
|
+
// immediately. resolveUserInput emits its own update to flip it back.
|
|
1038
|
+
// Guard against an inactive session: emitting then would carry
|
|
1039
|
+
// archived=true and bounce the PWA off the page. (Should not normally
|
|
1040
|
+
// happen — the CLI is alive whenever it asks for permission — but the
|
|
1041
|
+
// race is cheap to guard.)
|
|
1042
|
+
if (this.sessions.has(request.sessionId)) {
|
|
1043
|
+
this.emitSessionUpdate(request.sessionId);
|
|
1044
|
+
}
|
|
923
1045
|
});
|
|
924
1046
|
}
|
|
925
1047
|
}
|