shraga 0.1.15 → 0.1.17

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routine",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Daily operating contract — cadence (dispatcher ticks + work blocks), OKR-anchored priorities, autonomy tiers, comms policy, and an agenda seed.",
5
5
  "configSchema": {
6
6
  "workingDays": { "type": "string", "default": "Sun–Thu", "description": "Working days, prose (shown in the skill)" },
@@ -21,7 +21,7 @@
21
21
  "def": "tick",
22
22
  "name": "routine-tick",
23
23
  "trigger": { "kind": "cron", "expr": "{{tickCron}}", "tz": "{{tz}}" },
24
- "task": { "kind": "prompt", "prompt": "Dispatcher tick: load the routine skill (Dispatcher tick rules) and workspace/agenda.md, then follow the tick logic. Most ticks should end in a skip.", "model": "{{tickModel}}" }
24
+ "task": { "kind": "prompt", "prompt": "Dispatcher tick (pager only — no cognitive work): load the routine skill (Dispatcher tick rules) and workspace/agenda.md, then decide wake/skip per the tick logic. If work is due, book an immediate self-wake-* once-schedule with the intention (NO model override) and end — never do the work in this session. Most ticks should end in a skip.", "model": "{{tickModel}}" }
25
25
  },
26
26
  {
27
27
  "def": "work-block",
@@ -24,10 +24,16 @@ When choosing what to pick up, rank by:
24
24
  ## Cadence
25
25
 
26
26
  - **Pre-daily** (start of day): absorbs the morning sweep — review inboxes/boards, refresh `workspace/agenda.md` (Today/Focus/Queue), pick the day's focus.
27
- - **Dispatcher tick** (hourly during work hours) — the tick decides, it does not grind:
27
+ - **Dispatcher tick** (hourly during work hours) — the tick is a PAGER, not a worker. It runs on a
28
+ cheap model and must do NO cognitive work beyond the wake/skip decision. Never execute a task,
29
+ never read beyond the agenda + schedule list, never write anything but the agenda's wake note:
28
30
  - **Skip** if a session is already actively working — most ticks should end in a skip.
29
31
  - **Defer to self-wake**: if a `self-wake-*` schedule is already booked for the current thread, skip — the wake owns the continuation.
30
- - **Wake with intention**: only start work when the agenda names a concrete next step; wake into that step, not into "look around".
32
+ - **Dispatch, don't do**: when the agenda names a concrete, green-lit next step and nothing is on
33
+ it, book an IMMEDIATE one-shot schedule (`once` trigger, now) named `self-wake-<slug>` whose
34
+ prompt states the intention ("Work session: <the step>, per the agenda/board — load the routine
35
+ skill first"). Do NOT set a model on it — the work session must run on the full default model.
36
+ Note the dispatch in the agenda, then end the tick. The tick session itself NEVER does the work.
31
37
  - **Escalate when stuck**: the same item blocked across 3 consecutive ticks → escalate per the comms policy instead of ticking on it again.
32
38
  - **Post-daily** (end of day): absorbs the EOD digest — summarize what moved, update the agenda's Parked/Blocked, leave breadcrumbs.
33
39
  - **Weekly self-audit**: once a week, review the log against `{{okrSource}}` — is the priority function actually being followed?
@@ -39,7 +45,9 @@ The `routine-work-block` schedule is a superseded fallback (ships disabled); the
39
45
  I MAY book my own continuations as one-shot schedules (`once` trigger) named `self-wake-<slug>`. Bounds:
40
46
  - tier-a work only — never self-wake into tier b/c actions;
41
47
  - working hours only;
42
- - max {{maxSelfWakesPerDay}} self-wakes per day;
48
+ - max {{maxSelfWakesPerDay}} FUTURE-scheduled self-wakes per day (continuations I book ahead of
49
+ time). Immediate dispatch wakes booked by the tick don't count — they ARE the routine working,
50
+ not extra sessions;
43
51
  - each booking notes "why then" in `workspace/agenda.md` (what the wake will do and why that time).
44
52
 
45
53
  ## Breadcrumb contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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
  }