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.
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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.
|
|
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",
|
package/src/server/boot.ts
CHANGED
|
@@ -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 =
|
|
1814
|
-
if (running
|
|
1815
|
-
console.log(`[server] waiting for ${running
|
|
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
|
-
|
|
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) {
|
package/src/server/sessions.ts
CHANGED
|
@@ -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
|
}
|