@ctrl-spc/cs 0.7.15 → 0.7.16

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/mcp.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { deferMcpCleanup, pendingMcpCleanup, acknowledgeMcpCleanup } from './daemon-processes.js';
1
2
  import { listStructureArtifactsHandler, getStructureArtifactHandler, searchAgentCardsHandler, createStructureArtifactHandler, updateStructureArtifactHandler } from './product-tools.js';
2
3
  import { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler } from './product-tools.js';
3
4
  export { createEpicHandler, createSprintHandler, listStructureHandler, updateStructureHandler } from './product-tools.js';
@@ -20,9 +21,10 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
20
21
  import { z } from 'zod';
21
22
  import { workflowToolPermission, validateWorkflowToolTarget, WORKFLOW_TOOL_TEACHING, WORKFLOW_AUTHORING_TEACHING, authorToolMentions } from './workflows.js';
22
23
  import { workflowToolForName, validateApprovalAction } from './workflow-tool-mentions.js';
24
+ import { clientSessionCurrent } from './supabase.js';
23
25
  import { TOOLS_SERVER_PORT, SESSION_TTL_MS } from './env.js';
24
26
  import { agentPath } from './agents.js';
25
- import { mcpToken, readMcpToken, readSession } from './config.js';
27
+ import { mcpToken, readMcpToken } from './config.js';
26
28
  import { FIREWALL_WRITING_RULE } from './firewall.js';
27
29
  /* The path rule itself, moved out so `workflows.ts` can ask the same question
28
30
  before it builds a workflow. `refuseAbsolutePaths` below is still v2's own
@@ -9237,6 +9239,7 @@ runTodoIdSource = null) {
9237
9239
  await releaseSessionReservations([prevTodo.sessionId], client);
9238
9240
  }
9239
9241
  openSessions.set(connectionId, {
9242
+ accountId: userId,
9240
9243
  sessionId: opened.id,
9241
9244
  taskId: null,
9242
9245
  todoId: args.todo_id,
@@ -9380,6 +9383,7 @@ runTodoIdSource = null) {
9380
9383
  // Record the new session in the registry keyed by connectionId, with a
9381
9384
  // fresh activity-TTL (extended by every subsequent tool call).
9382
9385
  openSessions.set(connectionId, {
9386
+ accountId: userId,
9383
9387
  sessionId: session.id,
9384
9388
  taskId: session.task_id,
9385
9389
  expiresAt: Date.now() + SESSION_TTL_MS,
@@ -11034,7 +11038,7 @@ export async function releaseSessionReservations(sessionIds,
11034
11038
  /** The client to use. The lifecycle helpers hold `toolsClient`; the tool
11035
11039
  * handlers hold their own per-connection client and pass it, matching what
11036
11040
  * they use for the session-status write immediately above each call. */
11037
- client = toolsClient) {
11041
+ client = toolsClient, strict = false) {
11038
11042
  if (!client || sessionIds.length === 0)
11039
11043
  return;
11040
11044
  try {
@@ -11049,6 +11053,8 @@ client = toolsClient) {
11049
11053
  throw new Error(error.message);
11050
11054
  }
11051
11055
  catch (err) {
11056
+ if (strict)
11057
+ throw err;
11052
11058
  /* NOTHING ABOVE THIS, following `noteDroppedActivity` and the session
11053
11059
  helpers. The session is already ended, which is the fact the product
11054
11060
  reads; an unreleased lease degrades to exactly the pre-slice behaviour
@@ -11366,64 +11372,49 @@ export async function noteDroppedActivity(todoId) {
11366
11372
  console.warn(`recording a dropped activity write failed: ${errorMessage(err)}`);
11367
11373
  }
11368
11374
  }
11369
- /** Mark ONE connection's open session ended (connection closed). Best-effort. */
11370
- export async function endConnectionSession(connectionId) {
11371
- const s = openSessions.get(connectionId);
11372
- /* 18c Slice 6: the remembered session is cleared HERE and only here, because
11373
- it is scoped to the CONNECTION rather than to the session. The connection
11374
- is gone, so nothing can produce anything else under it. */
11375
- lastSessionByConnection.delete(connectionId);
11376
- if (!toolsClient || !s) {
11377
- openSessions.delete(connectionId);
11375
+ /** Flush only the authenticated account's durable cleanup, using its captured client. */
11376
+ export async function reconcileMcpCleanup(client, accountId) {
11377
+ const sessionIds = pendingMcpCleanup(accountId);
11378
+ if (!sessionIds.length)
11378
11379
  return;
11379
- }
11380
+ const { error } = await client.from('cliv2_agent_sessions').update({ status: 'ended' }).in('id', sessionIds);
11381
+ if (error)
11382
+ throw error;
11383
+ await releaseSessionReservations(sessionIds, client, true);
11384
+ acknowledgeMcpCleanup(accountId, sessionIds);
11385
+ }
11386
+ /** Retire the local connection immediately; cloud cleanup survives authentication loss. */
11387
+ export async function endConnectionSession(connectionId) {
11388
+ const client = toolsClient;
11389
+ const session = openSessions.get(connectionId);
11390
+ if (session)
11391
+ deferMcpCleanup([session]);
11380
11392
  openSessions.delete(connectionId);
11381
- try {
11382
- const { error } = await toolsClient
11383
- .from('cliv2_agent_sessions')
11384
- .update({ status: 'ended' })
11385
- .eq('id', s.sessionId);
11386
- if (error)
11387
- throw error;
11388
- }
11389
- catch (err) {
11390
- console.warn(`ending session on connection close failed: ${err.message}`);
11391
- }
11392
- /* 18c SLICE 9 (GAP 15), ROUTE 4a. THE CONNECTION IS GONE, so nothing can
11393
- ever release these leases by asking. This is what covers every orchestrator
11394
- finish path: they all end the agent PROCESS, and a dead process's MCP
11395
- connection closes here. See `releaseSessionReservations` for why the fix
11396
- hangs off the session rather than off those ten callers. */
11397
- await releaseSessionReservations([s.sessionId]);
11398
- }
11399
- /** Mark ALL open sessions ended and clear the registry (server stop / logout). */
11400
- export async function endAllOpenSessions() {
11401
- if (openSessions.size === 0) {
11402
- return;
11393
+ lastSessionByConnection.delete(connectionId);
11394
+ if (client && session) {
11395
+ try {
11396
+ await reconcileMcpCleanup(client, session.accountId);
11397
+ }
11398
+ catch {
11399
+ console.warn('Session cleanup is waiting for its original account to reconnect.');
11400
+ }
11403
11401
  }
11404
- const sessionIds = [...openSessions.values()].map((s) => s.sessionId);
11402
+ }
11403
+ export async function endAllOpenSessions(client = toolsClient) {
11404
+ const sessions = [...openSessions.values()];
11405
+ deferMcpCleanup(sessions);
11405
11406
  openSessions.clear();
11406
- /* 18c Slice 6: server stop or logout. Every connection is going away, so no
11407
- later write can legitimately attribute to any of these. */
11408
11407
  lastSessionByConnection.clear();
11409
- if (!toolsClient)
11408
+ if (!client)
11410
11409
  return;
11411
- try {
11412
- const { error } = await toolsClient
11413
- .from('cliv2_agent_sessions')
11414
- .update({ status: 'ended' })
11415
- .in('id', sessionIds);
11416
- if (error)
11417
- throw error;
11418
- }
11419
- catch (err) {
11420
- console.warn(`ending all open sessions failed: ${err.message}`);
11410
+ for (const accountId of new Set(sessions.map(session => session.accountId))) {
11411
+ try {
11412
+ await reconcileMcpCleanup(client, accountId);
11413
+ }
11414
+ catch {
11415
+ console.warn('Session cleanup is waiting for its original account to reconnect.');
11416
+ }
11421
11417
  }
11422
- /* 18c SLICE 9 (GAP 15), ROUTE 4b. Server stop or logout: every session is
11423
- over at once, so every lease any of them holds goes with them. On logout in
11424
- particular, leaving them active would have the next sign-in's first run
11425
- collide with an account that is no longer even connected. */
11426
- await releaseSessionReservations(sessionIds);
11427
11418
  }
11428
11419
  async function readJsonBody(req) {
11429
11420
  const chunks = [];
@@ -11537,7 +11528,7 @@ export async function startToolsServer(deps) {
11537
11528
  // clear session.json — it can't reach this server's in-memory session. So
11538
11529
  // every request (including ones on an already-open MCP connection) re-checks
11539
11530
  // the on-disk session; once it's gone, nothing is served for the old account.
11540
- if (!readSession()) {
11531
+ if (!clientSessionCurrent(deps.client)) {
11541
11532
  res.writeHead(401, { 'Content-Type': 'text/plain' }).end('Logged out');
11542
11533
  return;
11543
11534
  }
@@ -11563,6 +11554,12 @@ export async function startToolsServer(deps) {
11563
11554
  await existing.handleRequest(req, res);
11564
11555
  return;
11565
11556
  }
11557
+ if (sessionId) {
11558
+ // MCP clients reinitialize after a lost session only when it returns 404.
11559
+ sessionTodoIds.delete(sessionId);
11560
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Session not found');
11561
+ return;
11562
+ }
11566
11563
  if (req.method !== 'POST') {
11567
11564
  res.writeHead(400, { 'Content-Type': 'text/plain' }).end('Bad Request: no valid session ID provided');
11568
11565
  return;
@@ -11668,9 +11665,11 @@ export async function stopToolsServer() {
11668
11665
  // D2b: flip every open work session to ended (this needs toolsClient) BEFORE
11669
11666
  // dropping the client reference, so the board's "working" chips clear on server
11670
11667
  // stop / logout; then release the client alongside the reg-status resets.
11671
- await endAllOpenSessions();
11672
- toolsClient = null;
11668
+ const client = toolsClient;
11673
11669
  await h.close();
11670
+ void endAllOpenSessions(client).catch(error => console.warn(`Session cleanup remains incomplete: ${String(error)}`));
11671
+ if (toolsClient === client)
11672
+ toolsClient = null;
11674
11673
  }
11675
11674
  /** Whether the tools server is currently listening, and its fixed port. */
11676
11675
  export function toolsServerStatus() {