@aexol/spectral 0.7.5 → 0.7.7
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/relay/dispatcher.js +2 -2
- package/dist/server/pi-bridge.js +59 -0
- package/dist/server/session-stream.js +19 -1
- package/dist/server/storage.js +40 -26
- package/package.json +1 -1
package/dist/relay/dispatcher.js
CHANGED
|
@@ -472,7 +472,7 @@ function makeRelaySubscriber(sessionId, relay) {
|
|
|
472
472
|
* an `error` event to subscribers, so we don't double-report.
|
|
473
473
|
*/
|
|
474
474
|
export function handleClientMessage(frame, deps) {
|
|
475
|
-
const { sessionId, message, modelId } = frame;
|
|
475
|
+
const { sessionId, message, modelId, reasoningEffort } = frame;
|
|
476
476
|
const { manager, relay, subscribers } = deps;
|
|
477
477
|
const logger = deps.logger ?? console;
|
|
478
478
|
// 0. Shutdown gate. Once `gracefulShutdown` flips the flag we refuse
|
|
@@ -583,7 +583,7 @@ export function handleClientMessage(frame, deps) {
|
|
|
583
583
|
//
|
|
584
584
|
// When `loop: true`, loop state is set before prompting; the normal
|
|
585
585
|
// model is used — loop replay is handled by session-stream on agent_end.
|
|
586
|
-
manager.prompt(sessionId, content, modelId, validImages).catch((err) => {
|
|
586
|
+
manager.prompt(sessionId, content, modelId, validImages, reasoningEffort).catch((err) => {
|
|
587
587
|
logger.error?.(`[dispatcher] manager.prompt failed for ${sessionId}:`, err);
|
|
588
588
|
});
|
|
589
589
|
}
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -842,6 +842,65 @@ export class PiBridge {
|
|
|
842
842
|
return false;
|
|
843
843
|
}
|
|
844
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Map a frontend reasoning-effort string to pi's ThinkingLevel.
|
|
847
|
+
* Frontend sends: xhigh | high | medium | low | minimal | none | undefined
|
|
848
|
+
* Pi expects: "high" | "medium" | "low" | "minimal" | "off"
|
|
849
|
+
*
|
|
850
|
+
* Mapping:
|
|
851
|
+
* xhigh → high (pi doesn't have xhigh, default to max)
|
|
852
|
+
* high → high
|
|
853
|
+
* medium → medium
|
|
854
|
+
* low → low
|
|
855
|
+
* minimal → minimal
|
|
856
|
+
* none → off
|
|
857
|
+
* undefined → no-op (pi keeps whatever it has currently)
|
|
858
|
+
*/
|
|
859
|
+
mapReasoningEffortToThinkingLevel(effort) {
|
|
860
|
+
if (effort === undefined)
|
|
861
|
+
return undefined;
|
|
862
|
+
switch (effort) {
|
|
863
|
+
case "xhigh":
|
|
864
|
+
return "high";
|
|
865
|
+
case "high":
|
|
866
|
+
case "medium":
|
|
867
|
+
case "low":
|
|
868
|
+
case "minimal":
|
|
869
|
+
return effort;
|
|
870
|
+
case "none":
|
|
871
|
+
return "off";
|
|
872
|
+
default:
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Set the reasoning/thinking effort level for the next prompt.
|
|
878
|
+
* Pass `undefined` to leave pi's current level unchanged.
|
|
879
|
+
*
|
|
880
|
+
* The caller (SessionStreamManager) is responsible for persisting the
|
|
881
|
+
* value to SQLite; this method only applies it to pi's in-memory session.
|
|
882
|
+
*/
|
|
883
|
+
setReasoningEffort(effort) {
|
|
884
|
+
const level = this.mapReasoningEffortToThinkingLevel(effort);
|
|
885
|
+
if (level === undefined)
|
|
886
|
+
return;
|
|
887
|
+
if (!this.session) {
|
|
888
|
+
// Bridge hasn't started yet — the reasoning effort will be picked
|
|
889
|
+
// up from storage on the next prompt via session-stream's persistence.
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
this.session.setThinkingLevel(level);
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
897
|
+
this.opts.onError?.(e);
|
|
898
|
+
this.opts.emit({
|
|
899
|
+
type: "error",
|
|
900
|
+
message: `Failed to set reasoning effort: ${e.message}`,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
845
904
|
/**
|
|
846
905
|
* Forward a user message to pi. Resolves when the full turn ends.
|
|
847
906
|
* The caller is responsible for persisting the user message to SQLite
|
|
@@ -483,7 +483,7 @@ export class SessionStreamManager {
|
|
|
483
483
|
* - When neither envelope nor SQLite have a value, we leave model
|
|
484
484
|
* selection to pi's own settings file (pre-Phase-3 behaviour).
|
|
485
485
|
*/
|
|
486
|
-
async prompt(sessionId, content, modelId, images) {
|
|
486
|
+
async prompt(sessionId, content, modelId, images, reasoningEffort) {
|
|
487
487
|
if (this.disposed)
|
|
488
488
|
throw new Error("SessionStreamManager disposed");
|
|
489
489
|
let stream = this.streams.get(sessionId);
|
|
@@ -599,6 +599,24 @@ export class SessionStreamManager {
|
|
|
599
599
|
console.warn(`[spectral] warn: failed to persist sticky model for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
|
+
// Sticky reasoning-effort resolution. Same order as modelId:
|
|
603
|
+
// a) Envelope-supplied value → apply + persist.
|
|
604
|
+
// b) Otherwise, look up the persisted value from SQLite.
|
|
605
|
+
// c) When neither is present, leave pi's current thinking level unchanged.
|
|
606
|
+
// We apply BEFORE persisting the user message so the turn runs with the
|
|
607
|
+
// correct reasoning level.
|
|
608
|
+
const effectiveReasoningEffort = reasoningEffort ?? this.store.getSessionReasoningEffort(sessionId) ?? undefined;
|
|
609
|
+
if (effectiveReasoningEffort && stream.bridge.setReasoningEffort) {
|
|
610
|
+
stream.bridge.setReasoningEffort(effectiveReasoningEffort);
|
|
611
|
+
}
|
|
612
|
+
if (reasoningEffort !== undefined) {
|
|
613
|
+
try {
|
|
614
|
+
this.store.setSessionReasoningEffort(sessionId, reasoningEffort || null);
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
console.warn(`[spectral] warn: failed to persist reasoning effort for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
602
620
|
// 1. Persist user message first (survives mid-prompt failures).
|
|
603
621
|
let stored;
|
|
604
622
|
try {
|
package/dist/server/storage.js
CHANGED
|
@@ -32,9 +32,10 @@ import { dirname, join } from "node:path";
|
|
|
32
32
|
import { getConfigDir } from "../config.js";
|
|
33
33
|
import { stripJsoncComments } from "../studio-binding.js";
|
|
34
34
|
/**
|
|
35
|
-
* Schema version
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Schema version — informational, stamped into every new database.
|
|
36
|
+
*
|
|
37
|
+
* ALL schema changes are additive (ALTER TABLE ADD COLUMN). The version is
|
|
38
|
+
* never used to trigger destructive drops; it is purely for troubleshooting.
|
|
38
39
|
*/
|
|
39
40
|
const SCHEMA_VERSION = 4;
|
|
40
41
|
const SCHEMA_SQL = `
|
|
@@ -54,7 +55,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
54
55
|
title TEXT NOT NULL,
|
|
55
56
|
created_at INTEGER NOT NULL,
|
|
56
57
|
updated_at INTEGER NOT NULL,
|
|
57
|
-
model_id TEXT
|
|
58
|
+
model_id TEXT,
|
|
59
|
+
reasoning_effort TEXT
|
|
58
60
|
);
|
|
59
61
|
|
|
60
62
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id, updated_at DESC);
|
|
@@ -130,7 +132,6 @@ function applyBindingFields(project) {
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
/** Tables we own — used by the migration drop step. */
|
|
133
|
-
const KNOWN_TABLES = ["prompt_queue", "project_observations", "session_memory_snapshots", "messages", "sessions", "projects"];
|
|
134
135
|
/** Best-effort parse of the `images_json` column into `ImageAttachment[]`. */
|
|
135
136
|
function parseImagesJson(raw) {
|
|
136
137
|
if (!raw || raw === "")
|
|
@@ -188,6 +189,8 @@ export class SessionStore {
|
|
|
188
189
|
// Phase 3: per-session sticky model.
|
|
189
190
|
stmtGetSessionModel;
|
|
190
191
|
stmtSetSessionModel;
|
|
192
|
+
stmtGetSessionReasoningEffort;
|
|
193
|
+
stmtSetSessionReasoningEffort;
|
|
191
194
|
// Fork & Compact: flag + per-session source tracking.
|
|
192
195
|
stmtSetForkCompactSource;
|
|
193
196
|
stmtGetForkCompactSource;
|
|
@@ -210,28 +213,17 @@ export class SessionStore {
|
|
|
210
213
|
this.db = new Database(path);
|
|
211
214
|
this.db.pragma("journal_mode = WAL");
|
|
212
215
|
this.db.pragma("foreign_keys = ON");
|
|
213
|
-
// ----
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.db.pragma("foreign_keys = OFF");
|
|
224
|
-
const tx = this.db.transaction(() => {
|
|
225
|
-
for (const t of KNOWN_TABLES) {
|
|
226
|
-
this.db.exec(`DROP TABLE IF EXISTS ${t}`);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
tx();
|
|
230
|
-
this.db.pragma("foreign_keys = ON");
|
|
231
|
-
}
|
|
216
|
+
// ---- schema creation -------------------------------------------------
|
|
217
|
+
// SCHEMA_SQL uses CREATE TABLE IF NOT EXISTS for every table, so it is
|
|
218
|
+
// always safe to run — on a fresh database all tables are created;
|
|
219
|
+
// on an existing one every statement is a no-op. Schema changes that
|
|
220
|
+
// add columns are handled by the additive migration block below.
|
|
221
|
+
//
|
|
222
|
+
// We NO LONGER drop tables when the stored user_version differs from
|
|
223
|
+
// SCHEMA_VERSION. The previous destructive behaviour caused permanent
|
|
224
|
+
// data loss on version bumps. SCHEMA_VERSION is now informational — it
|
|
225
|
+
// is stamped into every new database purely for troubleshooting.
|
|
232
226
|
this.db.exec(SCHEMA_SQL);
|
|
233
|
-
// Stamp the version AFTER schema creation so a crash mid-create is
|
|
234
|
-
// detected next open and re-runs the (idempotent) create.
|
|
235
227
|
this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
236
228
|
// ---- additive column migration ---------------------------------------
|
|
237
229
|
// Phase 3 (Available Models): per-session sticky model selection lives in
|
|
@@ -248,6 +240,9 @@ export class SessionStore {
|
|
|
248
240
|
if (!sessionCols.some((c) => c.name === "model_id")) {
|
|
249
241
|
this.db.exec(`ALTER TABLE sessions ADD COLUMN model_id TEXT`);
|
|
250
242
|
}
|
|
243
|
+
if (!sessionCols.some((c) => c.name === "reasoning_effort")) {
|
|
244
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN reasoning_effort TEXT`);
|
|
245
|
+
}
|
|
251
246
|
// Additive migration: images_json column for base64 image attachments on
|
|
252
247
|
// user messages. Older rows default to '' (no images).
|
|
253
248
|
const msgCols = this.db
|
|
@@ -338,6 +333,8 @@ export class SessionStore {
|
|
|
338
333
|
this.stmtDeleteSessionMemorySnapshot = this.db.prepare(`DELETE FROM session_memory_snapshots WHERE session_id = ?`);
|
|
339
334
|
this.stmtGetSessionModel = this.db.prepare(`SELECT model_id FROM sessions WHERE id = ?`);
|
|
340
335
|
this.stmtSetSessionModel = this.db.prepare(`UPDATE sessions SET model_id = ? WHERE id = ?`);
|
|
336
|
+
this.stmtGetSessionReasoningEffort = this.db.prepare(`SELECT reasoning_effort FROM sessions WHERE id = ?`);
|
|
337
|
+
this.stmtSetSessionReasoningEffort = this.db.prepare(`UPDATE sessions SET reasoning_effort = ? WHERE id = ?`);
|
|
341
338
|
this.stmtSetForkCompactSource = this.db.prepare(`UPDATE sessions SET fork_compact_source_id = ? WHERE id = ?`);
|
|
342
339
|
this.stmtGetForkCompactSource = this.db.prepare(`SELECT fork_compact_source_id FROM sessions WHERE id = ?`);
|
|
343
340
|
this.stmtInsertProjectObs = this.db.prepare(`INSERT OR REPLACE INTO project_observations (id, project_id, session_id, content, relevance, created_at)
|
|
@@ -645,6 +642,23 @@ export class SessionStore {
|
|
|
645
642
|
setSessionModel(sessionId, modelId) {
|
|
646
643
|
this.stmtSetSessionModel.run(modelId, sessionId);
|
|
647
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Get the persisted reasoningEffort for a session, or null if none
|
|
647
|
+
* was ever set. Returns null for unknown sessions (consistent with
|
|
648
|
+
* getSessionModel's contract).
|
|
649
|
+
*/
|
|
650
|
+
getSessionReasoningEffort(sessionId) {
|
|
651
|
+
const row = this.stmtGetSessionReasoningEffort.get(sessionId);
|
|
652
|
+
return row?.reasoning_effort ?? null;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Persist the reasoningEffort for a session. Pass `null` to clear.
|
|
656
|
+
* Same semantics as setSessionModel — best-effort metadata, does
|
|
657
|
+
* not bump `updated_at`.
|
|
658
|
+
*/
|
|
659
|
+
setSessionReasoningEffort(sessionId, effort) {
|
|
660
|
+
this.stmtSetSessionReasoningEffort.run(effort, sessionId);
|
|
661
|
+
}
|
|
648
662
|
// ----------------------------------------------------------------------
|
|
649
663
|
// Fork & Compact
|
|
650
664
|
// ----------------------------------------------------------------------
|