shraga 0.1.15 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -26,7 +26,7 @@ import { mountFeatures, registerFeature, resumeFeatureSession, collectFeatureFla
26
26
  import { registerSpaCatchAll } from './spa-catchall.ts';
27
27
  import { slackFeature } from './slack/feature.ts';
28
28
  import { dataPath } from './paths.ts';
29
- import { getAllSessions, getSession, getSessionHistory, upsertSession, appendMessage, saveConversation, loadConversation, setSessionDirectives, getAutoApprove, setAutoApprove, getSessionsByScheduleId, getSessionsVisibleTo, isSessionVisibleTo, setRunStatus, incrementRetryCount, resetRetryCount, getRunningSessions, updateScheduledSessionStatus, setShuttingDown, backfillSessionVisibility, writePartial, readPartial, clearPartial, registerLivePartial, unregisterLivePartial, readLivePartial, acquireSessionLock, releaseSessionLock, replaceSessionLock, isSessionLocked, getSessionAbortController, forkSession, generateSessionTitle, type ConvBlock, type ConvMessage, type SessionMeta } from './sessions.ts';
29
+ import { getAllSessions, getSession, getSessionHistory, upsertSession, appendMessage, saveConversation, loadConversation, setSessionDirectives, getAutoApprove, setAutoApprove, getSessionsByScheduleId, getSessionsVisibleTo, isSessionVisibleTo, setRunStatus, incrementRetryCount, resetRetryCount, getRunningSessions, getActiveLockCount, updateScheduledSessionStatus, setShuttingDown, backfillSessionVisibility, writePartial, readPartial, clearPartial, registerLivePartial, unregisterLivePartial, readLivePartial, acquireSessionLock, releaseSessionLock, replaceSessionLock, isSessionLocked, getSessionAbortController, forkSession, generateSessionTitle, type ConvBlock, type ConvMessage, type SessionMeta } from './sessions.ts';
30
30
  import { setBroadcaster } from './session-bus.ts';
31
31
  import * as scheduler from './scheduler/index.ts';
32
32
  import { initPolls } from './polls.ts';
@@ -1808,16 +1808,19 @@ async function gracefulShutdown(signal: string, opts: { exit?: boolean } = {}) {
1808
1808
  // Agent turns routinely run for minutes; give in-flight streams time to finish before we force-kill
1809
1809
  // their Claude Code child processes (which would otherwise surface as `exited with code 143`).
1810
1810
  // Keep this under the service manager's stop timeout (e.g. systemd TimeoutStopSec) so the drain wins, not SIGKILL.
1811
+ // Drain on the LIVE lock count, not the persisted index: setRunStatus freezes `running` in the
1812
+ // index at shutdown (the crash-recovery marker), so index entries can never go idle mid-drain —
1813
+ // polling the index made every restart with an active session (or a stale marker from a prior
1814
+ // crash) sit out the full 90s.
1811
1815
  const deadline = Date.now() + 90_000;
1812
1816
  while (Date.now() < deadline) {
1813
- const running = getRunningSessions();
1814
- if (running.length === 0) break;
1815
- console.log(`[server] waiting for ${running.length} active stream(s)…`);
1817
+ const running = getActiveLockCount();
1818
+ if (running === 0) break;
1819
+ console.log(`[server] waiting for ${running} active stream(s)…`);
1816
1820
  await new Promise((r) => setTimeout(r, 2000));
1817
1821
  }
1818
1822
 
1819
- const remaining = getRunningSessions();
1820
- console.log(`[server] drain complete — ${remaining.length} stream(s) still active, closing`);
1823
+ console.log(`[server] drain complete — ${getActiveLockCount()} stream(s) still active, closing`);
1821
1824
 
1822
1825
  for (const [ws, session] of activeConnections) {
1823
1826
  for (const ac of session.abortControllers.values()) ac.abort();
@@ -140,6 +140,10 @@ export async function runSchedule(
140
140
  if (eventCtx) base = `${base}\n\n---\n${formatEventBlock(eventCtx)}`;
141
141
  prompt = base;
142
142
  }
143
+ // task.model rides the same [model] prompt-directive channel users type by hand — parseDirectives
144
+ // strips it and resolves aliases. Prepending (vs new plumbing) also persists the choice into the
145
+ // saved prompt, so the session UI shows which model the schedule actually requested.
146
+ if (!resume && task.model) prompt = `[${task.model}] ${prompt}`;
143
147
 
144
148
  // Save the synthesized user prompt to the conversation (skip on resume — task prompt already persisted).
145
149
  if (!resume) {
@@ -326,6 +326,14 @@ export function isSessionLocked(sessionId: string): boolean {
326
326
  return globalSessionLocks.has(sessionId);
327
327
  }
328
328
 
329
+ /** Count of queries streaming in THIS process right now. The drain loop must use this, not the
330
+ * persisted index: setRunStatus freezes `running` at shutdown as the crash-recovery marker, so
331
+ * index entries can't go idle mid-drain (and stale entries from a prior crash would stall the
332
+ * drain for sessions that aren't streaming at all). */
333
+ export function getActiveLockCount(): number {
334
+ return globalSessionLocks.size;
335
+ }
336
+
329
337
  export function replaceSessionLock(sessionId: string, origin: SessionLock['origin'], abortController: AbortController): void {
330
338
  globalSessionLocks.set(sessionId, { origin, abortController, startedAt: Date.now() });
331
339
  }