@aexol/spectral 0.8.5 → 0.8.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/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +11 -11
- package/dist/cli.js +1 -1
- package/dist/commands/serve.d.ts +3 -3
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +5 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/designer/index.d.ts +3 -3
- package/dist/designer/index.d.ts.map +1 -1
- package/dist/designer/index.js +9 -9
- package/dist/extensions/aexol-mcp.d.ts +6 -6
- package/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +12 -12
- package/dist/extensions/kanban-bridge.d.ts +2 -2
- package/dist/extensions/kanban-bridge.d.ts.map +1 -1
- package/dist/extensions/kanban-bridge.js +3 -3
- package/dist/extensions/openrouter-attribution.d.ts +1 -1
- package/dist/extensions/openrouter-attribution.d.ts.map +1 -1
- package/dist/extensions/openrouter-attribution.js +2 -2
- package/dist/extensions/spectral-vision-fallback.d.ts +1 -1
- package/dist/extensions/spectral-vision-fallback.d.ts.map +1 -1
- package/dist/extensions/spectral-vision-fallback.js +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp/commands.d.ts +1 -1
- package/dist/mcp/commands.d.ts.map +1 -1
- package/dist/mcp/commands.js +1 -1
- package/dist/mcp/config.d.ts +5 -5
- package/dist/mcp/config.d.ts.map +1 -1
- package/dist/mcp/config.js +15 -15
- package/dist/mcp/host-html-template.js +3 -3
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +15 -13
- package/dist/mcp/init.d.ts +1 -1
- package/dist/mcp/init.d.ts.map +1 -1
- package/dist/mcp/init.js +4 -4
- package/dist/mcp/mcp-oauth-provider.js +1 -1
- package/dist/mcp/proxy-modes.d.ts +1 -1
- package/dist/mcp/proxy-modes.d.ts.map +1 -1
- package/dist/mcp/proxy-modes.js +2 -2
- package/dist/mcp/server-manager.js +2 -2
- package/dist/mcp/state-getter.d.ts +14 -0
- package/dist/mcp/state-getter.d.ts.map +1 -0
- package/dist/mcp/state-getter.js +21 -0
- package/dist/mcp/tool-registrar.js +1 -1
- package/dist/mcp/ui-server.js +1 -1
- package/dist/mcp/ui-stream-types.d.ts +11 -11
- package/dist/mcp/ui-stream-types.d.ts.map +1 -1
- package/dist/mcp/ui-stream-types.js +5 -5
- package/dist/mcp/utils.d.ts +2 -2
- package/dist/mcp/utils.d.ts.map +1 -1
- package/dist/mcp/utils.js +10 -10
- package/dist/mcp-client.d.ts +1 -1
- package/dist/mcp-client.js +1 -1
- package/dist/memory/commands/status.d.ts +1 -1
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +2 -2
- package/dist/memory/commands/view.d.ts +1 -1
- package/dist/memory/commands/view.d.ts.map +1 -1
- package/dist/memory/commands/view.js +2 -2
- package/dist/memory/hooks/compaction-hook.d.ts +1 -1
- package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-hook.js +5 -5
- package/dist/memory/hooks/compaction-trigger.d.ts +1 -1
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +3 -3
- package/dist/memory/hooks/observer-trigger.d.ts +1 -1
- package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/observer-trigger.js +4 -4
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +9 -9
- package/dist/memory/tools/read-project-observations.d.ts +1 -1
- package/dist/memory/tools/read-project-observations.d.ts.map +1 -1
- package/dist/memory/tools/read-project-observations.js +2 -2
- package/dist/memory/tools/recall-observation.d.ts +1 -1
- package/dist/memory/tools/recall-observation.d.ts.map +1 -1
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/memory/tools/write-project-observation.d.ts +1 -1
- package/dist/memory/tools/write-project-observation.d.ts.map +1 -1
- package/dist/memory/tools/write-project-observation.js +2 -2
- package/dist/preflight.d.ts +1 -1
- package/dist/preflight.js +1 -1
- package/dist/relay/auto-research.d.ts +2 -2
- package/dist/relay/auto-research.js +34 -34
- package/dist/relay/dispatcher.d.ts +15 -6
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +33 -6
- package/dist/relay/models-fetch.d.ts +1 -1
- package/dist/relay/models-fetch.js +4 -4
- package/dist/sdk/ai/types.d.ts +1 -1
- package/dist/sdk/ai/utils/oauth/openai-codex.d.ts +1 -1
- package/dist/sdk/ai/utils/oauth/openai-codex.js +2 -2
- package/dist/sdk/coding-agent/core/agent-session.d.ts +2 -2
- package/dist/sdk/coding-agent/core/agent-session.js +3 -3
- package/dist/sdk/coding-agent/core/auth-storage.d.ts +2 -2
- package/dist/sdk/coding-agent/core/auth-storage.js +2 -2
- package/dist/sdk/coding-agent/core/bash-executor.js +1 -1
- package/dist/sdk/coding-agent/core/compaction/branch-summarization.js +1 -1
- package/dist/sdk/coding-agent/core/compaction/compaction.js +1 -1
- package/dist/sdk/coding-agent/core/extensions/loader.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/loader.js +18 -22
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.js +1 -1
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +9 -9
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/package-manager.d.ts +1 -1
- package/dist/sdk/coding-agent/core/package-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/package-manager.js +14 -14
- package/dist/sdk/coding-agent/core/sdk.d.ts +1 -1
- package/dist/sdk/coding-agent/core/sdk.js +2 -2
- package/dist/sdk/coding-agent/core/session-manager.d.ts +2 -2
- package/dist/sdk/coding-agent/core/session-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +7 -7
- package/dist/sdk/coding-agent/core/tools/bash.d.ts +2 -2
- package/dist/sdk/coding-agent/core/tools/bash.js +3 -3
- package/dist/sdk/coding-agent/core/tools/output-accumulator.js +1 -1
- package/dist/sdk/coding-agent/migrations.d.ts +1 -1
- package/dist/sdk/coding-agent/migrations.js +4 -4
- package/dist/sdk/coding-agent/modes/print-mode.d.ts +2 -2
- package/dist/sdk/coding-agent/modes/print-mode.js +2 -2
- package/dist/sdk/coding-agent/utils/clipboard-image.js +1 -1
- package/dist/sdk/coding-agent/utils/spectral-user-agent.d.ts +2 -0
- package/dist/sdk/coding-agent/utils/spectral-user-agent.d.ts.map +1 -0
- package/dist/sdk/coding-agent/utils/spectral-user-agent.js +3 -0
- package/dist/sdk/coding-agent/utils/version-check.d.ts +5 -5
- package/dist/sdk/coding-agent/utils/version-check.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/version-check.js +7 -7
- package/dist/sdk/coding-agent/utils/windows-self-update.js +1 -1
- package/dist/server/agent-bridge.d.ts +35 -35
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +59 -59
- package/dist/server/handlers/mcp-status.d.ts +21 -0
- package/dist/server/handlers/mcp-status.d.ts.map +1 -0
- package/dist/server/handlers/mcp-status.js +52 -0
- package/dist/server/handlers/sessions.d.ts +1 -1
- package/dist/server/handlers/sessions.js +1 -1
- package/dist/server/handlers/settings.d.ts +30 -0
- package/dist/server/handlers/settings.d.ts.map +1 -0
- package/dist/server/handlers/settings.js +123 -0
- package/dist/server/paths.d.ts +2 -2
- package/dist/server/paths.js +2 -2
- package/dist/server/session-stream.d.ts +25 -25
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +66 -38
- package/dist/server/shutdown.d.ts +3 -3
- package/dist/server/shutdown.d.ts.map +1 -1
- package/dist/server/shutdown.js +3 -3
- package/dist/server/storage.d.ts +4 -4
- package/dist/server/storage.js +6 -6
- package/dist/server/wire.d.ts +8 -8
- package/dist/server/wire.d.ts.map +1 -1
- package/dist/server/wire.js +1 -1
- package/package.json +1 -1
- package/dist/sdk/coding-agent/utils/pi-user-agent.d.ts +0 -2
- package/dist/sdk/coding-agent/utils/pi-user-agent.d.ts.map +0 -1
- package/dist/sdk/coding-agent/utils/pi-user-agent.js +0 -3
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-connection
|
|
2
|
+
* Per-connection spectral SDK lifecycle.
|
|
3
3
|
*
|
|
4
4
|
* One `AgentBridge` instance per active WebSocket connection. Wraps:
|
|
5
5
|
* - `createAgentSession` (in-memory session manager — we own persistence in
|
|
6
|
-
* SQLite;
|
|
7
|
-
* - `subscribe` listener that translates
|
|
6
|
+
* SQLite; spectral doesn't need to write its own JSONL files).
|
|
7
|
+
* - `subscribe` listener that translates spectral `AgentSessionEvent`s into our
|
|
8
8
|
* own `ServerEvent` wire format and pushes them through a caller-supplied
|
|
9
9
|
* sink.
|
|
10
10
|
* - `prompt(text)` to send user input.
|
|
11
11
|
* - `dispose()` for clean teardown on WS close.
|
|
12
12
|
*
|
|
13
|
-
* Event mapping (
|
|
13
|
+
* Event mapping (agent → wire):
|
|
14
14
|
* - `message_start` (assistant) → emit our own `message_start` with a
|
|
15
|
-
* fresh UUID `messageId`.
|
|
15
|
+
* fresh UUID `messageId`. spectral's AssistantMessage has no stable id field, so
|
|
16
16
|
* we mint one per turn and use it for all subsequent deltas/tool events
|
|
17
17
|
* until `message_end`.
|
|
18
18
|
* - `message_update` w/ inner text_delta / thinking_delta → wire `text_delta`
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Persistence shape:
|
|
31
31
|
* `events_jsonl` is the newline-delimited JSON of the wire-format
|
|
32
|
-
* `ServerEvent`s we emitted for this message — NOT raw
|
|
32
|
+
* `ServerEvent`s we emitted for this message — NOT raw spectral
|
|
33
33
|
* `AgentSessionEvent`s. This guarantees the client's `parseWireEvents`
|
|
34
34
|
* reducer can rehydrate the turn after a refresh using the exact same
|
|
35
35
|
* reducer it uses for the live broadcast.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* `createAgentSession` is called, each message is appended to the
|
|
46
46
|
* in-memory SessionManager so the LLM sees the full conversation
|
|
47
47
|
* context from the very first prompt. Multi-turn conversations within
|
|
48
|
-
* a single
|
|
48
|
+
* a single spectral session also work normally (the same AgentSession
|
|
49
49
|
* instance is reused across `prompt()` calls).
|
|
50
50
|
*/
|
|
51
51
|
import { createJiti } from "@mariozechner/jiti";
|
|
@@ -64,12 +64,12 @@ import { Runtime } from "../memory/runtime.js";
|
|
|
64
64
|
import { OBSERVATIONAL_MEMORY_CONTEXT_CUSTOM_TYPE, OBSERVATIONAL_MEMORY_SNAPSHOT_CUSTOM_TYPE, } from "../memory/types.js";
|
|
65
65
|
import { fetchAllowedModels as defaultFetchAllowedModels, } from "../relay/models-fetch.js";
|
|
66
66
|
/**
|
|
67
|
-
* Synthetic provider names registered with
|
|
67
|
+
* Synthetic provider names registered with spectral's `ModelRegistry`. They route
|
|
68
68
|
* 100% of inference traffic through the backend (`${backendUrl}/v1`), which
|
|
69
69
|
* authenticates with the machine JWT and forwards to the upstream provider
|
|
70
70
|
* with centrally-managed API keys.
|
|
71
71
|
*
|
|
72
|
-
* Two providers (rather than one) because
|
|
72
|
+
* Two providers (rather than one) because spectral's `ProviderConfigInput.api`
|
|
73
73
|
* picks the request shape per registration. Backend supports both, but a
|
|
74
74
|
* single bag would force all models onto one shape; instead we group by
|
|
75
75
|
* upstream provider type.
|
|
@@ -84,7 +84,7 @@ const SPECTRAL_PROXY_USER_MODEL = "spectral-proxy-user-model";
|
|
|
84
84
|
*
|
|
85
85
|
* Directories are returned in order from the deepest ancestor to `cwd` so
|
|
86
86
|
* project-local skills override ancestor-level ones when there are name
|
|
87
|
-
* collisions (
|
|
87
|
+
* collisions (spectral's skill loader keeps the first found).
|
|
88
88
|
*/
|
|
89
89
|
function collectAncestorSkillDirs(cwd, relativePaths) {
|
|
90
90
|
const found = [];
|
|
@@ -111,8 +111,8 @@ function collectAncestorSkillDirs(cwd, relativePaths) {
|
|
|
111
111
|
/**
|
|
112
112
|
* Concatenate text from an `AssistantMessage.content` array. Returns the
|
|
113
113
|
* empty string when no text blocks are present (tool-only turns) or when
|
|
114
|
-
* the input is missing/non-array (defensive —
|
|
115
|
-
* carries an array, but we don't want to crash on a future
|
|
114
|
+
* the input is missing/non-array (defensive — spectral's `message_end` always
|
|
115
|
+
* carries an array, but we don't want to crash on a future sdk change).
|
|
116
116
|
*/
|
|
117
117
|
function extractTextFromContent(content) {
|
|
118
118
|
if (!Array.isArray(content))
|
|
@@ -129,7 +129,7 @@ function extractTextFromContent(content) {
|
|
|
129
129
|
return out;
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
-
* Resolve the entry point of the
|
|
132
|
+
* Resolve the entry point of the spectral-mcp-adapter extension.
|
|
133
133
|
* Uses the bundled copy in dist/mcp/index.js (same directory as this file).
|
|
134
134
|
* Returns the absolute path, or null if the bundled file is missing.
|
|
135
135
|
*/
|
|
@@ -145,8 +145,8 @@ function resolveMcpAdapterEntry() {
|
|
|
145
145
|
/**
|
|
146
146
|
* Token pricing per model (USD per 1M tokens). Matches provider list
|
|
147
147
|
* prices as of May 2026. Used to compute token cost server-side when
|
|
148
|
-
*
|
|
149
|
-
* registered with zero cost to avoid
|
|
148
|
+
* spectral's own cost field is unavailable (synthetic proxy models are
|
|
149
|
+
* registered with zero cost to avoid agent-side billing).
|
|
150
150
|
*
|
|
151
151
|
* Keys are matched as prefix substrings against modelId, so
|
|
152
152
|
* `"claude-sonnet-4"` covers both `claude-sonnet-4-20250514` and any
|
|
@@ -386,7 +386,7 @@ export class AgentBridge {
|
|
|
386
386
|
disposed = false;
|
|
387
387
|
opts;
|
|
388
388
|
/**
|
|
389
|
-
*
|
|
389
|
+
* spectral's model registry. Built lazily in `start()` so we can resolve a
|
|
390
390
|
* `modelId` (envelope-supplied or SQLite-persisted) to a concrete `Model`
|
|
391
391
|
* via `registry.getAll().find(m => m.id === modelId)` before invoking
|
|
392
392
|
* `session.setModel()`. Phase 3 (Available Models whitelist).
|
|
@@ -400,12 +400,12 @@ export class AgentBridge {
|
|
|
400
400
|
allowedModels;
|
|
401
401
|
/**
|
|
402
402
|
* Last `modelId` we successfully applied via `session.setModel()`, or
|
|
403
|
-
* `undefined` if we never applied one (
|
|
403
|
+
* `undefined` if we never applied one (spectral falls back to its own settings
|
|
404
404
|
* file in that case, matching pre-Phase-3 behaviour). Tracked so repeated
|
|
405
|
-
* envelopes carrying the same modelId don't churn
|
|
405
|
+
* envelopes carrying the same modelId don't churn spectral's internal state.
|
|
406
406
|
*/
|
|
407
407
|
lastAppliedModelId;
|
|
408
|
-
/** Current model's credit rates (from
|
|
408
|
+
/** Current model's credit rates (from availableAgentModels), used for token_usage. */
|
|
409
409
|
activeCreditRates = null;
|
|
410
410
|
memoryRuntime = new Runtime();
|
|
411
411
|
memoryPhase = "idle";
|
|
@@ -413,38 +413,38 @@ export class AgentBridge {
|
|
|
413
413
|
this.opts = opts;
|
|
414
414
|
}
|
|
415
415
|
/**
|
|
416
|
-
* Create the
|
|
416
|
+
* Create the spectral session, wire up subscription, and return.
|
|
417
417
|
* Throws on creation failure (caller should surface to client).
|
|
418
418
|
*/
|
|
419
419
|
async start() {
|
|
420
420
|
if (this.disposed)
|
|
421
421
|
throw new Error("AgentBridge already disposed");
|
|
422
|
-
const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (
|
|
423
|
-
// Load
|
|
422
|
+
const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (ext) => { spectralVisionExtension(ext); }, async (ext) => { subagentExt(ext); }, async (ext) => { designerExtension(ext); }, async (ext) => { observationalMemory(ext); }];
|
|
423
|
+
// Load spectral-mcp-adapter via jiti so tsc never crawls its .ts files in
|
|
424
424
|
// node_modules. The static `import` was causing tsc to type-check
|
|
425
|
-
//
|
|
426
|
-
// jiti is the same loader
|
|
425
|
+
// spectral-mcp-adapter's source and fail the build on its type errors.
|
|
426
|
+
// jiti is the same loader spectral uses internally for all extensions.
|
|
427
427
|
const mcpAdapterPath = resolveMcpAdapterEntry();
|
|
428
428
|
if (mcpAdapterPath) {
|
|
429
429
|
try {
|
|
430
430
|
const jiti = createJiti(import.meta.url, { moduleCache: false });
|
|
431
431
|
const mcpAdapterFactory = await jiti.import(mcpAdapterPath, { default: true });
|
|
432
432
|
if (typeof mcpAdapterFactory === "function") {
|
|
433
|
-
extensionFactories.push(async (
|
|
433
|
+
extensionFactories.push(async (ext) => mcpAdapterFactory(ext));
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
catch {
|
|
437
|
-
console.info("[AgentBridge]
|
|
437
|
+
console.info("[AgentBridge] spectral-mcp-adapter not found; standard MCP servers disabled.");
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
else {
|
|
441
|
-
console.info("[AgentBridge]
|
|
441
|
+
console.info("[AgentBridge] spectral-mcp-adapter not found; standard MCP servers disabled.");
|
|
442
442
|
}
|
|
443
443
|
// ResourceLoader with extensions wired in via factories.
|
|
444
|
-
// Each factory's signature `(
|
|
444
|
+
// Each factory's signature `(ext: ExtensionAPI) => Promise<void>` matches
|
|
445
445
|
// the ExtensionFactory type exactly, so we can pass them directly.
|
|
446
446
|
//
|
|
447
|
-
// Skill discovery:
|
|
447
|
+
// Skill discovery: spectral's defaults scan ~/.spectral/agent/skills/ (user),
|
|
448
448
|
// .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
|
|
449
449
|
// (ancestor-walked). We additionally walk ancestors for
|
|
450
450
|
// .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
|
|
@@ -581,7 +581,7 @@ export class AgentBridge {
|
|
|
581
581
|
// provider credentials and the only allowed inference target. We then
|
|
582
582
|
// register synthetic providers (`spectral-proxy-anthropic` /
|
|
583
583
|
// `spectral-proxy-openai`) whose `baseUrl` points at the backend's
|
|
584
|
-
// `/v1` proxy and whose `apiKey` is the machine JWT.
|
|
584
|
+
// `/v1` proxy and whose `apiKey` is the machine JWT. spectral will then
|
|
585
585
|
// POST `${baseUrl}/messages` (or `/chat/completions`) with
|
|
586
586
|
// `Authorization: Bearer <machineJwt>` for every turn — the backend
|
|
587
587
|
// verifies the JWT, looks up the requested `model` in the BaseModel
|
|
@@ -639,7 +639,7 @@ export class AgentBridge {
|
|
|
639
639
|
}
|
|
640
640
|
catch { /* best-effort */ }
|
|
641
641
|
});
|
|
642
|
-
// Emit session_start so extensions can initialize (e.g.
|
|
642
|
+
// Emit session_start so extensions can initialize (e.g. spectral-mcp-adapter
|
|
643
643
|
// connects to MCP servers, loads configs from ~/.config/mcp/mcp.json etc.).
|
|
644
644
|
// bindExtensions also fires resources_discover for dynamic skill/prompt
|
|
645
645
|
// registration.
|
|
@@ -684,7 +684,7 @@ export class AgentBridge {
|
|
|
684
684
|
* (OpenAI-compatible API). The backend supports both endpoints natively
|
|
685
685
|
* (verified in F1).
|
|
686
686
|
*
|
|
687
|
-
*
|
|
687
|
+
* spectral will send `Authorization: Bearer ${apiKey}` (because `authHeader: true`)
|
|
688
688
|
* which carries the machine JWT — the only credential the backend trusts.
|
|
689
689
|
*
|
|
690
690
|
* The `id` we register is the raw `modelId` (e.g. `claude-3-5-haiku-latest`),
|
|
@@ -707,7 +707,7 @@ export class AgentBridge {
|
|
|
707
707
|
id: m.modelId,
|
|
708
708
|
name: m.displayName,
|
|
709
709
|
api: "anthropic-messages",
|
|
710
|
-
// Pin provider/baseUrl explicitly so
|
|
710
|
+
// Pin provider/baseUrl explicitly so spectral's ModelRegistry doesn't
|
|
711
711
|
// auto-derive `provider` from a slash-prefixed id (e.g. treating
|
|
712
712
|
// `deepseek/deepseek-v4-pro` as provider `"deepseek"`), which would
|
|
713
713
|
// make `hasConfiguredAuth(model)` look up the wrong provider key
|
|
@@ -735,7 +735,7 @@ export class AgentBridge {
|
|
|
735
735
|
id: m.modelId,
|
|
736
736
|
name: m.displayName,
|
|
737
737
|
api: "openai-completions",
|
|
738
|
-
// See anthropic batch above for rationale — without these,
|
|
738
|
+
// See anthropic batch above for rationale — without these, spectral
|
|
739
739
|
// auto-derives `provider` from slash-prefixed ids like
|
|
740
740
|
// `deepseek/deepseek-v4-pro` or `meta-llama/llama-3.3-70b-instruct`,
|
|
741
741
|
// breaking auth lookup against our synthetic proxy provider.
|
|
@@ -781,7 +781,7 @@ export class AgentBridge {
|
|
|
781
781
|
}
|
|
782
782
|
}
|
|
783
783
|
/**
|
|
784
|
-
* Apply a sticky model selection to the underlying
|
|
784
|
+
* Apply a sticky model selection to the underlying spectral session, if it
|
|
785
785
|
* differs from what was last applied. No-ops when:
|
|
786
786
|
* - `modelId` is null/undefined (caller passed nothing to apply)
|
|
787
787
|
* - the same modelId was already applied to this session
|
|
@@ -792,7 +792,7 @@ export class AgentBridge {
|
|
|
792
792
|
* Returns true when the requested model is now in effect (either because
|
|
793
793
|
* we just applied it or because it was already applied). Returns false
|
|
794
794
|
* on resolution failure so the caller can skip `prompt()` and avoid
|
|
795
|
-
* driving
|
|
795
|
+
* driving spectral against the wrong model.
|
|
796
796
|
*
|
|
797
797
|
* Phase 3 (Available Models whitelist).
|
|
798
798
|
*/
|
|
@@ -809,7 +809,7 @@ export class AgentBridge {
|
|
|
809
809
|
return this.allowedModels?.[0]?.modelId;
|
|
810
810
|
}
|
|
811
811
|
/**
|
|
812
|
-
* Return current session context usage from
|
|
812
|
+
* Return current session context usage from spectral's built-in estimator.
|
|
813
813
|
* Used after compaction and session start to push updated context-window
|
|
814
814
|
* stats to the frontend without waiting for the next assistant turn.
|
|
815
815
|
*/
|
|
@@ -850,7 +850,7 @@ export class AgentBridge {
|
|
|
850
850
|
}
|
|
851
851
|
async setModel(modelId) {
|
|
852
852
|
if (!modelId)
|
|
853
|
-
return true; // nothing to apply —
|
|
853
|
+
return true; // nothing to apply — spectral keeps its current model
|
|
854
854
|
if (!this.session)
|
|
855
855
|
throw new Error("AgentBridge.start() not called");
|
|
856
856
|
if (this.lastAppliedModelId === modelId)
|
|
@@ -858,7 +858,7 @@ export class AgentBridge {
|
|
|
858
858
|
if (!this.modelRegistry) {
|
|
859
859
|
// Defensive: start() always populates this; if it didn't we can't
|
|
860
860
|
// resolve and must surface to the client rather than silently using
|
|
861
|
-
//
|
|
861
|
+
// spectral's default.
|
|
862
862
|
this.opts.emit({
|
|
863
863
|
type: "error",
|
|
864
864
|
message: `Cannot apply modelId "${modelId}": model registry unavailable`,
|
|
@@ -884,7 +884,7 @@ export class AgentBridge {
|
|
|
884
884
|
if (!model) {
|
|
885
885
|
this.opts.emit({
|
|
886
886
|
type: "error",
|
|
887
|
-
message: `Unknown modelId "${modelId}" — not found in
|
|
887
|
+
message: `Unknown modelId "${modelId}" — not found in spectral model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
|
|
888
888
|
});
|
|
889
889
|
return false;
|
|
890
890
|
}
|
|
@@ -914,18 +914,18 @@ export class AgentBridge {
|
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
916
|
/**
|
|
917
|
-
* Map a frontend reasoning-effort string to
|
|
917
|
+
* Map a frontend reasoning-effort string to spectral's ThinkingLevel.
|
|
918
918
|
* Frontend sends: xhigh | high | medium | low | minimal | none | undefined
|
|
919
|
-
*
|
|
919
|
+
* spectral expects: "high" | "medium" | "low" | "minimal" | "off"
|
|
920
920
|
*
|
|
921
921
|
* Mapping:
|
|
922
|
-
* xhigh → high (
|
|
922
|
+
* xhigh → high (spectral doesn't have xhigh, default to max)
|
|
923
923
|
* high → high
|
|
924
924
|
* medium → medium
|
|
925
925
|
* low → low
|
|
926
926
|
* minimal → minimal
|
|
927
927
|
* none → off
|
|
928
|
-
* undefined → no-op (
|
|
928
|
+
* undefined → no-op (spectral keeps whatever it has currently)
|
|
929
929
|
*/
|
|
930
930
|
mapReasoningEffortToThinkingLevel(effort) {
|
|
931
931
|
if (effort === undefined)
|
|
@@ -946,10 +946,10 @@ export class AgentBridge {
|
|
|
946
946
|
}
|
|
947
947
|
/**
|
|
948
948
|
* Set the reasoning/thinking effort level for the next prompt.
|
|
949
|
-
* Pass `undefined` to leave
|
|
949
|
+
* Pass `undefined` to leave spectral's current level unchanged.
|
|
950
950
|
*
|
|
951
951
|
* The caller (SessionStreamManager) is responsible for persisting the
|
|
952
|
-
* value to SQLite; this method only applies it to
|
|
952
|
+
* value to SQLite; this method only applies it to spectral's in-memory session.
|
|
953
953
|
*/
|
|
954
954
|
setReasoningEffort(effort) {
|
|
955
955
|
const level = this.mapReasoningEffortToThinkingLevel(effort);
|
|
@@ -973,13 +973,13 @@ export class AgentBridge {
|
|
|
973
973
|
}
|
|
974
974
|
}
|
|
975
975
|
/**
|
|
976
|
-
* Forward a user message to
|
|
976
|
+
* Forward a user message to ext. Resolves when the full turn ends.
|
|
977
977
|
* The caller is responsible for persisting the user message to SQLite
|
|
978
|
-
* BEFORE invoking this — we don't do it here because
|
|
978
|
+
* BEFORE invoking this — we don't do it here because spectral's `prompt` may
|
|
979
979
|
* fail and we still want the user message recorded.
|
|
980
980
|
*
|
|
981
981
|
* When `images` is non-empty, each base64-encoded attachment is converted
|
|
982
|
-
* to a
|
|
982
|
+
* to a spectral `ImageContent` block and passed as `options.images` to
|
|
983
983
|
* `session.prompt()`. If the current model does not support image inputs,
|
|
984
984
|
* images are instead converted to text placeholders so the conversation
|
|
985
985
|
* can continue without errors.
|
|
@@ -1012,7 +1012,7 @@ export class AgentBridge {
|
|
|
1012
1012
|
}
|
|
1013
1013
|
}
|
|
1014
1014
|
/**
|
|
1015
|
-
* Manually compact the session context via
|
|
1015
|
+
* Manually compact the session context via spectral's built-in compaction.
|
|
1016
1016
|
* Pi generates a summary of older conversation history, preserving the
|
|
1017
1017
|
* most recent ~20K tokens verbatim. Compaction events are forwarded to
|
|
1018
1018
|
* the wire through `handleEvent()`.
|
|
@@ -1136,8 +1136,8 @@ export class AgentBridge {
|
|
|
1136
1136
|
}
|
|
1137
1137
|
/**
|
|
1138
1138
|
* Subscriber callback. Public so tests can drive event flow without
|
|
1139
|
-
* spinning up a real
|
|
1140
|
-
* directly;
|
|
1139
|
+
* spinning up a real spectral session — production code never calls this
|
|
1140
|
+
* directly; spectral's `subscribe()` does, via the closure registered in
|
|
1141
1141
|
* `start()`.
|
|
1142
1142
|
*/
|
|
1143
1143
|
handleEvent(ev) {
|
|
@@ -1152,7 +1152,7 @@ export class AgentBridge {
|
|
|
1152
1152
|
// Finalize the previous pending message (if any) before starting a
|
|
1153
1153
|
// new one. This captures tool events that arrived after the previous
|
|
1154
1154
|
// `message_end` but before this `message_start`. Deferred persistence
|
|
1155
|
-
// is necessary because
|
|
1155
|
+
// is necessary because spectral fires tool_execution_* events BETWEEN
|
|
1156
1156
|
// messages — after the previous `message_end` nulled the pending in
|
|
1157
1157
|
// the old code, those tool events were lost from history.
|
|
1158
1158
|
this.finalizePendingMessage();
|
|
@@ -1327,7 +1327,7 @@ export class AgentBridge {
|
|
|
1327
1327
|
if (ev.message.role !== "assistant" || !this.pending)
|
|
1328
1328
|
return;
|
|
1329
1329
|
const { messageId, text } = this.pending;
|
|
1330
|
-
// Prefer the assembled text from
|
|
1330
|
+
// Prefer the assembled text from spectral's final AssistantMessage
|
|
1331
1331
|
// (authoritative — providers like deepseek only populate this and
|
|
1332
1332
|
// skip per-token `text_delta`s entirely). Fall back to the
|
|
1333
1333
|
// accumulator for providers that stream deltas without re-asserting
|
|
@@ -1338,7 +1338,7 @@ export class AgentBridge {
|
|
|
1338
1338
|
const endEvent = { type: "message_end", messageId };
|
|
1339
1339
|
this.pending.wireEvents.push(endEvent);
|
|
1340
1340
|
this.opts.emit(endEvent);
|
|
1341
|
-
// Emit token usage for this assistant message.
|
|
1341
|
+
// Emit token usage for this assistant message. spectral provides token
|
|
1342
1342
|
// counts via ev.message.usage; credits are computed from the active
|
|
1343
1343
|
// model's configured credit rates.
|
|
1344
1344
|
//
|
|
@@ -1355,7 +1355,7 @@ export class AgentBridge {
|
|
|
1355
1355
|
(usage?.cacheRead ?? 0) +
|
|
1356
1356
|
(usage?.cacheWrite ?? 0);
|
|
1357
1357
|
if (usage && totalTokens > 0) {
|
|
1358
|
-
// Resolve current context window usage from
|
|
1358
|
+
// Resolve current context window usage from spectral's built-in
|
|
1359
1359
|
// `getContextUsage()`, which handles post-compaction ambiguity
|
|
1360
1360
|
// and estimates total tokens from the full session tree.
|
|
1361
1361
|
const ctxUsage = this.session?.getContextUsage();
|
|
@@ -1378,7 +1378,7 @@ export class AgentBridge {
|
|
|
1378
1378
|
this.opts.emit(usageEvent);
|
|
1379
1379
|
}
|
|
1380
1380
|
// Defer persistence: keep `this.pending` alive so tool events that
|
|
1381
|
-
// arrive after `message_end` (
|
|
1381
|
+
// arrive after `message_end` (spectral fires tool_execution_* events
|
|
1382
1382
|
// BETWEEN messages) are buffered into `pending.wireEvents`. We store
|
|
1383
1383
|
// the final content and persist later — when the next `message_start`
|
|
1384
1384
|
// signals a new step, or when `agent_end` closes the turn.
|
|
@@ -1415,7 +1415,7 @@ export class AgentBridge {
|
|
|
1415
1415
|
return;
|
|
1416
1416
|
}
|
|
1417
1417
|
default:
|
|
1418
|
-
// Other
|
|
1418
|
+
// Other spectral-internal events (turn_start, queue_update,
|
|
1419
1419
|
// auto_retry_*, tool_execution_update) are intentionally not on
|
|
1420
1420
|
// the wire surface for MVP and are NOT persisted — the wire format
|
|
1421
1421
|
// is the source of truth for replay.
|
|
@@ -1457,7 +1457,7 @@ function detectNotifSystem(message) {
|
|
|
1457
1457
|
/**
|
|
1458
1458
|
* Create a minimal ExtensionUIContext that forwards `notify()` calls as
|
|
1459
1459
|
* `agent_notification` wire events. All other UI methods are no-ops —
|
|
1460
|
-
* only extensions (not
|
|
1460
|
+
* only extensions (not spectral's TUI) call into the UI context in headless mode,
|
|
1461
1461
|
* and extensions that call methods other than `notify()` are expected to
|
|
1462
1462
|
* guard with `ctx.hasUI` first.
|
|
1463
1463
|
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for `GET /api/mcp-status`.
|
|
3
|
+
*
|
|
4
|
+
* Returns a snapshot of the locally connected MCP servers managed by
|
|
5
|
+
* `spectral serve`. Pure handler pattern — no framework, no side effects.
|
|
6
|
+
* The caller (the relay dispatcher) injects `McpExtensionState`.
|
|
7
|
+
*
|
|
8
|
+
* This module intentionally imports ONLY `McpExtensionState` (a pure-type
|
|
9
|
+
* import from `state.ts`). It avoids `init.ts` / `config.ts` / etc. so the
|
|
10
|
+
* dispatcher never pulls in heavy transitive deps.
|
|
11
|
+
*/
|
|
12
|
+
import type { McpExtensionState } from "../../mcp/state.js";
|
|
13
|
+
export interface McpServerStatus {
|
|
14
|
+
name: string;
|
|
15
|
+
status: "connected" | "disconnected" | "needs-auth" | "failed" | "cached";
|
|
16
|
+
toolCount: number;
|
|
17
|
+
/** Epoch-ms of the most recent connection failure, if the server is in a failed state. */
|
|
18
|
+
failedAt?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function handleListMcpStatus(state: McpExtensionState): McpServerStatus[];
|
|
21
|
+
//# sourceMappingURL=mcp-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-status.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/mcp-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,iBAAiB,GACvB,eAAe,EAAE,CA0BnB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for `GET /api/mcp-status`.
|
|
3
|
+
*
|
|
4
|
+
* Returns a snapshot of the locally connected MCP servers managed by
|
|
5
|
+
* `spectral serve`. Pure handler pattern — no framework, no side effects.
|
|
6
|
+
* The caller (the relay dispatcher) injects `McpExtensionState`.
|
|
7
|
+
*
|
|
8
|
+
* This module intentionally imports ONLY `McpExtensionState` (a pure-type
|
|
9
|
+
* import from `state.ts`). It avoids `init.ts` / `config.ts` / etc. so the
|
|
10
|
+
* dispatcher never pulls in heavy transitive deps.
|
|
11
|
+
*/
|
|
12
|
+
/** Maximum age (ms) of a failure entry before we treat it as stale. */
|
|
13
|
+
const FAILURE_BACKOFF_MS = 60_000;
|
|
14
|
+
export function handleListMcpStatus(state) {
|
|
15
|
+
const result = [];
|
|
16
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
17
|
+
const connection = state.manager.getConnection(name);
|
|
18
|
+
const metadata = state.toolMetadata.get(name);
|
|
19
|
+
const toolCount = metadata?.length ?? 0;
|
|
20
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
21
|
+
let status = "disconnected";
|
|
22
|
+
let failedAt;
|
|
23
|
+
if (connection?.status === "connected") {
|
|
24
|
+
status = "connected";
|
|
25
|
+
}
|
|
26
|
+
else if (connection?.status === "needs-auth") {
|
|
27
|
+
status = "needs-auth";
|
|
28
|
+
}
|
|
29
|
+
else if (failedAgo !== null) {
|
|
30
|
+
status = "failed";
|
|
31
|
+
failedAt = state.failureTracker.get(name) ?? undefined;
|
|
32
|
+
}
|
|
33
|
+
else if (metadata !== undefined) {
|
|
34
|
+
status = "cached";
|
|
35
|
+
}
|
|
36
|
+
result.push({ name, status, toolCount, failedAt });
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Mirrors `init.ts#getFailureAgeSeconds` without pulling in init.ts's
|
|
42
|
+
* heavy runtime deps (config, consent manager, UI resource handler, etc.).
|
|
43
|
+
*/
|
|
44
|
+
function getFailureAgeSeconds(state, serverName) {
|
|
45
|
+
const failedAt = state.failureTracker.get(serverName);
|
|
46
|
+
if (!failedAt)
|
|
47
|
+
return null;
|
|
48
|
+
const ageMs = Date.now() - failedAt;
|
|
49
|
+
if (ageMs > FAILURE_BACKOFF_MS)
|
|
50
|
+
return null;
|
|
51
|
+
return Math.round(ageMs / 1000);
|
|
52
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same shape as projects handlers — see that file's header for the contract.
|
|
5
5
|
*
|
|
6
|
-
* Note: session deletion does not tear down the in-flight
|
|
6
|
+
* Note: session deletion does not tear down the in-flight spectral stream itself.
|
|
7
7
|
* The Batch 3 dispatcher must call `manager.disposeSessionStream(id)` BEFORE
|
|
8
8
|
* invoking `handleDeleteSession` so the FK cascade doesn't leave a zombie
|
|
9
9
|
* bridge driving events at a row that no longer exists. This matches the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same shape as projects handlers — see that file's header for the contract.
|
|
5
5
|
*
|
|
6
|
-
* Note: session deletion does not tear down the in-flight
|
|
6
|
+
* Note: session deletion does not tear down the in-flight spectral stream itself.
|
|
7
7
|
* The Batch 3 dispatcher must call `manager.disposeSessionStream(id)` BEFORE
|
|
8
8
|
* invoking `handleDeleteSession` so the FK cascade doesn't leave a zombie
|
|
9
9
|
* bridge driving events at a row that no longer exists. This matches the
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
|
|
3
|
+
*
|
|
4
|
+
* Settings are stored in the machine's global `settings.json` under the
|
|
5
|
+
* `"observational-memory"` namespace (see `src/memory/config.ts`).
|
|
6
|
+
*
|
|
7
|
+
* GET /api/settings → returns full observational-memory config
|
|
8
|
+
* PUT /api/settings → updates a single key via `{ key, value }` body
|
|
9
|
+
*
|
|
10
|
+
* All paths are machine-level — no session context needed.
|
|
11
|
+
*/
|
|
12
|
+
export interface PutSettingsInput {
|
|
13
|
+
key?: unknown;
|
|
14
|
+
value?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface PutSettingsOutput {
|
|
17
|
+
ok: true;
|
|
18
|
+
key: string;
|
|
19
|
+
value: number;
|
|
20
|
+
}
|
|
21
|
+
export interface GetSettingsOutput {
|
|
22
|
+
observationThresholdTokens: number;
|
|
23
|
+
compactionThresholdTokens: number;
|
|
24
|
+
reflectionThresholdTokens: number;
|
|
25
|
+
passive: boolean;
|
|
26
|
+
debugLog: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function handleGetSettings(cwd: string): GetSettingsOutput;
|
|
29
|
+
export declare function handlePutSettings(cwd: string, body: PutSettingsInput): PutSettingsOutput;
|
|
30
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAsFD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAShE;AAMD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,iBAAiB,CA0BnB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
|
|
3
|
+
*
|
|
4
|
+
* Settings are stored in the machine's global `settings.json` under the
|
|
5
|
+
* `"observational-memory"` namespace (see `src/memory/config.ts`).
|
|
6
|
+
*
|
|
7
|
+
* GET /api/settings → returns full observational-memory config
|
|
8
|
+
* PUT /api/settings → updates a single key via `{ key, value }` body
|
|
9
|
+
*
|
|
10
|
+
* All paths are machine-level — no session context needed.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import lockfile from "proper-lockfile";
|
|
15
|
+
import { getAgentDir } from "../../sdk/coding-agent/index.js";
|
|
16
|
+
import { loadConfig } from "../../memory/config.js";
|
|
17
|
+
import { BadRequestError } from "./errors.js";
|
|
18
|
+
/** Recognised setting keys (dot-separated path within the config object). */
|
|
19
|
+
const ALLOWED_KEYS = new Set([
|
|
20
|
+
"observationThresholdTokens",
|
|
21
|
+
"compactionThresholdTokens",
|
|
22
|
+
"reflectionThresholdTokens",
|
|
23
|
+
]);
|
|
24
|
+
/** Sane bounds for token thresholds. */
|
|
25
|
+
const MIN_TOKENS = 1_000;
|
|
26
|
+
const MAX_TOKENS = 1_000_000;
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// File helpers (inlined to avoid pulling in the full SettingsManager)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function acquireLockSyncWithRetry(path) {
|
|
31
|
+
const maxAttempts = 10;
|
|
32
|
+
const delayMs = 20;
|
|
33
|
+
let lastError;
|
|
34
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
return lockfile.lockSync(path, { realpath: false });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
40
|
+
? String(error.code)
|
|
41
|
+
: undefined;
|
|
42
|
+
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
lastError = error;
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
while (Date.now() - start < delayMs) {
|
|
48
|
+
// busy-wait; synchronous by design
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw lastError ?? new Error("Failed to acquire settings lock");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Atomically read-modify-write the global `settings.json` under the
|
|
56
|
+
* `"observational-memory"` namespace.
|
|
57
|
+
*/
|
|
58
|
+
function withObservationalMemoryLock(fn) {
|
|
59
|
+
const path = join(getAgentDir(), "settings.json");
|
|
60
|
+
const dir = dirname(path);
|
|
61
|
+
let release;
|
|
62
|
+
try {
|
|
63
|
+
const fileExists = existsSync(path);
|
|
64
|
+
if (fileExists) {
|
|
65
|
+
release = acquireLockSyncWithRetry(path);
|
|
66
|
+
}
|
|
67
|
+
const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
const ns = (parsed["observational-memory"] ?? {});
|
|
70
|
+
const next = fn(ns);
|
|
71
|
+
if (next !== undefined) {
|
|
72
|
+
parsed["observational-memory"] = { ...ns, ...next };
|
|
73
|
+
if (!existsSync(dir)) {
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
if (!release) {
|
|
77
|
+
release = acquireLockSyncWithRetry(path);
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (release) {
|
|
84
|
+
release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// GET handler
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
export function handleGetSettings(cwd) {
|
|
92
|
+
const config = loadConfig(cwd);
|
|
93
|
+
return {
|
|
94
|
+
observationThresholdTokens: config.observationThresholdTokens,
|
|
95
|
+
compactionThresholdTokens: config.compactionThresholdTokens,
|
|
96
|
+
reflectionThresholdTokens: config.reflectionThresholdTokens,
|
|
97
|
+
passive: config.passive,
|
|
98
|
+
debugLog: config.debugLog,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// PUT handler
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
export function handlePutSettings(cwd, body) {
|
|
105
|
+
// Validate key
|
|
106
|
+
if (typeof body.key !== "string" || !ALLOWED_KEYS.has(body.key)) {
|
|
107
|
+
throw new BadRequestError(`settings key must be one of: ${[...ALLOWED_KEYS].join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
const key = body.key;
|
|
110
|
+
// Validate value
|
|
111
|
+
if (typeof body.value !== "number" || !Number.isInteger(body.value)) {
|
|
112
|
+
throw new BadRequestError("settings value must be an integer");
|
|
113
|
+
}
|
|
114
|
+
if (body.value < MIN_TOKENS || body.value > MAX_TOKENS) {
|
|
115
|
+
throw new BadRequestError(`token threshold must be between ${MIN_TOKENS.toLocaleString()} and ${MAX_TOKENS.toLocaleString()}`);
|
|
116
|
+
}
|
|
117
|
+
const value = body.value;
|
|
118
|
+
// Persist
|
|
119
|
+
withObservationalMemoryLock((current) => {
|
|
120
|
+
return { ...current, [key]: value };
|
|
121
|
+
});
|
|
122
|
+
return { ok: true, key, value };
|
|
123
|
+
}
|