shraga 0.1.93 → 0.1.95
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/client/assets/{index-B--gTLyT.css → index-CJDfTuzn.css} +1 -1
- package/dist/client/assets/{index-C3l1Wz_q.js → index-D2xviJL0.js} +238 -238
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/client/components/ConfigPanel.tsx +2 -20
- package/src/client/components/ConversationHeader.tsx +62 -27
- package/src/client/components/SchedulesManager.tsx +2 -0
- package/src/client/components/schedules/ScheduleEditor.tsx +65 -1
- package/src/client/hooks/useEngines.ts +52 -0
- package/src/client/lib/prompt-directives.ts +65 -0
- package/src/client/lib/schedule-types.ts +2 -2
- package/src/server/data-sync.ts +7 -1
- package/src/server/directives.ts +32 -19
- package/src/server/engine/index.ts +4 -19
- package/src/server/engine/model-resolver.ts +36 -0
- package/src/server/modules/service.ts +4 -0
- package/src/server/scheduler/builtins.ts +37 -15
- package/src/server/scheduler/runner.ts +19 -11
- package/src/server/scheduler/types.ts +9 -4
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Only state.json answers "is the module on".
|
|
12
12
|
* - Reconcile is idempotent: write-if-changed skills, upserts preserve enabled/runCount/lastRun.
|
|
13
13
|
*/
|
|
14
|
+
import { foldLegacyRuntimePin } from '../scheduler/builtins.ts';
|
|
14
15
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, renameSync, rmSync, copyFileSync, statSync, appendFileSync } from 'node:fs';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import { dataPath, DATA_DIR, PACKAGE_ROOT } from '../paths.ts';
|
|
@@ -305,6 +306,9 @@ function applyModule(rec: InstalledModule): void {
|
|
|
305
306
|
const t = rendered.task as Record<string, unknown>;
|
|
306
307
|
if (k in t && !t[k]) delete t[k];
|
|
307
308
|
}
|
|
309
|
+
// A runtime knob is now expressed as the prompt's own `[engine:…,model:…]` directive, not as a
|
|
310
|
+
// shadow field — fold whatever the manifest templated into it.
|
|
311
|
+
foldLegacyRuntimePin(rendered.task as Record<string, any>);
|
|
308
312
|
const existing = scheduler.getSchedule(id);
|
|
309
313
|
const now = Date.now();
|
|
310
314
|
const schedule: Schedule = {
|
|
@@ -56,6 +56,30 @@ const FAILURE_NOTIFIER_PROMPT = [
|
|
|
56
56
|
const LEGACY_SESSION_LINE = /^.*<deployment URL>\/\?session=<sessionId>(.*)$/gm;
|
|
57
57
|
const SESSION_LINE_FIX = " *Session:* <the payload's sessionUrl, verbatim — omit this line if absent>";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* One source of truth for a prompt run's runtime: the leading `[engine:…,model:…]` directive of the
|
|
61
|
+
* prompt. `task.model`/`task.engine` were an INVISIBLE second copy — no UI ever showed them, which
|
|
62
|
+
* is how nine live schedules drifted to a half-pin (`model` set, `engine` not) that nobody could
|
|
63
|
+
* see or fix. Fold such a pin into the prompt exactly as runner.ts used to synthesize it, so the
|
|
64
|
+
* run is byte-identical for a `prompt` task and the selection is now editable where the prompt is.
|
|
65
|
+
* NOT byte-identical for a `promptFile` task: runner.ts treated `task.prompt` as dead text once
|
|
66
|
+
* `promptFile` was set, and it now survives as the directive-carrying prefix. No live schedule uses
|
|
67
|
+
* `promptFile`, and dropping the prefix instead would silently lose the pin these lines exist to
|
|
68
|
+
* preserve — so the prefix is the deliberate choice, not an oversight.
|
|
69
|
+
*
|
|
70
|
+
* Applied to schedules loaded from disk AND to a module's freshly rendered task, so a manifest that
|
|
71
|
+
* still templates a `model` knob keeps working instead of silently losing its pin.
|
|
72
|
+
* `bash`/`job` tasks have no prompt to hold a directive, so their (unreadable) pin is just dropped.
|
|
73
|
+
*/
|
|
74
|
+
export function foldLegacyRuntimePin(task: Record<string, any>): void {
|
|
75
|
+
if (task.kind === 'prompt' && (task.model || task.engine)) {
|
|
76
|
+
const pins = [task.engine && `engine:${task.engine}`, task.model && `model:${task.model}`].filter(Boolean);
|
|
77
|
+
task.prompt = `[${pins.join(',')}] ${task.prompt ?? ''}`;
|
|
78
|
+
}
|
|
79
|
+
delete task.model;
|
|
80
|
+
delete task.engine;
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export function isSystemSchedule(schedule: Schedule): boolean {
|
|
60
84
|
return schedule.scope === 'system';
|
|
61
85
|
}
|
|
@@ -86,6 +110,16 @@ export function backfillScope(schedules: Schedule[]): void {
|
|
|
86
110
|
if (task.kind === 'job' && task.command === 'bun run summarize:conversations') {
|
|
87
111
|
task.command = SUMMARIZER_CMD;
|
|
88
112
|
}
|
|
113
|
+
// Undo the `engine: 'claude-code'` pin reconcile stamped onto the failure notifier. It shipped on
|
|
114
|
+
// a premise the box's own logs disprove — nothing was ever misrouted; the deployment was simply
|
|
115
|
+
// globally on claude-code then and on agentx now — and it made the alarm the second casualty of
|
|
116
|
+
// the real failure mode (an org cap on the pinned provider). Narrow on purpose: exactly the
|
|
117
|
+
// state that clause produced (this id, that engine, no model), so it cannot eat a real choice.
|
|
118
|
+
if (s.id === FAILURE_NOTIFIER_SCHEDULE_ID && task.engine === 'claude-code' && !task.model) {
|
|
119
|
+
delete task.engine;
|
|
120
|
+
}
|
|
121
|
+
foldLegacyRuntimePin(task);
|
|
122
|
+
|
|
89
123
|
// Heal a persisted failure-notifier prompt still carrying the un-supplied "<deployment URL>"
|
|
90
124
|
// placeholder — reconcile won't touch task.prompt, so this is the only path that reaches it.
|
|
91
125
|
if (s.id === FAILURE_NOTIFIER_SCHEDULE_ID && typeof task.prompt === 'string') {
|
|
@@ -138,11 +172,7 @@ export function ensureBuiltinSchedules(schedules: Schedule[]): Schedule[] {
|
|
|
138
172
|
match: { status: 'error' },
|
|
139
173
|
throttle: { byFields: ['name', 'error'], windowSec: 21600 },
|
|
140
174
|
},
|
|
141
|
-
|
|
142
|
-
// agent-config.json's global `engine` happens to be — and an optional engine can simply not
|
|
143
|
-
// register on a given boot (add-on absent, missing API key), which is exactly the failure this
|
|
144
|
-
// job exists to report. Its own alert must not be the second casualty.
|
|
145
|
-
task: { kind: 'prompt', prompt: FAILURE_NOTIFIER_PROMPT, engine: 'claude-code' },
|
|
175
|
+
task: { kind: 'prompt', prompt: FAILURE_NOTIFIER_PROMPT },
|
|
146
176
|
scope: 'system',
|
|
147
177
|
createdBy: { uid: SYSTEM_UID, email: 'system@shraga.local' },
|
|
148
178
|
createdAt: now,
|
|
@@ -167,16 +197,8 @@ export function ensureBuiltinSchedules(schedules: Schedule[]): Schedule[] {
|
|
|
167
197
|
existing.scope = builtin.scope;
|
|
168
198
|
existing.trigger = existing.trigger ?? builtin.trigger;
|
|
169
199
|
existing.createdBy = builtin.createdBy;
|
|
170
|
-
// A builtin's stored task is preserved
|
|
171
|
-
//
|
|
172
|
-
// was resolved against whatever the ambient global config was at the time, which is how the
|
|
173
|
-
// failure notifier ended up pinned to `composer-2.5` while running on claude-code. So when the
|
|
174
|
-
// builtin pins an engine and the stored task pins NONE, adopt the builtin's pair wholesale.
|
|
175
|
-
// An explicit stored `engine` is a real operator choice and is left untouched.
|
|
176
|
-
if (builtin.task.kind !== 'job' && existing.task.kind !== 'job' && builtin.task.engine && !existing.task.engine) {
|
|
177
|
-
existing.task.engine = builtin.task.engine;
|
|
178
|
-
existing.task.model = builtin.task.model;
|
|
179
|
-
}
|
|
200
|
+
// A builtin's stored task is otherwise preserved — deployments edit the prompt (and with it
|
|
201
|
+
// the runtime directive), and those edits must survive an upgrade.
|
|
180
202
|
} else {
|
|
181
203
|
schedules.push(builtin);
|
|
182
204
|
}
|
|
@@ -9,6 +9,17 @@ import type { Schedule, ScheduleRunSummary } from './types.ts';
|
|
|
9
9
|
import { updateRunLockPid, clearRunningMarker, loadSchedules } from './storage.ts';
|
|
10
10
|
import { readOutcome, clearOutcome, pendingDeadline, outcomePrompt, MAX_PENDING_MS } from './outcome.ts';
|
|
11
11
|
import { addUnread } from '../unread.ts';
|
|
12
|
+
import { splitDirectiveGroups } from '../directives.ts';
|
|
13
|
+
|
|
14
|
+
/** The prompt a `prompt` task runs, from wherever it is stored. Its leading `[…]` directive is the
|
|
15
|
+
* task's runtime selection — there is no separate stored pin.
|
|
16
|
+
* With a `promptFile`, `prompt` is a PREFIX rather than dead text: the directive has to live
|
|
17
|
+
* somewhere the editor can rewrite, and that must not be someone's workspace file. */
|
|
18
|
+
function taskPromptText(task: { prompt?: string; promptFile?: string }): string {
|
|
19
|
+
if (!task.promptFile) return task.prompt ?? '';
|
|
20
|
+
const body = readFileSync(resolvePromptFile(task.promptFile), 'utf-8').trim();
|
|
21
|
+
return task.prompt ? `${task.prompt.trim()} ${body}` : body;
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
export interface RunContext {
|
|
14
25
|
sessionId: string;
|
|
@@ -194,13 +205,19 @@ export async function runSchedule(
|
|
|
194
205
|
|
|
195
206
|
let prompt: string;
|
|
196
207
|
if (resume) {
|
|
197
|
-
// The original task prompt is already in the conversation from the interrupted run
|
|
208
|
+
// The original task prompt is already in the conversation from the interrupted run — but a
|
|
209
|
+
// resume prompt is a fresh "continue where you left off" string, not the saved original, so the
|
|
210
|
+
// task's own leading `[engine:…,model:…]` directive has to be carried onto it. Without that the
|
|
211
|
+
// resumed turn runs on the global default: a different engine, and a different vendor's bill.
|
|
198
212
|
prompt = resume.prompt;
|
|
213
|
+
const groups = task.kind === 'prompt' ? splitDirectiveGroups(taskPromptText(task), { strict: true }).groups : [];
|
|
214
|
+
const raw = groups.filter(Boolean).join(',');
|
|
215
|
+
if (raw) prompt = `[${raw}] ${prompt}`;
|
|
199
216
|
} else if (task.kind === 'bash') {
|
|
200
217
|
const cmd = override || task.command;
|
|
201
218
|
prompt = `Run exactly this bash command and report the result concisely:\n\n\`\`\`bash\n${cmd}\n\`\`\``;
|
|
202
219
|
} else {
|
|
203
|
-
let base =
|
|
220
|
+
let base = taskPromptText(task);
|
|
204
221
|
if (override) base = `${base}\n\n---\nAdditional instructions for this run:\n${override}`;
|
|
205
222
|
if (eventCtx) base = `${base}\n\n---\n${formatEventBlock(eventCtx)}`;
|
|
206
223
|
prompt = base;
|
|
@@ -216,15 +233,6 @@ export async function runSchedule(
|
|
|
216
233
|
prompt = `${prompt}\n\n---\n${outcomePrompt(sessionId)}`;
|
|
217
234
|
}
|
|
218
235
|
|
|
219
|
-
// task.engine/task.model ride the same prompt-directive channel users type by hand —
|
|
220
|
-
// parseDirectives strips them and resolves aliases. Prepending (vs new plumbing) also persists the
|
|
221
|
-
// choice into the saved prompt, so the session UI shows what the schedule actually requested.
|
|
222
|
-
// Applied on RESUME too: a resume's prompt is a fresh "continue where you left off" string, not the
|
|
223
|
-
// saved original, so skipping this here silently dropped the schedule's engine/model pin and let the
|
|
224
|
-
// resumed turn run on the global default — a different engine, and a different vendor's bill.
|
|
225
|
-
const pins = [task.engine && `engine:${task.engine}`, task.model && `model:${task.model}`].filter(Boolean);
|
|
226
|
-
if (pins.length) prompt = `[${pins.join(',')}] ${prompt}`;
|
|
227
|
-
|
|
228
236
|
// Save the synthesized user prompt to the conversation (skip on resume — task prompt already persisted).
|
|
229
237
|
if (!resume) {
|
|
230
238
|
appendMessage(sessionId, {
|
|
@@ -19,11 +19,16 @@ export interface EventThrottle {
|
|
|
19
19
|
windowSec: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
22
|
+
/** The runtime a prompt run uses is NOT a field here: it is the leading `[engine:…,model:…]`
|
|
23
|
+
* directive of the prompt itself (or of the promptFile's contents) — one source of truth, editable
|
|
24
|
+
* and visible in the same place the prompt is. A separate stored pin shadowed it invisibly, which
|
|
25
|
+
* is how nine live schedules ended up half-pinned (`model` set, `engine` not) with no UI to see it.
|
|
26
|
+
* `bash` and `job` carry no selector: `bash` has no prompt to hold a directive (its prompt is a
|
|
27
|
+
* fixed synthesized wrapper) and `job` never reaches an agent at all — both follow agent config.
|
|
28
|
+
* Pin a shell command's runtime by writing a prompt task that runs it. */
|
|
24
29
|
export type Task =
|
|
25
|
-
| { kind: 'prompt'; prompt?: string; promptFile?: string
|
|
26
|
-
| { kind: 'bash'; command: string
|
|
30
|
+
| { kind: 'prompt'; prompt?: string; promptFile?: string }
|
|
31
|
+
| { kind: 'bash'; command: string }
|
|
27
32
|
| { kind: 'job'; command: string };
|
|
28
33
|
|
|
29
34
|
/** Visibility: 'system' schedules + their sessions are shared with all whitelisted users; 'user' is private to createdBy. */
|